Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions packages/runtime/src/local-helpers/zod-utils.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand Down
72 changes: 48 additions & 24 deletions packages/schema/src/plugins/zod/generator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DELEGATE_AUX_RELATION_PREFIX } from '@zenstackhq/runtime';
import { upperCaseFirst } from '@zenstackhq/runtime/local-helpers';
import {
ExpressionContext,
PluginError,
Expand All @@ -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';
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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})`;
}
Expand All @@ -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())])`;
}
Expand All @@ -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
);
Expand Down Expand Up @@ -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`;
Expand Down Expand Up @@ -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;
Expand Down
20 changes: 11 additions & 9 deletions packages/schema/src/plugins/zod/utils/schema-gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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})`;
}
}
}

Expand Down
Loading