Skip to content

Commit 522df7a

Browse files
authored
fix: decimal field zod validation (#660)
1 parent 6275701 commit 522df7a

File tree

5 files changed

+64
-21
lines changed

5 files changed

+64
-21
lines changed

packages/schema/src/plugins/zod/generator.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ async function generateCommonSchemas(project: Project, output: string) {
119119
path.join(output, 'common', 'index.ts'),
120120
`
121121
import { z } from 'zod';
122-
export const DecimalSchema = z.union([z.number(), z.string(), z.object({d: z.number().array(), e: z.number(), s: z.number()})]);
122+
export const DecimalSchema = z.union([z.number(), z.string(), z.object({d: z.number().array(), e: z.number(), s: z.number()}).passthrough()]);
123123
124124
// https://stackoverflow.com/a/54487392/20415796
125125
type OmitDistributive<T, K extends PropertyKey> = T extends any ? (T extends object ? OmitRecursively<T, K> : T) : never;
@@ -236,6 +236,7 @@ async function generateModelSchema(model: DataModel, project: Project, output: s
236236
// import Decimal
237237
if (fields.some((field) => field.type.type === 'Decimal')) {
238238
writer.writeLine(`import { DecimalSchema } from '../common';`);
239+
writer.writeLine(`import { Decimal } from 'decimal.js';`);
239240
}
240241

241242
// create base schema

packages/schema/src/plugins/zod/utils/schema-gen.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99

1010
export function makeFieldSchema(field: DataModelField) {
1111
let schema = makeZodSchema(field);
12+
const isDecimal = field.type.type === 'Decimal';
1213

1314
for (const attr of field.attributes) {
1415
const message = getAttrLiteralArg<string>(attr, 'message');
@@ -70,28 +71,28 @@ export function makeFieldSchema(field: DataModelField) {
7071
case '@gt': {
7172
const value = getAttrLiteralArg<number>(attr, 'value');
7273
if (value !== undefined) {
73-
schema += `.gt(${value}${messageArg})`;
74+
schema += isDecimal ? refineDecimal('gt', value, messageArg) : `.gt(${value}${messageArg})`;
7475
}
7576
break;
7677
}
7778
case '@gte': {
7879
const value = getAttrLiteralArg<number>(attr, 'value');
7980
if (value !== undefined) {
80-
schema += `.gte(${value}${messageArg})`;
81+
schema += isDecimal ? refineDecimal('gte', value, messageArg) : `.gte(${value}${messageArg})`;
8182
}
8283
break;
8384
}
8485
case '@lt': {
8586
const value = getAttrLiteralArg<number>(attr, 'value');
8687
if (value !== undefined) {
87-
schema += `.lt(${value}${messageArg})`;
88+
schema += isDecimal ? refineDecimal('lt', value, messageArg) : `.lt(${value}${messageArg})`;
8889
}
8990
break;
9091
}
9192
case '@lte': {
9293
const value = getAttrLiteralArg<number>(attr, 'value');
9394
if (value !== undefined) {
94-
schema += `.lte(${value}${messageArg})`;
95+
schema += isDecimal ? refineDecimal('lte', value, messageArg) : `.lte(${value}${messageArg})`;
9596
}
9697
break;
9798
}
@@ -182,3 +183,13 @@ function getAttrLiteralArg<T extends string | number>(attr: DataModelFieldAttrib
182183
const arg = attr.args.find((arg) => arg.$resolvedParam?.name === paramName);
183184
return arg && getLiteral<T>(arg.value);
184185
}
186+
187+
function refineDecimal(op: 'gt' | 'gte' | 'lt' | 'lte', value: number, messageArg: string) {
188+
return `.refine(v => {
189+
try {
190+
return new Decimal(v.toString()).${op}(${value});
191+
} catch {
192+
return false;
193+
}
194+
}${messageArg})`;
195+
}

packages/testtools/src/package.template.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"prisma": "^4.8.0",
1616
"typescript": "^4.9.3",
1717
"zenstack": "file:<root>/packages/schema/dist",
18-
"zod": "3.21.1"
18+
"zod": "3.21.1",
19+
"decimal.js": "^10.4.2"
1920
}
2021
}

pnpm-lock.yaml

Lines changed: 15 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { loadSchema } from '@zenstackhq/testtools';
2+
import Decimal from 'decimal.js';
3+
4+
describe('Regression: issue 657', () => {
5+
it('regression', async () => {
6+
const { zodSchemas } = await loadSchema(`
7+
model Foo {
8+
id Int @id @default(autoincrement())
9+
intNumber Int @gt(0)
10+
floatNumber Float @gt(0)
11+
decimalNumber Decimal @gt(0.1) @lte(10)
12+
}
13+
`);
14+
15+
const schema = zodSchemas.models.FooUpdateSchema;
16+
expect(schema.safeParse({ intNumber: 0 }).success).toBeFalsy();
17+
expect(schema.safeParse({ intNumber: 1 }).success).toBeTruthy();
18+
expect(schema.safeParse({ floatNumber: 0 }).success).toBeFalsy();
19+
expect(schema.safeParse({ floatNumber: 1.1 }).success).toBeTruthy();
20+
expect(schema.safeParse({ decimalNumber: 0 }).success).toBeFalsy();
21+
expect(schema.safeParse({ decimalNumber: '0' }).success).toBeFalsy();
22+
expect(schema.safeParse({ decimalNumber: new Decimal(0) }).success).toBeFalsy();
23+
expect(schema.safeParse({ decimalNumber: 11 }).success).toBeFalsy();
24+
expect(schema.safeParse({ decimalNumber: '11.123456789' }).success).toBeFalsy();
25+
expect(schema.safeParse({ decimalNumber: new Decimal('11.123456789') }).success).toBeFalsy();
26+
expect(schema.safeParse({ decimalNumber: 10 }).success).toBeTruthy();
27+
expect(schema.safeParse({ decimalNumber: '10' }).success).toBeTruthy();
28+
expect(schema.safeParse({ decimalNumber: new Decimal('10') }).success).toBeTruthy();
29+
});
30+
});

0 commit comments

Comments
 (0)