diff --git a/packages/schema/src/plugins/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts index 33557de9d..267f9dd29 100644 --- a/packages/schema/src/plugins/zod/generator.ts +++ b/packages/schema/src/plugins/zod/generator.ts @@ -119,7 +119,7 @@ async function generateCommonSchemas(project: Project, output: string) { path.join(output, 'common', 'index.ts'), ` import { z } from 'zod'; -export const DecimalSchema = z.union([z.number(), z.string(), z.object({d: z.number().array(), e: z.number(), s: z.number()})]); +export const DecimalSchema = z.union([z.number(), z.string(), z.object({d: z.number().array(), e: z.number(), s: z.number()}).passthrough()]); // https://stackoverflow.com/a/54487392/20415796 type OmitDistributive = T extends any ? (T extends object ? OmitRecursively : T) : never; @@ -236,6 +236,7 @@ async function generateModelSchema(model: DataModel, project: Project, output: s // import Decimal if (fields.some((field) => field.type.type === 'Decimal')) { writer.writeLine(`import { DecimalSchema } from '../common';`); + writer.writeLine(`import { Decimal } from 'decimal.js';`); } // create base schema diff --git a/packages/schema/src/plugins/zod/utils/schema-gen.ts b/packages/schema/src/plugins/zod/utils/schema-gen.ts index 34f53083d..0676c40d6 100644 --- a/packages/schema/src/plugins/zod/utils/schema-gen.ts +++ b/packages/schema/src/plugins/zod/utils/schema-gen.ts @@ -9,6 +9,7 @@ import { export function makeFieldSchema(field: DataModelField) { let schema = makeZodSchema(field); + const isDecimal = field.type.type === 'Decimal'; for (const attr of field.attributes) { const message = getAttrLiteralArg(attr, 'message'); @@ -70,28 +71,28 @@ export function makeFieldSchema(field: DataModelField) { case '@gt': { const value = getAttrLiteralArg(attr, 'value'); if (value !== undefined) { - schema += `.gt(${value}${messageArg})`; + schema += isDecimal ? refineDecimal('gt', value, messageArg) : `.gt(${value}${messageArg})`; } break; } case '@gte': { const value = getAttrLiteralArg(attr, 'value'); if (value !== undefined) { - schema += `.gte(${value}${messageArg})`; + schema += isDecimal ? refineDecimal('gte', value, messageArg) : `.gte(${value}${messageArg})`; } break; } case '@lt': { const value = getAttrLiteralArg(attr, 'value'); if (value !== undefined) { - schema += `.lt(${value}${messageArg})`; + schema += isDecimal ? refineDecimal('lt', value, messageArg) : `.lt(${value}${messageArg})`; } break; } case '@lte': { const value = getAttrLiteralArg(attr, 'value'); if (value !== undefined) { - schema += `.lte(${value}${messageArg})`; + schema += isDecimal ? refineDecimal('lte', value, messageArg) : `.lte(${value}${messageArg})`; } break; } @@ -182,3 +183,13 @@ function getAttrLiteralArg(attr: DataModelFieldAttrib const arg = attr.args.find((arg) => arg.$resolvedParam?.name === paramName); return arg && getLiteral(arg.value); } + +function refineDecimal(op: 'gt' | 'gte' | 'lt' | 'lte', value: number, messageArg: string) { + return `.refine(v => { + try { + return new Decimal(v.toString()).${op}(${value}); + } catch { + return false; + } + }${messageArg})`; +} diff --git a/packages/testtools/src/package.template.json b/packages/testtools/src/package.template.json index 5d46d1d79..b8557bc7b 100644 --- a/packages/testtools/src/package.template.json +++ b/packages/testtools/src/package.template.json @@ -15,6 +15,7 @@ "prisma": "^4.8.0", "typescript": "^4.9.3", "zenstack": "file:/packages/schema/dist", - "zod": "3.21.1" + "zod": "3.21.1", + "decimal.js": "^10.4.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39e590778..ae3990d4d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,7 +124,7 @@ importers: version: 0.2.1 ts-jest: specifier: ^29.0.5 - version: 29.0.5(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5) + version: 29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5) typescript: specifier: ^4.9.5 version: 4.9.5 @@ -874,13 +874,13 @@ importers: version: 3.0.3 next: specifier: ^12.3.1 - version: 12.3.1(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0) + version: 12.3.1(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0) tmp: specifier: ^0.2.1 version: 0.2.1 ts-jest: specifier: ^29.0.1 - version: 29.0.1(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.6.2) + version: 29.0.1(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.6.2) ts-node: specifier: ^10.9.1 version: 10.9.1(@types/node@18.0.0)(typescript@4.6.2) @@ -7672,7 +7672,7 @@ packages: pretty-format: 29.5.0 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.1(@types/node@18.0.0)(typescript@4.8.4) + ts-node: 10.9.1(@types/node@18.0.0)(typescript@4.6.2) transitivePeerDependencies: - supports-color dev: true @@ -8594,7 +8594,7 @@ packages: engines: {node: '>=10'} dev: false - /next@12.3.1(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0): + /next@12.3.1(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-l7bvmSeIwX5lp07WtIiP9u2ytZMv7jIeB8iacR28PuUEFG5j0HGAPnMqyG5kbZNBG2H7tRsrQ4HCjuMOPnANZw==} engines: {node: '>=12.22.0'} hasBin: true @@ -8618,7 +8618,7 @@ packages: postcss: 8.4.14 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - styled-jsx: 5.0.7(@babel/core@7.22.9)(react@18.2.0) + styled-jsx: 5.0.7(@babel/core@7.22.5)(react@18.2.0) use-sync-external-store: 1.2.0(react@18.2.0) optionalDependencies: '@next/swc-android-arm-eabi': 12.3.1 @@ -10369,7 +10369,7 @@ packages: acorn: 8.9.0 dev: true - /styled-jsx@5.0.7(@babel/core@7.22.9)(react@18.2.0): + /styled-jsx@5.0.7(@babel/core@7.22.5)(react@18.2.0): resolution: {integrity: sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA==} engines: {node: '>= 12.0.0'} peerDependencies: @@ -10382,7 +10382,7 @@ packages: babel-plugin-macros: optional: true dependencies: - '@babel/core': 7.22.9 + '@babel/core': 7.22.5 react: 18.2.0 dev: true @@ -10730,7 +10730,7 @@ packages: engines: {node: '>=10'} dev: false - /ts-jest@29.0.1(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.6.2): + /ts-jest@29.0.1(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.6.2): resolution: {integrity: sha512-htQOHshgvhn93QLxrmxpiQPk69+M1g7govO1g6kf6GsjCv4uvRV0znVmDrrvjUrVCnTYeY4FBxTYYYD4airyJA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -10751,7 +10751,7 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.22.9 + '@babel/core': 7.22.5 bs-logger: 0.2.6 esbuild: 0.18.13 fast-json-stable-stringify: 2.1.0 @@ -10800,7 +10800,7 @@ packages: yargs-parser: 21.1.1 dev: true - /ts-jest@29.0.5(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5): + /ts-jest@29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.4): resolution: {integrity: sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -10821,7 +10821,7 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.22.5 + '@babel/core': 7.22.9 bs-logger: 0.2.6 esbuild: 0.18.13 fast-json-stable-stringify: 2.1.0 @@ -10831,11 +10831,11 @@ packages: lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.5.3 - typescript: 4.9.5 + typescript: 4.9.4 yargs-parser: 21.1.1 dev: true - /ts-jest@29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.4): + /ts-jest@29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5): resolution: {integrity: sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -10866,7 +10866,7 @@ packages: lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.5.3 - typescript: 4.9.4 + typescript: 4.9.5 yargs-parser: 21.1.1 dev: true diff --git a/tests/integration/tests/regression/issue-657.test.ts b/tests/integration/tests/regression/issue-657.test.ts new file mode 100644 index 000000000..1d35894f1 --- /dev/null +++ b/tests/integration/tests/regression/issue-657.test.ts @@ -0,0 +1,30 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import Decimal from 'decimal.js'; + +describe('Regression: issue 657', () => { + it('regression', async () => { + const { zodSchemas } = await loadSchema(` +model Foo { + id Int @id @default(autoincrement()) + intNumber Int @gt(0) + floatNumber Float @gt(0) + decimalNumber Decimal @gt(0.1) @lte(10) +} + `); + + const schema = zodSchemas.models.FooUpdateSchema; + expect(schema.safeParse({ intNumber: 0 }).success).toBeFalsy(); + expect(schema.safeParse({ intNumber: 1 }).success).toBeTruthy(); + expect(schema.safeParse({ floatNumber: 0 }).success).toBeFalsy(); + expect(schema.safeParse({ floatNumber: 1.1 }).success).toBeTruthy(); + expect(schema.safeParse({ decimalNumber: 0 }).success).toBeFalsy(); + expect(schema.safeParse({ decimalNumber: '0' }).success).toBeFalsy(); + expect(schema.safeParse({ decimalNumber: new Decimal(0) }).success).toBeFalsy(); + expect(schema.safeParse({ decimalNumber: 11 }).success).toBeFalsy(); + expect(schema.safeParse({ decimalNumber: '11.123456789' }).success).toBeFalsy(); + expect(schema.safeParse({ decimalNumber: new Decimal('11.123456789') }).success).toBeFalsy(); + expect(schema.safeParse({ decimalNumber: 10 }).success).toBeTruthy(); + expect(schema.safeParse({ decimalNumber: '10' }).success).toBeTruthy(); + expect(schema.safeParse({ decimalNumber: new Decimal('10') }).success).toBeTruthy(); + }); +});