Skip to content

Commit 7ca1c5f

Browse files
committed
test: clean relationship cache on delete
1 parent dec123e commit 7ca1c5f

File tree

7 files changed

+323
-224
lines changed

7 files changed

+323
-224
lines changed

src/__tests__/create/create-connect.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
import { PrismaClient, type Post, Comment } from '@prisma/client';
1+
import { PrismaClient } from '@prisma/client';
22

3-
import { resetDb, seededPosts, simulateSeed } from '../../../testing';
3+
import { resetDb, seededPosts, simulateSeed, type PostWithComments } from '../../../testing';
44
import { PrismockClient, PrismockClientType, relationshipStore } from '../../lib/client';
55

66
jest.setTimeout(40000);
77

8-
type PostWithComments = Post & { comments: Comment[] };
9-
108
describe('create (connect)', () => {
119
let prismock: PrismockClientType;
1210
let prisma: PrismaClient;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { PrismaClient } from '@prisma/client';
2+
3+
import { resetDb, simulateSeed, type PostWithComments } from '../../../testing';
4+
import { PrismockClient, PrismockClientType, relationshipStore } from '../../lib/client';
5+
6+
describe('deleteMany', () => {
7+
let prismock: PrismockClientType;
8+
let prisma: PrismaClient;
9+
10+
beforeEach(() => relationshipStore.resetValues());
11+
12+
beforeAll(async () => {
13+
await resetDb();
14+
15+
prisma = new PrismaClient();
16+
prismock = new PrismockClient() as PrismockClientType;
17+
await simulateSeed(prismock);
18+
});
19+
20+
it('Should reset many to many relationships', async () => {
21+
const connectPayload = {
22+
where: { id: 1 },
23+
data: {
24+
comments: {
25+
connect: [{ id: 1 }, { id: 2 }],
26+
},
27+
},
28+
};
29+
30+
await prisma.post.update(connectPayload);
31+
await prismock.post.update(connectPayload);
32+
33+
await prisma.comment.delete({ where: { id: 1 } });
34+
await prismock.comment.delete({ where: { id: 1 } });
35+
36+
await prisma.comment.create({
37+
data: {
38+
body: 'yo',
39+
},
40+
});
41+
await prismock.comment.create({
42+
data: {
43+
id: 1,
44+
body: 'yo',
45+
},
46+
});
47+
48+
const post = await prisma.post.findFirst({ where: { id: 1 }, include: { comments: true } });
49+
const mockedPost = await prismock.post.findFirst({ where: { id: 1 }, include: { comments: true } });
50+
51+
expect(post).toMatchObject(mockedPost as PostWithComments);
52+
});
53+
});

src/__tests__/symmetrical-relationships.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { PrismaClient, User } from '@prisma/client';
22

3-
import { resetDb, simulateSeed } from '../../testing';
3+
import { resetDb, simulateSeed, seededUsers } from '../../testing';
44
import { PrismockClient, PrismockClientType, relationshipStore } from '../lib/client';
55

66
jest.setTimeout(40000);
77

8-
describe('create (connect)', () => {
8+
describe('Symmetrical relationships', () => {
99
let prismock: PrismockClientType;
1010
let prisma: PrismaClient;
1111

@@ -57,12 +57,12 @@ describe('create (connect)', () => {
5757
});
5858
}
5959

60-
it('Should connect many to many relationship', async () => {
60+
it('Should connect entities', async () => {
6161
await addConnection(prisma, 1, 2);
6262
await addConnection(prismock, 1, 2);
6363

6464
await Promise.all(
65-
[1, 2, 3].map(async (id) => {
65+
seededUsers.map(async ({ id }) => {
6666
const realUser = await prisma.user.findFirst({ where: { id }, include: { connections: true } });
6767
const fakeUser = await prismock.user.findFirst({ where: { id }, include: { connections: true } });
6868
expect(realUser).toMatchObject(fakeUser as User);
@@ -77,7 +77,7 @@ describe('create (connect)', () => {
7777
await addConnection(prismock, 1, 3);
7878

7979
await Promise.all(
80-
[1, 2, 3].map(async (id) => {
80+
seededUsers.map(async ({ id }) => {
8181
const realUser = await prisma.user.findFirst({ where: { id }, include: { connections: true } });
8282
const fakeUser = await prismock.user.findFirst({ where: { id }, include: { connections: true } });
8383
expect(realUser).toMatchObject(fakeUser as User);
@@ -88,7 +88,28 @@ describe('create (connect)', () => {
8888
await removeConnection(prismock, 1, 3);
8989

9090
await Promise.all(
91-
[1, 2, 3].map(async (id) => {
91+
seededUsers.map(async ({ id }) => {
92+
const realUser = await prisma.user.findFirst({ where: { id }, include: { connections: true } });
93+
const fakeUser = await prismock.user.findFirst({ where: { id }, include: { connections: true } });
94+
expect(realUser).toMatchObject(fakeUser as User);
95+
}),
96+
);
97+
});
98+
99+
it('Should reset symmetrical many to many relationships', async () => {
100+
await addConnection(prisma, 1, 2);
101+
await addConnection(prismock, 1, 2);
102+
103+
await prisma.post.deleteMany();
104+
105+
await prisma.user.delete({ where: { id: 2 } });
106+
await prismock.user.delete({ where: { id: 2 } });
107+
108+
await prisma.user.create({ data: seededUsers[1] });
109+
await prismock.user.create({ data: seededUsers[1] });
110+
111+
await Promise.all(
112+
seededUsers.map(async ({ id }) => {
92113
const realUser = await prisma.user.findFirst({ where: { id }, include: { connections: true } });
93114
const fakeUser = await prismock.user.findFirst({ where: { id }, include: { connections: true } });
94115
expect(realUser).toMatchObject(fakeUser as User);

src/lib/client.ts

Lines changed: 1 addition & 207 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { DMMF } from '@prisma/generator-helper';
44

55
import { Delegate } from './delegate';
66
import { Data, Delegates, generateDelegates } from './prismock';
7-
import { FindWhereArgs } from './types';
7+
import { RelationshipStore } from './relationship-store';
88

99
type GetData = () => Data;
1010
type SetData = (data: Data) => void;
@@ -120,209 +120,3 @@ export function createPrismock(instance: PrismaModule) {
120120
}
121121

122122
export const PrismockClient = createPrismock(Prisma);
123-
124-
type RelationshipEntry = { a: number; b: number };
125-
126-
type Relationship = {
127-
name: string;
128-
a: { name: string; type: string };
129-
b: { name: string; type: string };
130-
values: RelationshipEntry[];
131-
};
132-
133-
type RelationActionParams = {
134-
relationshipName: string;
135-
fieldName: string;
136-
id: number;
137-
values: { id: number } | { id: number }[];
138-
};
139-
140-
type MatchParams = {
141-
type: string;
142-
name: string;
143-
itemId: number;
144-
where: FindWhereArgs;
145-
};
146-
147-
class RelationshipStore {
148-
private relationships: Record<string, Relationship>;
149-
constructor(models: DMMF.Model[]) {
150-
this.relationships = {};
151-
this.populateRelationships(models);
152-
}
153-
154-
getRelationships() {
155-
return this.relationships;
156-
}
157-
158-
findRelationship(name: string) {
159-
return this.relationships[name];
160-
}
161-
162-
findRelationshipBy(type: string, name: string) {
163-
return Object.values(this.relationships).find(
164-
({ a, b }) => (a.type === type && b.name === name) || (b.type === type && a.name === name),
165-
);
166-
}
167-
168-
match({ type, name, itemId, where }: MatchParams) {
169-
const relationship = this.findRelationshipBy(type, name);
170-
if (!relationship) {
171-
return 0;
172-
}
173-
if (where.none && !relationship.values.length) {
174-
return 1;
175-
}
176-
177-
const [valueField, targetField] = this.getRelationshipFieldNames(relationship, type);
178-
const found = relationship.values.find((x) => {
179-
if (where.some) {
180-
return x[valueField] === itemId && x[targetField] === (where.some as { id: number }).id;
181-
}
182-
if (where.none) {
183-
return x[valueField] !== itemId || x[targetField] !== (where.none as { id: number }).id;
184-
}
185-
return false;
186-
});
187-
if (!found) {
188-
return -1;
189-
}
190-
return 1;
191-
}
192-
193-
getRelationshipIds(name: string, type: string, id: FindWhereArgs | number) {
194-
const relationship = this.findRelationship(name);
195-
if (!relationship) {
196-
return false;
197-
}
198-
if (this.isSymmetrical(relationship)) {
199-
return this.extractSymmetricalValues(relationship, id);
200-
}
201-
const [valueField] = this.getRelationshipFieldNames(relationship, type);
202-
return relationship.values.map((x) => x[valueField]);
203-
}
204-
205-
connectToRelationship({ relationshipName, fieldName, id, values }: RelationActionParams) {
206-
const relationship = this.findRelationship(relationshipName);
207-
if (!relationship) {
208-
return;
209-
}
210-
if (!Array.isArray(values)) {
211-
const value = this.getActionValue({ relationship, fieldName, id, value: values });
212-
relationship.values = relationship.values.find((x) => this.matchEntry(x, value))
213-
? relationship.values
214-
: [...relationship.values, value];
215-
return;
216-
}
217-
relationship.values = [
218-
...relationship.values,
219-
...values
220-
.map((x) => this.getActionValue({ relationship, fieldName, id, value: x }))
221-
.map((x) => (relationship.values.find((y) => this.matchEntry(x, y)) ? null : x))
222-
.filter((x) => !!x),
223-
];
224-
}
225-
226-
disconnectFromRelation({ relationshipName, fieldName, id, values }: RelationActionParams) {
227-
const relationship = this.findRelationship(relationshipName);
228-
if (!relationship) {
229-
return;
230-
}
231-
if (!Array.isArray(values)) {
232-
const value = this.getActionValue({ relationship, fieldName, id, value: values });
233-
relationship.values = relationship.values.filter((x) => this.matchEntry(x, value));
234-
return;
235-
}
236-
relationship.values = relationship.values.filter(
237-
(x) =>
238-
!values
239-
.map((x) => this.getActionValue({ relationship, fieldName, id, value: x }))
240-
.find((y) => this.matchEntry(x, y)),
241-
);
242-
}
243-
244-
resetValues() {
245-
Object.values(this.relationships).forEach((x) => (x.values = []));
246-
}
247-
248-
private getRelationshipFieldNames({ a }: Relationship, type: string): ['a', 'b'] | ['b', 'a'] {
249-
return a.type === type ? ['a', 'b'] : ['b', 'a'];
250-
}
251-
252-
private matchEntry(x: RelationshipEntry, y: RelationshipEntry) {
253-
return x.a === y.a && x.b === y.b;
254-
}
255-
256-
private isSymmetrical({ a, b }: Relationship) {
257-
return a.type === b.type;
258-
}
259-
260-
private extractSymmetricalValues({ values }: Relationship, id: FindWhereArgs | number) {
261-
if (typeof id === 'number') {
262-
return values.filter(({ a, b }) => a === id || b === id).map(({ a, b }) => (a === id ? b : a));
263-
}
264-
return (id.in as number[]).some((id) =>
265-
values.filter(({ a, b }) => a === id || b === id).map(({ a, b }) => (a === id ? b : a)),
266-
);
267-
}
268-
269-
private getActionValue({
270-
relationship,
271-
fieldName,
272-
id,
273-
value,
274-
}: {
275-
relationship: Relationship;
276-
fieldName: string;
277-
id: number;
278-
value: { id: number };
279-
}) {
280-
if (relationship.a.name === fieldName) {
281-
return { a: value.id, b: id };
282-
}
283-
return { a: id, b: value.id };
284-
}
285-
286-
private populateRelationships(models: DMMF.Model[]) {
287-
this.relationships = Object.entries(
288-
this.groupBy(
289-
models.flatMap((x) => x.fields),
290-
(x) => x.relationName as string,
291-
),
292-
)
293-
.filter(
294-
([key, fields]) =>
295-
key !== 'undefined' &&
296-
fields.every(({ relationFromFields: from, relationToFields: to }) => !from?.length && !to?.length),
297-
)
298-
.map(([_, fields]) => fields.sort((a, b) => ((a?.name as string) > (b?.name as string) ? 1 : -1)))
299-
.reduce(
300-
(memo, [a, b]) => ({
301-
...memo,
302-
[a?.relationName as string]: {
303-
name: a?.relationName as string,
304-
values: [],
305-
a: {
306-
name: a?.name as string,
307-
type: a?.type as string,
308-
},
309-
b: {
310-
name: b?.name as string,
311-
type: b?.type as string,
312-
},
313-
},
314-
}),
315-
{},
316-
);
317-
}
318-
319-
private groupBy<T>(array: T[], aggregator: (item: T) => string): Record<string, T[]> {
320-
return array.reduce((memo, item) => {
321-
const key = aggregator(item);
322-
return {
323-
...memo,
324-
[key]: memo[key] ? [...memo[key], item] : [item],
325-
};
326-
}, {} as Record<string, T[]>);
327-
}
328-
}

src/lib/operations/delete.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Delegates } from '../prismock';
22
import { Item, Delegate } from '../delegate';
33
import { FindWhereArgs, SelectArgs } from '../types';
44
import { pipe } from '../helpers';
5+
import { relationshipStore } from '../client';
56

67
import { getDelegateFromField, getFieldFromRelationshipWhere, getJoinField, includes, select, where } from './find';
78

@@ -41,21 +42,23 @@ export function deleteMany(args: DeleteArgs, current: Delegate, delegates: Deleg
4142

4243
toDelete.forEach((item: Item) => {
4344
current.model.fields.forEach((field) => {
44-
const joinfield = getJoinField(field, delegates);
45-
if (!joinfield) return;
45+
relationshipStore.cleanupRelationships({ type: field.type, id: (args?.where as { id: number })?.id });
46+
47+
const joinField = getJoinField(field, delegates);
48+
if (!joinField) return;
4649

4750
const delegate = getDelegateFromField(field, delegates);
4851

49-
if (joinfield.relationOnDelete === 'SetNull') {
52+
if (joinField.relationOnDelete === 'SetNull') {
5053
delegate.updateMany({
51-
where: getFieldFromRelationshipWhere(item, joinfield),
54+
where: getFieldFromRelationshipWhere(item, joinField),
5255
data: {
53-
[joinfield.relationFromFields![0]]: null,
56+
[joinField.relationFromFields![0]]: null,
5457
},
5558
});
56-
} else if (joinfield.relationOnDelete === 'Cascade') {
59+
} else if (joinField.relationOnDelete === 'Cascade') {
5760
delegate.deleteMany({
58-
where: getFieldFromRelationshipWhere(item, joinfield),
61+
where: getFieldFromRelationshipWhere(item, joinField),
5962
});
6063
}
6164
});

0 commit comments

Comments
 (0)