From f51424a1aa744d190c7ce894dd86e6d13f83975f Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 8 Sep 2025 23:48:54 -0700 Subject: [PATCH 1/2] fix: issue with zod generation for self-relation involving delegate types --- .../schema/src/plugins/zod/transformer.ts | 15 ++++++++- packages/sdk/src/utils.ts | 10 +++++- tests/regression/tests/issue-2226.test.ts | 31 +++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 tests/regression/tests/issue-2226.test.ts diff --git a/packages/schema/src/plugins/zod/transformer.ts b/packages/schema/src/plugins/zod/transformer.ts index f79c945cc..1eeb85dee 100644 --- a/packages/schema/src/plugins/zod/transformer.ts +++ b/packages/schema/src/plugins/zod/transformer.ts @@ -342,7 +342,20 @@ export default class Transformer { // "Input" or "NestedInput" suffix mappedInputTypeName += match[4]; - processedInputType = { ...inputType, type: mappedInputTypeName }; + // Prisma's naming is inconsistent for update input types, so we need + // to check for a few other candidates and use the one that matches + // a DMMF input type name + const candidates = [mappedInputTypeName]; + if (mappedInputTypeName.includes('UpdateOne')) { + candidates.push(...candidates.map((name) => name.replace('UpdateOne', 'Update'))); + } + if (mappedInputTypeName.includes('NestedInput')) { + candidates.push(...candidates.map((name) => name.replace('NestedInput', 'Input'))); + } + const foundInputType = this.inputObjectTypes.find((it) => candidates.includes(it.name)); + const finalMappedName = foundInputType ? foundInputType.name : mappedInputTypeName; + + processedInputType = { ...inputType, type: finalMappedName }; } return processedInputType; } diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index 09a538f77..0427ab4f0 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -659,8 +659,16 @@ export function getRelationBackLink(field: DataModelField) { const targetModel = field.type.reference.ref as DataModel; + const sameField = (f1: DataModelField, f2: DataModelField) => { + // for fields inherited from a delegate model, always use + // the base to compare + const parent1 = f1.$inheritedFrom ?? f1.$container; + const parent2 = f2.$inheritedFrom ?? f2.$container; + return f1.name === f2.name && parent1 === parent2; + }; + for (const otherField of targetModel.fields) { - if (otherField === field) { + if (sameField(otherField, field)) { // backlink field is never self continue; } diff --git a/tests/regression/tests/issue-2226.test.ts b/tests/regression/tests/issue-2226.test.ts new file mode 100644 index 000000000..8dc55bfad --- /dev/null +++ b/tests/regression/tests/issue-2226.test.ts @@ -0,0 +1,31 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 2226', () => { + it('regression', async () => { + const { zodSchemas } = await loadSchema( + ` +model Registration { + id String @id + regType String + @@delegate(regType) + + replacedRegistrationId String? + replacedRegistration Registration? @relation("ReplacedBy", fields: [replacedRegistrationId], references: [id]) + replacements Registration[] @relation("ReplacedBy") +} + +// Delegated subtype +model RegistrationFramework extends Registration { +} +`, + { fullZod: true } + ); + + const schema = zodSchemas.objects.RegistrationFrameworkUpdateInputObjectSchema; + expect(schema).toBeDefined(); + const parsed = schema.safeParse({ + replacedRegistrationId: '123', + }); + expect(parsed.success).toBe(true); + }); +}); From be6eabcb4f19953c849cb7039b8d37c967304604 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 9 Sep 2025 00:05:28 -0700 Subject: [PATCH 2/2] improvement --- packages/schema/src/plugins/zod/transformer.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/schema/src/plugins/zod/transformer.ts b/packages/schema/src/plugins/zod/transformer.ts index 1eeb85dee..16e1451bf 100644 --- a/packages/schema/src/plugins/zod/transformer.ts +++ b/packages/schema/src/plugins/zod/transformer.ts @@ -352,8 +352,9 @@ export default class Transformer { if (mappedInputTypeName.includes('NestedInput')) { candidates.push(...candidates.map((name) => name.replace('NestedInput', 'Input'))); } - const foundInputType = this.inputObjectTypes.find((it) => candidates.includes(it.name)); - const finalMappedName = foundInputType ? foundInputType.name : mappedInputTypeName; + + const finalMappedName = + candidates.find((name) => this.inputObjectTypes.some((it) => it.name === name)) ?? mappedInputTypeName; processedInputType = { ...inputType, type: finalMappedName }; }