Skip to content

Commit 8fbe27d

Browse files
authored
feat: implement field validation (#290)
* feat: implement field validation * update * update * update
1 parent f1a8cef commit 8fbe27d

File tree

20 files changed

+940
-95
lines changed

20 files changed

+940
-95
lines changed
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

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: Any, _ 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: Any, _ 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: Any, _ 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: Any, _ message: String?) @@@targetField([IntField, FloatField, DecimalField, BigIntField]) @@@validation
562562

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

packages/runtime/src/client/contract.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 GetModels, type IsDelegateModel, type ProcedureDef, type SchemaDef } from '../schema';
33
import type { AuthType } from '../schema/auth';
44
import type { OrUndefinedIf, Simplify, UnwrapTuplePromises } from '../utils/type-utils';

packages/runtime/src/client/crud/operations/base.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -131,15 +131,10 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
131131
model: GetModels<Schema>,
132132
filter: any,
133133
): Promise<unknown | undefined> {
134-
const idFields = requireIdFields(this.schema, model);
135-
const _filter = flattenCompoundUniqueFilters(this.schema, model, filter);
136-
const query = kysely
137-
.selectFrom(model)
138-
.where((eb) => eb.and(_filter))
139-
.select(idFields.map((f) => kysely.dynamic.ref(f)))
140-
.limit(1)
141-
.modifyEnd(this.makeContextComment({ model, operation: 'read' }));
142-
return this.executeQueryTakeFirst(kysely, query, 'exists');
134+
return this.readUnique(kysely, model, {
135+
where: filter,
136+
select: this.makeIdSelect(model),
137+
});
143138
}
144139

145140
protected async read(

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

Lines changed: 53 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,18 @@ import stableStringify from 'json-stable-stringify';
44
import { match, P } from 'ts-pattern';
55
import { z, ZodSchema, ZodType } from 'zod';
66
import {
7+
type AttributeApplication,
78
type BuiltinType,
89
type EnumDef,
910
type FieldDef,
1011
type GetModels,
1112
type ModelDef,
1213
type SchemaDef,
13-
} from '../../schema';
14-
import { enumerate } from '../../utils/enumerate';
15-
import { extractFields } from '../../utils/object-utils';
16-
import { formatError } from '../../utils/zod-utils';
17-
import { AGGREGATE_OPERATORS, LOGICAL_COMBINATORS, NUMERIC_FIELD_TYPES } from '../constants';
14+
} from '../../../schema';
15+
import { enumerate } from '../../../utils/enumerate';
16+
import { extractFields } from '../../../utils/object-utils';
17+
import { formatError } from '../../../utils/zod-utils';
18+
import { AGGREGATE_OPERATORS, LOGICAL_COMBINATORS, NUMERIC_FIELD_TYPES } from '../../constants';
1819
import {
1920
type AggregateArgs,
2021
type CountArgs,
@@ -29,16 +30,23 @@ import {
2930
type UpdateManyAndReturnArgs,
3031
type UpdateManyArgs,
3132
type UpsertArgs,
32-
} from '../crud-types';
33-
import { InputValidationError, InternalError } from '../errors';
33+
} from '../../crud-types';
34+
import { InputValidationError, InternalError } from '../../errors';
3435
import {
3536
fieldHasDefaultValue,
3637
getDiscriminatorField,
3738
getEnum,
3839
getUniqueFields,
3940
requireField,
4041
requireModel,
41-
} from '../query-utils';
42+
} from '../../query-utils';
43+
import {
44+
addBigIntValidation,
45+
addCustomValidation,
46+
addDecimalValidation,
47+
addNumberValidation,
48+
addStringValidation,
49+
} from './utils';
4250

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

@@ -191,11 +199,14 @@ export class InputValidator<Schema extends SchemaDef> {
191199
schema = getSchema(model, options);
192200
this.schemaCache.set(cacheKey!, schema);
193201
}
194-
const { error } = schema.safeParse(args);
202+
const { error, data } = schema.safeParse(args);
195203
if (error) {
196-
throw new InputValidationError(`Invalid ${operation} args: ${formatError(error)}`, error);
204+
throw new InputValidationError(
205+
`Invalid ${operation} args for model "${model}": ${formatError(error)}`,
206+
error,
207+
);
197208
}
198-
return args as T;
209+
return data as T;
199210
}
200211

