Skip to content

Commit

Permalink
fix(delegate): clean up generated zod schemas for delegate auxiliary …
Browse files Browse the repository at this point in the history
…fields

fixes #1993
  • Loading branch information
ymc9 committed Feb 23, 2025
1 parent 317f535 commit e23f5c9
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 8 deletions.
16 changes: 12 additions & 4 deletions packages/schema/src/plugins/zod/generator.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { DELEGATE_AUX_RELATION_PREFIX } from '@zenstackhq/runtime';
import {
ExpressionContext,
PluginError,
Expand Down Expand Up @@ -88,12 +89,18 @@ export class ZodSchemaGenerator {
(o) => !excludeModels.find((e) => e === o.model)
);

// TODO: better way of filtering than string startsWith?
const inputObjectTypes = prismaClientDmmf.schema.inputObjectTypes.prisma.filter(
(type) => !excludeModels.find((e) => type.name.toLowerCase().startsWith(e.toLocaleLowerCase()))
(type) =>
!excludeModels.some((e) => type.name.toLowerCase().startsWith(e.toLocaleLowerCase())) &&
// exclude delegate aux related types
!type.name.toLowerCase().includes(DELEGATE_AUX_RELATION_PREFIX)
);

const outputObjectTypes = prismaClientDmmf.schema.outputObjectTypes.prisma.filter(
(type) => !excludeModels.find((e) => type.name.toLowerCase().startsWith(e.toLowerCase()))
(type) =>
!excludeModels.some((e) => type.name.toLowerCase().startsWith(e.toLowerCase())) &&
// exclude delegate aux related types
!type.name.toLowerCase().includes(DELEGATE_AUX_RELATION_PREFIX)
);

const models: DMMF.Model[] = prismaClientDmmf.datamodel.models.filter(
Expand Down Expand Up @@ -236,7 +243,8 @@ export class ZodSchemaGenerator {

const moduleNames: string[] = [];
for (let i = 0; i < inputObjectTypes.length; i += 1) {
const fields = inputObjectTypes[i]?.fields;
// exclude delegate aux fields
const fields = inputObjectTypes[i]?.fields?.filter((f) => !f.name.startsWith(DELEGATE_AUX_RELATION_PREFIX));
const name = inputObjectTypes[i]?.name;

if (!generateUnchecked && name.includes('Unchecked')) {
Expand Down
45 changes: 41 additions & 4 deletions packages/schema/src/plugins/zod/transformer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { DELEGATE_AUX_RELATION_PREFIX } from '@zenstackhq/runtime';
import {
getForeignKeyFields,
hasAttribute,
indentString,
isDelegateModel,
isDiscriminatorField,
type PluginOptions,
} from '@zenstackhq/sdk';
Expand Down Expand Up @@ -67,7 +69,11 @@ export default class Transformer {
const filePath = path.join(Transformer.outputPath, `enums/${name}.schema.ts`);
const content = `${this.generateImportZodStatement()}\n${this.generateExportSchemaStatement(
`${name}`,
`z.enum(${JSON.stringify(enumType.values)})`
`z.enum(${JSON.stringify(
enumType.values
// exclude fields generated for delegate models
.filter((v) => !v.startsWith(DELEGATE_AUX_RELATION_PREFIX))
)})`
)}`;
this.sourceFiles.push(this.project.createSourceFile(filePath, content, { overwrite: true }));
generated.push(enumType.name);
Expand Down Expand Up @@ -243,12 +249,16 @@ export default class Transformer {
!isFieldRef &&
(inputType.namespace === 'prisma' || isEnum)
) {
if (inputType.type !== this.originalName && typeof inputType.type === 'string') {
this.addSchemaImport(inputType.type);
// reduce concrete input types to their delegate base types
// e.g.: "UserCreateNestedOneWithoutDelegate_aux_PostInput" => "UserCreateWithoutAssetInput"
const mappedInputType = this.mapDelegateInputType(inputType, contextDataModel);

if (mappedInputType.type !== this.originalName && typeof mappedInputType.type === 'string') {
this.addSchemaImport(mappedInputType.type);
}

const contextField = contextDataModel?.fields.find((f) => f.name === field.name);
result.push(this.generatePrismaStringLine(field, inputType, lines.length, contextField));
result.push(this.generatePrismaStringLine(field, mappedInputType, lines.length, contextField));
}
}

Expand Down Expand Up @@ -289,6 +299,33 @@ export default class Transformer {
return [[` ${fieldName} ${resString} `, field, true]];
}

private mapDelegateInputType(inputType: PrismaDMMF.InputTypeRef, contextDataModel: DataModel | undefined) {
let processedInputType = inputType;
// captures: model name and operation, "Without" part that references a concrete model,
// and the "Input" or "NestedInput" suffix
const match = inputType.type.match(/^(\S+?)((NestedOne)?WithoutDelegate_aux\S+?)((Nested)?Input)$/);
if (match) {
let mappedInputTypeName = match[1];

if (contextDataModel) {
// find the parent delegate model and replace the "Without" part with it
const delegateBase = contextDataModel.superTypes
.map((t) => t.ref)
.filter((t) => t && isDelegateModel(t))?.[0];
if (delegateBase) {
mappedInputTypeName += `Without${upperCaseFirst(delegateBase.name)}`;
}
}

// "Input" or "NestedInput" suffix
mappedInputTypeName += match[4];

processedInputType = { ...inputType, type: mappedInputTypeName };
// console.log('Replacing type', inputTyp.type, 'with', processedInputType.type);
}
return processedInputType;
}

wrapWithZodValidators(
mainValidators: string | string[],
field: PrismaDMMF.SchemaArg,
Expand Down
45 changes: 45 additions & 0 deletions tests/regression/tests/issue-1993.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { loadSchema } from '@zenstackhq/testtools';

describe('issue 1993', () => {
it('regression', async () => {
await loadSchema(
`
enum UserType {
UserLocal
UserGoogle
}
model User {
id String @id @default(cuid())
companyId String?
type UserType
@@delegate(type)
userFolders UserFolder[]
@@allow('all', true)
}
model UserLocal extends User {
email String
password String
}
model UserGoogle extends User {
googleId String
}
model UserFolder {
id String @id @default(cuid())
userId String
path String
user User @relation(fields: [userId], references: [id])
@@allow('all', true)
} `,
{ pushDb: false, fullZod: true, compile: true, output: 'lib/zenstack' }
);
});
});

0 comments on commit e23f5c9

Please sign in to comment.