diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5952082c..a7b15f01 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -16,23 +16,25 @@ enum Role { } model User { - id Int @id @default(autoincrement()) - email String @unique - password String - role Role @default(USER) - warnings Int? - banned Boolean @default(false) - money BigInt @default(0) - friends Float @default(0) - birthday DateTime? - signal Bytes? - parameters Json @default("{}") - posts Post[] - Blog Blog[] - profile Profile? - service Service[] - subscriptions Subscription[] - reactions Reaction[] + id Int @id @default(autoincrement()) + email String @unique + password String + role Role @default(USER) + warnings Int? + banned Boolean @default(false) + money BigInt @default(0) + friends Float @default(0) + birthday DateTime? + signal Bytes? + parameters Json @default("{}") + posts Post[] + Blog Blog[] + profile Profile? + service Service[] + subscriptions Subscription[] + reactions Reaction[] + connections User[] @relation("Connections") + symmetricalConnections User[] @relation("Connections") } model Profile { @@ -49,14 +51,21 @@ enum Gender { } model Post { - id Int @id @default(autoincrement()) - title String @unique @db.VarChar(255) - createdAt DateTime @default(now()) - imprint String @default(uuid()) - author User @relation(fields: [authorId], references: [id]) + id Int @id @default(autoincrement()) + title String @unique @db.VarChar(255) + createdAt DateTime @default(now()) + imprint String @default(uuid()) + author User @relation(fields: [authorId], references: [id]) authorId Int - blog Blog @relation(fields: [blogId], references: [id], onDelete: Cascade) + blog Blog @relation(fields: [blogId], references: [id], onDelete: Cascade) blogId Int + comments Comment[] +} + +model Comment { + id Int @id @default(autoincrement()) + body String @db.VarChar(255) + posts Post[] } model Blog { diff --git a/src/__tests__/client/client-custom.test.ts b/src/__tests__/client/client-custom.test.ts index d2b1ab4c..8f09c4ef 100755 --- a/src/__tests__/client/client-custom.test.ts +++ b/src/__tests__/client/client-custom.test.ts @@ -27,6 +27,7 @@ describe('client (custom)', () => { profile: [], service: [], subscription: [], + comment: [], }; if (provider !== 'mongodb') { @@ -63,6 +64,7 @@ describe('client (custom)', () => { profile: [], service: [], subscription: [], + comment: [], }; if (provider !== 'mongodb') { diff --git a/src/__tests__/create/create-connect.test.ts b/src/__tests__/create/create-connect.test.ts index 6d791d03..94483812 100755 --- a/src/__tests__/create/create-connect.test.ts +++ b/src/__tests__/create/create-connect.test.ts @@ -1,7 +1,7 @@ import { PrismaClient } from '@prisma/client'; -import { resetDb, seededPosts, simulateSeed } from '../../../testing'; -import { PrismockClient, PrismockClientType } from '../../lib/client'; +import { resetDb, seededPosts, simulateSeed, type PostWithComments } from '../../../testing'; +import { PrismockClient, PrismockClientType, relationshipStore } from '../../lib/client'; jest.setTimeout(40000); @@ -9,6 +9,8 @@ describe('create (connect)', () => { let prismock: PrismockClientType; let prisma: PrismaClient; + beforeEach(() => relationshipStore.resetValues()); + beforeAll(async () => { await resetDb(); @@ -92,4 +94,25 @@ describe('create (connect)', () => { expect(realBlog).toEqual(expected); expect(mockBlog).toEqual(expected); }); + + it('Should handle many to many relationship', async () => { + const payload = { + data: { + id: 99, + title: 'Title', + authorId: 1, + blogId: 1, + comments: { + connect: [{ id: 1 }, { id: 2 }], + }, + }, + }; + await prisma.post.create(payload); + await prismock.post.create(payload); + + const realPost = await prisma.post.findFirst({ where: { id: 99 }, include: { comments: true } }); + const mockPost = await prismock.post.findFirst({ where: { id: 99 }, include: { comments: true } }); + + expect((mockPost as PostWithComments).comments).toMatchObject((realPost as PostWithComments).comments); + }); }); diff --git a/src/__tests__/delete/delete-many-to-many.test.ts b/src/__tests__/delete/delete-many-to-many.test.ts new file mode 100644 index 00000000..4d024987 --- /dev/null +++ b/src/__tests__/delete/delete-many-to-many.test.ts @@ -0,0 +1,53 @@ +import { PrismaClient } from '@prisma/client'; + +import { resetDb, simulateSeed, type PostWithComments } from '../../../testing'; +import { PrismockClient, PrismockClientType, relationshipStore } from '../../lib/client'; + +describe('deleteMany', () => { + let prismock: PrismockClientType; + let prisma: PrismaClient; + + beforeEach(() => relationshipStore.resetValues()); + + beforeAll(async () => { + await resetDb(); + + prisma = new PrismaClient(); + prismock = new PrismockClient() as PrismockClientType; + await simulateSeed(prismock); + }); + + it('Should reset many to many relationships', async () => { + const connectPayload = { + where: { id: 1 }, + data: { + comments: { + connect: [{ id: 1 }, { id: 2 }], + }, + }, + }; + + await prisma.post.update(connectPayload); + await prismock.post.update(connectPayload); + + await prisma.comment.delete({ where: { id: 1 } }); + await prismock.comment.delete({ where: { id: 1 } }); + + await prisma.comment.create({ + data: { + body: 'yo', + }, + }); + await prismock.comment.create({ + data: { + id: 1, + body: 'yo', + }, + }); + + const post = await prisma.post.findFirst({ where: { id: 1 }, include: { comments: true } }); + const mockedPost = await prismock.post.findFirst({ where: { id: 1 }, include: { comments: true } }); + + expect(post).toMatchObject(mockedPost as PostWithComments); + }); +}); diff --git a/src/__tests__/relationship-store.test.ts b/src/__tests__/relationship-store.test.ts new file mode 100644 index 00000000..62bf1668 --- /dev/null +++ b/src/__tests__/relationship-store.test.ts @@ -0,0 +1,590 @@ +import { DMMF } from '@prisma/generator-helper'; +import { RelationshipStore } from '../lib/relationship-store'; +import { FindWhereArgs } from '../lib/types'; + +const userModel = { + name: 'User', + dbName: null, + fields: [ + { + name: 'id', + kind: 'scalar', + isList: false, + isRequired: true, + isUnique: false, + isId: true, + isReadOnly: false, + hasDefaultValue: true, + type: 'Int', + default: { + name: 'autoincrement', + args: [], + }, + isGenerated: false, + isUpdatedAt: false, + }, + { + name: 'posts', + kind: 'object', + isList: true, + isRequired: true, + isUnique: false, + isId: false, + isReadOnly: false, + hasDefaultValue: false, + type: 'Post', + relationName: 'PostToUser', + relationFromFields: [], + relationToFields: [], + isGenerated: false, + isUpdatedAt: false, + }, + { + name: 'connections', + kind: 'object', + isList: true, + isRequired: true, + isUnique: false, + isId: false, + isReadOnly: false, + hasDefaultValue: false, + type: 'User', + relationName: 'Connections', + relationFromFields: [], + relationToFields: [], + isGenerated: false, + isUpdatedAt: false, + }, + { + name: 'symmetricalConnections', + kind: 'object', + isList: true, + isRequired: true, + isUnique: false, + isId: false, + isReadOnly: false, + hasDefaultValue: false, + type: 'User', + relationName: 'Connections', + relationFromFields: [], + relationToFields: [], + isGenerated: false, + isUpdatedAt: false, + }, + ], + primaryKey: null, + uniqueFields: [], + uniqueIndexes: [], + isGenerated: false, +} as DMMF.Model; + +const postModel = { + name: 'Post', + dbName: null, + fields: [ + { + name: 'id', + kind: 'scalar', + isList: false, + isRequired: true, + isUnique: false, + isId: true, + isReadOnly: false, + hasDefaultValue: true, + type: 'Int', + default: { + name: 'autoincrement', + args: [], + }, + isGenerated: false, + isUpdatedAt: false, + }, + { + name: 'author', + kind: 'object', + isList: false, + isRequired: true, + isUnique: false, + isId: false, + isReadOnly: false, + hasDefaultValue: false, + type: 'User', + relationName: 'PostToUser', + relationFromFields: ['authorId'], + relationToFields: ['id'], + isGenerated: false, + isUpdatedAt: false, + }, + { + name: 'authorId', + kind: 'scalar', + isList: false, + isRequired: true, + isUnique: false, + isId: false, + isReadOnly: true, + hasDefaultValue: false, + type: 'Int', + isGenerated: false, + isUpdatedAt: false, + }, + { + name: 'comments', + kind: 'object', + isList: true, + isRequired: true, + isUnique: false, + isId: false, + isReadOnly: false, + hasDefaultValue: false, + type: 'Comment', + relationName: 'CommentToPost', + relationFromFields: [], + relationToFields: [], + isGenerated: false, + isUpdatedAt: false, + }, + ], + primaryKey: null, + uniqueFields: [], + uniqueIndexes: [], + isGenerated: false, +} as DMMF.Model; + +const commentModel = { + name: 'Comment', + dbName: null, + fields: [ + { + name: 'id', + kind: 'scalar', + isList: false, + isRequired: true, + isUnique: false, + isId: true, + isReadOnly: false, + hasDefaultValue: true, + type: 'Int', + default: { + name: 'autoincrement', + args: [], + }, + isGenerated: false, + isUpdatedAt: false, + }, + { + name: 'posts', + kind: 'object', + isList: true, + isRequired: true, + isUnique: false, + isId: false, + isReadOnly: false, + hasDefaultValue: false, + type: 'Post', + relationName: 'CommentToPost', + relationFromFields: [], + relationToFields: [], + isGenerated: false, + isUpdatedAt: false, + }, + ], + primaryKey: null, + uniqueFields: [], + uniqueIndexes: [], + isGenerated: false, +} as DMMF.Model; + +describe('RelationshipStore', () => { + describe('constructor', () => { + it('populates the internal relationships', () => { + const store = new RelationshipStore([userModel, postModel, commentModel]); + expect(store.getRelationships()).toMatchObject({ + Connections: { + name: 'Connections', + values: [], + a: { name: 'connections', type: 'User' }, + b: { name: 'symmetricalConnections', type: 'User' }, + }, + CommentToPost: { + name: 'CommentToPost', + values: [], + a: { name: 'comments', type: 'Comment' }, + b: { name: 'posts', type: 'Post' }, + }, + }); + }); + }); + + describe('connectToRelationship', () => { + it('does nothing if no relationship is found', () => { + const store = new RelationshipStore([userModel, postModel, commentModel]); + store.connectToRelationship({ relationshipName: 'NonExisting', fieldName: 'irrelevant', id: 1, values: { id: 2 } }); + Object.values(store.getRelationships()).forEach(({ values }) => expect(values).toEqual([])); + }); + + it('stores the given connection values', () => { + const store = new RelationshipStore([userModel, postModel, commentModel]); + store.connectToRelationship({ relationshipName: 'CommentToPost', fieldName: 'comments', id: 1, values: { id: 2 } }); + expect(store.findRelationship('CommentToPost')?.values).toEqual([{ a: 2, b: 1 }]); + }); + + it('stores the given connection values idempotently', () => { + const store = new RelationshipStore([userModel, postModel, commentModel]); + store.connectToRelationship({ relationshipName: 'CommentToPost', fieldName: 'comments', id: 1, values: { id: 2 } }); + store.connectToRelationship({ relationshipName: 'CommentToPost', fieldName: 'comments', id: 1, values: { id: 2 } }); + expect(store.findRelationship('CommentToPost')?.values).toEqual([{ a: 2, b: 1 }]); + }); + + it('stores the given array of connection values', () => { + const store = new RelationshipStore([userModel, postModel, commentModel]); + store.connectToRelationship({ + relationshipName: 'CommentToPost', + fieldName: 'comments', + id: 2, + values: [{ id: 3 }, { id: 4 }], + }); + expect(store.findRelationship('CommentToPost')?.values).toEqual([ + { a: 3, b: 2 }, + { a: 4, b: 2 }, + ]); + }); + + it('stores the given connection for a symmetrical relationship', () => { + const store = new RelationshipStore([userModel, postModel, commentModel]); + store.connectToRelationship({ + relationshipName: 'Connections', + fieldName: 'connections', + id: 1, + values: { id: 2 }, + }); + expect(store.findRelationship('Connections')?.values).toEqual([{ a: 2, b: 1 }]); + }); + }); + + describe('disconnectFromRelationship', () => { + it('does nothing if no relationship is found', () => { + const store = new RelationshipStore([userModel, postModel, commentModel]); + store.disconnectFromRelationship({ + relationshipName: 'NonExisting', + fieldName: 'irrelevant', + id: 1, + values: { id: 2 }, + }); + Object.values(store.getRelationships()).forEach(({ values }) => expect(values).toEqual([])); + }); + + it('removes the entry from the corresponding relationship', () => { + const store = new RelationshipStore([userModel, postModel, commentModel]); + store.connectToRelationship({ relationshipName: 'CommentToPost', fieldName: 'comments', id: 1, values: { id: 2 } }); + store.connectToRelationship({ relationshipName: 'CommentToPost', fieldName: 'comments', id: 1, values: { id: 3 } }); + + store.disconnectFromRelationship({ + relationshipName: 'CommentToPost', + fieldName: 'comments', + id: 1, + values: { id: 3 }, + }); + + expect(store.findRelationship('CommentToPost')?.values).toEqual([{ a: 2, b: 1 }]); + }); + + it('stores the given array of connection values', () => { + const store = new RelationshipStore([userModel, postModel, commentModel]); + store.connectToRelationship({ relationshipName: 'CommentToPost', fieldName: 'comments', id: 1, values: { id: 2 } }); + store.connectToRelationship({ relationshipName: 'CommentToPost', fieldName: 'comments', id: 1, values: { id: 3 } }); + store.connectToRelationship({ relationshipName: 'CommentToPost', fieldName: 'comments', id: 1, values: { id: 4 } }); + + store.disconnectFromRelationship({ + relationshipName: 'CommentToPost', + fieldName: 'comments', + id: 1, + values: [{ id: 3 }, { id: 2 }], + }); + + expect(store.findRelationship('CommentToPost')?.values).toEqual([{ a: 4, b: 1 }]); + }); + + it('removes the entry from the corresponding symmetrical relationship', () => { + const store = new RelationshipStore([userModel, postModel, commentModel]); + store.connectToRelationship({ + relationshipName: 'Connections', + fieldName: 'connections', + id: 1, + values: { id: 2 }, + }); + store.connectToRelationship({ + relationshipName: 'Connections', + fieldName: 'connections', + id: 2, + values: { id: 3 }, + }); + + store.disconnectFromRelationship({ + relationshipName: 'Connections', + fieldName: 'connections', + id: 2, + values: { id: 3 }, + }); + + expect(store.findRelationship('Connections')?.values).toEqual([{ a: 2, b: 1 }]); + }); + }); + + describe('match - some', () => { + it('returns 0 if the relationship does not exist', () => { + const store = new RelationshipStore([userModel, postModel, commentModel]); + + const match = store.match({ + type: 'NonExisting', + name: 'irrelevant', + itemId: 1, + where: { + some: { + id: 1, + }, + } as FindWhereArgs, + }); + + expect(match).toBe(0); + }); + + it('returns -1 if the relationship exists but the entry does not exist', () => { + const store = new RelationshipStore([userModel, postModel, commentModel]); + + store.connectToRelationship({ + relationshipName: 'CommentToPost', + fieldName: 'comments', + id: 1, + values: { id: 2 }, + }); + + const match = store.match({ + type: 'Comment', + name: 'posts', + itemId: 69, + where: { + some: { + id: 69, + }, + } as FindWhereArgs, + }); + + expect(match).toBe(-1); + }); + + it('returns 1 if the relationship exists and the entry exists', () => { + const store = new RelationshipStore([userModel, postModel, commentModel]); + + store.connectToRelationship({ relationshipName: 'CommentToPost', fieldName: 'comments', id: 1, values: { id: 2 } }); + + const match = store.match({ + type: 'Comment', + name: 'posts', + itemId: 2, + where: { + some: { + id: 1, + }, + } as FindWhereArgs, + }); + + expect(match).toBe(1); + }); + }); + + describe('match - none', () => { + it('returns 0 if the relationship does not exist', () => { + const store = new RelationshipStore([userModel, postModel, commentModel]); + + const match = store.match({ + type: 'NonExisting', + name: 'irrelevant', + itemId: 1, + where: { + none: { + id: 1, + }, + } as FindWhereArgs, + }); + + expect(match).toBe(0); + }); + + it('returns 1 if the relationship exists and has no entries', () => { + const store = new RelationshipStore([userModel, postModel, commentModel]); + + const match = store.match({ + type: 'Comment', + name: 'posts', + itemId: 1, + where: { + none: { + id: 2, + }, + } as FindWhereArgs, + }); + + expect(match).toBe(1); + }); + + it('returns 1 if the relationship exists and the entry does not exist', () => { + const store = new RelationshipStore([userModel, postModel, commentModel]); + + store.connectToRelationship({ + relationshipName: 'CommentToPost', + fieldName: 'comments', + id: 2, + values: { id: 2 }, + }); + + const match = store.match({ + type: 'Comment', + name: 'posts', + itemId: 2, + where: { + none: { + id: 1, + }, + } as FindWhereArgs, + }); + + expect(match).toBe(1); + }); + + it('returns -1 if the relationship exists and the entry exists', () => { + const store = new RelationshipStore([userModel, postModel, commentModel]); + + store.connectToRelationship({ relationshipName: 'CommentToPost', fieldName: 'comments', id: 1, values: { id: 2 } }); + + const match = store.match({ + type: 'Comment', + name: 'posts', + itemId: 2, + where: { + none: { + id: 1, + }, + } as FindWhereArgs, + }); + + expect(match).toBe(-1); + }); + + describe('match - another case', () => { + it('returns 0', () => { + const store = new RelationshipStore([userModel, postModel, commentModel]); + + store.connectToRelationship({ + relationshipName: 'CommentToPost', + fieldName: 'comments', + id: 1, + values: { id: 2 }, + }); + + const match = store.match({ + type: 'Comment', + name: 'posts', + itemId: 2, + where: { + id: 1, + } as FindWhereArgs, + }); + + expect(match).toBe(-1); + }); + }); + }); + + describe('getRelationshipIds', () => { + it('does nothing if no relationship is found', () => { + const store = new RelationshipStore([userModel, postModel, commentModel]); + const ids = store.getRelationshipIds('NonEsisting', 'irrelevant', 1); + expect(ids).toEqual([]); + }); + + it('returns the ids for the given params', () => { + const store = new RelationshipStore([userModel, postModel, commentModel]); + store.connectToRelationship({ relationshipName: 'CommentToPost', fieldName: 'comments', id: 1, values: { id: 1 } }); + store.connectToRelationship({ relationshipName: 'CommentToPost', fieldName: 'comments', id: 2, values: { id: 1 } }); + store.connectToRelationship({ relationshipName: 'CommentToPost', fieldName: 'comments', id: 3, values: { id: 3 } }); + + expect(store.getRelationshipIds('CommentToPost', 'Comment', 1)).toEqual([1]); + expect(store.getRelationshipIds('CommentToPost', 'Post', 1)).toEqual([1, 2]); + }); + + it('returns the ids for the given params of a symmetrical relationship', () => { + const store = new RelationshipStore([userModel, postModel, commentModel]); + store.connectToRelationship({ relationshipName: 'Connections', fieldName: 'connections', id: 1, values: { id: 2 } }); + + expect(store.getRelationshipIds('Connections', 'connections', 1)).toEqual([2]); + expect(store.getRelationshipIds('Connections', 'connections', 2)).toEqual([1]); + }); + + it('returns the ids for the given array of ids of a symmetrical relationship', () => { + const store = new RelationshipStore([userModel, postModel, commentModel]); + store.connectToRelationship({ relationshipName: 'Connections', fieldName: 'connections', id: 1, values: { id: 2 } }); + store.connectToRelationship({ relationshipName: 'Connections', fieldName: 'connections', id: 2, values: { id: 3 } }); + store.connectToRelationship({ relationshipName: 'Connections', fieldName: 'connections', id: 3, values: { id: 4 } }); + + expect(store.getRelationshipIds('Connections', 'connections', { in: [1, 2] } as FindWhereArgs)).toEqual([2, 3]); + }); + }); + + describe('cleanupRelationships', () => { + it('cleans the relationships values for the given type and id', () => { + { + const store = new RelationshipStore([userModel, postModel, commentModel]); + store.connectToRelationship({ relationshipName: 'CommentToPost', fieldName: 'comments', id: 1, values: { id: 2 } }); + store.connectToRelationship({ relationshipName: 'CommentToPost', fieldName: 'comments', id: 2, values: { id: 2 } }); + store.connectToRelationship({ relationshipName: 'CommentToPost', fieldName: 'comments', id: 3, values: { id: 1 } }); + + store.cleanupRelationships({ type: 'Post', id: 2 }); + expect(store.findRelationship('CommentToPost').values).toEqual([{ a: 1, b: 3 }]); + } + { + const store = new RelationshipStore([userModel, postModel, commentModel]); + store.connectToRelationship({ relationshipName: 'CommentToPost', fieldName: 'comments', id: 1, values: { id: 2 } }); + store.connectToRelationship({ relationshipName: 'CommentToPost', fieldName: 'comments', id: 2, values: { id: 2 } }); + + store.cleanupRelationships({ type: 'Comment', id: 1 }); + expect(store.findRelationship('CommentToPost').values).toEqual([{ a: 2, b: 2 }]); + } + }); + + it('cleans the symmetrical relationships values for the given type and id', () => { + const store = new RelationshipStore([userModel, postModel, commentModel]); + store.connectToRelationship({ + relationshipName: 'Connections', + fieldName: 'connection', + id: 1, + values: { id: 2 }, + }); + store.connectToRelationship({ + relationshipName: 'Connections', + fieldName: 'User', + id: 1, + values: { id: 3 }, + }); + store.connectToRelationship({ + relationshipName: 'Connections', + fieldName: 'connection', + id: 2, + values: { id: 3 }, + }); + + store.cleanupRelationships({ type: 'User', id: 1 }); + expect(store.findRelationship('Connections').values).toEqual([{ a: 2, b: 3 }]); + }); + }); + + describe('resetValues', () => { + it('resets the internal relationships values', () => { + const store = new RelationshipStore([userModel, postModel, commentModel]); + store.connectToRelationship({ relationshipName: 'CommentToPost', fieldName: 'comments', id: 1, values: { id: 2 } }); + store.connectToRelationship({ relationshipName: 'Connections', fieldName: 'connections', id: 1, values: { id: 2 } }); + store.resetValues(); + Object.values(store.getRelationships()).forEach(({ values }) => expect(values).toEqual([])); + }); + }); +}); diff --git a/src/__tests__/symmetrical-relationships.ts b/src/__tests__/symmetrical-relationships.ts new file mode 100644 index 00000000..90c06a35 --- /dev/null +++ b/src/__tests__/symmetrical-relationships.ts @@ -0,0 +1,119 @@ +import { PrismaClient, User } from '@prisma/client'; + +import { resetDb, simulateSeed, seededUsers } from '../../testing'; +import { PrismockClient, PrismockClientType, relationshipStore } from '../lib/client'; + +jest.setTimeout(40000); + +describe('Symmetrical relationships', () => { + let prismock: PrismockClientType; + let prisma: PrismaClient; + + beforeEach(() => relationshipStore.resetValues()); + + beforeAll(async () => { + await resetDb(); + + prisma = new PrismaClient(); + prismock = new PrismockClient() as PrismockClientType; + await simulateSeed(prismock); + }); + + async function addConnection(client: PrismaClient, firstId: number, secondId: number) { + await client.user.update({ + where: { id: firstId }, + data: { + connections: { + connect: [{ id: secondId }], + }, + }, + }); + await client.user.update({ + where: { id: secondId }, + data: { + connections: { + connect: [{ id: firstId }], + }, + }, + }); + } + + async function removeConnection(client: PrismaClient, firstId: number, secondId: number) { + await client.user.update({ + where: { id: firstId }, + data: { + connections: { + disconnect: [{ id: secondId }], + }, + }, + }); + await client.user.update({ + where: { id: secondId }, + data: { + connections: { + disconnect: [{ id: firstId }], + }, + }, + }); + } + + it('Should connect entities', async () => { + await addConnection(prisma, 1, 2); + await addConnection(prismock, 1, 2); + + await Promise.all( + seededUsers.map(async ({ id }) => { + const realUser = await prisma.user.findFirst({ where: { id }, include: { connections: true } }); + const fakeUser = await prismock.user.findFirst({ where: { id }, include: { connections: true } }); + expect(realUser).toMatchObject(fakeUser as User); + }), + ); + }); + + it('Should disconnect many to many relationship', async () => { + await addConnection(prisma, 1, 2); + await addConnection(prismock, 1, 2); + await addConnection(prisma, 1, 3); + await addConnection(prismock, 1, 3); + + await Promise.all( + seededUsers.map(async ({ id }) => { + const realUser = await prisma.user.findFirst({ where: { id }, include: { connections: true } }); + const fakeUser = await prismock.user.findFirst({ where: { id }, include: { connections: true } }); + expect(realUser).toMatchObject(fakeUser as User); + }), + ); + + await removeConnection(prisma, 1, 3); + await removeConnection(prismock, 1, 3); + + await Promise.all( + seededUsers.map(async ({ id }) => { + const realUser = await prisma.user.findFirst({ where: { id }, include: { connections: true } }); + const fakeUser = await prismock.user.findFirst({ where: { id }, include: { connections: true } }); + expect(realUser).toMatchObject(fakeUser as User); + }), + ); + }); + + it('Should reset symmetrical many to many relationships', async () => { + await addConnection(prisma, 1, 2); + await addConnection(prismock, 1, 2); + + await prisma.post.deleteMany(); + + await prisma.user.delete({ where: { id: 2 } }); + await prismock.user.delete({ where: { id: 2 } }); + + await prisma.user.create({ data: seededUsers[1] }); + await prismock.user.create({ data: seededUsers[1] }); + + await Promise.all( + seededUsers.map(async ({ id }) => { + const realUser = await prisma.user.findFirst({ where: { id }, include: { connections: true } }); + const fakeUser = await prismock.user.findFirst({ where: { id }, include: { connections: true } }); + expect(realUser).toMatchObject(fakeUser as User); + }), + ); + }); +}); diff --git a/src/__tests__/update/update-connect.test.ts b/src/__tests__/update/update-connect.test.ts index 793d09d1..d2c0ab6b 100755 --- a/src/__tests__/update/update-connect.test.ts +++ b/src/__tests__/update/update-connect.test.ts @@ -1,7 +1,9 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +// @ts-nocheck import { PrismaClient, User } from '@prisma/client'; import { resetDb, simulateSeed, seededPosts, seededUsers, formatEntries, formatEntry } from '../../../testing'; -import { PrismockClient, PrismockClientType } from '../../lib/client'; +import { PrismockClient, PrismockClientType, relationshipStore } from '../../lib/client'; jest.setTimeout(40000); @@ -15,6 +17,8 @@ describe('update (connect)', () => { let realAuthor: User; let mockAuthor: User; + beforeEach(() => relationshipStore.resetValues()); + beforeAll(async () => { await resetDb(); @@ -53,4 +57,84 @@ describe('update (connect)', () => { formatEntries(seededPosts.map(({ createdAt, imprint, blogId, ...post }) => ({ ...post, authorId: mockAuthor.id }))), ); }); + + it('Should connect many to many relationships', async () => { + const updatePayload = { + where: { id: 1 }, + data: { + comments: { + connect: [{ id: 1 }, { id: 2 }], + }, + }, + }; + + const findCommentsPayload = { + where: { + posts: { + some: { + id: 1, + }, + }, + }, + include: { posts: true }, + }; + await prisma.post.update(updatePayload); + await prismock.post.update(updatePayload); + + const findPostPayload = { where: { id: 1 }, include: { comments: true } }; + + const updatedPost = await prisma.post.findFirst(findPostPayload); + const updatedMockedPost = await prismock.post.findFirst(findPostPayload); + + const updatedComments = await prisma.comment.findMany(findCommentsPayload); + const updatedMockedComments = await prismock.comment.findMany(findCommentsPayload); + + expect(updatedPost).toMatchObject(updatedMockedPost); + expect(updatedComments).toMatchObject(updatedMockedComments); + }); + + it('Should disconnect many to many relationships', async () => { + const connectPayload = { + where: { id: 1 }, + data: { + comments: { + connect: [{ id: 1 }, { id: 2 }], + }, + }, + }; + const disconnectPayload = { + where: { id: 1 }, + data: { + comments: { + disconnect: [{ id: 2 }], + }, + }, + }; + const findPostPayload = { where: { id: 1 }, include: { comments: true } }; + const findCommentsPayload = { + where: { + posts: { + some: { + id: 1, + }, + }, + }, + include: { posts: true }, + }; + + await prisma.post.update(connectPayload); + await prismock.post.update(connectPayload); + + await prisma.post.update(disconnectPayload); + await prismock.post.update(disconnectPayload); + + const updatedPost = await prisma.post.findFirst(findPostPayload); + const updatedMockedPost = await prismock.post.findFirst(findPostPayload); + + const updatedComments = await prisma.comment.findMany(findCommentsPayload); + const updatedMockedComments = await prismock.comment.findMany(findCommentsPayload); + + expect(updatedPost).toMatchObject(updatedMockedPost); + expect(updatedComments).toMatchObject(updatedMockedComments); + }); }); diff --git a/src/lib/client.ts b/src/lib/client.ts index c99c9856..4ede8650 100755 --- a/src/lib/client.ts +++ b/src/lib/client.ts @@ -4,6 +4,7 @@ import { DMMF } from '@prisma/generator-helper'; import { Delegate } from './delegate'; import { Data, Delegates, generateDelegates } from './prismock'; +import { RelationshipStore } from './relationship-store'; type GetData = () => Data; type SetData = (data: Data) => void; @@ -53,6 +54,8 @@ type PrismaModule = { dmmf: runtime.BaseDMMF; }; +export let relationshipStore: RelationshipStore; + export function createPrismock(instance: PrismaModule) { return class Prismock { constructor() { @@ -64,8 +67,8 @@ export function createPrismock(instance: PrismaModule) { } private generate() { + relationshipStore = new RelationshipStore(instance.dmmf.datamodel.models as DMMF.Model[]); const { delegates, setData, getData } = generateDelegates({ models: instance.dmmf.datamodel.models as DMMF.Model[] }); - Object.entries({ ...delegates, setData, getData }).forEach(([key, value]) => { if (key in this) Object.assign((this as unknown as Delegates)[key], value); else Object.assign(this, { [key]: value }); diff --git a/src/lib/operations/create.ts b/src/lib/operations/create.ts index 2762bc34..bf3a1ca5 100755 --- a/src/lib/operations/create.ts +++ b/src/lib/operations/create.ts @@ -7,6 +7,7 @@ import { Delegate, DelegateProperties, Item } from '../delegate'; import { pipe, removeUndefined, uuid } from '../helpers'; import { Delegates } from '../prismock'; import { ConnectOrCreate, CreateArgs, FindWhereArgs } from '../types'; +import { relationshipStore } from '../client'; import { findNextIncrement, @@ -112,12 +113,20 @@ export function connectOrCreate(delegate: Delegate, delegates: Delegates) { if (typeof value === 'object' && (value as Record)?.connect) { const connect = (value as Record).connect as FindWhereArgs; - const field = delegate.model.fields.find((field) => field.name === key); const joinField = getJoinField(field!, delegates); const subDelegate = getDelegateFromField(field!, delegates); - - if (Array.isArray(connect)) { + const relationshipName = field?.relationName as string; + const relationship = relationshipStore.findRelationship(relationshipName); + + if (relationship) { + relationshipStore.connectToRelationship({ + relationshipName, + fieldName: field?.name as string, + id: item.id as number, + values: connect as unknown as { id: number }[], + }); + } else if (Array.isArray(connect)) { connect.forEach((c) => { subDelegate.update({ where: c, @@ -170,8 +179,8 @@ export function nestedCreate(current: Delegate, delegates: Delegates) { if (joinfield) { const delegate = getDelegateFromField(field, delegates); - const connect = getFieldFromRelationshipWhere(created, joinfield); + const connect = getFieldFromRelationshipWhere(created, joinfield); if ((value as { create: Item }).create) { delete created[field.name]; @@ -211,10 +220,10 @@ export function create( delegates: Delegates, onChange: (items: Item[]) => void, ) { - const formated = pipe(nestedCreate(delegate, delegates), connectOrCreate(delegate, delegates))(item); - const created = pipe(includes(options, delegate, delegates), select(options.select))(formated); + const formatted = pipe(nestedCreate(delegate, delegates), connectOrCreate(delegate, delegates))(item); + const created = pipe(includes(options, delegate, delegates), select(options.select))(formatted); - onChange([...delegate.getItems(), formated]); + onChange([...delegate.getItems(), formatted]); return created; } diff --git a/src/lib/operations/delete.ts b/src/lib/operations/delete.ts index 8639bbc5..13a53732 100755 --- a/src/lib/operations/delete.ts +++ b/src/lib/operations/delete.ts @@ -2,6 +2,7 @@ import { Delegates } from '../prismock'; import { Item, Delegate } from '../delegate'; import { FindWhereArgs, SelectArgs } from '../types'; import { pipe } from '../helpers'; +import { relationshipStore } from '../client'; import { getDelegateFromField, getFieldFromRelationshipWhere, getJoinField, includes, select, where } from './find'; @@ -41,21 +42,23 @@ export function deleteMany(args: DeleteArgs, current: Delegate, delegates: Deleg toDelete.forEach((item: Item) => { current.model.fields.forEach((field) => { - const joinfield = getJoinField(field, delegates); - if (!joinfield) return; + relationshipStore.cleanupRelationships({ type: field.type, id: (args?.where as { id: number })?.id }); + + const joinField = getJoinField(field, delegates); + if (!joinField) return; const delegate = getDelegateFromField(field, delegates); - if (joinfield.relationOnDelete === 'SetNull') { + if (joinField.relationOnDelete === 'SetNull') { delegate.updateMany({ - where: getFieldFromRelationshipWhere(item, joinfield), + where: getFieldFromRelationshipWhere(item, joinField), data: { - [joinfield.relationFromFields![0]]: null, + [joinField.relationFromFields![0]]: null, }, }); - } else if (joinfield.relationOnDelete === 'Cascade') { + } else if (joinField.relationOnDelete === 'Cascade') { delegate.deleteMany({ - where: getFieldFromRelationshipWhere(item, joinfield), + where: getFieldFromRelationshipWhere(item, joinField), }); } }); diff --git a/src/lib/operations/find/find.ts b/src/lib/operations/find/find.ts index 3ecde1e5..13e3c1cb 100755 --- a/src/lib/operations/find/find.ts +++ b/src/lib/operations/find/find.ts @@ -1,9 +1,10 @@ import { DMMF } from '@prisma/generator-helper'; -import { FindArgs, GroupByFieldArg, Order, OrderedValue } from '../../types'; +import { FindArgs, FindWhereArgs, GroupByFieldArg, Order, OrderedValue } from '../../types'; import { Delegate, DelegateProperties, Item } from '../../delegate'; import { camelize, pipe } from '../../helpers'; import { Delegates } from '../../prismock'; +import { relationshipStore } from '../../client'; import { matchMultiple } from './match'; @@ -30,7 +31,10 @@ export function findOne(args: FindArgs, current: Delegate, delegates: Delegates) } export function where(whereArgs: FindArgs['where'] = {}, current: Delegate, delegates: Delegates) { - return (item: Record) => matchMultiple(item, whereArgs, current, delegates); + return (item: Record) => { + const res = matchMultiple(item, whereArgs, current, delegates); + return res; + }; } function getOrderedValue(orderedValue: OrderedValue) { @@ -175,13 +179,25 @@ export function includes(args: FindArgs, current: Delegate, delegates: Delegates let subArgs = obj[key] === true ? {} : obj[key]; - subArgs = Object.assign(Object.assign({}, subArgs), { - where: Object.assign( - Object.assign({}, (subArgs as any).where), - getFieldRelationshipWhere(item, schema, delegates), - ), - }); - + const relation = relationshipStore.findRelationship(schema.relationName); + if (relation) { + subArgs = Object.assign(Object.assign({}, subArgs), { + where: Object.assign(Object.assign({}, (subArgs as any).where), { + id: { + in: relationshipStore.getRelationshipIds( + schema.relationName, + schema.type, + (args.where?.id as FindWhereArgs) || item.id, + ), + }, + }), + }); + } else { + const fieldRelationshipWhere = getFieldRelationshipWhere(item, schema, delegates); + subArgs = Object.assign(Object.assign({}, subArgs), { + where: Object.assign(Object.assign({}, (subArgs as any).where), fieldRelationshipWhere), + }); + } if (schema.isList) { Object.assign(newItem, { [key]: findMany(subArgs as Record, delegate, delegates) }); } else { @@ -209,11 +225,9 @@ export const getJoinField = (field: DMMF.Field, delegates: Delegates) => { const joinDelegate = Object.values(delegates).find((delegate) => { return delegate.model.name === field.type; }); - const joinfield = joinDelegate?.model.fields.find((f) => { return f.relationName === field.relationName; }); - return joinfield; }; @@ -228,9 +242,9 @@ export const getFieldRelationshipWhere = ( delegates: Delegates, ): Record => { if (field.relationToFields?.length === 0) { - field = getJoinField(field, delegates)!; + const joinField = getJoinField(field, delegates)!; return { - [field.relationFromFields![0]]: item[field.relationToFields![0]] as GroupByFieldArg, + [joinField.relationFromFields![0]]: item[joinField.relationToFields![0]] as GroupByFieldArg, }; } return { diff --git a/src/lib/operations/find/match.ts b/src/lib/operations/find/match.ts index 336f4fff..a6fdac67 100755 --- a/src/lib/operations/find/match.ts +++ b/src/lib/operations/find/match.ts @@ -5,6 +5,7 @@ import { Delegate, Item } from '../../delegate'; import { camelize, shallowCompare } from '../../helpers'; import { Delegates } from '../../prismock'; import { FindWhereArgs } from '../../types'; +import { relationshipStore } from '../../client'; import { getFieldRelationshipWhere } from './find'; @@ -43,7 +44,16 @@ export const matchMultiple = (item: Item, where: FindWhereArgs, current: Delegat function match(child: string, item: Item, where: FindWhereArgs) { let val: any = item[child]; const filter = where[child] as Prisma.Enumerable; - + const relationMatch = relationshipStore.match({ + type: current.model.name, + name: child, + itemId: item.id as number, + where: where[child] as FindWhereArgs, + }); + + if (relationMatch) { + return relationMatch > 0; + } if (child === 'OR') return matchOr(item, filter as FindWhereArgs[]); if (child === 'AND') return matchAnd(item, filter as FindWhereArgs[]); if (child === 'NOT') return !matchOr(item, filter instanceof Array ? filter : [filter]); diff --git a/src/lib/operations/update.ts b/src/lib/operations/update.ts index db8289c8..a7d4c99a 100755 --- a/src/lib/operations/update.ts +++ b/src/lib/operations/update.ts @@ -2,6 +2,7 @@ import { Delegate, Item } from '../delegate'; import { camelize, pipe, removeUndefined } from '../helpers'; import { Delegates } from '../prismock'; import { FindWhereArgs, SelectArgs, UpsertArgs } from '../types'; +import { relationshipStore } from '../client'; import { calculateDefaultFieldValue, connectOrCreate, create } from './create'; import { @@ -29,27 +30,53 @@ export type UpdateMap = { const update = (args: UpdateArgs, isCreating: boolean, item: Item, current: Delegate, delegates: Delegates) => { const { data }: any = args; - current.model.fields.forEach((field) => { if (data[field.name]) { const fieldData = data[field.name]; if (field.kind === 'object') { + if (fieldData.disconnect) { + const disconnected = data[field.name]; + delete data[field.name]; + const relationshipName = field?.relationName as string; + const relationship = relationshipStore.findRelationship(relationshipName); + if (relationship) { + relationshipStore.disconnectFromRelationship({ + relationshipName, + fieldName: field.name, + id: args.where.id as number, + values: disconnected.disconnect, + }); + } + } if (fieldData.connect) { const connected = data[field.name]; delete data[field.name]; const delegate = delegates[camelize(field.type)]; - const joinfield = getJoinField(field, delegates)!; - const joinValue = connected.connect[joinfield.relationToFields![0]]; - - // @TODO: what's happening if we try to udate on an Item that doesn't exist? - if (!joinfield.isList) { - const joined = findOne({ where: args.where }, getDelegateFromField(joinfield, delegates), delegates) as Item; - + const joinField = getJoinField(field, delegates)!; + const relationToField = joinField.relationToFields![0]; + + const joinValue = Array.isArray(fieldData.connect) + ? { in: fieldData.connect.map((x: any) => x[relationToField]) } + : fieldData.connect[relationToField]; + + const relationshipName = field?.relationName as string; + const relationship = relationshipStore.findRelationship(relationshipName); + + if (relationship) { + relationshipStore.connectToRelationship({ + relationshipName, + fieldName: field.name, + id: args.where.id as number, + values: fieldData.connect, + }); + } else if (!joinField.isList) { + // @TODO: what's happening if we try to update on an Item that doesn't exist? + const joined = findOne({ where: args.where }, getDelegateFromField(joinField, delegates), delegates) as Item; delegate.updateMany({ - where: { [joinfield.relationToFields![0]]: joinValue }, - data: getFieldFromRelationshipWhere(joined, joinfield), + where: { [relationToField]: joinValue }, + data: getFieldFromRelationshipWhere(joined, joinField), }); } else { const joined = findOne({ where: connected.connect }, getDelegateFromField(field, delegates), delegates) as Item; @@ -70,7 +97,7 @@ const update = (args: UpdateArgs, isCreating: boolean, item: Item, current: Dele delete data[field.name]; const delegate = getDelegateFromField(field, delegates); - const joinfield = getJoinField(field, delegates)!; + const joinField = getJoinField(field, delegates)!; if (field.relationFromFields?.[0]) { delegate.create(data[field.name].create); @@ -79,11 +106,11 @@ const update = (args: UpdateArgs, isCreating: boolean, item: Item, current: Dele const formatCreatedItem = (val: Item) => { return { ...val, - [joinfield.name]: { - connect: joinfield.relationToFields!.reduce((prev, cur) => { + [joinField.name]: { + connect: joinField.relationToFields!.reduce((prev, cur) => { let val = data[cur]; if (!isCreating && !val) { - val = findOne(args, delegates[camelize(joinfield.type)], delegates)?.[cur]; + val = findOne(args, delegates[camelize(joinField.type)], delegates)?.[cur]; } return { ...prev, [cur]: val }; }, {}), @@ -101,10 +128,10 @@ const update = (args: UpdateArgs, isCreating: boolean, item: Item, current: Dele .forEach((createSingle: Item) => delegate.create({ data: createSingle })); } else { const createData = { ...toCreate.create }; - const mapped = formatCreatedItem(toCreate.create)[joinfield.name].connect as Item; + const mapped = formatCreatedItem(toCreate.create)[joinField.name].connect as Item; - if (joinfield) { - Object.assign(createData, getFieldFromRelationshipWhere(mapped, joinfield)); + if (joinField) { + Object.assign(createData, getFieldFromRelationshipWhere(mapped, joinField)); } delegate.create({ data: createData }); @@ -113,11 +140,11 @@ const update = (args: UpdateArgs, isCreating: boolean, item: Item, current: Dele } } if (fieldData.update || fieldData.updateMany) { - const joinfield = getJoinField(field, delegates); + const joinField = getJoinField(field, delegates); const where = {}; - if (joinfield) { - Object.assign(where, getFieldFromRelationshipWhere(args.where, joinfield)); + if (joinField) { + Object.assign(where, getFieldFromRelationshipWhere(args.where, joinField)); } delete data[field.name]; @@ -134,7 +161,7 @@ const update = (args: UpdateArgs, isCreating: boolean, item: Item, current: Dele delegate.updateMany({ where, data: fieldData.updateMany.data ?? fieldData.updateMany }); } } else { - const joinfield = getJoinField(field, delegates)!; + const joinField = getJoinField(field, delegates)!; Object.assign(where, fieldData.update.where); if (Array.isArray(fieldData.update)) { @@ -142,7 +169,7 @@ const update = (args: UpdateArgs, isCreating: boolean, item: Item, current: Dele delegate.updateMany({ where, data: toUpdate.data ?? toUpdate }); }); } else { - const item = findOne(args, delegates[camelize(joinfield.type)], delegates)!; + const item = findOne(args, delegates[camelize(joinField.type)], delegates)!; delegate.updateMany({ where: getFieldRelationshipWhere(item, field, delegates), diff --git a/src/lib/relationship-store.ts b/src/lib/relationship-store.ts new file mode 100644 index 00000000..c86ca5cc --- /dev/null +++ b/src/lib/relationship-store.ts @@ -0,0 +1,239 @@ +import type { DMMF } from '@prisma/generator-helper'; + +import type { FindWhereArgs } from './types'; + +type RelationshipEntry = { a: number; b: number }; + +type Relationship = { + name: string; + a: { name: string; type: string }; + b: { name: string; type: string }; + values: RelationshipEntry[]; +}; + +type RelationActionParams = { + relationshipName: string; + fieldName: string; + id: number; + values: { id: number } | { id: number }[]; +}; + +type MatchParams = { + type: string; + name: string; + itemId: number; + where: FindWhereArgs; +}; + +type CleanupRelationshipsParams = { + type: string; + id: number; +}; + +export class RelationshipStore { + private relationships: Record; + + constructor(models: DMMF.Model[]) { + this.relationships = {}; + this.populateRelationships(models); + } + + getRelationships() { + return this.relationships; + } + + findRelationship(name: string) { + return this.relationships[name]; + } + + findRelationshipBy(type: string, name: string) { + return Object.values(this.relationships).find( + ({ a, b }) => (a.type === type && b.name === name) || (b.type === type && a.name === name), + ); + } + + match({ type, name, itemId, where }: MatchParams) { + const relationship = this.findRelationshipBy(type, name); + if (!relationship) { + return 0; + } + + if (where.none && !relationship.values.length) { + return 1; + } + + const [valueField, targetField] = this.getRelationshipFieldNames(relationship, type); + const found = relationship.values.find((x) => { + if (where.some) { + return x[valueField] === itemId && x[targetField] === (where.some as { id: number }).id; + } + if (where.none) { + return x[valueField] !== itemId || x[targetField] !== (where.none as { id: number }).id; + } + return false; + }); + return !found ? -1 : 1; + } + + getRelationshipIds(name: string, type: string, id: FindWhereArgs | number) { + const relationship = this.findRelationship(name); + + if (!relationship) { + return []; + } + + if (this.isSymmetrical(relationship)) { + return this.extractSymmetricalValues(relationship, id); + } + + const [valueField, targetField] = this.getRelationshipFieldNames(relationship, type); + const values = relationship.values.filter((x) => x[targetField] === id).map((x) => x[valueField]); + return values; + } + + connectToRelationship({ relationshipName, fieldName, id, values }: RelationActionParams) { + const relationship = this.findRelationship(relationshipName); + + if (!relationship) { + return; + } + + if (!Array.isArray(values)) { + const value = this.getActionValue({ relationship, fieldName, id, value: values }); + relationship.values = relationship.values.find((x) => this.matchEntry(x, value)) + ? relationship.values + : [...relationship.values, value]; + return; + } + + relationship.values = [ + ...relationship.values, + ...values + .map((x) => this.getActionValue({ relationship, fieldName, id, value: x })) + .map((x) => (relationship.values.find((y) => this.matchEntry(x, y)) ? null : x)) + .filter((x) => !!x), + ]; + } + + disconnectFromRelationship({ relationshipName, fieldName, id, values }: RelationActionParams) { + const relationship = this.findRelationship(relationshipName); + + if (!relationship) { + return; + } + + if (!Array.isArray(values)) { + const value = this.getActionValue({ relationship, fieldName, id, value: values }); + relationship.values = relationship.values.filter((x) => !this.matchEntry(x, value)); + return; + } + + relationship.values = relationship.values.filter( + (x) => + !values + .map((x) => this.getActionValue({ relationship, fieldName, id, value: x })) + .find((y) => this.matchEntry(x, y)), + ); + } + + cleanupRelationships({ type, id }: CleanupRelationshipsParams) { + Object.values(this.getRelationships()) + .filter(({ a, b }) => a.type === type || b.type === type) + .forEach((relationship) => { + if (this.isSymmetrical(relationship)) { + relationship.values = relationship.values.filter(({ a, b }) => a !== id && b !== id); + return; + } + const [_, targetField] = this.getRelationshipFieldNames(relationship, type); + relationship.values = relationship.values.filter((value) => value[targetField] !== id); + }); + } + + resetValues() { + Object.values(this.relationships).forEach((x) => (x.values = [])); + } + + private getRelationshipFieldNames({ a }: Relationship, type: string): ['a', 'b'] | ['b', 'a'] { + return a.type === type ? ['a', 'b'] : ['b', 'a']; + } + + private matchEntry(x: RelationshipEntry, y: RelationshipEntry) { + return x.a === y.a && x.b === y.b; + } + + private isSymmetrical({ a, b }: Relationship) { + return a.type === b.type; + } + + private extractSymmetricalValues({ values }: Relationship, id: FindWhereArgs | number) { + if (typeof id === 'number') { + return values.filter(({ a, b }) => a === id || b === id).map(({ a, b }) => (a === id ? b : a)); + } + + return values + .map(({ a, b }) => ((id.in as number[]).some((id) => a === id || b === id) ? [id, { a, b }] : null)) + .filter((x) => !!x) + .map(([id, { a, b }]) => (a === id ? b : a)); + } + + private getActionValue({ + relationship, + fieldName, + id, + value, + }: { + relationship: Relationship; + fieldName: string; + id: number; + value: { id: number }; + }) { + if (relationship.a.name === fieldName) { + return { a: value.id, b: id }; + } + + return { a: id, b: value.id }; + } + + private populateRelationships(models: DMMF.Model[]) { + this.relationships = Object.entries( + this.groupBy( + models.flatMap((x) => x.fields), + (x) => x.relationName as string, + ), + ) + .filter( + ([key, fields]) => + key !== 'undefined' && + fields.every(({ relationFromFields: from, relationToFields: to }) => !from?.length && !to?.length), + ) + .map(([_, fields]) => fields.sort((a, b) => ((a?.name as string) > (b?.name as string) ? 1 : -1))) + .reduce( + (memo, [a, b]) => ({ + ...memo, + [a?.relationName as string]: { + name: a?.relationName as string, + values: [], + a: { + name: a?.name as string, + type: a?.type as string, + }, + b: { + name: b?.name as string, + type: b?.type as string, + }, + }, + }), + {}, + ); + } + + private groupBy(array: T[], aggregator: (item: T) => string): Record { + return array.reduce((memo, item) => { + const key = aggregator(item); + return { + ...memo, + [key]: memo[key] ? [...memo[key], item] : [item], + }; + }, {} as Record); + } +} diff --git a/testing/index.ts b/testing/index.ts index 27882855..ab798c22 100755 --- a/testing/index.ts +++ b/testing/index.ts @@ -1,8 +1,9 @@ import { exec } from 'child_process'; -import { Blog, Post, PrismaClient, Reaction, Role, Service, Subscription, User } from '@prisma/client'; +import { Blog, Comment, Post, PrismaClient, Reaction, Role, Service, Subscription, User } from '@prisma/client'; import dotenv from 'dotenv'; import { createId } from '@paralleldrive/cuid2'; +export type PostWithComments = Post & { comments: Comment[] }; dotenv.config(); @@ -15,6 +16,8 @@ export const seededReactions = [ buildReaction({ userId: 1, emoji: 'rocket' }), ]; +export const seededComments = [buildComment(1), buildComment(2), buildComment(3)]; + export async function simulateSeed(prisma: PrismaClient) { await prisma.user.createMany({ data: seededUsers.map(({ id, ...user }) => user) }); await prisma.blog.createMany({ data: seededBlogs.map(({ id, ...blog }) => blog) }); @@ -23,6 +26,7 @@ export async function simulateSeed(prisma: PrismaClient) { // @ts-ignore MySQL / Tags await prisma.service.createMany({ data: seededServices }); await prisma.reaction.createMany({ data: seededReactions }); + await prisma.comment.createMany({ data: seededComments }); } export async function resetDb() { @@ -61,6 +65,14 @@ export function buildPost(id: number, post: Partial & { authorId: number; }; } +export function buildComment(id: number, comment: Partial = {}) { + return { + id, + body: `comment${id}`, + ...comment, + }; +} + export function buildBlog(id: number, blog: Partial) { const { title = '', imprint = createId(), priority = 1, category = 'normal', userId } = blog; return {