From 039ec89b188a80e3364c2de08047d0268ec3834e Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sun, 31 Dec 2023 23:05:18 +0800 Subject: [PATCH 1/4] fix: support default values in generated zod schemas --- packages/schema/src/plugins/zod/generator.ts | 11 ++-- .../src/plugins/zod/utils/schema-gen.ts | 52 +++++++++++++++++-- .../tests/regression/issue-864.test.ts | 2 +- .../tests/regression/issue-886.test.ts | 21 ++++++++ 4 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 tests/integration/tests/regression/issue-886.test.ts diff --git a/packages/schema/src/plugins/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts index ad857fbda..a21782f7c 100644 --- a/packages/schema/src/plugins/zod/generator.ts +++ b/packages/schema/src/plugins/zod/generator.ts @@ -26,7 +26,7 @@ import { name } from '.'; import { getDefaultOutputFolder } from '../plugin-utils'; import Transformer from './transformer'; import removeDir from './utils/removeDir'; -import { makeFieldSchema, makeValidationRefinements } from './utils/schema-gen'; +import { makeFieldSchema, makeValidationRefinements, getFieldSchemaDefault } from './utils/schema-gen'; export async function generate( model: Model, @@ -309,7 +309,7 @@ async function generateModelSchema(model: DataModel, project: Project, output: s writer.write(`const baseSchema = z.object(`); writer.inlineBlock(() => { scalarFields.forEach((field) => { - writer.writeLine(`${field.name}: ${makeFieldSchema(field)},`); + writer.writeLine(`${field.name}: ${makeFieldSchema(field, true)},`); }); }); writer.writeLine(');'); @@ -356,7 +356,12 @@ async function generateModelSchema(model: DataModel, project: Project, output: s //////////////////////////////////////////////// // 1. Model schema //////////////////////////////////////////////// - let modelSchema = makePartial('baseSchema'); + const fieldsWithoutDefault = scalarFields.filter((f) => !getFieldSchemaDefault(f)); + // mark fields without default value as optional + let modelSchema = makePartial( + 'baseSchema', + fieldsWithoutDefault.length < scalarFields.length ? fieldsWithoutDefault.map((f) => f.name) : undefined + ); // omit fields const fieldsToOmit = scalarFields.filter((field) => hasAttribute(field, '@omit')); diff --git a/packages/schema/src/plugins/zod/utils/schema-gen.ts b/packages/schema/src/plugins/zod/utils/schema-gen.ts index f84b25a2d..336484c75 100644 --- a/packages/schema/src/plugins/zod/utils/schema-gen.ts +++ b/packages/schema/src/plugins/zod/utils/schema-gen.ts @@ -1,5 +1,21 @@ -import { ExpressionContext, PluginError, getAttributeArg, getAttributeArgLiteral, getLiteral } from '@zenstackhq/sdk'; -import { DataModel, DataModelField, DataModelFieldAttribute, isDataModel, isEnum } from '@zenstackhq/sdk/ast'; +import { + ExpressionContext, + PluginError, + getAttributeArg, + getAttributeArgLiteral, + getLiteral, + isFromStdlib, +} from '@zenstackhq/sdk'; +import { + DataModel, + DataModelField, + DataModelFieldAttribute, + isDataModel, + isEnum, + isInvocationExpr, + isNumberLiteral, + isStringLiteral, +} from '@zenstackhq/sdk/ast'; import { upperCaseFirst } from 'upper-case-first'; import { name } from '..'; import { @@ -7,7 +23,7 @@ import { TypeScriptExpressionTransformerError, } from '../../../utils/typescript-expression-transformer'; -export function makeFieldSchema(field: DataModelField) { +export function makeFieldSchema(field: DataModelField, respectDefault = false) { if (isDataModel(field.type.reference?.ref)) { if (field.type.array) { // array field is always optional @@ -20,6 +36,13 @@ export function makeFieldSchema(field: DataModelField) { let schema = makeZodSchema(field); const isDecimal = field.type.type === 'Decimal'; + if (respectDefault) { + const schemaDefault = getFieldSchemaDefault(field); + if (schemaDefault) { + schema += `.default(${schemaDefault})`; + } + } + for (const attr of field.attributes) { const message = getAttrLiteralArg(attr, 'message'); const messageArg = message ? `, { message: ${JSON.stringify(message)} }` : ''; @@ -202,3 +225,26 @@ function refineDecimal(op: 'gt' | 'gte' | 'lt' | 'lte', value: number, messageAr } }${messageArg})`; } + +export function getFieldSchemaDefault(field: DataModelField) { + const attr = field.attributes.find((attr) => attr.decl.ref?.name === '@default'); + if (!attr) { + return undefined; + } + const arg = attr.args.find((arg) => arg.$resolvedParam?.name === 'value'); + if (arg) { + if (isStringLiteral(arg.value)) { + return JSON.stringify(arg.value.value); + } else if (isNumberLiteral(arg.value)) { + return arg.value.value; + } else if ( + isInvocationExpr(arg.value) && + isFromStdlib(arg.value.function.ref!) && + arg.value.function.$refText === 'now' + ) { + return `() => new Date()`; + } + } + + return undefined; +} diff --git a/tests/integration/tests/regression/issue-864.test.ts b/tests/integration/tests/regression/issue-864.test.ts index 02aab81d1..eb50bc95f 100644 --- a/tests/integration/tests/regression/issue-864.test.ts +++ b/tests/integration/tests/regression/issue-864.test.ts @@ -1,6 +1,6 @@ import { loadSchema } from '@zenstackhq/testtools'; -describe('Regression: issue nested create', () => { +describe('Regression: issue 864', () => { it('safe create', async () => { const { prisma, enhance } = await loadSchema( ` diff --git a/tests/integration/tests/regression/issue-886.test.ts b/tests/integration/tests/regression/issue-886.test.ts new file mode 100644 index 000000000..348c6cced --- /dev/null +++ b/tests/integration/tests/regression/issue-886.test.ts @@ -0,0 +1,21 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('Regression: issue 886', () => { + it('regression', async () => { + const { zodSchemas } = await loadSchema( + ` + model Model { + id Int @id @default(autoincrement()) + a Int @default(100) + b String @default('') + c DateTime @default(now()) + } + ` + ); + + const r = zodSchemas.models.ModelSchema.parse({}); + expect(r.a).toBe(100); + expect(r.b).toBe(''); + expect(r.c).toBeInstanceOf(Date); + }); +}); From c5582b7c927fa9ecfd1c623796307ec89c64ea9d Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sun, 31 Dec 2023 23:27:27 +0800 Subject: [PATCH 2/4] fix test --- .../schema/src/plugins/zod/utils/schema-gen.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/schema/src/plugins/zod/utils/schema-gen.ts b/packages/schema/src/plugins/zod/utils/schema-gen.ts index 336484c75..802127c58 100644 --- a/packages/schema/src/plugins/zod/utils/schema-gen.ts +++ b/packages/schema/src/plugins/zod/utils/schema-gen.ts @@ -36,13 +36,6 @@ export function makeFieldSchema(field: DataModelField, respectDefault = false) { let schema = makeZodSchema(field); const isDecimal = field.type.type === 'Decimal'; - if (respectDefault) { - const schemaDefault = getFieldSchemaDefault(field); - if (schemaDefault) { - schema += `.default(${schemaDefault})`; - } - } - for (const attr of field.attributes) { const message = getAttrLiteralArg(attr, 'message'); const messageArg = message ? `, { message: ${JSON.stringify(message)} }` : ''; @@ -131,6 +124,13 @@ export function makeFieldSchema(field: DataModelField, respectDefault = false) { } } + if (respectDefault) { + const schemaDefault = getFieldSchemaDefault(field); + if (schemaDefault) { + schema += `.default(${schemaDefault})`; + } + } + if (field.type.optional) { schema += '.nullish()'; } From cf55cb87979d15cfa1bc08c5fd4aaabc306a5efd Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 1 Jan 2024 10:11:02 +0800 Subject: [PATCH 3/4] fix code generation --- packages/schema/src/plugins/zod/generator.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/schema/src/plugins/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts index a21782f7c..2727a781f 100644 --- a/packages/schema/src/plugins/zod/generator.ts +++ b/packages/schema/src/plugins/zod/generator.ts @@ -480,9 +480,13 @@ async function generateModelSchema(model: DataModel, project: Project, output: s function makePartial(schema: string, fields?: string[]) { if (fields) { - return `${schema}.partial({ - ${fields.map((f) => `${f}: true`).join(', ')}, + if (fields.length === 0) { + return schema; + } else { + return `${schema}.partial({ + ${fields.map((f) => `${f}: true`).join(', ')} })`; + } } else { return `${schema}.partial()`; } From bb5afc751bda5691071f1fc0c389920b96b76172 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 1 Jan 2024 10:32:22 +0800 Subject: [PATCH 4/4] update --- tests/integration/tests/regression/issue-886.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/tests/regression/issue-886.test.ts b/tests/integration/tests/regression/issue-886.test.ts index 348c6cced..a749db61e 100644 --- a/tests/integration/tests/regression/issue-886.test.ts +++ b/tests/integration/tests/regression/issue-886.test.ts @@ -17,5 +17,6 @@ describe('Regression: issue 886', () => { expect(r.a).toBe(100); expect(r.b).toBe(''); expect(r.c).toBeInstanceOf(Date); + expect(r.id).toBeUndefined(); }); });