diff --git a/.vscode/tasks.json b/.vscode/tasks.json index bc7be710..65714616 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -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" + } } ] } diff --git a/packages/runtime/src/client/crud/validator.ts b/packages/runtime/src/client/crud/validator.ts index cf1c5974..602946b2 100644 --- a/packages/runtime/src/client/crud/validator.ts +++ b/packages/runtime/src/client/crud/validator.ts @@ -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, @@ -38,10 +38,10 @@ import { type GetSchemaFunc = ( model: GetModels, options: Options -) => ZodSchema; +) => ZodType; export class InputValidator { - private schemaCache = new Map(); + private schemaCache = new Map(); constructor(private readonly schema: Schema) {} @@ -246,7 +246,7 @@ export class InputValidator { ).optional(); } - let result: ZodSchema = z.object(fields).strict(); + let result: ZodType = z.object(fields).strict(); result = this.refineForSelectIncludeMutuallyExclusive(result); result = this.refineForSelectOmitMutuallyExclusive(result); @@ -275,7 +275,7 @@ export class InputValidator { model: string, unique: boolean, withoutRelationFields = false - ): ZodSchema { + ): ZodType { const modelDef = getModel(this.schema, model); if (!modelDef) { throw new QueryError(`Model "${model}" not found`); @@ -284,7 +284,7 @@ export class InputValidator { const fields: Record = {}; 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) { @@ -374,7 +374,7 @@ export class InputValidator { } // expression builder - fields['$expr'] = z.function().optional(); + fields['$expr'] = z.custom((v) => typeof v === 'function').optional(); // logical operators fields['AND'] = this.orArray( @@ -397,7 +397,7 @@ export class InputValidator { ).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 @@ -471,7 +471,7 @@ export class InputValidator { .exhaustive(); } - private makeDateTimeFilterSchema(optional: boolean): ZodSchema { + private makeDateTimeFilterSchema(optional: boolean): ZodType { return this.makeCommonPrimitiveFilterSchema( z.union([z.string().datetime(), z.date()]), optional, @@ -479,7 +479,7 @@ export class InputValidator { ); } - private makeBooleanFilterSchema(optional: boolean): ZodSchema { + private makeBooleanFilterSchema(optional: boolean): ZodType { return z.union([ this.nullableIf(z.boolean(), optional), z.object({ @@ -491,7 +491,7 @@ export class InputValidator { ]); } - private makeBytesFilterSchema(optional: boolean): ZodSchema { + private makeBytesFilterSchema(optional: boolean): ZodType { const baseSchema = z.instanceof(Uint8Array); const components = this.makeCommonPrimitiveFilterComponents( baseSchema, @@ -510,9 +510,9 @@ export class InputValidator { } private makeCommonPrimitiveFilterComponents( - baseSchema: ZodSchema, + baseSchema: ZodType, optional: boolean, - makeThis: () => ZodSchema + makeThis: () => ZodType ) { return { equals: this.nullableIf(baseSchema.optional(), optional), @@ -528,9 +528,9 @@ export class InputValidator { } private makeCommonPrimitiveFilterSchema( - baseSchema: ZodSchema, + baseSchema: ZodType, optional: boolean, - makeThis: () => ZodSchema + makeThis: () => ZodType ) { return z.union([ this.nullableIf(baseSchema, optional), @@ -545,15 +545,15 @@ export class InputValidator { } 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)) ); @@ -561,7 +561,7 @@ export class InputValidator { private makeSelectSchema(model: string) { const modelDef = requireModel(this.schema, model); - const fields: Record = {}; + const fields: Record = {}; for (const field of Object.keys(modelDef.fields)) { const fieldDef = requireField(this.schema, model, field); if (fieldDef.relation) { @@ -612,7 +612,7 @@ export class InputValidator { ]) .optional(), }), - {} as Record + {} as Record ) ), ]) @@ -624,7 +624,7 @@ export class InputValidator { private makeOmitSchema(model: string) { const modelDef = requireModel(this.schema, model); - const fields: Record = {}; + const fields: Record = {}; for (const field of Object.keys(modelDef.fields)) { const fieldDef = requireField(this.schema, model, field); if (!fieldDef.relation) { @@ -636,7 +636,7 @@ export class InputValidator { private makeIncludeSchema(model: string) { const modelDef = requireModel(this.schema, model); - const fields: Record = {}; + const fields: Record = {}; for (const field of Object.keys(modelDef.fields)) { const fieldDef = requireField(this.schema, model, field); if (fieldDef.relation) { @@ -674,7 +674,7 @@ export class InputValidator { WithAggregation: boolean ) { const modelDef = requireModel(this.schema, model); - const fields: Record = {}; + const fields: Record = {}; 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); @@ -815,7 +815,7 @@ export class InputValidator { } } - let fieldSchema: ZodSchema = z.lazy(() => + let fieldSchema: ZodType = z.lazy(() => this.makeRelationManipulationSchema( fieldDef, excludeFields, @@ -848,7 +848,7 @@ export class InputValidator { } regularAndRelationFields[field] = fieldSchema; } else { - let fieldSchema: ZodSchema = this.makePrimitiveSchema( + let fieldSchema: ZodType = this.makePrimitiveSchema( fieldDef.type ); @@ -904,7 +904,7 @@ export class InputValidator { ) { const fieldType = fieldDef.type; const array = !!fieldDef.array; - const fields: Record = { + const fields: Record = { create: this.makeCreateDataSchema( fieldDef.type, !!fieldDef.array, @@ -1168,7 +1168,7 @@ export class InputValidator { excludeFields.push(...oppositeFieldDef.relation.fields); } } - let fieldSchema: ZodSchema = z + let fieldSchema: ZodType = z .lazy(() => this.makeRelationManipulationSchema( fieldDef, @@ -1183,7 +1183,7 @@ export class InputValidator { } regularAndRelationFields[field] = fieldSchema; } else { - let fieldSchema: ZodSchema = this.makePrimitiveSchema( + let fieldSchema: ZodType = this.makePrimitiveSchema( fieldDef.type ).optional(); @@ -1304,7 +1304,7 @@ export class InputValidator { ...Object.keys(modelDef.fields).reduce((acc, field) => { acc[field] = z.literal(true).optional(); return acc; - }, {} as Record), + }, {} as Record), }) .strict(), ]); @@ -1343,7 +1343,7 @@ export class InputValidator { acc[field] = z.literal(true).optional(); } return acc; - }, {} as Record) + }, {} as Record) ); } @@ -1356,7 +1356,7 @@ export class InputValidator { acc[field] = z.literal(true).optional(); } return acc; - }, {} as Record) + }, {} as Record) ); } @@ -1366,14 +1366,14 @@ export class InputValidator { (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(), @@ -1420,25 +1420,25 @@ export class InputValidator { // #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(schema: T, canBeArray: boolean) { return canBeArray ? z.union([schema, z.array(schema)]) : schema; } diff --git a/packages/runtime/test/client-api/compound-id.test.ts b/packages/runtime/test/client-api/compound-id.test.ts index 6504b7a3..f1e0abe6 100644 --- a/packages/runtime/test/client-api/compound-id.test.ts +++ b/packages/runtime/test/client-api/compound-id.test.ts @@ -110,7 +110,7 @@ describe('Compound ID tests', () => { id1: 1, }, }) - ).rejects.toThrow(/Required/); + ).rejects.toThrow(/id1_id2/); }); it('works with update', async () => { diff --git a/packages/zod/src/index.ts b/packages/zod/src/index.ts index b8f98db9..978da782 100644 --- a/packages/zod/src/index.ts +++ b/packages/zod/src/index.ts @@ -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< @@ -28,20 +28,18 @@ function mapFields( const scalarFields = Object.entries(modelDef.fields).filter( ([_, fieldDef]) => !fieldDef.relation ); - const result: Record = {}; + const result: Record = {}; for (const [field, fieldDef] of scalarFields) { result[field] = makeScalarSchema(fieldDef); } return result; } -function makeScalarSchema( - fieldDef: FieldDef -): z.ZodType { +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()); } diff --git a/packages/zod/src/types.ts b/packages/zod/src/types.ts index e3eaaa1a..57e02e94 100644 --- a/packages/zod/src/types.ts +++ b/packages/zod/src/types.ts @@ -10,7 +10,7 @@ import type { ZodObject, ZodString, ZodUnknown, -} from 'zod'; +} from 'zod/v4'; export type SelectSchema< Schema extends SchemaDef, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d870931e..277bec85 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -212,7 +212,7 @@ importers: version: 11.0.5 zod: specifier: ^3.0.0 - version: 3.24.1 + version: 3.25.67 devDependencies: '@types/better-sqlite3': specifier: ^7.0.0 @@ -310,6 +310,18 @@ importers: specifier: ^0.2.6 version: 0.2.6 + packages/zod: + dependencies: + '@zenstackhq/runtime': + specifier: workspace:* + version: link:../runtime + ts-pattern: + specifier: ^5.6.0 + version: 5.7.1 + zod: + specifier: ^3.0.0 + version: 3.25.67 + samples/blog: dependencies: '@zenstackhq/runtime': @@ -2995,8 +3007,8 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - zod@3.24.1: - resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} + zod@3.25.67: + resolution: {integrity: sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==} snapshots: @@ -5657,4 +5669,4 @@ snapshots: yocto-queue@0.1.0: {} - zod@3.24.1: {} + zod@3.25.67: {}