From ab261aed8fd79491250901ecf6e4999456700cea Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sun, 15 Sep 2024 14:54:57 -0700 Subject: [PATCH] fix(delegate): concrete model fields are not properly included if queried from a nested context from a parent concrete model Fixes #1968 --- .../runtime/src/enhancements/node/delegate.ts | 22 +++--- .../with-delegate/enhanced-client.test.ts | 46 ++++++++++++ tests/regression/tests/issue-1698.test.ts | 74 +++++++++++++++++++ 3 files changed, 133 insertions(+), 9 deletions(-) create mode 100644 tests/regression/tests/issue-1698.test.ts diff --git a/packages/runtime/src/enhancements/node/delegate.ts b/packages/runtime/src/enhancements/node/delegate.ts index 8efad7568..da9570bd4 100644 --- a/packages/runtime/src/enhancements/node/delegate.ts +++ b/packages/runtime/src/enhancements/node/delegate.ts @@ -168,16 +168,20 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { } } - if (value !== undefined) { - if (value?.orderBy) { + // refetch the field select/include value because it may have been + // updated during injection + const fieldValue = args[kind][field]; + + if (fieldValue !== undefined) { + if (fieldValue.orderBy) { // `orderBy` may contain fields from base types - this.injectWhereHierarchy(fieldInfo.type, value.orderBy); + this.injectWhereHierarchy(fieldInfo.type, fieldValue.orderBy); } - if (this.injectBaseFieldSelect(model, field, value, args, kind)) { + if (this.injectBaseFieldSelect(model, field, fieldValue, args, kind)) { delete args[kind][field]; } else if (fieldInfo.isDataModel) { - let nextValue = value; + let nextValue = fieldValue; if (nextValue === true) { // make sure the payload is an object args[kind][field] = nextValue = {}; @@ -1158,11 +1162,11 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { const base = this.getBaseModel(model); if (base) { - // merge base fields + // fully merge base fields const baseRelationName = this.makeAuxRelationName(base); const baseData = entity[baseRelationName]; if (baseData && typeof baseData === 'object') { - const baseAssembled = this.assembleUp(base.name, baseData); + const baseAssembled = this.assembleHierarchy(base.name, baseData); Object.assign(result, baseAssembled); } } @@ -1209,14 +1213,14 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { const modelInfo = getModelInfo(this.options.modelMeta, model, true); if (modelInfo.discriminator) { - // model is a delegate, merge sub model fields + // model is a delegate, fully merge concrete model fields const subModelName = entity[modelInfo.discriminator]; if (subModelName) { const subModel = getModelInfo(this.options.modelMeta, subModelName, true); const subRelationName = this.makeAuxRelationName(subModel); const subData = entity[subRelationName]; if (subData && typeof subData === 'object') { - const subAssembled = this.assembleDown(subModel.name, subData); + const subAssembled = this.assembleHierarchy(subModel.name, subData); Object.assign(result, subAssembled); } } 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 4d71f26dc..59a3f68c0 100644 --- a/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts +++ b/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts @@ -1361,4 +1361,50 @@ describe('Polymorphism Test', () => { ], }); }); + + it('merges hierarchy correctly', async () => { + const { enhance } = await loadSchema( + ` + model Asset { + id Int @id @default(autoincrement()) + type String + viewCount Int + comments Comment[] + @@delegate(type) + } + + model Post extends Asset { + title String + } + + model Comment { + id Int @id @default(autoincrement()) + type String + asset Asset @relation(fields: [assetId], references: [id]) + assetId Int + moderated Boolean + @@delegate(type) + } + + model TextComment extends Comment { + text String + } + `, + { enhancements: ['delegate'] } + ); + + const db = enhance(); + const post = await db.post.create({ data: { title: 'Post1', viewCount: 1 } }); + const comment = await db.textComment.create({ + data: { text: 'Comment1', moderated: true, asset: { connect: { id: post.id } } }, + }); + + // delegate include delegate + let r = await db.asset.findFirst({ include: { comments: true } }); + expect(r).toMatchObject({ viewCount: 1, comments: [comment] }); + + // concrete include delegate + r = await db.post.findFirst({ include: { comments: true } }); + expect(r).toMatchObject({ ...post, comments: [comment] }); + }); }); diff --git a/tests/regression/tests/issue-1698.test.ts b/tests/regression/tests/issue-1698.test.ts new file mode 100644 index 000000000..4d6f52f54 --- /dev/null +++ b/tests/regression/tests/issue-1698.test.ts @@ -0,0 +1,74 @@ +import { loadSchema } from '@zenstackhq/testtools'; +describe('issue 1968', () => { + it('regression', async () => { + const { enhance } = await loadSchema( + ` + model House { + id Int @id @default(autoincrement()) + doorTypeId Int + door Door @relation(fields: [doorTypeId], references: [id]) + houseType String + @@delegate(houseType) + } + + model PrivateHouse extends House { + size Int + } + + model Skyscraper extends House { + height Int + } + + model Door { + id Int @id @default(autoincrement()) + color String + doorType String + houses House[] + @@delegate(doorType) + } + + model IronDoor extends Door { + strength Int + } + + model WoodenDoor extends Door { + texture String + } + `, + { enhancements: ['delegate'] } + ); + + const db = enhance(); + const door1 = await db.ironDoor.create({ + data: { strength: 100, color: 'blue' }, + }); + console.log(door1); + + const door2 = await db.woodenDoor.create({ + data: { texture: 'pine', color: 'red' }, + }); + console.log(door2); + + const house1 = await db.privateHouse.create({ + data: { size: 5000, door: { connect: { id: door1.id } } }, + }); + console.log(house1); + + const house2 = await db.skyscraper.create({ + data: { height: 3000, door: { connect: { id: door2.id } } }, + }); + console.log(house2); + + const r1 = await db.privateHouse.findFirst({ include: { door: true } }); + console.log(r1); + expect(r1).toMatchObject({ + door: { color: 'blue', strength: 100 }, + }); + + const r2 = (await db.skyscraper.findMany({ include: { door: true } }))[0]; + console.log(r2); + expect(r2).toMatchObject({ + door: { color: 'red', texture: 'pine' }, + }); + }); +});