Skip to content

Commit c52b1df

Browse files
committed
update
1 parent 9f5e0a7 commit c52b1df

File tree

12 files changed

+231
-70
lines changed

12 files changed

+231
-70
lines changed

packages/language/res/stdlib.zmodel

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -543,22 +543,22 @@ attribute @upper() @@@targetField([StringField]) @@@validation
543543
/**
544544
* Validates a number field is greater than the given value.
545545
*/
546-
attribute @gt(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation
546+
attribute @gt(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField, BigIntField]) @@@validation
547547

548548
/**
549549
* Validates a number field is greater than or equal to the given value.
550550
*/
551-
attribute @gte(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation
551+
attribute @gte(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField, BigIntField]) @@@validation
552552

553553
/**
554554
* Validates a number field is less than the given value.
555555
*/
556-
attribute @lt(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation
556+
attribute @lt(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField, BigIntField]) @@@validation
557557

558558
/**
559559
* Validates a number field is less than or equal to the given value.
560560
*/
561-
attribute @lte(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation
561+
attribute @lte(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField, BigIntField]) @@@validation
562562

563563
/**
564564
* Validates the entity with a complex condition.

packages/runtime/src/client/crud/dialects/postgresql.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { invariant } from '@zenstackhq/common-helpers';
2-
import Decimal from 'decimal.js';
2+
import { Decimal } from 'decimal.js';
33
import {
44
sql,
55
type Expression,

packages/runtime/src/client/crud/dialects/sqlite.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { invariant } from '@zenstackhq/common-helpers';
2-
import Decimal from 'decimal.js';
2+
import { Decimal } from 'decimal.js';
33
import {
44
ExpressionWrapper,
55
sql,

packages/runtime/src/client/crud/validator/index.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { invariant } from '@zenstackhq/common-helpers';
2-
import Decimal from 'decimal.js';
2+
import { Decimal } from 'decimal.js';
33
import stableStringify from 'json-stable-stringify';
44
import { match, P } from 'ts-pattern';
55
import { z, ZodSchema, ZodType } from 'zod';
@@ -40,7 +40,13 @@ import {
4040
requireField,
4141
requireModel,
4242
} from '../../query-utils';
43-
import { addCustomValidation, addNumberValidation, addStringValidation } from './utils';
43+
import {
44+
addBigIntValidation,
45+
addCustomValidation,
46+
addDecimalValidation,
47+
addNumberValidation,
48+
addStringValidation,
49+
} from './utils';
4450

4551
type GetSchemaFunc<Schema extends SchemaDef, Options> = (model: GetModels<Schema>, options: Options) => ZodType;
4652

@@ -247,10 +253,21 @@ export class InputValidator<Schema extends SchemaDef> {
247253
return match(type)
248254
.with('String', () => addStringValidation(z.string(), attributes))
249255
.with('Int', () => addNumberValidation(z.number().int(), attributes))
250-
.with('Float', () => z.number())
256+
.with('Float', () => addNumberValidation(z.number(), attributes))
251257
.with('Boolean', () => z.boolean())
252-
.with('BigInt', () => z.union([z.number().int(), z.bigint()]))
253-
.with('Decimal', () => z.union([z.number(), z.instanceof(Decimal), z.string()]))
258+
.with('BigInt', () =>
259+
z.union([
260+
addNumberValidation(z.number().int(), attributes),
261+
addBigIntValidation(z.bigint(), attributes),
262+
]),
263+
)
264+
.with('Decimal', () =>
265+
z.union([
266+
addNumberValidation(z.number(), attributes),
267+
addDecimalValidation(z.instanceof(Decimal), attributes),
268+
addDecimalValidation(z.string(), attributes),
269+
]),
270+
)
254271
.with('DateTime', () => z.union([z.date(), z.string().datetime()]))
255272
.with('Bytes', () => z.instanceof(Uint8Array))
256273
.otherwise(() => z.unknown());

packages/runtime/src/client/crud/validator/utils.ts

Lines changed: 111 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
MemberExpression,
99
UnaryExpression,
1010
} from '@zenstackhq/sdk/schema';
11+
import { Decimal } from 'decimal.js';
1112
import { match, P } from 'ts-pattern';
1213
import { z } from 'zod';
1314
import { ExpressionUtils } from '../../../schema';
@@ -25,95 +26,180 @@ export function addStringValidation(schema: z.ZodString, attributes: AttributeAp
2526
return schema;
2627
}
2728

29+
let result = schema;
2830
for (const attr of attributes) {
2931
match(attr.name)
3032
.with('@length', () => {
3133
const min = getArgValue<number>(attr.args?.[0]?.value);
3234
if (min !== undefined) {
33-
schema = schema.min(min);
35+
result = result.min(min);
3436
}
3537
const max = getArgValue<number>(attr.args?.[1]?.value);
3638
if (max !== undefined) {
37-
schema = schema.max(max);
39+
result = result.max(max);
3840
}
3941
})
4042
.with('@startsWith', () => {
4143
const value = getArgValue<string>(attr.args?.[0]?.value);
4244
if (value !== undefined) {
43-
schema = schema.startsWith(value);
45+
result = result.startsWith(value);
4446
}
4547
})
4648
.with('@endsWith', () => {
4749
const value = getArgValue<string>(attr.args?.[0]?.value);
4850
if (value !== undefined) {
49-
schema = schema.endsWith(value);
51+
result = result.endsWith(value);
5052
}
5153
})
5254
.with('@contains', () => {
5355
const value = getArgValue<string>(attr.args?.[0]?.value);
5456
if (value !== undefined) {
55-
schema = schema.includes(value);
57+
result = result.includes(value);
5658
}
5759
})
5860
.with('@regex', () => {
5961
const pattern = getArgValue<string>(attr.args?.[0]?.value);
6062
if (pattern !== undefined) {
61-
schema = schema.regex(new RegExp(pattern));
63+
result = result.regex(new RegExp(pattern));
6264
}
6365
})
6466
.with('@email', () => {
65-
schema = schema.email();
67+
result = result.email();
6668
})
6769
.with('@datetime', () => {
68-
schema = schema.datetime();
70+
result = result.datetime();
6971
})
7072
.with('@url', () => {
71-
schema = schema.url();
73+
result = result.url();
7274
})
7375
.with('@trim', () => {
74-
schema = schema.trim();
76+
result = result.trim();
7577
})
7678
.with('@lower', () => {
77-
schema = schema.toLowerCase();
79+
result = result.toLowerCase();
7880
})
7981
.with('@upper', () => {
80-
schema = schema.toUpperCase();
82+
result = result.toUpperCase();
8183
});
8284
}
83-
return schema;
85+
return result;
8486
}
8587

8688
export function addNumberValidation(schema: z.ZodNumber, attributes: AttributeApplication[] | undefined): z.ZodSchema {
8789
if (!attributes || attributes.length === 0) {
8890
return schema;
8991
}
9092

93+
let result = schema;
9194
for (const attr of attributes) {
9295
const val = getArgValue<number>(attr.args?.[0]?.value);
9396
if (val === undefined) {
9497
continue;
9598
}
9699
match(attr.name)
97100
.with('@gt', () => {
98-
schema = schema.gt(val);
101+
result = result.gt(val);
99102
})
100103
.with('@gte', () => {
101-
schema = schema.gte(val);
104+
result = result.gte(val);
102105
})
103106
.with('@lt', () => {
104-
schema = schema.lt(val);
107+
result = result.lt(val);
105108
})
106109
.with('@lte', () => {
107-
schema = schema.lte(val);
110+
result = result.lte(val);
111+
});
112+
}
113+
return result;
114+
}
115+
116+
export function addBigIntValidation(schema: z.ZodBigInt, attributes: AttributeApplication[] | undefined): z.ZodSchema {
117+
if (!attributes || attributes.length === 0) {
118+
return schema;
119+
}
120+
121+
let result = schema;
122+
for (const attr of attributes) {
123+
const val = getArgValue<number>(attr.args?.[0]?.value);
124+
if (val === undefined) {
125+
continue;
126+
}
127+
const bigIntVal = BigInt(val);
128+
match(attr.name)
129+
.with('@gt', () => {
130+
result = result.gt(bigIntVal);
131+
})
132+
.with('@gte', () => {
133+
result = result.gte(bigIntVal);
108134
})
109135
.with('@lt', () => {
110-
schema = schema.lt(val);
136+
result = result.lt(bigIntVal);
111137
})
112138
.with('@lte', () => {
113-
schema = schema.lte(val);
139+
result = result.lte(bigIntVal);
114140
});
115141
}
116-
return schema;
142+
return result;
143+
}
144+
145+
export function addDecimalValidation(
146+
schema: z.ZodType<Decimal> | z.ZodString,
147+
attributes: AttributeApplication[] | undefined,
148+
): z.ZodSchema {
149+
let result: z.ZodSchema = schema;
150+
151+
// parse string to Decimal
152+
if (schema instanceof z.ZodString) {
153+
result = schema
154+
.superRefine((v, ctx) => {
155+
try {
156+
new Decimal(v);
157+
} catch (err) {
158+
ctx.addIssue({
159+
code: z.ZodIssueCode.custom,
160+
message: `Invalid decimal: ${err}`,
161+
});
162+
}
163+
})
164+
.transform((val) => new Decimal(val));
165+
}
166+
167+
// add validations
168+
169+
function refine(schema: z.ZodSchema, op: 'gt' | 'gte' | 'lt' | 'lte', value: number) {
170+
return schema.superRefine((v, ctx) => {
171+
const base = z.number();
172+
const { error } = base[op](value).safeParse((v as Decimal).toNumber());
173+
error?.errors.forEach((e) => {
174+
ctx.addIssue(e);
175+
});
176+
});
177+
}
178+
179+
if (attributes) {
180+
for (const attr of attributes) {
181+
const val = getArgValue<number>(attr.args?.[0]?.value);
182+
if (val === undefined) {
183+
continue;
184+
}
185+
186+
match(attr.name)
187+
.with('@gt', () => {
188+
result = refine(result, 'gt', val);
189+
})
190+
.with('@gte', () => {
191+
result = refine(result, 'gte', val);
192+
})
193+
.with('@lt', () => {
194+
result = refine(result, 'lt', val);
195+
})
196+
.with('@lte', () => {
197+
result = refine(result, 'lte', val);
198+
});
199+
}
200+
}
201+
202+
return result;
117203
}
118204

119205
export function addCustomValidation(schema: z.ZodSchema, attributes: AttributeApplication[] | undefined): z.ZodSchema {
@@ -122,6 +208,7 @@ export function addCustomValidation(schema: z.ZodSchema, attributes: AttributeAp
122208
return schema;
123209
}
124210

211+
let result = schema;
125212
for (const attr of attrs) {
126213
const expr = attr.args?.[0]?.value;
127214
if (!expr) {
@@ -133,9 +220,9 @@ export function addCustomValidation(schema: z.ZodSchema, attributes: AttributeAp
133220
if (pathExpr && ExpressionUtils.isArray(pathExpr)) {
134221
path = pathExpr.items.map((e) => ExpressionUtils.getLiteralValue(e) as string);
135222
}
136-
schema = applyValidation(schema, expr, message, path);
223+
result = applyValidation(result, expr, message, path);
137224
}
138-
return schema;
225+
return result;
139226
}
140227

141228
function applyValidation(
@@ -245,10 +332,10 @@ function evalCall(data: any, expr: CallExpression) {
245332

246333
const min = getArgValue<number>(expr.args?.[1]);
247334
const max = getArgValue<number>(expr.args?.[2]);
248-
if (min && fieldArg.length < min) {
335+
if (min !== undefined && fieldArg.length < min) {
249336
return false;
250337
}
251-
if (max && fieldArg.length > max) {
338+
if (max !== undefined && fieldArg.length > max) {
252339
return false;
253340
}
254341
return true;

packages/runtime/src/client/query-builder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type Decimal from 'decimal.js';
1+
import { type Decimal } from 'decimal.js';
22
import type { Generated, Kysely } from 'kysely';
33
import type {
44
FieldHasDefault,

packages/runtime/src/utils/type-utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type Decimal from 'decimal.js';
1+
import type { Decimal } from 'decimal.js';
22

33
export type Optional<T extends object, K extends keyof T = keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
44

packages/sdk/src/schema/schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type Decimal from 'decimal.js';
1+
import type { Decimal } from 'decimal.js';
22
import type { Expression } from './expression';
33

44
export type DataSourceProviderType = 'sqlite' | 'postgresql';

tests/e2e/orm/client-api/type-coverage.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import Decimal from 'decimal.js';
2-
import { describe, expect, it } from 'vitest';
31
import { createTestClient, getTestDbProvider } from '@zenstackhq/testtools';
2+
import { Decimal } from 'decimal.js';
3+
import { describe, expect, it } from 'vitest';
44

55
describe('Zmodel type coverage tests', () => {
66
it('supports all types - plain', async () => {

tests/e2e/orm/validation/custom-validation.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,13 @@ describe('Custom validation tests', () => {
3939
{ provider: 'postgresql' },
4040
);
4141

42-
await db.foo.create({ data: { id: 1 } });
42+
await db.foo.create({ data: { id: 100 } });
4343

4444
for (const action of ['create', 'update']) {
4545
const _t =
4646
action === 'create'
47-
? (data: any) => db.foo.create({ data: { id: 2, ...data } })
48-
: (data: any) => db.foo.update({ where: { id: 1 }, data });
47+
? (data: any) => db.foo.create({ data })
48+
: (data: any) => db.foo.update({ where: { id: 100 }, data });
4949
// violates length
5050
await expect(_t({ str1: '[email protected]' })).toBeRejectedByValidation(['invalid fields']);
5151
await expect(_t({ str1: '[email protected]' })).toBeRejectedByValidation(['invalid fields']);
@@ -91,7 +91,7 @@ describe('Custom validation tests', () => {
9191
thrown = true;
9292
expect((err as any).cause.issues[0].path).toEqual(['data', 'x', 'y']);
9393
}
94-
expect(thrown);
94+
expect(thrown).toBe(true);
9595

9696
// satisfies all
9797
await expect(

0 commit comments

Comments
 (0)