From ceae1461a392b54a27f7a0aa5e0f5e8ada9c3192 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 19 May 2025 09:36:20 -0700 Subject: [PATCH 1/3] fix(delegate): update `@updatedAt` fields inherited from delegate bases automatically fixes #2101 --- .../runtime/src/enhancements/node/delegate.ts | 16 ++++++ .../with-delegate/enhanced-client.test.ts | 49 ++++++++++--------- .../tests/enhancements/with-delegate/utils.ts | 1 + 3 files changed, 44 insertions(+), 22 deletions(-) diff --git a/packages/runtime/src/enhancements/node/delegate.ts b/packages/runtime/src/enhancements/node/delegate.ts index 54c1abf96..f4f7ddc6c 100644 --- a/packages/runtime/src/enhancements/node/delegate.ts +++ b/packages/runtime/src/enhancements/node/delegate.ts @@ -10,6 +10,7 @@ import { NestedWriteVisitor, clone, enumerate, + getFields, getIdFields, getModelInfo, isDelegateModel, @@ -1053,6 +1054,21 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { delete data[field]; } } + + // if we're updating any field, we need to take care of updating `@updatedAt` + // fields inherited from delegate base models + if (Object.keys(data).length > 0) { + const modelFields = getFields(this.options.modelMeta, model); + for (const fieldInfo of Object.values(modelFields)) { + if ( + fieldInfo.attributes?.some((attr) => attr.name === '@updatedAt') && + fieldInfo.inheritedFrom && + isDelegateModel(this.options.modelMeta, fieldInfo.inheritedFrom) + ) { + this.injectBaseFieldData(model, fieldInfo, new Date(), data, 'update'); + } + } + } } // #endregion diff --git a/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts b/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts index 1f7a40129..d35fc02c6 100644 --- a/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts +++ b/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts @@ -476,6 +476,8 @@ describe('Polymorphism Test', () => { it('update simple', async () => { const { db, videoWithOwner: video } = await setup(); + const read = await db.ratedVideo.findUnique({ where: { id: video.id } }); + // update with concrete let updated = await db.ratedVideo.update({ where: { id: video.id }, @@ -484,6 +486,7 @@ describe('Polymorphism Test', () => { }); expect(updated.rating).toBe(200); expect(updated.owner).toBeTruthy(); + expect(updated.updatedAt.getTime()).toBeGreaterThan(read.updatedAt.getTime()); // update with base updated = await db.video.update({ @@ -613,17 +616,18 @@ describe('Polymorphism Test', () => { }); // updateMany with filter - await expect( - db.user.update({ - where: { id: user.id }, - data: { - ratedVideos: { updateMany: { where: { duration: 1 }, data: { rating: 333 } } }, - }, - include: { ratedVideos: true }, - }) - ).resolves.toMatchObject({ + const read = await db.ratedVideo.findFirst({ where: { duration: 1 } }); + const r = await db.user.update({ + where: { id: user.id }, + data: { + ratedVideos: { updateMany: { where: { duration: 1 }, data: { rating: 333 } } }, + }, + include: { ratedVideos: true }, + }); + expect(r).toMatchObject({ ratedVideos: expect.arrayContaining([expect.objectContaining({ rating: 333 })]), }); + expect(r.ratedVideos[0].updatedAt.getTime()).toBeGreaterThan(read.updatedAt.getTime()); // updateMany without filter await expect( @@ -1025,22 +1029,23 @@ describe('Polymorphism Test', () => { ).rejects.toThrow('is a delegate'); // update - await expect( - db.ratedVideo.upsert({ - where: { id: video.id }, - create: { - viewCount: 1, - duration: 300, - url: 'xyz', - rating: 100, - owner: { connect: { id: user.id } }, - }, - update: { duration: 200 }, - }) - ).resolves.toMatchObject({ + const read = await db.ratedVideo.findUnique({ where: { id: video.id } }); + const r = await db.ratedVideo.upsert({ + where: { id: video.id }, + create: { + viewCount: 1, + duration: 300, + url: 'xyz', + rating: 100, + owner: { connect: { id: user.id } }, + }, + update: { duration: 200 }, + }); + expect(r).toMatchObject({ id: video.id, duration: 200, }); + expect(r.updatedAt.getTime()).toBeGreaterThan(read.updatedAt.getTime()); // create const created = await db.ratedVideo.upsert({ diff --git a/tests/integration/tests/enhancements/with-delegate/utils.ts b/tests/integration/tests/enhancements/with-delegate/utils.ts index 41700bd4b..23bae33db 100644 --- a/tests/integration/tests/enhancements/with-delegate/utils.ts +++ b/tests/integration/tests/enhancements/with-delegate/utils.ts @@ -12,6 +12,7 @@ model User { model Asset { id Int @id @default(autoincrement()) createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt viewCount Int @default(0) owner User? @relation(fields: [ownerId], references: [id]) ownerId Int? From a81001b6d75aa94f8fffbb56100471104616f4e1 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 19 May 2025 10:26:09 -0700 Subject: [PATCH 2/3] update --- .../runtime/src/enhancements/node/delegate.ts | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/packages/runtime/src/enhancements/node/delegate.ts b/packages/runtime/src/enhancements/node/delegate.ts index f4f7ddc6c..3a4c0a585 100644 --- a/packages/runtime/src/enhancements/node/delegate.ts +++ b/packages/runtime/src/enhancements/node/delegate.ts @@ -817,12 +817,19 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { return super.updateMany(args); } - const simpleUpdateMany = Object.keys(args.data).every((key) => { + let simpleUpdateMany = Object.keys(args.data).every((key) => { // check if the `data` clause involves base fields const fieldInfo = resolveField(this.options.modelMeta, this.model, key); return !fieldInfo?.inheritedFrom; }); + // check if there are any `@updatedAt` fields from delegate base models + if (simpleUpdateMany) { + if (this.getUpdatedAtFromDelegateBases(this.model).length > 0) { + simpleUpdateMany = false; + } + } + return this.queryUtils.transaction(this.prisma, (tx) => this.doUpdateMany(tx, this.model, args, simpleUpdateMany) ); @@ -948,6 +955,13 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { return !fieldInfo?.inheritedFrom; }); + // check if there are any `@updatedAt` fields from delegate base models + if (simpleUpdateMany) { + if (this.getUpdatedAtFromDelegateBases(model).length > 0) { + simpleUpdateMany = false; + } + } + if (simpleUpdateMany) { // check if the `where` clause involves base fields simpleUpdateMany = Object.keys(args.where || {}).every((key) => { @@ -1058,15 +1072,9 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { // if we're updating any field, we need to take care of updating `@updatedAt` // fields inherited from delegate base models if (Object.keys(data).length > 0) { - const modelFields = getFields(this.options.modelMeta, model); - for (const fieldInfo of Object.values(modelFields)) { - if ( - fieldInfo.attributes?.some((attr) => attr.name === '@updatedAt') && - fieldInfo.inheritedFrom && - isDelegateModel(this.options.modelMeta, fieldInfo.inheritedFrom) - ) { - this.injectBaseFieldData(model, fieldInfo, new Date(), data, 'update'); - } + const updatedAtFields = this.getUpdatedAtFromDelegateBases(model); + for (const fieldInfo of updatedAtFields) { + this.injectBaseFieldData(model, fieldInfo, new Date(), data, 'update'); } } } @@ -1493,5 +1501,20 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { return result; } + private getUpdatedAtFromDelegateBases(model: string) { + const result: FieldInfo[] = []; + const modelFields = getFields(this.options.modelMeta, model); + for (const fieldInfo of Object.values(modelFields)) { + if ( + fieldInfo.attributes?.some((attr) => attr.name === '@updatedAt') && + fieldInfo.inheritedFrom && + isDelegateModel(this.options.modelMeta, fieldInfo.inheritedFrom) + ) { + result.push(fieldInfo); + } + } + return result; + } + // #endregion } From 3cecf6cf6f60f7b605dd5d0e10682d71b278d1d4 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 19 May 2025 11:10:12 -0700 Subject: [PATCH 3/3] fix tests --- .../enhancements/with-delegate/plugin-interaction.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/integration/tests/enhancements/with-delegate/plugin-interaction.test.ts b/tests/integration/tests/enhancements/with-delegate/plugin-interaction.test.ts index 8247c2e45..aceb50932 100644 --- a/tests/integration/tests/enhancements/with-delegate/plugin-interaction.test.ts +++ b/tests/integration/tests/enhancements/with-delegate/plugin-interaction.test.ts @@ -65,6 +65,7 @@ describe('Polymorphic Plugin Interaction Test', () => { id: 1, assetType: 'video', createdAt: new Date(), + updatedAt: new Date(), viewCount: 100, }) ).toBeTruthy(); @@ -74,6 +75,7 @@ describe('Polymorphic Plugin Interaction Test', () => { id: 1, assetType: 'video', createdAt: new Date(), + updatedAt: new Date(), viewCount: 100, videoType: 'ratedVideo', // should be stripped }).videoType @@ -87,6 +89,7 @@ describe('Polymorphic Plugin Interaction Test', () => { duration: 100, url: 'http://example.com', createdAt: new Date(), + updatedAt: new Date(), viewCount: 100, }) ).toBeTruthy(); @@ -98,6 +101,7 @@ describe('Polymorphic Plugin Interaction Test', () => { videoType: 'ratedVideo', url: 'http://example.com', createdAt: new Date(), + updatedAt: new Date(), viewCount: 100, }) ).toThrow('duration');