Skip to content

Commit

Permalink
fix: decimal field zod validation (zenstackhq#660)
Browse files Browse the repository at this point in the history
  • Loading branch information
ymc9 authored Aug 31, 2023
1 parent 6275701 commit 522df7a
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 21 deletions.
3 changes: 2 additions & 1 deletion packages/schema/src/plugins/zod/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, K extends PropertyKey> = T extends any ? (T extends object ? OmitRecursively<T, K> : T) : never;
Expand Down Expand Up @@ -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
Expand Down
19 changes: 15 additions & 4 deletions packages/schema/src/plugins/zod/utils/schema-gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>(attr, 'message');
Expand Down Expand Up @@ -70,28 +71,28 @@ export function makeFieldSchema(field: DataModelField) {
case '@gt': {
const value = getAttrLiteralArg<number>(attr, 'value');
if (value !== undefined) {
schema += `.gt(${value}${messageArg})`;
schema += isDecimal ? refineDecimal('gt', value, messageArg) : `.gt(${value}${messageArg})`;
}
break;
}
case '@gte': {
const value = getAttrLiteralArg<number>(attr, 'value');
if (value !== undefined) {
schema += `.gte(${value}${messageArg})`;
schema += isDecimal ? refineDecimal('gte', value, messageArg) : `.gte(${value}${messageArg})`;
}
break;
}
case '@lt': {
const value = getAttrLiteralArg<number>(attr, 'value');
if (value !== undefined) {
schema += `.lt(${value}${messageArg})`;
schema += isDecimal ? refineDecimal('lt', value, messageArg) : `.lt(${value}${messageArg})`;
}
break;
}
case '@lte': {
const value = getAttrLiteralArg<number>(attr, 'value');
if (value !== undefined) {
schema += `.lte(${value}${messageArg})`;
schema += isDecimal ? refineDecimal('lte', value, messageArg) : `.lte(${value}${messageArg})`;
}
break;
}
Expand Down Expand Up @@ -182,3 +183,13 @@ function getAttrLiteralArg<T extends string | number>(attr: DataModelFieldAttrib
const arg = attr.args.find((arg) => arg.$resolvedParam?.name === paramName);
return arg && getLiteral<T>(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})`;
}
3 changes: 2 additions & 1 deletion packages/testtools/src/package.template.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"prisma": "^4.8.0",
"typescript": "^4.9.3",
"zenstack": "file:<root>/packages/schema/dist",
"zod": "3.21.1"
"zod": "3.21.1",
"decimal.js": "^10.4.2"
}
}
30 changes: 15 additions & 15 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions tests/integration/tests/regression/issue-657.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});

0 comments on commit 522df7a

Please sign in to comment.