201212
// #region Find
@@ -235,17 +246,28 @@ export class InputValidator<Schema extends SchemaDef> {
235246
return result;
236247
}
237248

238-
private makePrimitiveSchema(type: string) {
249+
private makePrimitiveSchema(type: string, attributes?: AttributeApplication[]) {
239250
if (this.schema.typeDefs && type in this.schema.typeDefs) {
240251
return this.makeTypeDefSchema(type);
241252
} else {
242253
return match(type)
243-
.with('String', () => z.string())
244-
.with('Int', () => z.number().int())
245-
.with('Float', () => z.number())
254+
.with('String', () => addStringValidation(z.string(), attributes))
255+
.with('Int', () => addNumberValidation(z.number().int(), attributes))
256+
.with('Float', () => addNumberValidation(z.number(), attributes))
246257
.with('Boolean', () => z.boolean())
247-
.with('BigInt', () => z.union([z.number().int(), z.bigint()]))
248-
.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+
)
249271
.with('DateTime', () => z.union([z.date(), z.string().datetime()]))
250272
.with('Bytes', () => z.instanceof(Uint8Array))
251273
.otherwise(() => z.unknown());
@@ -860,7 +882,7 @@ export class InputValidator<Schema extends SchemaDef> {
860882
uncheckedVariantFields[field] = fieldSchema;
861883
}
862884
} else {
863-
let fieldSchema: ZodType = this.makePrimitiveSchema(fieldDef.type);
885+
let fieldSchema: ZodType = this.makePrimitiveSchema(fieldDef.type, fieldDef.attributes);
864886

865887
if (fieldDef.array) {
866888
fieldSchema = z
@@ -889,14 +911,17 @@ export class InputValidator<Schema extends SchemaDef> {
889911
}
890912
});
891913

914+
const uncheckedCreateSchema = addCustomValidation(z.strictObject(uncheckedVariantFields), modelDef.attributes);
915+
const checkedCreateSchema = addCustomValidation(z.strictObject(checkedVariantFields), modelDef.attributes);
916+
892917
if (!hasRelation) {
893-
return this.orArray(z.strictObject(uncheckedVariantFields), canBeArray);
918+
return this.orArray(uncheckedCreateSchema, canBeArray);
894919
} else {
895920
return z.union([
896-
z.strictObject(uncheckedVariantFields),
897-
z.strictObject(checkedVariantFields),
898-
...(canBeArray ? [z.array(z.strictObject(uncheckedVariantFields))] : []),
899-
...(canBeArray ? [z.array(z.strictObject(checkedVariantFields))] : []),
921+
uncheckedCreateSchema,
922+
checkedCreateSchema,
923+
...(canBeArray ? [z.array(uncheckedCreateSchema)] : []),
924+
...(canBeArray ? [z.array(checkedCreateSchema)] : []),
900925
]);
901926
}
902927
}
@@ -1112,7 +1137,7 @@ export class InputValidator<Schema extends SchemaDef> {
11121137
uncheckedVariantFields[field] = fieldSchema;
11131138
}
11141139
} else {
1115-
let fieldSchema: ZodType = this.makePrimitiveSchema(fieldDef.type).optional();
1140+
let fieldSchema: ZodType = this.makePrimitiveSchema(fieldDef.type, fieldDef.attributes).optional();
11161141

11171142
if (this.isNumericField(fieldDef)) {
11181143
fieldSchema = z.union([
@@ -1161,10 +1186,12 @@ export class InputValidator<Schema extends SchemaDef> {
11611186
}
11621187
});
11631188

1189+
const uncheckedUpdateSchema = addCustomValidation(z.strictObject(uncheckedVariantFields), modelDef.attributes);
1190+
const checkedUpdateSchema = addCustomValidation(z.strictObject(checkedVariantFields), modelDef.attributes);
11641191
if (!hasRelation) {
1165-
return z.strictObject(uncheckedVariantFields);
1192+
return uncheckedUpdateSchema;
11661193
} else {
1167-
return z.union([z.strictObject(uncheckedVariantFields), z.strictObject(checkedVariantFields)]);
1194+
return z.union([uncheckedUpdateSchema, checkedUpdateSchema]);
11681195
}
11691196
}
11701197

0 commit comments

Comments
 (0)