Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,42 @@
"version": "2.0.0",
"tasks": [
{
"label": "Build z-model-language",
"command": "npm run langium:generate && npm run build",
"label": "Build all",
"command": "pnpm build",
"type": "shell",
"group": {
"kind": "build",
"isDefault": true
},
"detail": "Langium: Generate grammar and build the z-model-language language",
"icon": {
"color": "terminal.ansiGreen",
"id": "server-process"
}
},
{
"label": "Build all - watch",
"command": "pnpm watch",
"type": "shell",
"group": {
"kind": "build"
},
"icon": {
"color": "terminal.ansiBlue",
"id": "server-process"
}
},
{
"label": "Test all",
"command": "pnpm test",
"type": "shell",
"group": {
"kind": "test",
"isDefault": true
},
"icon": {
"color": "terminal.ansiMagenta",
"id": "server-process"
}
}
]
}
78 changes: 39 additions & 39 deletions packages/runtime/src/client/crud/validator.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Decimal from 'decimal.js';
import stableStringify from 'json-stable-stringify';
import { match, P } from 'ts-pattern';
import { z, ZodSchema } from 'zod';
import { z, ZodType } from 'zod/v4';
import type {
BuiltinType,
EnumDef,
Expand Down Expand Up @@ -38,10 +38,10 @@ import {
type GetSchemaFunc<Schema extends SchemaDef, Options> = (
model: GetModels<Schema>,
options: Options
) => ZodSchema;
) => ZodType;

export class InputValidator<Schema extends SchemaDef> {
private schemaCache = new Map<string, ZodSchema>();
private schemaCache = new Map<string, ZodType>();

constructor(private readonly schema: Schema) {}

Expand Down Expand Up @@ -246,7 +246,7 @@ export class InputValidator<Schema extends SchemaDef> {
).optional();
}

let result: ZodSchema = z.object(fields).strict();
let result: ZodType = z.object(fields).strict();
result = this.refineForSelectIncludeMutuallyExclusive(result);
result = this.refineForSelectOmitMutuallyExclusive(result);

Expand Down Expand Up @@ -275,7 +275,7 @@ export class InputValidator<Schema extends SchemaDef> {
model: string,
unique: boolean,
withoutRelationFields = false
): ZodSchema {
): ZodType {
const modelDef = getModel(this.schema, model);
if (!modelDef) {
throw new QueryError(`Model "${model}" not found`);
Expand All @@ -284,7 +284,7 @@ export class InputValidator<Schema extends SchemaDef> {
const fields: Record<string, any> = {};
for (const field of Object.keys(modelDef.fields)) {
const fieldDef = requireField(this.schema, model, field);
let fieldSchema: ZodSchema | undefined;
let fieldSchema: ZodType | undefined;

if (fieldDef.relation) {
if (withoutRelationFields) {
Expand Down Expand Up @@ -374,7 +374,7 @@ export class InputValidator<Schema extends SchemaDef> {
}

// expression builder
fields['$expr'] = z.function().optional();
fields['$expr'] = z.custom((v) => typeof v === 'function').optional();

// logical operators
fields['AND'] = this.orArray(
Expand All @@ -397,7 +397,7 @@ export class InputValidator<Schema extends SchemaDef> {
).optional();

const baseWhere = z.object(fields).strict();
let result: ZodSchema = baseWhere;
let result: ZodType = baseWhere;

if (unique) {
// requires at least one unique field (field set) is required
Expand Down Expand Up @@ -471,15 +471,15 @@ export class InputValidator<Schema extends SchemaDef> {
.exhaustive();
}

private makeDateTimeFilterSchema(optional: boolean): ZodSchema {
private makeDateTimeFilterSchema(optional: boolean): ZodType {
return this.makeCommonPrimitiveFilterSchema(
z.union([z.string().datetime(), z.date()]),
optional,
() => z.lazy(() => this.makeDateTimeFilterSchema(optional))
);
}

private makeBooleanFilterSchema(optional: boolean): ZodSchema {
private makeBooleanFilterSchema(optional: boolean): ZodType {
return z.union([
this.nullableIf(z.boolean(), optional),
z.object({
Expand All @@ -491,7 +491,7 @@ export class InputValidator<Schema extends SchemaDef> {
]);
}

private makeBytesFilterSchema(optional: boolean): ZodSchema {
private makeBytesFilterSchema(optional: boolean): ZodType {
const baseSchema = z.instanceof(Uint8Array);
const components = this.makeCommonPrimitiveFilterComponents(
baseSchema,
Expand All @@ -510,9 +510,9 @@ export class InputValidator<Schema extends SchemaDef> {
}

private makeCommonPrimitiveFilterComponents(
baseSchema: ZodSchema,
baseSchema: ZodType,
optional: boolean,
makeThis: () => ZodSchema
makeThis: () => ZodType
) {
return {
equals: this.nullableIf(baseSchema.optional(), optional),
Expand All @@ -528,9 +528,9 @@ export class InputValidator<Schema extends SchemaDef> {
}

private makeCommonPrimitiveFilterSchema(
baseSchema: ZodSchema,
baseSchema: ZodType,
optional: boolean,
makeThis: () => ZodSchema
makeThis: () => ZodType
) {
return z.union([
this.nullableIf(baseSchema, optional),
Expand All @@ -545,23 +545,23 @@ export class InputValidator<Schema extends SchemaDef> {
}

private makeNumberFilterSchema(
baseSchema: ZodSchema,
baseSchema: ZodType,
optional: boolean
): ZodSchema {
): ZodType {
return this.makeCommonPrimitiveFilterSchema(baseSchema, optional, () =>
z.lazy(() => this.makeNumberFilterSchema(baseSchema, optional))
);
}

private makeStringFilterSchema(optional: boolean): ZodSchema {
private makeStringFilterSchema(optional: boolean): ZodType {
return this.makeCommonPrimitiveFilterSchema(z.string(), optional, () =>
z.lazy(() => this.makeStringFilterSchema(optional))
);
}

private makeSelectSchema(model: string) {
const modelDef = requireModel(this.schema, model);
const fields: Record<string, ZodSchema> = {};
const fields: Record<string, ZodType> = {};
for (const field of Object.keys(modelDef.fields)) {
const fieldDef = requireField(this.schema, model, field);
if (fieldDef.relation) {
Expand Down Expand Up @@ -612,7 +612,7 @@ export class InputValidator<Schema extends SchemaDef> {
])
.optional(),
}),
{} as Record<string, ZodSchema>
{} as Record<string, ZodType>
)
),
])
Expand All @@ -624,7 +624,7 @@ export class InputValidator<Schema extends SchemaDef> {

private makeOmitSchema(model: string) {
const modelDef = requireModel(this.schema, model);
const fields: Record<string, ZodSchema> = {};
const fields: Record<string, ZodType> = {};
for (const field of Object.keys(modelDef.fields)) {
const fieldDef = requireField(this.schema, model, field);
if (!fieldDef.relation) {
Expand All @@ -636,7 +636,7 @@ export class InputValidator<Schema extends SchemaDef> {

private makeIncludeSchema(model: string) {
const modelDef = requireModel(this.schema, model);
const fields: Record<string, ZodSchema> = {};
const fields: Record<string, ZodType> = {};
for (const field of Object.keys(modelDef.fields)) {
const fieldDef = requireField(this.schema, model, field);
if (fieldDef.relation) {
Expand Down Expand Up @@ -674,7 +674,7 @@ export class InputValidator<Schema extends SchemaDef> {
WithAggregation: boolean
) {
const modelDef = requireModel(this.schema, model);
const fields: Record<string, ZodSchema> = {};
const fields: Record<string, ZodType> = {};
const sort = z.union([z.literal('asc'), z.literal('desc')]);
for (const field of Object.keys(modelDef.fields)) {
const fieldDef = requireField(this.schema, model, field);
Expand Down Expand Up @@ -815,7 +815,7 @@ export class InputValidator<Schema extends SchemaDef> {
}
}

let fieldSchema: ZodSchema = z.lazy(() =>
let fieldSchema: ZodType = z.lazy(() =>
this.makeRelationManipulationSchema(
fieldDef,
excludeFields,
Expand Down Expand Up @@ -848,7 +848,7 @@ export class InputValidator<Schema extends SchemaDef> {
}
regularAndRelationFields[field] = fieldSchema;
} else {
let fieldSchema: ZodSchema = this.makePrimitiveSchema(
let fieldSchema: ZodType = this.makePrimitiveSchema(
fieldDef.type
);

Expand Down Expand Up @@ -904,7 +904,7 @@ export class InputValidator<Schema extends SchemaDef> {
) {
const fieldType = fieldDef.type;
const array = !!fieldDef.array;
const fields: Record<string, ZodSchema> = {
const fields: Record<string, ZodType> = {
create: this.makeCreateDataSchema(
fieldDef.type,
!!fieldDef.array,
Expand Down Expand Up @@ -1168,7 +1168,7 @@ export class InputValidator<Schema extends SchemaDef> {
excludeFields.push(...oppositeFieldDef.relation.fields);
}
}
let fieldSchema: ZodSchema = z
let fieldSchema: ZodType = z
.lazy(() =>
this.makeRelationManipulationSchema(
fieldDef,
Expand All @@ -1183,7 +1183,7 @@ export class InputValidator<Schema extends SchemaDef> {
}
regularAndRelationFields[field] = fieldSchema;
} else {
let fieldSchema: ZodSchema = this.makePrimitiveSchema(
let fieldSchema: ZodType = this.makePrimitiveSchema(
fieldDef.type
).optional();

Expand Down Expand Up @@ -1304,7 +1304,7 @@ export class InputValidator<Schema extends SchemaDef> {
...Object.keys(modelDef.fields).reduce((acc, field) => {
acc[field] = z.literal(true).optional();
return acc;
}, {} as Record<string, ZodSchema>),
}, {} as Record<string, ZodType>),
})
.strict(),
]);
Expand Down Expand Up @@ -1343,7 +1343,7 @@ export class InputValidator<Schema extends SchemaDef> {
acc[field] = z.literal(true).optional();
}
return acc;
}, {} as Record<string, ZodSchema>)
}, {} as Record<string, ZodType>)
);
}

Expand All @@ -1356,7 +1356,7 @@ export class InputValidator<Schema extends SchemaDef> {
acc[field] = z.literal(true).optional();
}
return acc;
}, {} as Record<string, ZodSchema>)
}, {} as Record<string, ZodType>)
);
}

Expand All @@ -1366,14 +1366,14 @@ export class InputValidator<Schema extends SchemaDef> {
(field) => !modelDef.fields[field]?.relation
);

let schema: ZodSchema = z
let schema = z
.object({
where: this.makeWhereSchema(model, false).optional(),
orderBy: this.orArray(
this.makeOrderBySchema(model, false, true),
true
).optional(),
by: this.orArray(z.enum(nonRelationFields as any), true),
by: this.orArray(z.enum(nonRelationFields), true),
having: this.makeWhereSchema(model, false, true).optional(),
skip: z.number().int().nonnegative().optional(),
take: z.number().int().optional(),
Expand Down Expand Up @@ -1420,25 +1420,25 @@ export class InputValidator<Schema extends SchemaDef> {

// #region Helpers

private refineForSelectIncludeMutuallyExclusive(schema: ZodSchema) {
private refineForSelectIncludeMutuallyExclusive(schema: ZodType) {
return schema.refine(
(value) => !(value['select'] && value['include']),
(value: any) => !(value['select'] && value['include']),
'"select" and "include" cannot be used together'
);
}

private refineForSelectOmitMutuallyExclusive(schema: ZodSchema) {
private refineForSelectOmitMutuallyExclusive(schema: ZodType) {
return schema.refine(
(value) => !(value['select'] && value['omit']),
(value: any) => !(value['select'] && value['omit']),
'"select" and "omit" cannot be used together'
);
}

private nullableIf(schema: ZodSchema, nullable: boolean) {
private nullableIf(schema: ZodType, nullable: boolean) {
return nullable ? schema.nullable() : schema;
}

private orArray(schema: ZodSchema, canBeArray: boolean) {
private orArray<T extends ZodType>(schema: T, canBeArray: boolean) {
return canBeArray ? z.union([schema, z.array(schema)]) : schema;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/test/client-api/compound-id.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ describe('Compound ID tests', () => {
id1: 1,
},
})
).rejects.toThrow(/Required/);
).rejects.toThrow(/id1_id2/);
});

it('works with update', async () => {
Expand Down
10 changes: 4 additions & 6 deletions packages/zod/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {
SchemaDef,
} from '@zenstackhq/runtime/schema';
import { match, P } from 'ts-pattern';
import { z, ZodSchema } from 'zod';
import { z, ZodType } from 'zod/v4';
import type { SelectSchema } from './types';

export function makeSelectSchema<
Expand All @@ -28,20 +28,18 @@ function mapFields<Schema extends SchemaDef>(
const scalarFields = Object.entries(modelDef.fields).filter(
([_, fieldDef]) => !fieldDef.relation
);
const result: Record<string, ZodSchema> = {};
const result: Record<string, ZodType> = {};
for (const [field, fieldDef] of scalarFields) {
result[field] = makeScalarSchema(fieldDef);
}
return result;
}

function makeScalarSchema(
fieldDef: FieldDef
): z.ZodType<any, z.ZodTypeDef, any> {
function makeScalarSchema(fieldDef: FieldDef): ZodType {
return match(fieldDef.type)
.with('String', () => z.string())
.with(P.union('Int', 'BigInt', 'Float', 'Decimal'), () => z.number())
.with('Boolean', () => z.boolean())
.with('DateTime', () => z.string().datetime())
.with('DateTime', () => z.iso.datetime())
.otherwise(() => z.unknown());
}
2 changes: 1 addition & 1 deletion packages/zod/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
ZodObject,
ZodString,
ZodUnknown,
} from 'zod';
} from 'zod/v4';

export type SelectSchema<
Schema extends SchemaDef,
Expand Down
Loading