From f6bb6fdaf4a94eb250711656345b770d0308bb8a Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sun, 7 Sep 2025 12:31:20 -0700 Subject: [PATCH 1/3] chore: run tests with zod4 --- LICENSE | 2 +- packages/LICENSE | 2 +- script/test-scaffold.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index 91d4584d3..46a34ef46 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 ZenStack +Copyright (c) 2025 ZenStack Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/packages/LICENSE b/packages/LICENSE index 91d4584d3..46a34ef46 100644 --- a/packages/LICENSE +++ b/packages/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 ZenStack +Copyright (c) 2025 ZenStack Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/script/test-scaffold.ts b/script/test-scaffold.ts index b9c4aae4d..71f80bd25 100644 --- a/script/test-scaffold.ts +++ b/script/test-scaffold.ts @@ -20,7 +20,7 @@ function run(cmd: string) { run('npm init -y'); run( - 'npm i --no-audit --no-fund typescript@~5.8.0 prisma@6.11.x @prisma/client@6.11.x zod@^3.25.0 decimal.js @types/node' + 'npm i --no-audit --no-fund typescript@~5.8.0 prisma@6.11.x @prisma/client@6.11.x zod@^4.0.0 decimal.js @types/node' ); console.log('Test scaffold setup complete.'); From 5390d6549d8fbffa680fca3493a724436cc845a7 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sun, 7 Sep 2025 23:26:53 -0700 Subject: [PATCH 2/3] fix: zod4 compatibility regarding fields with default values --- .../runtime/src/local-helpers/zod-utils.ts | 7 +- packages/schema/src/plugins/zod/generator.ts | 72 ++++++++++++------- .../src/plugins/zod/utils/schema-gen.ts | 20 +++--- 3 files changed, 62 insertions(+), 37 deletions(-) diff --git a/packages/runtime/src/local-helpers/zod-utils.ts b/packages/runtime/src/local-helpers/zod-utils.ts index f463e5114..01e6b9d66 100644 --- a/packages/runtime/src/local-helpers/zod-utils.ts +++ b/packages/runtime/src/local-helpers/zod-utils.ts @@ -1,7 +1,6 @@ -import { type ZodError } from 'zod'; +/* eslint-disable @typescript-eslint/no-explicit-any */ import { fromZodError as fromZodErrorV3 } from 'zod-validation-error/v3'; import { fromZodError as fromZodErrorV4 } from 'zod-validation-error/v4'; -import { type ZodError as Zod4Error } from 'zod/v4'; /** * Formats a Zod error message for better readability. Compatible with both Zod v3 and v4. @@ -13,9 +12,9 @@ export function getZodErrorMessage(err: unknown): string { try { if ('_zod' in err) { - return fromZodErrorV4(err as Zod4Error).message; + return fromZodErrorV4(err as any).message; } else { - return fromZodErrorV3(err as ZodError).message; + return fromZodErrorV3(err as any).message; } } catch { return err.message; diff --git a/packages/schema/src/plugins/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts index 68922a662..acc88dd9a 100644 --- a/packages/schema/src/plugins/zod/generator.ts +++ b/packages/schema/src/plugins/zod/generator.ts @@ -1,4 +1,5 @@ import { DELEGATE_AUX_RELATION_PREFIX } from '@zenstackhq/runtime'; +import { upperCaseFirst } from '@zenstackhq/runtime/local-helpers'; import { ExpressionContext, PluginError, @@ -23,10 +24,19 @@ import { resolvePath, saveSourceFile, } from '@zenstackhq/sdk'; -import { DataModel, EnumField, Model, TypeDef, isArrayExpr, isDataModel, isEnum, isTypeDef } from '@zenstackhq/sdk/ast'; +import { + DataModel, + DataModelField, + EnumField, + Model, + TypeDef, + isArrayExpr, + isDataModel, + isEnum, + isTypeDef, +} from '@zenstackhq/sdk/ast'; import { addMissingInputObjectTypes, resolveAggregateOperationSupport } from '@zenstackhq/sdk/dmmf-helpers'; import { getPrismaClientImportSpec, supportCreateMany, type DMMF } from '@zenstackhq/sdk/prisma'; -import { upperCaseFirst } from '@zenstackhq/runtime/local-helpers'; import { streamAllContents } from 'langium'; import path from 'path'; import type { CodeBlockWriter, SourceFile } from 'ts-morph'; @@ -418,25 +428,10 @@ export const ${typeDef.name}Schema = ${refineFuncName}(${noRefineSchema}); this.addPreludeAndImports(model, writer, output); // base schema - including all scalar fields, with optionality following the schema - writer.write(`const baseSchema = z.object(`); - writer.inlineBlock(() => { - scalarFields.forEach((field) => { - writer.writeLine(`${field.name}: ${makeFieldSchema(field)},`); - }); - }); + this.createModelBaseSchema('baseSchema', writer, scalarFields, true); - switch (this.options.mode) { - case 'strip': - // zod strips by default - writer.writeLine(')'); - break; - case 'passthrough': - writer.writeLine(').passthrough();'); - break; - default: - writer.writeLine(').strict();'); - break; - } + // base schema without field defaults + this.createModelBaseSchema('baseSchemaWithoutDefaults', writer, scalarFields, false); // relation fields @@ -536,7 +531,9 @@ export const ${upperCaseFirst(model.name)}Schema = ${modelSchema}; //////////////////////////////////////////////// // schema for validating prisma create input (all fields optional) - let prismaCreateSchema = this.makePassthrough(this.makePartial(`baseSchema${omitDiscriminators}`)); + let prismaCreateSchema = this.makePassthrough( + this.makePartial(`baseSchemaWithoutDefaults${omitDiscriminators}`) + ); if (refineFuncName) { prismaCreateSchema = `${refineFuncName}(${prismaCreateSchema})`; } @@ -554,7 +551,7 @@ export const ${upperCaseFirst(model.name)}PrismaCreateSchema = ${prismaCreateSch ${scalarFields .filter((f) => !isDiscriminatorField(f)) .map((field) => { - let fieldSchema = makeFieldSchema(field); + let fieldSchema = makeFieldSchema(field, false); if (field.type.type === 'Int' || field.type.type === 'Float') { fieldSchema = `z.union([${fieldSchema}, z.record(z.unknown())])`; } @@ -577,7 +574,7 @@ export const ${upperCaseFirst(model.name)}PrismaUpdateSchema = ${prismaUpdateSch // 3. Create schema //////////////////////////////////////////////// - let createSchema = `baseSchema${omitDiscriminators}`; + let createSchema = `baseSchemaWithoutDefaults${omitDiscriminators}`; const fieldsWithDefault = scalarFields.filter( (field) => hasAttribute(field, '@default') || hasAttribute(field, '@updatedAt') || field.type.array ); @@ -631,7 +628,7 @@ export const ${upperCaseFirst(model.name)}CreateSchema = ${createSchema}; //////////////////////////////////////////////// // for update all fields are optional - let updateSchema = this.makePartial(`baseSchema${omitDiscriminators}`); + let updateSchema = this.makePartial(`baseSchemaWithoutDefaults${omitDiscriminators}`); // export schema with only scalar fields: `[Model]UpdateScalarSchema` const updateScalarSchema = `${upperCaseFirst(model.name)}UpdateScalarSchema`; @@ -673,6 +670,33 @@ export const ${upperCaseFirst(model.name)}UpdateSchema = ${updateSchema}; return schemaName; } + private createModelBaseSchema( + name: string, + writer: CodeBlockWriter, + scalarFields: DataModelField[], + addDefaults: boolean + ) { + writer.write(`const ${name} = z.object(`); + writer.inlineBlock(() => { + scalarFields.forEach((field) => { + writer.writeLine(`${field.name}: ${makeFieldSchema(field, addDefaults)},`); + }); + }); + + switch (this.options.mode) { + case 'strip': + // zod strips by default + writer.writeLine(')'); + break; + case 'passthrough': + writer.writeLine(').passthrough();'); + break; + default: + writer.writeLine(').strict();'); + break; + } + } + private createRefineFunction(decl: DataModel | TypeDef, writer: CodeBlockWriter) { const refinements = this.makeValidationRefinements(decl); let refineFuncName: string | undefined; diff --git a/packages/schema/src/plugins/zod/utils/schema-gen.ts b/packages/schema/src/plugins/zod/utils/schema-gen.ts index 47f95e3c4..95a0d3730 100644 --- a/packages/schema/src/plugins/zod/utils/schema-gen.ts +++ b/packages/schema/src/plugins/zod/utils/schema-gen.ts @@ -14,7 +14,7 @@ import { import { upperCaseFirst } from '@zenstackhq/runtime/local-helpers'; import { isDefaultWithAuth } from '../../enhancer/enhancer-utils'; -export function makeFieldSchema(field: DataModelField | TypeDefField) { +export function makeFieldSchema(field: DataModelField | TypeDefField, addDefaults: boolean = true) { if (isDataModel(field.type.reference?.ref)) { if (field.type.array) { // array field is always optional @@ -141,14 +141,16 @@ export function makeFieldSchema(field: DataModelField | TypeDefField) { schema += '.optional()'; } } else { - const schemaDefault = getFieldSchemaDefault(field); - if (schemaDefault !== undefined) { - if (field.type.type === 'BigInt') { - // we can't use the `n` BigInt literal notation, since it needs - // ES2020 or later, which TypeScript doesn't use by default - schema += `.default(BigInt("${schemaDefault}"))`; - } else { - schema += `.default(${schemaDefault})`; + if (addDefaults) { + const schemaDefault = getFieldSchemaDefault(field); + if (schemaDefault !== undefined) { + if (field.type.type === 'BigInt') { + // we can't use the `n` BigInt literal notation, since it needs + // ES2020 or later, which TypeScript doesn't use by default + schema += `.default(BigInt("${schemaDefault}"))`; + } else { + schema += `.default(${schemaDefault})`; + } } } From bc16e358b5aa2bd93c6eb0b1d4fe968f901766c5 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sun, 7 Sep 2025 23:27:57 -0700 Subject: [PATCH 3/3] update --- script/test-scaffold.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/test-scaffold.ts b/script/test-scaffold.ts index 71f80bd25..b9c4aae4d 100644 --- a/script/test-scaffold.ts +++ b/script/test-scaffold.ts @@ -20,7 +20,7 @@ function run(cmd: string) { run('npm init -y'); run( - 'npm i --no-audit --no-fund typescript@~5.8.0 prisma@6.11.x @prisma/client@6.11.x zod@^4.0.0 decimal.js @types/node' + 'npm i --no-audit --no-fund typescript@~5.8.0 prisma@6.11.x @prisma/client@6.11.x zod@^3.25.0 decimal.js @types/node' ); console.log('Test scaffold setup complete.');