diff --git a/packages/runtime/src/enhancements/node/delegate.ts b/packages/runtime/src/enhancements/node/delegate.ts index 54c1abf96..3a4c0a585 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, @@ -816,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) ); @@ -947,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) => { @@ -1053,6 +1068,15 @@ 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 updatedAtFields = this.getUpdatedAtFromDelegateBases(model); + for (const fieldInfo of updatedAtFields) { + this.injectBaseFieldData(model, fieldInfo, new Date(), data, 'update'); + } + } } // #endregion @@ -1477,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 } 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/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'); 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?