From 4d221dd796c4caf3b0307628d240c8907c23a18b Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 26 Jan 2026 21:52:46 +0800 Subject: [PATCH 1/9] WIP: more aggressive caching of validation zod schemas --- .../orm/src/client/crud/validator/index.ts | 2656 ++++++++++------- packages/schema/src/schema.ts | 1 + packages/sdk/src/ts-schema-generator.ts | 2 + pnpm-lock.yaml | 11 +- samples/orm/zenstack/schema.ts | 1 + tests/e2e/apps/rally/zenstack/schema.ts | 14 + tests/e2e/github-repos/cal.com/schema.ts | 34 + tests/e2e/github-repos/formbricks/schema.ts | 24 + tests/e2e/github-repos/trigger.dev/schema.ts | 34 + tests/e2e/orm/schemas/basic/schema.ts | 1 + tests/e2e/orm/schemas/name-mapping/schema.ts | 1 + tests/e2e/orm/schemas/procedures/schema.ts | 1 + tests/e2e/orm/schemas/typed-json/schema.ts | 1 + tests/e2e/orm/schemas/typing/schema.ts | 2 + tests/regression/package.json | 1 + tests/regression/test/issue-204/schema.ts | 1 + 16 files changed, 1668 insertions(+), 1117 deletions(-) diff --git a/packages/orm/src/client/crud/validator/index.ts b/packages/orm/src/client/crud/validator/index.ts index 76dd58529..e73fa3e39 100644 --- a/packages/orm/src/client/crud/validator/index.ts +++ b/packages/orm/src/client/crud/validator/index.ts @@ -82,119 +82,7 @@ export class InputValidator { return this.client.$options.validateInput !== false; } - validateProcedureInput(proc: string, input: unknown): unknown { - const procDef = (this.schema.procedures ?? {})[proc] as ProcedureDef | undefined; - invariant(procDef, `Procedure "${proc}" not found in schema`); - - const params = Object.values(procDef.params ?? {}); - - // For procedures where every parameter is optional, allow omitting the input entirely. - if (typeof input === 'undefined') { - if (params.length === 0) { - return undefined; - } - if (params.every((p) => p.optional)) { - return undefined; - } - throw createInvalidInputError('Missing procedure arguments', `$procs.${proc}`); - } - - if (typeof input !== 'object') { - throw createInvalidInputError('Procedure input must be an object', `$procs.${proc}`); - } - - const envelope = input as Record; - const argsPayload = Object.prototype.hasOwnProperty.call(envelope, 'args') ? (envelope as any).args : undefined; - - if (params.length === 0) { - if (typeof argsPayload === 'undefined') { - return input; - } - if (!argsPayload || typeof argsPayload !== 'object' || Array.isArray(argsPayload)) { - throw createInvalidInputError('Procedure `args` must be an object', `$procs.${proc}`); - } - if (Object.keys(argsPayload as any).length === 0) { - return input; - } - throw createInvalidInputError('Procedure does not accept arguments', `$procs.${proc}`); - } - - if (typeof argsPayload === 'undefined') { - if (params.every((p) => p.optional)) { - return input; - } - throw createInvalidInputError('Missing procedure arguments', `$procs.${proc}`); - } - - if (!argsPayload || typeof argsPayload !== 'object' || Array.isArray(argsPayload)) { - throw createInvalidInputError('Procedure `args` must be an object', `$procs.${proc}`); - } - - const obj = argsPayload as Record; - - for (const param of params) { - const value = (obj as any)[param.name]; - - if (!Object.prototype.hasOwnProperty.call(obj, param.name)) { - if (param.optional) { - continue; - } - throw createInvalidInputError(`Missing procedure argument: ${param.name}`, `$procs.${proc}`); - } - - if (typeof value === 'undefined') { - if (param.optional) { - continue; - } - throw createInvalidInputError( - `Invalid procedure argument: ${param.name} is required`, - `$procs.${proc}`, - ); - } - - const schema = this.makeProcedureParamSchema(param); - const parsed = schema.safeParse(value); - if (!parsed.success) { - throw createInvalidInputError( - `Invalid procedure argument: ${param.name}: ${formatError(parsed.error)}`, - `$procs.${proc}`, - ); - } - } - - return input; - } - - private makeProcedureParamSchema(param: { type: string; array?: boolean; optional?: boolean }): z.ZodType { - let schema: z.ZodType; - - if (isTypeDef(this.schema, param.type)) { - schema = this.makeTypeDefSchema(param.type); - } else if (isEnum(this.schema, param.type)) { - schema = this.makeEnumSchema(param.type); - } else if (param.type in (this.schema.models ?? {})) { - // For model-typed values, accept any object (no deep shape validation). - schema = z.record(z.string(), z.unknown()); - } else { - // Builtin scalar types. - schema = this.makeScalarSchema(param.type as BuiltinType); - - // If a type isn't recognized by any of the above branches, `makeScalarSchema` returns `unknown`. - // Treat it as configuration/schema error. - if (schema instanceof z.ZodUnknown) { - throw createInternalError(`Unsupported procedure parameter type: ${param.type}`); - } - } - - if (param.array) { - schema = schema.array(); - } - if (param.optional) { - schema = schema.optional(); - } - - return schema; - } + // #region Entry points validateFindArgs( model: GetModels, @@ -335,28 +223,114 @@ export class InputValidator { ); } - private getSchemaCache(cacheKey: string) { - return this.schemaCache.get(cacheKey); - } + // TODO: turn it into a Zod schema and cache + validateProcedureInput(proc: string, input: unknown): unknown { + const procDef = (this.schema.procedures ?? {})[proc] as ProcedureDef | undefined; + invariant(procDef, `Procedure "${proc}" not found in schema`); - private setSchemaCache(cacheKey: string, schema: ZodType) { - return this.schemaCache.set(cacheKey, schema); + const params = Object.values(procDef.params ?? {}); + + // For procedures where every parameter is optional, allow omitting the input entirely. + if (typeof input === 'undefined') { + if (params.length === 0) { + return undefined; + } + if (params.every((p) => p.optional)) { + return undefined; + } + throw createInvalidInputError('Missing procedure arguments', `$procs.${proc}`); + } + + if (typeof input !== 'object') { + throw createInvalidInputError('Procedure input must be an object', `$procs.${proc}`); + } + + const envelope = input as Record; + const argsPayload = Object.prototype.hasOwnProperty.call(envelope, 'args') ? (envelope as any).args : undefined; + + if (params.length === 0) { + if (typeof argsPayload === 'undefined') { + return input; + } + if (!argsPayload || typeof argsPayload !== 'object' || Array.isArray(argsPayload)) { + throw createInvalidInputError('Procedure `args` must be an object', `$procs.${proc}`); + } + if (Object.keys(argsPayload as any).length === 0) { + return input; + } + throw createInvalidInputError('Procedure does not accept arguments', `$procs.${proc}`); + } + + if (typeof argsPayload === 'undefined') { + if (params.every((p) => p.optional)) { + return input; + } + throw createInvalidInputError('Missing procedure arguments', `$procs.${proc}`); + } + + if (!argsPayload || typeof argsPayload !== 'object' || Array.isArray(argsPayload)) { + throw createInvalidInputError('Procedure `args` must be an object', `$procs.${proc}`); + } + + const obj = argsPayload as Record; + + for (const param of params) { + const value = (obj as any)[param.name]; + + if (!Object.prototype.hasOwnProperty.call(obj, param.name)) { + if (param.optional) { + continue; + } + throw createInvalidInputError(`Missing procedure argument: ${param.name}`, `$procs.${proc}`); + } + + if (typeof value === 'undefined') { + if (param.optional) { + continue; + } + throw createInvalidInputError( + `Invalid procedure argument: ${param.name} is required`, + `$procs.${proc}`, + ); + } + + const schema = this.makeProcedureParamSchema(param); + const parsed = schema.safeParse(value); + if (!parsed.success) { + throw createInvalidInputError( + `Invalid procedure argument: ${param.name}: ${formatError(parsed.error)}`, + `$procs.${proc}`, + ); + } + } + + return input; } + // #endregion + + // #region Validation helpers + private validate(model: GetModels, operation: string, getSchema: GetSchemaFunc, args: unknown) { - const cacheKey = stableStringify({ - type: 'model', - model, - operation, - extraValidationsEnabled: this.extraValidationsEnabled, - }); - let schema = this.getSchemaCache(cacheKey!); - if (!schema) { - schema = getSchema(model); - this.setSchemaCache(cacheKey!, schema); - } + // const schema = this.cached( + // { + // type: 'model', + // model, + // operation, + // extraValidationsEnabled: this.extraValidationsEnabled, + // }, + // () => getSchema(model), + // ); + + const schema = getSchema(model); const { error, data } = schema.safeParse(args); + + // const start1 = new Date(); + // schema.safeParse(args); + // const took1 = new Date().getTime() - start1.getTime(); + // console.log('Validation took:', took1); + if (error) { throw createInvalidInputError( `Invalid ${operation} args for model "${model}": ${formatError(error)}`, @@ -453,51 +427,70 @@ export class InputValidator { return result; } + // #endregion + // #region Find private makeFindSchema(model: string, operation: CoreCrudOperations) { - const fields: Record = {}; - const unique = operation === 'findUnique'; - const findOne = operation === 'findUnique' || operation === 'findFirst'; - const where = this.makeWhereSchema(model, unique); - if (unique) { - fields['where'] = where; - } else { - fields['where'] = where.optional(); - } + return this.cached( + { + type: 'findArgs', + model, + operation, + }, + () => { + const fields: Record = {}; + const unique = operation === 'findUnique'; + const findOne = operation === 'findUnique' || operation === 'findFirst'; + const where = this.makeWhereSchema(model, unique); + if (unique) { + fields['where'] = where; + } else { + fields['where'] = where.optional(); + } - fields['select'] = this.makeSelectSchema(model).optional().nullable(); - fields['include'] = this.makeIncludeSchema(model).optional().nullable(); - fields['omit'] = this.makeOmitSchema(model).optional().nullable(); + fields['select'] = this.makeSelectSchema(model).optional().nullable(); + fields['include'] = this.makeIncludeSchema(model).optional().nullable(); + fields['omit'] = this.makeOmitSchema(model).optional().nullable(); - if (!unique) { - fields['skip'] = this.makeSkipSchema().optional(); - if (findOne) { - fields['take'] = z.literal(1).optional(); - } else { - fields['take'] = this.makeTakeSchema().optional(); - } - fields['orderBy'] = this.orArray(this.makeOrderBySchema(model, true, false), true).optional(); - fields['cursor'] = this.makeCursorSchema(model).optional(); - fields['distinct'] = this.makeDistinctSchema(model).optional(); - } + if (!unique) { + fields['skip'] = this.makeSkipSchema().optional(); + if (findOne) { + fields['take'] = z.literal(1).optional(); + } else { + fields['take'] = this.makeTakeSchema().optional(); + } + fields['orderBy'] = this.orArray(this.makeOrderBySchema(model, true, false), true).optional(); + fields['cursor'] = this.makeCursorSchema(model).optional(); + fields['distinct'] = this.makeDistinctSchema(model).optional(); + } - const baseSchema = z.strictObject(fields); - let result: ZodType = this.mergePluginArgsSchema(baseSchema, operation); - result = this.refineForSelectIncludeMutuallyExclusive(result); - result = this.refineForSelectOmitMutuallyExclusive(result); + const baseSchema = z.strictObject(fields); + let result: ZodType = this.mergePluginArgsSchema(baseSchema, operation); + result = this.refineForSelectIncludeMutuallyExclusive(result); + result = this.refineForSelectOmitMutuallyExclusive(result); - if (!unique) { - result = result.optional(); - } - return result; + if (!unique) { + result = result.optional(); + } + return result; + }, + ); } private makeExistsSchema(model: string) { - const baseSchema = z.strictObject({ - where: this.makeWhereSchema(model, false).optional(), - }); - return this.mergePluginArgsSchema(baseSchema, 'exists').optional(); + return this.cached( + { + type: 'existsArgs', + model, + }, + () => { + const baseSchema = z.strictObject({ + where: this.makeWhereSchema(model, false).optional(), + }); + return this.mergePluginArgsSchema(baseSchema, 'exists').optional(); + }, + ); } private makeScalarSchema(type: string, attributes?: readonly AttributeApplication[]) { @@ -540,60 +533,51 @@ export class InputValidator { } private makeEnumSchema(type: string) { - const key = stableStringify({ - type: 'enum', - name: type, + return this.cached({ type: 'enum', name: type }, () => { + const enumDef = getEnum(this.schema, type); + invariant(enumDef, `Enum "${type}" not found in schema`); + return z.enum(Object.keys(enumDef.values) as [string, ...string[]]); }); - let schema = this.getSchemaCache(key!); - if (schema) { - return schema; - } - const enumDef = getEnum(this.schema, type); - invariant(enumDef, `Enum "${type}" not found in schema`); - schema = z.enum(Object.keys(enumDef.values) as [string, ...string[]]); - this.setSchemaCache(key!, schema); - return schema; } private makeTypeDefSchema(type: string): z.ZodType { - const key = stableStringify({ - type: 'typedef', - name: type, - extraValidationsEnabled: this.extraValidationsEnabled, - }); - let schema = this.getSchemaCache(key!); - if (schema) { - return schema; - } - const typeDef = getTypeDef(this.schema, type); - invariant(typeDef, `Type definition "${type}" not found in schema`); - schema = z.looseObject( - Object.fromEntries( - Object.entries(typeDef.fields).map(([field, def]) => { - let fieldSchema = this.makeScalarSchema(def.type); - if (def.array) { - fieldSchema = fieldSchema.array(); - } - if (def.optional) { - fieldSchema = fieldSchema.nullish(); - } - return [field, fieldSchema]; - }), - ), - ); + return this.cached( + { + type: 'typedef', + name: type, + extraValidationsEnabled: this.extraValidationsEnabled, + }, + () => { + const typeDef = getTypeDef(this.schema, type); + invariant(typeDef, `Type definition "${type}" not found in schema`); + const schema = z.looseObject( + Object.fromEntries( + Object.entries(typeDef.fields).map(([field, def]) => { + let fieldSchema = this.makeScalarSchema(def.type); + if (def.array) { + fieldSchema = fieldSchema.array(); + } + if (def.optional) { + fieldSchema = fieldSchema.nullish(); + } + return [field, fieldSchema]; + }), + ), + ); - // zod doesn't preserve object field order after parsing, here we use a - // validation-only custom schema and use the original data if parsing - // is successful - const finalSchema = z.any().superRefine((value, ctx) => { - const parseResult = schema.safeParse(value); - if (!parseResult.success) { - parseResult.error.issues.forEach((issue) => ctx.addIssue(issue as any)); - } - }); + // zod doesn't preserve object field order after parsing, here we use a + // validation-only custom schema and use the original data if parsing + // is successful + const finalSchema = z.any().superRefine((value, ctx) => { + const parseResult = schema.safeParse(value); + if (!parseResult.success) { + parseResult.error.issues.forEach((issue) => ctx.addIssue(issue as any)); + } + }); - this.setSchemaCache(key!, finalSchema); - return finalSchema; + return finalSchema; + }, + ); } private makeWhereSchema( @@ -602,253 +586,308 @@ export class InputValidator { withoutRelationFields = false, withAggregations = false, ): ZodType { - const modelDef = requireModel(this.schema, model); + return this.cached( + { + type: 'where', + model, + unique, + withoutRelationFields, + withAggregations, + }, + () => { + const modelDef = requireModel(this.schema, model); + + const fields: Record = {}; + for (const field of Object.keys(modelDef.fields)) { + const fieldDef = requireField(this.schema, model, field); + let fieldSchema: ZodType | undefined; - const fields: Record = {}; - for (const field of Object.keys(modelDef.fields)) { - const fieldDef = requireField(this.schema, model, field); - let fieldSchema: ZodType | undefined; + if (fieldDef.relation) { + if (withoutRelationFields) { + continue; + } + fieldSchema = z.lazy(() => this.makeWhereSchema(fieldDef.type, false).optional()); - if (fieldDef.relation) { - if (withoutRelationFields) { - continue; - } - fieldSchema = z.lazy(() => this.makeWhereSchema(fieldDef.type, false).optional()); + // optional to-one relation allows null + fieldSchema = this.nullableIf(fieldSchema, !fieldDef.array && !!fieldDef.optional); - // optional to-one relation allows null - fieldSchema = this.nullableIf(fieldSchema, !fieldDef.array && !!fieldDef.optional); + if (fieldDef.array) { + // to-many relation + fieldSchema = z.union([ + fieldSchema, + z.strictObject({ + some: fieldSchema.optional(), + every: fieldSchema.optional(), + none: fieldSchema.optional(), + }), + ]); + } else { + // to-one relation + fieldSchema = z.union([ + fieldSchema, + z.strictObject({ + is: fieldSchema.optional(), + isNot: fieldSchema.optional(), + }), + ]); + } + } else { + const enumDef = getEnum(this.schema, fieldDef.type); + if (enumDef) { + // enum + if (Object.keys(enumDef.values).length > 0) { + fieldSchema = this.makeEnumFilterSchema( + fieldDef.type, + enumDef, + !!fieldDef.optional, + withAggregations, + !!fieldDef.array, + ); + } + } else if (fieldDef.array) { + // array field + fieldSchema = this.makeArrayFilterSchema(fieldDef.type as BuiltinType); + } else if (this.isTypeDefType(fieldDef.type)) { + fieldSchema = this.makeTypedJsonFilterSchema( + fieldDef.type, + !!fieldDef.optional, + !!fieldDef.array, + ); + } else { + // primitive field + fieldSchema = this.makePrimitiveFilterSchema( + fieldDef.type as BuiltinType, + !!fieldDef.optional, + withAggregations, + ); + } + } - if (fieldDef.array) { - // to-many relation - fieldSchema = z.union([ - fieldSchema, - z.strictObject({ - some: fieldSchema.optional(), - every: fieldSchema.optional(), - none: fieldSchema.optional(), - }), - ]); - } else { - // to-one relation - fieldSchema = z.union([ - fieldSchema, - z.strictObject({ - is: fieldSchema.optional(), - isNot: fieldSchema.optional(), - }), - ]); - } - } else { - const enumDef = getEnum(this.schema, fieldDef.type); - if (enumDef) { - // enum - if (Object.keys(enumDef.values).length > 0) { - fieldSchema = this.makeEnumFilterSchema( - enumDef, - !!fieldDef.optional, - withAggregations, - !!fieldDef.array, - ); + if (fieldSchema) { + fields[field] = fieldSchema.optional(); } - } else if (fieldDef.array) { - // array field - fieldSchema = this.makeArrayFilterSchema(fieldDef.type as BuiltinType); - } else if (this.isTypeDefType(fieldDef.type)) { - fieldSchema = this.makeTypedJsonFilterSchema(fieldDef.type, !!fieldDef.optional, !!fieldDef.array); - } else { - // primitive field - fieldSchema = this.makePrimitiveFilterSchema( - fieldDef.type as BuiltinType, - !!fieldDef.optional, - withAggregations, - ); } - } - - if (fieldSchema) { - fields[field] = fieldSchema.optional(); - } - } - if (unique) { - // add compound unique fields, e.g. `{ id1_id2: { id1: 1, id2: 1 } }` - const uniqueFields = getUniqueFields(this.schema, model); - for (const uniqueField of uniqueFields) { - if ('defs' in uniqueField) { - fields[uniqueField.name] = z - .object( - Object.fromEntries( - Object.entries(uniqueField.defs).map(([key, def]) => { - invariant(!def.relation, 'unique field cannot be a relation'); - let fieldSchema: ZodType; - const enumDef = getEnum(this.schema, def.type); - if (enumDef) { - // enum - if (Object.keys(enumDef.values).length > 0) { - fieldSchema = this.makeEnumFilterSchema( - enumDef, - !!def.optional, - false, - false, - ); - } else { - fieldSchema = z.never(); - } - } else { - // regular field - fieldSchema = this.makePrimitiveFilterSchema( - def.type as BuiltinType, - !!def.optional, - false, - ); - } - return [key, fieldSchema]; - }), - ), - ) - .optional(); + if (unique) { + // add compound unique fields, e.g. `{ id1_id2: { id1: 1, id2: 1 } }` + const uniqueFields = getUniqueFields(this.schema, model); + for (const uniqueField of uniqueFields) { + if ('defs' in uniqueField) { + fields[uniqueField.name] = z + .object( + Object.fromEntries( + Object.entries(uniqueField.defs).map(([key, def]) => { + invariant(!def.relation, 'unique field cannot be a relation'); + let fieldSchema: ZodType; + const enumDef = getEnum(this.schema, def.type); + if (enumDef) { + // enum + if (Object.keys(enumDef.values).length > 0) { + fieldSchema = this.makeEnumFilterSchema( + def.type, + enumDef, + !!def.optional, + false, + false, + ); + } else { + fieldSchema = z.never(); + } + } else { + // regular field + fieldSchema = this.makePrimitiveFilterSchema( + def.type as BuiltinType, + !!def.optional, + false, + ); + } + return [key, fieldSchema]; + }), + ), + ) + .optional(); + } + } } - } - } - // expression builder - fields['$expr'] = z.custom((v) => typeof v === 'function', { error: '"$expr" must be a function' }).optional(); - - // logical operators - fields['AND'] = this.orArray( - z.lazy(() => this.makeWhereSchema(model, false, withoutRelationFields)), - true, - ).optional(); - fields['OR'] = z - .lazy(() => this.makeWhereSchema(model, false, withoutRelationFields)) - .array() - .optional(); - fields['NOT'] = this.orArray( - z.lazy(() => this.makeWhereSchema(model, false, withoutRelationFields)), - true, - ).optional(); - - const baseWhere = z.strictObject(fields); - let result: ZodType = baseWhere; - - if (unique) { - // requires at least one unique field (field set) is required - const uniqueFields = getUniqueFields(this.schema, model); - if (uniqueFields.length === 0) { - throw createInternalError(`Model "${model}" has no unique fields`); - } + // expression builder + fields['$expr'] = z + .custom((v) => typeof v === 'function', { error: '"$expr" must be a function' }) + .optional(); - if (uniqueFields.length === 1) { - // only one unique field (set), mark the field(s) required - result = baseWhere.required({ - [uniqueFields[0]!.name]: true, - } as any); - } else { - result = baseWhere.refine((value) => { - // check that at least one unique field is set - return uniqueFields.some(({ name }) => value[name] !== undefined); - }, `At least one unique field or field set must be set`); - } - } + // logical operators + fields['AND'] = this.orArray( + z.lazy(() => this.makeWhereSchema(model, false, withoutRelationFields)), + true, + ).optional(); + fields['OR'] = z + .lazy(() => this.makeWhereSchema(model, false, withoutRelationFields)) + .array() + .optional(); + fields['NOT'] = this.orArray( + z.lazy(() => this.makeWhereSchema(model, false, withoutRelationFields)), + true, + ).optional(); - return result; - } + const baseWhere = z.strictObject(fields); + let result: ZodType = baseWhere; - private makeTypedJsonFilterSchema(type: string, optional: boolean, array: boolean) { - const typeDef = getTypeDef(this.schema, type); - invariant(typeDef, `Type definition "${type}" not found in schema`); - - const candidates: z.ZodType[] = []; - - if (!array) { - // fields filter - const fieldSchemas: Record = {}; - for (const [fieldName, fieldDef] of Object.entries(typeDef.fields)) { - if (this.isTypeDefType(fieldDef.type)) { - // recursive typed JSON - fieldSchemas[fieldName] = this.makeTypedJsonFilterSchema( - fieldDef.type, - !!fieldDef.optional, - !!fieldDef.array, - ).optional(); - } else { - // enum, array, primitives - const enumDef = getEnum(this.schema, fieldDef.type); - if (enumDef) { - fieldSchemas[fieldName] = this.makeEnumFilterSchema( - enumDef, - !!fieldDef.optional, - false, - !!fieldDef.array, - ).optional(); - } else if (fieldDef.array) { - fieldSchemas[fieldName] = this.makeArrayFilterSchema(fieldDef.type as BuiltinType).optional(); + if (unique) { + // requires at least one unique field (field set) is required + const uniqueFields = getUniqueFields(this.schema, model); + if (uniqueFields.length === 0) { + throw createInternalError(`Model "${model}" has no unique fields`); + } + + if (uniqueFields.length === 1) { + // only one unique field (set), mark the field(s) required + result = baseWhere.required({ + [uniqueFields[0]!.name]: true, + } as any); } else { - fieldSchemas[fieldName] = this.makePrimitiveFilterSchema( - fieldDef.type as BuiltinType, - !!fieldDef.optional, - false, - ).optional(); + result = baseWhere.refine((value) => { + // check that at least one unique field is set + return uniqueFields.some(({ name }) => value[name] !== undefined); + }, `At least one unique field or field set must be set`); } } - } - candidates.push(z.strictObject(fieldSchemas)); - } + return result; + }, + ); + } - const recursiveSchema = z.lazy(() => this.makeTypedJsonFilterSchema(type, optional, false)).optional(); - if (array) { - // array filter - candidates.push( - z.strictObject({ - some: recursiveSchema, - every: recursiveSchema, - none: recursiveSchema, - }), - ); - } else { - // is / isNot filter - candidates.push( - z.strictObject({ - is: recursiveSchema, - isNot: recursiveSchema, - }), - ); - } + private makeTypedJsonFilterSchema(type: string, optional: boolean, array: boolean) { + return this.cached( + { + type: 'typedJsonFilter', + dataType: type, + optional, + array, + }, + () => { + const typeDef = getTypeDef(this.schema, type); + invariant(typeDef, `Type definition "${type}" not found in schema`); + + const candidates: z.ZodType[] = []; + + if (!array) { + // fields filter + const fieldSchemas: Record = {}; + for (const [fieldName, fieldDef] of Object.entries(typeDef.fields)) { + if (this.isTypeDefType(fieldDef.type)) { + // recursive typed JSON + fieldSchemas[fieldName] = this.makeTypedJsonFilterSchema( + fieldDef.type, + !!fieldDef.optional, + !!fieldDef.array, + ).optional(); + } else { + // enum, array, primitives + const enumDef = getEnum(this.schema, fieldDef.type); + if (enumDef) { + fieldSchemas[fieldName] = this.makeEnumFilterSchema( + fieldDef.type, + enumDef, + !!fieldDef.optional, + false, + !!fieldDef.array, + ).optional(); + } else if (fieldDef.array) { + fieldSchemas[fieldName] = this.makeArrayFilterSchema( + fieldDef.type as BuiltinType, + ).optional(); + } else { + fieldSchemas[fieldName] = this.makePrimitiveFilterSchema( + fieldDef.type as BuiltinType, + !!fieldDef.optional, + false, + ).optional(); + } + } + } - // plain json filter - candidates.push(this.makeJsonFilterSchema(optional)); + candidates.push(z.strictObject(fieldSchemas)); + } - if (optional) { - // allow null as well - candidates.push(z.null()); - } + const recursiveSchema = z.lazy(() => this.makeTypedJsonFilterSchema(type, optional, false)).optional(); + if (array) { + // array filter + candidates.push( + z.strictObject({ + some: recursiveSchema, + every: recursiveSchema, + none: recursiveSchema, + }), + ); + } else { + // is / isNot filter + candidates.push( + z.strictObject({ + is: recursiveSchema, + isNot: recursiveSchema, + }), + ); + } + + // plain json filter + candidates.push(this.makeJsonFilterSchema(optional)); - // either plain json filter or field filters - return z.union(candidates); + if (optional) { + // allow null as well + candidates.push(z.null()); + } + + // either plain json filter or field filters + return z.union(candidates); + }, + ); } private isTypeDefType(type: string) { return this.schema.typeDefs && type in this.schema.typeDefs; } - private makeEnumFilterSchema(enumDef: EnumDef, optional: boolean, withAggregations: boolean, array: boolean) { - const baseSchema = z.enum(Object.keys(enumDef.values) as [string, ...string[]]); - if (array) { - return this.internalMakeArrayFilterSchema(baseSchema); - } - const components = this.makeCommonPrimitiveFilterComponents( - baseSchema, - optional, - () => z.lazy(() => this.makeEnumFilterSchema(enumDef, optional, withAggregations, array)), - ['equals', 'in', 'notIn', 'not'], - withAggregations ? ['_count', '_min', '_max'] : undefined, + private makeEnumFilterSchema( + enumName: string, + enumDef: EnumDef, + optional: boolean, + withAggregations: boolean, + array: boolean, + ) { + return this.cached( + { + type: 'enumFilter', + enum: enumName, + optional, + array, + withAggregations, + }, + () => { + const baseSchema = z.enum(Object.keys(enumDef.values) as [string, ...string[]]); + if (array) { + return this.internalMakeArrayFilterSchema(baseSchema); + } + const components = this.makeCommonPrimitiveFilterComponents( + baseSchema, + optional, + () => z.lazy(() => this.makeEnumFilterSchema(enumName, enumDef, optional, withAggregations, array)), + ['equals', 'in', 'notIn', 'not'], + withAggregations ? ['_count', '_min', '_max'] : undefined, + ); + return z.union([this.nullableIf(baseSchema, optional), z.strictObject(components)]); + }, ); - return z.union([this.nullableIf(baseSchema, optional), z.strictObject(components)]); } private makeArrayFilterSchema(type: BuiltinType) { - return this.internalMakeArrayFilterSchema(this.makeScalarSchema(type)); + return this.cached( + { + type: 'arrayFilter', + dataType: type, + }, + () => this.internalMakeArrayFilterSchema(this.makeScalarSchema(type)), + ); } private internalMakeArrayFilterSchema(elementSchema: ZodType) { @@ -862,17 +901,26 @@ export class InputValidator { } private makePrimitiveFilterSchema(type: BuiltinType, optional: boolean, withAggregations: boolean) { - return match(type) - .with('String', () => this.makeStringFilterSchema(optional, withAggregations)) - .with(P.union('Int', 'Float', 'Decimal', 'BigInt'), (type) => - this.makeNumberFilterSchema(this.makeScalarSchema(type), optional, withAggregations), - ) - .with('Boolean', () => this.makeBooleanFilterSchema(optional, withAggregations)) - .with('DateTime', () => this.makeDateTimeFilterSchema(optional, withAggregations)) - .with('Bytes', () => this.makeBytesFilterSchema(optional, withAggregations)) - .with('Json', () => this.makeJsonFilterSchema(optional)) - .with('Unsupported', () => z.never()) - .exhaustive(); + return this.cached( + { + type: 'primitiveFilter', + dataType: type, + optional, + withAggregations, + }, + () => + match(type) + .with('String', () => this.makeStringFilterSchema(optional, withAggregations)) + .with(P.union('Int', 'Float', 'Decimal', 'BigInt'), (type) => + this.makeNumberFilterSchema(this.makeScalarSchema(type), optional, withAggregations), + ) + .with('Boolean', () => this.makeBooleanFilterSchema(optional, withAggregations)) + .with('DateTime', () => this.makeDateTimeFilterSchema(optional, withAggregations)) + .with('Bytes', () => this.makeBytesFilterSchema(optional, withAggregations)) + .with('Json', () => this.makeJsonFilterSchema(optional)) + .with('Unsupported', () => z.never()) + .exhaustive(), + ); } private makeJsonValueSchema(nullable: boolean, forFilter: boolean): z.ZodType { @@ -903,51 +951,84 @@ export class InputValidator { } private makeJsonFilterSchema(optional: boolean) { - const valueSchema = this.makeJsonValueSchema(optional, true); - return z.strictObject({ - path: z.string().optional(), - equals: valueSchema.optional(), - not: valueSchema.optional(), - string_contains: z.string().optional(), - string_starts_with: z.string().optional(), - string_ends_with: z.string().optional(), - mode: this.makeStringModeSchema().optional(), - array_contains: valueSchema.optional(), - array_starts_with: valueSchema.optional(), - array_ends_with: valueSchema.optional(), - }); + return this.cached( + { + type: 'jsonFilter', + optional, + }, + () => { + const valueSchema = this.makeJsonValueSchema(optional, true); + return z.strictObject({ + path: z.string().optional(), + equals: valueSchema.optional(), + not: valueSchema.optional(), + string_contains: z.string().optional(), + string_starts_with: z.string().optional(), + string_ends_with: z.string().optional(), + mode: this.makeStringModeSchema().optional(), + array_contains: valueSchema.optional(), + array_starts_with: valueSchema.optional(), + array_ends_with: valueSchema.optional(), + }); + }, + ); } private makeDateTimeFilterSchema(optional: boolean, withAggregations: boolean): ZodType { - return this.makeCommonPrimitiveFilterSchema( - z.union([z.iso.datetime(), z.date()]), - optional, - () => z.lazy(() => this.makeDateTimeFilterSchema(optional, withAggregations)), - withAggregations ? ['_count', '_min', '_max'] : undefined, + return this.cached( + { + type: 'dateTimeFilter', + optional, + withAggregations, + }, + () => + this.makeCommonPrimitiveFilterSchema( + z.union([z.iso.datetime(), z.date()]), + optional, + () => z.lazy(() => this.makeDateTimeFilterSchema(optional, withAggregations)), + withAggregations ? ['_count', '_min', '_max'] : undefined, + ), ); } private makeBooleanFilterSchema(optional: boolean, withAggregations: boolean): ZodType { - const components = this.makeCommonPrimitiveFilterComponents( - z.boolean(), - optional, - () => z.lazy(() => this.makeBooleanFilterSchema(optional, withAggregations)), - ['equals', 'not'], - withAggregations ? ['_count', '_min', '_max'] : undefined, + return this.cached( + { + type: 'booleanFilter', + optional, + withAggregations, + }, + () => { + const components = this.makeCommonPrimitiveFilterComponents( + z.boolean(), + optional, + () => z.lazy(() => this.makeBooleanFilterSchema(optional, withAggregations)), + ['equals', 'not'], + withAggregations ? ['_count', '_min', '_max'] : undefined, + ); + return z.union([this.nullableIf(z.boolean(), optional), z.strictObject(components)]); + }, ); - return z.union([this.nullableIf(z.boolean(), optional), z.strictObject(components)]); } private makeBytesFilterSchema(optional: boolean, withAggregations: boolean): ZodType { - const baseSchema = z.instanceof(Uint8Array); - const components = this.makeCommonPrimitiveFilterComponents( - baseSchema, - optional, - () => z.instanceof(Uint8Array), - ['equals', 'in', 'notIn', 'not'], - withAggregations ? ['_count', '_min', '_max'] : undefined, + return this.cached( + { + type: 'bytesFilter', + withAggregations, + }, + () => { + const baseSchema = z.instanceof(Uint8Array); + const components = this.makeCommonPrimitiveFilterComponents( + baseSchema, + optional, + () => z.instanceof(Uint8Array), + ['equals', 'in', 'notIn', 'not'], + withAggregations ? ['_count', '_min', '_max'] : undefined, + ); + return z.union([this.nullableIf(baseSchema, optional), z.strictObject(components)]); + }, ); - return z.union([this.nullableIf(baseSchema, optional), z.strictObject(components)]); } private makeCommonPrimitiveFilterComponents( @@ -1036,187 +1117,253 @@ export class InputValidator { } private makeSelectSchema(model: string) { - const modelDef = requireModel(this.schema, model); - const fields: Record = {}; - for (const field of Object.keys(modelDef.fields)) { - const fieldDef = requireField(this.schema, model, field); - if (fieldDef.relation) { - fields[field] = this.makeRelationSelectIncludeSchema(fieldDef).optional(); - } else { - fields[field] = z.boolean().optional(); - } - } + return this.cached( + { + type: 'select', + model, + }, + () => { + const modelDef = requireModel(this.schema, model); + const fields: Record = {}; + for (const field of Object.keys(modelDef.fields)) { + const fieldDef = requireField(this.schema, model, field); + if (fieldDef.relation) { + fields[field] = this.makeRelationSelectIncludeSchema(fieldDef).optional(); + } else { + fields[field] = z.boolean().optional(); + } + } - const _countSchema = this.makeCountSelectionSchema(modelDef); - if (_countSchema) { - fields['_count'] = _countSchema; - } + const _countSchema = this.makeCountSelectionSchema(modelDef); + if (!(_countSchema instanceof z.ZodNever)) { + fields['_count'] = _countSchema; + } - return z.strictObject(fields); + return z.strictObject(fields); + }, + ); } private makeCountSelectionSchema(modelDef: ModelDef) { - const toManyRelations = Object.values(modelDef.fields).filter((def) => def.relation && def.array); - if (toManyRelations.length > 0) { - return z - .union([ - z.literal(true), - z.strictObject({ - select: z.strictObject( - toManyRelations.reduce( - (acc, fieldDef) => ({ - ...acc, - [fieldDef.name]: z - .union([ - z.boolean(), - z.strictObject({ - where: this.makeWhereSchema(fieldDef.type, false, false), - }), - ]) - .optional(), - }), - {} as Record, - ), - ), - }), - ]) - .optional(); - } else { - return undefined; - } + return this.cached( + { + type: 'countSelection', + model: modelDef.name, + }, + () => { + const toManyRelations = Object.values(modelDef.fields).filter((def) => def.relation && def.array); + if (toManyRelations.length > 0) { + return z + .union([ + z.literal(true), + z.strictObject({ + select: z.strictObject( + toManyRelations.reduce( + (acc, fieldDef) => ({ + ...acc, + [fieldDef.name]: z + .union([ + z.boolean(), + z.strictObject({ + where: this.makeWhereSchema(fieldDef.type, false, false), + }), + ]) + .optional(), + }), + {} as Record, + ), + ), + }), + ]) + .optional(); + } else { + return z.never(); + } + }, + ); } private makeRelationSelectIncludeSchema(fieldDef: FieldDef) { - let objSchema: z.ZodType = z.strictObject({ - ...(fieldDef.array || fieldDef.optional - ? { - // to-many relations and optional to-one relations are filterable - where: z.lazy(() => this.makeWhereSchema(fieldDef.type, false)).optional(), - } - : {}), - select: z - .lazy(() => this.makeSelectSchema(fieldDef.type)) - .optional() - .nullable(), - include: z - .lazy(() => this.makeIncludeSchema(fieldDef.type)) - .optional() - .nullable(), - omit: z - .lazy(() => this.makeOmitSchema(fieldDef.type)) - .optional() - .nullable(), - ...(fieldDef.array - ? { - // to-many relations can be ordered, skipped, taken, and cursor-located - orderBy: z - .lazy(() => this.orArray(this.makeOrderBySchema(fieldDef.type, true, false), true)) - .optional(), - skip: this.makeSkipSchema().optional(), - take: this.makeTakeSchema().optional(), - cursor: this.makeCursorSchema(fieldDef.type).optional(), - distinct: this.makeDistinctSchema(fieldDef.type).optional(), - } - : {}), - }); - - objSchema = this.refineForSelectIncludeMutuallyExclusive(objSchema); - objSchema = this.refineForSelectOmitMutuallyExclusive(objSchema); - - return z.union([z.boolean(), objSchema]); + return this.cached( + { + type: 'relationSelectInclude', + model: fieldDef.type, + field: fieldDef.name, + }, + () => { + let objSchema: z.ZodType = z.strictObject({ + ...(fieldDef.array || fieldDef.optional + ? { + // to-many relations and optional to-one relations are filterable + where: z.lazy(() => this.makeWhereSchema(fieldDef.type, false)).optional(), + } + : {}), + select: z + .lazy(() => this.makeSelectSchema(fieldDef.type)) + .optional() + .nullable(), + include: z + .lazy(() => this.makeIncludeSchema(fieldDef.type)) + .optional() + .nullable(), + omit: z + .lazy(() => this.makeOmitSchema(fieldDef.type)) + .optional() + .nullable(), + ...(fieldDef.array + ? { + // to-many relations can be ordered, skipped, taken, and cursor-located + orderBy: z + .lazy(() => this.orArray(this.makeOrderBySchema(fieldDef.type, true, false), true)) + .optional(), + skip: this.makeSkipSchema().optional(), + take: this.makeTakeSchema().optional(), + cursor: this.makeCursorSchema(fieldDef.type).optional(), + distinct: this.makeDistinctSchema(fieldDef.type).optional(), + } + : {}), + }); + + objSchema = this.refineForSelectIncludeMutuallyExclusive(objSchema); + objSchema = this.refineForSelectOmitMutuallyExclusive(objSchema); + + return z.union([z.boolean(), objSchema]); + }, + ); } private makeOmitSchema(model: string) { - const modelDef = requireModel(this.schema, model); - const fields: Record = {}; - for (const field of Object.keys(modelDef.fields)) { - const fieldDef = requireField(this.schema, model, field); - if (!fieldDef.relation) { - if (this.options.allowQueryTimeOmitOverride !== false) { - // if override is allowed, use boolean - fields[field] = z.boolean().optional(); - } else { - // otherwise only allow true - fields[field] = z.literal(true).optional(); + return this.cached( + { + type: 'omit', + model, + }, + () => { + const modelDef = requireModel(this.schema, model); + const fields: Record = {}; + for (const field of Object.keys(modelDef.fields)) { + const fieldDef = requireField(this.schema, model, field); + if (!fieldDef.relation) { + if (this.options.allowQueryTimeOmitOverride !== false) { + // if override is allowed, use boolean + fields[field] = z.boolean().optional(); + } else { + // otherwise only allow true + fields[field] = z.literal(true).optional(); + } + } } - } - } - return z.strictObject(fields); + return z.strictObject(fields); + }, + ); } private makeIncludeSchema(model: string) { - const modelDef = requireModel(this.schema, model); - const fields: Record = {}; - for (const field of Object.keys(modelDef.fields)) { - const fieldDef = requireField(this.schema, model, field); - if (fieldDef.relation) { - fields[field] = this.makeRelationSelectIncludeSchema(fieldDef).optional(); - } - } + return this.cached( + { + type: 'include', + model, + }, + () => { + const modelDef = requireModel(this.schema, model); + const fields: Record = {}; + for (const field of Object.keys(modelDef.fields)) { + const fieldDef = requireField(this.schema, model, field); + if (fieldDef.relation) { + fields[field] = this.makeRelationSelectIncludeSchema(fieldDef).optional(); + } + } - const _countSchema = this.makeCountSelectionSchema(modelDef); - if (_countSchema) { - fields['_count'] = _countSchema; - } + const _countSchema = this.makeCountSelectionSchema(modelDef); + if (!(_countSchema instanceof z.ZodNever)) { + fields['_count'] = _countSchema; + } - return z.strictObject(fields); + return z.strictObject(fields); + }, + ); } private makeOrderBySchema(model: string, withRelation: boolean, WithAggregation: boolean) { - const modelDef = requireModel(this.schema, model); - 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); - if (fieldDef.relation) { - // relations - if (withRelation) { - fields[field] = z.lazy(() => { - let relationOrderBy = this.makeOrderBySchema(fieldDef.type, withRelation, WithAggregation); - if (fieldDef.array) { - relationOrderBy = relationOrderBy.extend({ - _count: sort, + return this.cached( + { + type: 'orderBy', + model, + withRelation, + WithAggregation, + }, + () => { + const modelDef = requireModel(this.schema, model); + 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); + if (fieldDef.relation) { + // relations + if (withRelation) { + fields[field] = z.lazy(() => { + let relationOrderBy = this.makeOrderBySchema( + fieldDef.type, + withRelation, + WithAggregation, + ); + if (fieldDef.array) { + relationOrderBy = relationOrderBy.extend({ + _count: sort, + }); + } + return relationOrderBy.optional(); }); } - return relationOrderBy.optional(); - }); - } - } else { - // scalars - if (fieldDef.optional) { - fields[field] = z - .union([ - sort, - z.strictObject({ - sort, - nulls: z.union([z.literal('first'), z.literal('last')]), - }), - ]) - .optional(); - } else { - fields[field] = sort.optional(); + } else { + // scalars + if (fieldDef.optional) { + fields[field] = z + .union([ + sort, + z.strictObject({ + sort, + nulls: z.union([z.literal('first'), z.literal('last')]), + }), + ]) + .optional(); + } else { + fields[field] = sort.optional(); + } + } } - } - } - // aggregations - if (WithAggregation) { - const aggregationFields = ['_count', '_avg', '_sum', '_min', '_max']; - for (const agg of aggregationFields) { - fields[agg] = z.lazy(() => this.makeOrderBySchema(model, true, false).optional()); - } - } + // aggregations + if (WithAggregation) { + const aggregationFields = ['_count', '_avg', '_sum', '_min', '_max']; + for (const agg of aggregationFields) { + fields[agg] = z.lazy(() => this.makeOrderBySchema(model, true, false).optional()); + } + } - return z.strictObject(fields); + return z.strictObject(fields); + }, + ); } private makeDistinctSchema(model: string) { - const modelDef = requireModel(this.schema, model); - const nonRelationFields = Object.keys(modelDef.fields).filter((field) => !modelDef.fields[field]?.relation); - return this.orArray(z.enum(nonRelationFields as any), true); + return this.cached( + { + type: 'distinct', + model, + }, + () => { + const modelDef = requireModel(this.schema, model); + const nonRelationFields = Object.keys(modelDef.fields).filter( + (field) => !modelDef.fields[field]?.relation, + ); + return this.orArray(z.enum(nonRelationFields as any), true); + }, + ); } private makeCursorSchema(model: string) { + // `makeWhereSchema` is already cached return this.makeWhereSchema(model, true, true).optional(); } @@ -1225,31 +1372,53 @@ export class InputValidator { // #region Create private makeCreateSchema(model: string) { - const dataSchema = this.makeCreateDataSchema(model, false); - const baseSchema = z.strictObject({ - data: dataSchema, - select: this.makeSelectSchema(model).optional().nullable(), - include: this.makeIncludeSchema(model).optional().nullable(), - omit: this.makeOmitSchema(model).optional().nullable(), - }); - let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'create'); - schema = this.refineForSelectIncludeMutuallyExclusive(schema); - schema = this.refineForSelectOmitMutuallyExclusive(schema); - return schema; + return this.cached( + { + type: 'create', + model, + }, + () => { + const dataSchema = this.makeCreateDataSchema(model, false); + const baseSchema = z.strictObject({ + data: dataSchema, + select: this.makeSelectSchema(model).optional().nullable(), + include: this.makeIncludeSchema(model).optional().nullable(), + omit: this.makeOmitSchema(model).optional().nullable(), + }); + let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'create'); + schema = this.refineForSelectIncludeMutuallyExclusive(schema); + schema = this.refineForSelectOmitMutuallyExclusive(schema); + return schema; + }, + ); } private makeCreateManySchema(model: string) { - return this.mergePluginArgsSchema(this.makeCreateManyDataSchema(model, []), 'createMany').optional(); + return this.cached( + { + type: 'createMany', + model, + }, + () => this.mergePluginArgsSchema(this.makeCreateManyDataSchema(model, []), 'createMany').optional(), + ); } private makeCreateManyAndReturnSchema(model: string) { - const base = this.makeCreateManyDataSchema(model, []); - let result: ZodObject = base.extend({ - select: this.makeSelectSchema(model).optional().nullable(), - omit: this.makeOmitSchema(model).optional().nullable(), - }); - result = this.mergePluginArgsSchema(result, 'createManyAndReturn'); - return this.refineForSelectOmitMutuallyExclusive(result).optional(); + return this.cached( + { + type: 'createManyAndReturn', + model, + }, + () => { + const base = this.makeCreateManyDataSchema(model, []); + let result: ZodObject = base.extend({ + select: this.makeSelectSchema(model).optional().nullable(), + omit: this.makeOmitSchema(model).optional().nullable(), + }); + result = this.mergePluginArgsSchema(result, 'createManyAndReturn'); + return this.refineForSelectOmitMutuallyExclusive(result).optional(); + }, + ); } private makeCreateDataSchema( @@ -1258,124 +1427,135 @@ export class InputValidator { withoutFields: string[] = [], withoutRelationFields = false, ) { - const uncheckedVariantFields: Record = {}; - const checkedVariantFields: Record = {}; - const modelDef = requireModel(this.schema, model); - const hasRelation = - !withoutRelationFields && - Object.entries(modelDef.fields).some(([f, def]) => !withoutFields.includes(f) && def.relation); - - Object.keys(modelDef.fields).forEach((field) => { - if (withoutFields.includes(field)) { - return; - } - const fieldDef = requireField(this.schema, model, field); - if (fieldDef.computed) { - return; - } - - if (this.isDelegateDiscriminator(fieldDef)) { - // discriminator field is auto-assigned - return; - } - - if (fieldDef.relation) { - if (withoutRelationFields) { - return; - } - const excludeFields: string[] = []; - const oppositeField = fieldDef.relation.opposite; - if (oppositeField) { - excludeFields.push(oppositeField); - const oppositeFieldDef = requireField(this.schema, fieldDef.type, oppositeField); - if (oppositeFieldDef.relation?.fields) { - excludeFields.push(...oppositeFieldDef.relation.fields); + return this.cached( + { + type: 'createData', + model, + canBeArray, + withoutFields: [...withoutFields].sort(), + withoutRelationFields, + }, + () => { + const uncheckedVariantFields: Record = {}; + const checkedVariantFields: Record = {}; + const modelDef = requireModel(this.schema, model); + const hasRelation = + !withoutRelationFields && + Object.entries(modelDef.fields).some(([f, def]) => !withoutFields.includes(f) && def.relation); + + Object.keys(modelDef.fields).forEach((field) => { + if (withoutFields.includes(field)) { + return; } - } - - let fieldSchema: ZodType = z.lazy(() => - this.makeRelationManipulationSchema(fieldDef, excludeFields, 'create'), - ); - - if (fieldDef.optional || fieldDef.array) { - // optional or array relations are optional - fieldSchema = fieldSchema.optional(); - } else { - // if all fk fields are optional, the relation is optional - let allFksOptional = false; - if (fieldDef.relation.fields) { - allFksOptional = fieldDef.relation.fields.every((f) => { - const fkDef = requireField(this.schema, model, f); - return fkDef.optional || fieldHasDefaultValue(fkDef); - }); + const fieldDef = requireField(this.schema, model, field); + if (fieldDef.computed) { + return; } - if (allFksOptional) { - fieldSchema = fieldSchema.optional(); + + if (this.isDelegateDiscriminator(fieldDef)) { + // discriminator field is auto-assigned + return; } - } - // optional to-one relation can be null - if (fieldDef.optional && !fieldDef.array) { - fieldSchema = fieldSchema.nullable(); - } - checkedVariantFields[field] = fieldSchema; - if (fieldDef.array || !fieldDef.relation.references) { - // non-owned relation - uncheckedVariantFields[field] = fieldSchema; - } - } else { - let fieldSchema = this.makeScalarSchema(fieldDef.type, fieldDef.attributes); + if (fieldDef.relation) { + if (withoutRelationFields) { + return; + } + const excludeFields: string[] = []; + const oppositeField = fieldDef.relation.opposite; + if (oppositeField) { + excludeFields.push(oppositeField); + const oppositeFieldDef = requireField(this.schema, fieldDef.type, oppositeField); + if (oppositeFieldDef.relation?.fields) { + excludeFields.push(...oppositeFieldDef.relation.fields); + } + } - if (fieldDef.array) { - fieldSchema = addListValidation(fieldSchema.array(), fieldDef.attributes); - fieldSchema = z - .union([ - fieldSchema, - z.strictObject({ - set: fieldSchema, - }), - ]) - .optional(); - } + let fieldSchema: ZodType = z.lazy(() => + this.makeRelationManipulationSchema(fieldDef, excludeFields, 'create'), + ); - if (fieldDef.optional || fieldHasDefaultValue(fieldDef)) { - fieldSchema = fieldSchema.optional(); - } + if (fieldDef.optional || fieldDef.array) { + // optional or array relations are optional + fieldSchema = fieldSchema.optional(); + } else { + // if all fk fields are optional, the relation is optional + let allFksOptional = false; + if (fieldDef.relation.fields) { + allFksOptional = fieldDef.relation.fields.every((f) => { + const fkDef = requireField(this.schema, model, f); + return fkDef.optional || fieldHasDefaultValue(fkDef); + }); + } + if (allFksOptional) { + fieldSchema = fieldSchema.optional(); + } + } - if (fieldDef.optional) { - if (fieldDef.type === 'Json') { - // DbNull for Json fields - fieldSchema = z.union([fieldSchema, z.instanceof(DbNullClass)]); + // optional to-one relation can be null + if (fieldDef.optional && !fieldDef.array) { + fieldSchema = fieldSchema.nullable(); + } + checkedVariantFields[field] = fieldSchema; + if (fieldDef.array || !fieldDef.relation.references) { + // non-owned relation + uncheckedVariantFields[field] = fieldSchema; + } } else { - fieldSchema = fieldSchema.nullable(); - } - } + let fieldSchema = this.makeScalarSchema(fieldDef.type, fieldDef.attributes); - uncheckedVariantFields[field] = fieldSchema; - if (!fieldDef.foreignKeyFor) { - // non-fk field - checkedVariantFields[field] = fieldSchema; - } - } - }); + if (fieldDef.array) { + fieldSchema = addListValidation(fieldSchema.array(), fieldDef.attributes); + fieldSchema = z + .union([ + fieldSchema, + z.strictObject({ + set: fieldSchema, + }), + ]) + .optional(); + } - const uncheckedCreateSchema = this.extraValidationsEnabled - ? addCustomValidation(z.strictObject(uncheckedVariantFields), modelDef.attributes) - : z.strictObject(uncheckedVariantFields); - const checkedCreateSchema = this.extraValidationsEnabled - ? addCustomValidation(z.strictObject(checkedVariantFields), modelDef.attributes) - : z.strictObject(checkedVariantFields); + if (fieldDef.optional || fieldHasDefaultValue(fieldDef)) { + fieldSchema = fieldSchema.optional(); + } - if (!hasRelation) { - return this.orArray(uncheckedCreateSchema, canBeArray); - } else { - return z.union([ - uncheckedCreateSchema, - checkedCreateSchema, - ...(canBeArray ? [z.array(uncheckedCreateSchema)] : []), - ...(canBeArray ? [z.array(checkedCreateSchema)] : []), - ]); - } + if (fieldDef.optional) { + if (fieldDef.type === 'Json') { + // DbNull for Json fields + fieldSchema = z.union([fieldSchema, z.instanceof(DbNullClass)]); + } else { + fieldSchema = fieldSchema.nullable(); + } + } + + uncheckedVariantFields[field] = fieldSchema; + if (!fieldDef.foreignKeyFor) { + // non-fk field + checkedVariantFields[field] = fieldSchema; + } + } + }); + + const uncheckedCreateSchema = this.extraValidationsEnabled + ? addCustomValidation(z.strictObject(uncheckedVariantFields), modelDef.attributes) + : z.strictObject(uncheckedVariantFields); + const checkedCreateSchema = this.extraValidationsEnabled + ? addCustomValidation(z.strictObject(checkedVariantFields), modelDef.attributes) + : z.strictObject(checkedVariantFields); + + if (!hasRelation) { + return this.orArray(uncheckedCreateSchema, canBeArray); + } else { + return z.union([ + uncheckedCreateSchema, + checkedCreateSchema, + ...(canBeArray ? [z.array(uncheckedCreateSchema)] : []), + ...(canBeArray ? [z.array(checkedCreateSchema)] : []), + ]); + } + }, + ); } private isDelegateDiscriminator(fieldDef: FieldDef) { @@ -1388,121 +1568,182 @@ export class InputValidator { } private makeRelationManipulationSchema(fieldDef: FieldDef, withoutFields: string[], mode: 'create' | 'update') { - const fieldType = fieldDef.type; - const array = !!fieldDef.array; - const fields: Record = { - create: this.makeCreateDataSchema(fieldDef.type, !!fieldDef.array, withoutFields).optional(), - - connect: this.makeConnectDataSchema(fieldType, array).optional(), - - connectOrCreate: this.makeConnectOrCreateDataSchema(fieldType, array, withoutFields).optional(), - }; - - if (array) { - fields['createMany'] = this.makeCreateManyDataSchema(fieldType, withoutFields).optional(); - } + return this.cached( + { + type: 'relationManipulation', + model: fieldDef.type, + field: fieldDef.name, + withoutFields: [...withoutFields].sort(), + mode, + }, + () => { + const fieldType = fieldDef.type; + const array = !!fieldDef.array; + const fields: Record = { + create: this.makeCreateDataSchema(fieldDef.type, !!fieldDef.array, withoutFields).optional(), + + connect: this.makeConnectDataSchema(fieldType, array).optional(), + + connectOrCreate: this.makeConnectOrCreateDataSchema(fieldType, array, withoutFields).optional(), + }; + + if (array) { + fields['createMany'] = this.makeCreateManyDataSchema(fieldType, withoutFields).optional(); + } - if (mode === 'update') { - if (fieldDef.optional || fieldDef.array) { - // disconnect and delete are only available for optional/to-many relations - fields['disconnect'] = this.makeDisconnectDataSchema(fieldType, array).optional(); + if (mode === 'update') { + if (fieldDef.optional || fieldDef.array) { + // disconnect and delete are only available for optional/to-many relations + fields['disconnect'] = this.makeDisconnectDataSchema(fieldType, array).optional(); - fields['delete'] = this.makeDeleteRelationDataSchema(fieldType, array, true).optional(); - } + fields['delete'] = this.makeDeleteRelationDataSchema(fieldType, array, true).optional(); + } - fields['update'] = array - ? this.orArray( - z.strictObject({ - where: this.makeWhereSchema(fieldType, true), - data: this.makeUpdateDataSchema(fieldType, withoutFields), - }), - true, - ).optional() - : z - .union([ - z.strictObject({ - where: this.makeWhereSchema(fieldType, false).optional(), - data: this.makeUpdateDataSchema(fieldType, withoutFields), - }), - this.makeUpdateDataSchema(fieldType, withoutFields), - ]) - .optional(); - - let upsertWhere = this.makeWhereSchema(fieldType, true); - if (!fieldDef.array) { - // to-one relation, can upsert without where clause - upsertWhere = upsertWhere.optional(); - } - fields['upsert'] = this.orArray( - z.strictObject({ - where: upsertWhere, - create: this.makeCreateDataSchema(fieldType, false, withoutFields), - update: this.makeUpdateDataSchema(fieldType, withoutFields), - }), - true, - ).optional(); + fields['update'] = array + ? this.orArray( + z.strictObject({ + where: this.makeWhereSchema(fieldType, true), + data: this.makeUpdateDataSchema(fieldType, withoutFields), + }), + true, + ).optional() + : z + .union([ + z.strictObject({ + where: this.makeWhereSchema(fieldType, false).optional(), + data: this.makeUpdateDataSchema(fieldType, withoutFields), + }), + this.makeUpdateDataSchema(fieldType, withoutFields), + ]) + .optional(); + + let upsertWhere = this.makeWhereSchema(fieldType, true); + if (!fieldDef.array) { + // to-one relation, can upsert without where clause + upsertWhere = upsertWhere.optional(); + } + fields['upsert'] = this.orArray( + z.strictObject({ + where: upsertWhere, + create: this.makeCreateDataSchema(fieldType, false, withoutFields), + update: this.makeUpdateDataSchema(fieldType, withoutFields), + }), + true, + ).optional(); - if (array) { - // to-many relation specifics - fields['set'] = this.makeSetDataSchema(fieldType, true).optional(); + if (array) { + // to-many relation specifics + fields['set'] = this.makeSetDataSchema(fieldType, true).optional(); - fields['updateMany'] = this.orArray( - z.strictObject({ - where: this.makeWhereSchema(fieldType, false, true), - data: this.makeUpdateDataSchema(fieldType, withoutFields), - }), - true, - ).optional(); + fields['updateMany'] = this.orArray( + z.strictObject({ + where: this.makeWhereSchema(fieldType, false, true), + data: this.makeUpdateDataSchema(fieldType, withoutFields), + }), + true, + ).optional(); - fields['deleteMany'] = this.makeDeleteRelationDataSchema(fieldType, true, false).optional(); - } - } + fields['deleteMany'] = this.makeDeleteRelationDataSchema(fieldType, true, false).optional(); + } + } - return z.strictObject(fields); + return z.strictObject(fields); + }, + ); } private makeSetDataSchema(model: string, canBeArray: boolean) { - return this.orArray(this.makeWhereSchema(model, true), canBeArray); + return this.cached( + { + type: 'setData', + model, + canBeArray, + }, + () => this.orArray(this.makeWhereSchema(model, true), canBeArray), + ); } private makeConnectDataSchema(model: string, canBeArray: boolean) { - return this.orArray(this.makeWhereSchema(model, true), canBeArray); + return this.cached( + { + type: 'connectData', + model, + canBeArray, + }, + () => this.orArray(this.makeWhereSchema(model, true), canBeArray), + ); } private makeDisconnectDataSchema(model: string, canBeArray: boolean) { - if (canBeArray) { - // to-many relation, must be unique filters - return this.orArray(this.makeWhereSchema(model, true), canBeArray); - } else { - // to-one relation, can be boolean or a regular filter - the entity - // being disconnected is already uniquely identified by its parent - return z.union([z.boolean(), this.makeWhereSchema(model, false)]); - } + return this.cached( + { + type: 'disconnectData', + model, + canBeArray, + }, + () => { + if (canBeArray) { + // to-many relation, must be unique filters + return this.orArray(this.makeWhereSchema(model, true), canBeArray); + } else { + // to-one relation, can be boolean or a regular filter - the entity + // being disconnected is already uniquely identified by its parent + return z.union([z.boolean(), this.makeWhereSchema(model, false)]); + } + }, + ); } private makeDeleteRelationDataSchema(model: string, toManyRelation: boolean, uniqueFilter: boolean) { - return toManyRelation - ? this.orArray(this.makeWhereSchema(model, uniqueFilter), true) - : z.union([z.boolean(), this.makeWhereSchema(model, uniqueFilter)]); + return this.cached( + { + type: 'deleteRelationData', + model, + toManyRelation, + uniqueFilter, + }, + () => + toManyRelation + ? this.orArray(this.makeWhereSchema(model, uniqueFilter), true) + : z.union([z.boolean(), this.makeWhereSchema(model, uniqueFilter)]), + ); } private makeConnectOrCreateDataSchema(model: string, canBeArray: boolean, withoutFields: string[]) { - const whereSchema = this.makeWhereSchema(model, true); - const createSchema = this.makeCreateDataSchema(model, false, withoutFields); - return this.orArray( - z.strictObject({ - where: whereSchema, - create: createSchema, - }), - canBeArray, + return this.cached( + { + type: 'connectOrCreateData', + model, + canBeArray, + withoutFields: [...withoutFields].sort(), + }, + () => { + const whereSchema = this.makeWhereSchema(model, true); + const createSchema = this.makeCreateDataSchema(model, false, withoutFields); + return this.orArray( + z.strictObject({ + where: whereSchema, + create: createSchema, + }), + canBeArray, + ); + }, ); } private makeCreateManyDataSchema(model: string, withoutFields: string[]) { - return z.strictObject({ - data: this.makeCreateDataSchema(model, true, withoutFields, true), - skipDuplicates: z.boolean().optional(), - }); + return this.cached( + { + type: 'createManyData', + model, + withoutFields: [...withoutFields].sort(), + }, + () => + z.strictObject({ + data: this.makeCreateDataSchema(model, true, withoutFields, true), + skipDuplicates: z.boolean().optional(), + }), + ); } // #endregion @@ -1510,160 +1751,204 @@ export class InputValidator { // #region Update private makeUpdateSchema(model: string) { - const baseSchema = z.strictObject({ - where: this.makeWhereSchema(model, true), - data: this.makeUpdateDataSchema(model), - select: this.makeSelectSchema(model).optional().nullable(), - include: this.makeIncludeSchema(model).optional().nullable(), - omit: this.makeOmitSchema(model).optional().nullable(), - }); - let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'update'); - schema = this.refineForSelectIncludeMutuallyExclusive(schema); - schema = this.refineForSelectOmitMutuallyExclusive(schema); - return schema; + return this.cached( + { + type: 'update', + model, + }, + () => { + const baseSchema = z.strictObject({ + where: this.makeWhereSchema(model, true), + data: this.makeUpdateDataSchema(model), + select: this.makeSelectSchema(model).optional().nullable(), + include: this.makeIncludeSchema(model).optional().nullable(), + omit: this.makeOmitSchema(model).optional().nullable(), + }); + let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'update'); + schema = this.refineForSelectIncludeMutuallyExclusive(schema); + schema = this.refineForSelectOmitMutuallyExclusive(schema); + return schema; + }, + ); } private makeUpdateManySchema(model: string) { - return this.mergePluginArgsSchema( - z.strictObject({ - where: this.makeWhereSchema(model, false).optional(), - data: this.makeUpdateDataSchema(model, [], true), - limit: z.number().int().nonnegative().optional(), - }), - 'updateMany', + return this.cached( + { + type: 'updateMany', + model, + }, + () => + this.mergePluginArgsSchema( + z.strictObject({ + where: this.makeWhereSchema(model, false).optional(), + data: this.makeUpdateDataSchema(model, [], true), + limit: z.number().int().nonnegative().optional(), + }), + 'updateMany', + ), ); } private makeUpdateManyAndReturnSchema(model: string) { - // plugin extended args schema is merged in `makeUpdateManySchema` - const baseSchema: ZodObject = this.makeUpdateManySchema(model); - let schema: ZodType = baseSchema.extend({ - select: this.makeSelectSchema(model).optional().nullable(), - omit: this.makeOmitSchema(model).optional().nullable(), - }); - schema = this.refineForSelectOmitMutuallyExclusive(schema); - return schema; + return this.cached( + { + type: 'updateManyAndReturn', + model, + }, + () => { + // plugin extended args schema is merged in `makeUpdateManySchema` + const baseSchema: ZodObject = this.makeUpdateManySchema(model); + let schema: ZodType = baseSchema.extend({ + select: this.makeSelectSchema(model).optional().nullable(), + omit: this.makeOmitSchema(model).optional().nullable(), + }); + schema = this.refineForSelectOmitMutuallyExclusive(schema); + return schema; + }, + ); } private makeUpsertSchema(model: string) { - const baseSchema = z.strictObject({ - where: this.makeWhereSchema(model, true), - create: this.makeCreateDataSchema(model, false), - update: this.makeUpdateDataSchema(model), - select: this.makeSelectSchema(model).optional().nullable(), - include: this.makeIncludeSchema(model).optional().nullable(), - omit: this.makeOmitSchema(model).optional().nullable(), - }); - let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'upsert'); - schema = this.refineForSelectIncludeMutuallyExclusive(schema); - schema = this.refineForSelectOmitMutuallyExclusive(schema); - return schema; + return this.cached( + { + type: 'upsert', + model, + }, + () => { + const baseSchema = z.strictObject({ + where: this.makeWhereSchema(model, true), + create: this.makeCreateDataSchema(model, false), + update: this.makeUpdateDataSchema(model), + select: this.makeSelectSchema(model).optional().nullable(), + include: this.makeIncludeSchema(model).optional().nullable(), + omit: this.makeOmitSchema(model).optional().nullable(), + }); + let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'upsert'); + schema = this.refineForSelectIncludeMutuallyExclusive(schema); + schema = this.refineForSelectOmitMutuallyExclusive(schema); + return schema; + }, + ); } private makeUpdateDataSchema(model: string, withoutFields: string[] = [], withoutRelationFields = false) { - const uncheckedVariantFields: Record = {}; - const checkedVariantFields: Record = {}; - const modelDef = requireModel(this.schema, model); - const hasRelation = Object.entries(modelDef.fields).some( - ([key, value]) => value.relation && !withoutFields.includes(key), - ); - - Object.keys(modelDef.fields).forEach((field) => { - if (withoutFields.includes(field)) { - return; - } - const fieldDef = requireField(this.schema, model, field); + return this.cached( + { + type: 'updateData', + model, + withoutFields: [...withoutFields].sort(), + withoutRelationFields, + }, + () => { + const uncheckedVariantFields: Record = {}; + const checkedVariantFields: Record = {}; + const modelDef = requireModel(this.schema, model); + const hasRelation = Object.entries(modelDef.fields).some( + ([key, value]) => value.relation && !withoutFields.includes(key), + ); - if (fieldDef.relation) { - if (withoutRelationFields) { - return; - } - const excludeFields: string[] = []; - const oppositeField = fieldDef.relation.opposite; - if (oppositeField) { - excludeFields.push(oppositeField); - const oppositeFieldDef = requireField(this.schema, fieldDef.type, oppositeField); - if (oppositeFieldDef.relation?.fields) { - excludeFields.push(...oppositeFieldDef.relation.fields); + Object.keys(modelDef.fields).forEach((field) => { + if (withoutFields.includes(field)) { + return; } - } - let fieldSchema: ZodType = z - .lazy(() => this.makeRelationManipulationSchema(fieldDef, excludeFields, 'update')) - .optional(); - // optional to-one relation can be null - if (fieldDef.optional && !fieldDef.array) { - fieldSchema = fieldSchema.nullable(); - } - checkedVariantFields[field] = fieldSchema; - if (fieldDef.array || !fieldDef.relation.references) { - // non-owned relation - uncheckedVariantFields[field] = fieldSchema; - } - } else { - let fieldSchema = this.makeScalarSchema(fieldDef.type, fieldDef.attributes); - - if (this.isNumericField(fieldDef)) { - fieldSchema = z.union([ - fieldSchema, - z - .object({ - set: this.nullableIf(z.number().optional(), !!fieldDef.optional).optional(), - increment: z.number().optional(), - decrement: z.number().optional(), - multiply: z.number().optional(), - divide: z.number().optional(), - }) - .refine( - (v) => Object.keys(v).length === 1, - 'Only one of "set", "increment", "decrement", "multiply", or "divide" can be provided', - ), - ]); - } - - if (fieldDef.array) { - const arraySchema = addListValidation(fieldSchema.array(), fieldDef.attributes); - fieldSchema = z.union([ - arraySchema, - z - .object({ - set: arraySchema.optional(), - push: z.union([fieldSchema, fieldSchema.array()]).optional(), - }) - .refine((v) => Object.keys(v).length === 1, 'Only one of "set", "push" can be provided'), - ]); - } + const fieldDef = requireField(this.schema, model, field); - if (fieldDef.optional) { - if (fieldDef.type === 'Json') { - // DbNull for Json fields - fieldSchema = z.union([fieldSchema, z.instanceof(DbNullClass)]); + if (fieldDef.relation) { + if (withoutRelationFields) { + return; + } + const excludeFields: string[] = []; + const oppositeField = fieldDef.relation.opposite; + if (oppositeField) { + excludeFields.push(oppositeField); + const oppositeFieldDef = requireField(this.schema, fieldDef.type, oppositeField); + if (oppositeFieldDef.relation?.fields) { + excludeFields.push(...oppositeFieldDef.relation.fields); + } + } + let fieldSchema: ZodType = z + .lazy(() => this.makeRelationManipulationSchema(fieldDef, excludeFields, 'update')) + .optional(); + // optional to-one relation can be null + if (fieldDef.optional && !fieldDef.array) { + fieldSchema = fieldSchema.nullable(); + } + checkedVariantFields[field] = fieldSchema; + if (fieldDef.array || !fieldDef.relation.references) { + // non-owned relation + uncheckedVariantFields[field] = fieldSchema; + } } else { - fieldSchema = fieldSchema.nullable(); - } - } + let fieldSchema = this.makeScalarSchema(fieldDef.type, fieldDef.attributes); + + if (this.isNumericField(fieldDef)) { + fieldSchema = z.union([ + fieldSchema, + z + .object({ + set: this.nullableIf(z.number().optional(), !!fieldDef.optional).optional(), + increment: z.number().optional(), + decrement: z.number().optional(), + multiply: z.number().optional(), + divide: z.number().optional(), + }) + .refine( + (v) => Object.keys(v).length === 1, + 'Only one of "set", "increment", "decrement", "multiply", or "divide" can be provided', + ), + ]); + } - // all fields are optional in update - fieldSchema = fieldSchema.optional(); + if (fieldDef.array) { + const arraySchema = addListValidation(fieldSchema.array(), fieldDef.attributes); + fieldSchema = z.union([ + arraySchema, + z + .object({ + set: arraySchema.optional(), + push: z.union([fieldSchema, fieldSchema.array()]).optional(), + }) + .refine( + (v) => Object.keys(v).length === 1, + 'Only one of "set", "push" can be provided', + ), + ]); + } - uncheckedVariantFields[field] = fieldSchema; - if (!fieldDef.foreignKeyFor) { - // non-fk field - checkedVariantFields[field] = fieldSchema; - } - } - }); + if (fieldDef.optional) { + if (fieldDef.type === 'Json') { + // DbNull for Json fields + fieldSchema = z.union([fieldSchema, z.instanceof(DbNullClass)]); + } else { + fieldSchema = fieldSchema.nullable(); + } + } - const uncheckedUpdateSchema = this.extraValidationsEnabled - ? addCustomValidation(z.strictObject(uncheckedVariantFields), modelDef.attributes) - : z.strictObject(uncheckedVariantFields); - const checkedUpdateSchema = this.extraValidationsEnabled - ? addCustomValidation(z.strictObject(checkedVariantFields), modelDef.attributes) - : z.strictObject(checkedVariantFields); - if (!hasRelation) { - return uncheckedUpdateSchema; - } else { - return z.union([uncheckedUpdateSchema, checkedUpdateSchema]); - } + // all fields are optional in update + fieldSchema = fieldSchema.optional(); + + uncheckedVariantFields[field] = fieldSchema; + if (!fieldDef.foreignKeyFor) { + // non-fk field + checkedVariantFields[field] = fieldSchema; + } + } + }); + + const uncheckedUpdateSchema = this.extraValidationsEnabled + ? addCustomValidation(z.strictObject(uncheckedVariantFields), modelDef.attributes) + : z.strictObject(uncheckedVariantFields); + const checkedUpdateSchema = this.extraValidationsEnabled + ? addCustomValidation(z.strictObject(checkedVariantFields), modelDef.attributes) + : z.strictObject(checkedVariantFields); + if (!hasRelation) { + return uncheckedUpdateSchema; + } else { + return z.union([uncheckedUpdateSchema, checkedUpdateSchema]); + } + }, + ); } // #endregion @@ -1671,26 +1956,41 @@ export class InputValidator { // #region Delete private makeDeleteSchema(model: GetModels) { - const baseSchema = z.strictObject({ - where: this.makeWhereSchema(model, true), - select: this.makeSelectSchema(model).optional().nullable(), - include: this.makeIncludeSchema(model).optional().nullable(), - omit: this.makeOmitSchema(model).optional().nullable(), - }); - let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'delete'); - schema = this.refineForSelectIncludeMutuallyExclusive(schema); - schema = this.refineForSelectOmitMutuallyExclusive(schema); - return schema; + return this.cached( + { + type: 'delete', + model, + }, + () => { + const baseSchema = z.strictObject({ + where: this.makeWhereSchema(model, true), + select: this.makeSelectSchema(model).optional().nullable(), + include: this.makeIncludeSchema(model).optional().nullable(), + omit: this.makeOmitSchema(model).optional().nullable(), + }); + let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'delete'); + schema = this.refineForSelectIncludeMutuallyExclusive(schema); + schema = this.refineForSelectOmitMutuallyExclusive(schema); + return schema; + }, + ); } private makeDeleteManySchema(model: GetModels) { - return this.mergePluginArgsSchema( - z.strictObject({ - where: this.makeWhereSchema(model, false).optional(), - limit: z.number().int().nonnegative().optional(), - }), - 'deleteMany', - ).optional(); + return this.cached( + { + type: 'deleteMany', + model, + }, + () => + this.mergePluginArgsSchema( + z.strictObject({ + where: this.makeWhereSchema(model, false).optional(), + limit: z.number().int().nonnegative().optional(), + }), + 'deleteMany', + ).optional(), + ); } // #endregion @@ -1698,33 +1998,48 @@ export class InputValidator { // #region Count makeCountSchema(model: GetModels) { - return this.mergePluginArgsSchema( - z.strictObject({ - where: this.makeWhereSchema(model, false).optional(), - skip: this.makeSkipSchema().optional(), - take: this.makeTakeSchema().optional(), - orderBy: this.orArray(this.makeOrderBySchema(model, true, false), true).optional(), - select: this.makeCountAggregateInputSchema(model).optional(), - }), - 'count', - ).optional(); + return this.cached( + { + type: 'count', + model, + }, + () => + this.mergePluginArgsSchema( + z.strictObject({ + where: this.makeWhereSchema(model, false).optional(), + skip: this.makeSkipSchema().optional(), + take: this.makeTakeSchema().optional(), + orderBy: this.orArray(this.makeOrderBySchema(model, true, false), true).optional(), + select: this.makeCountAggregateInputSchema(model).optional(), + }), + 'count', + ).optional(), + ); } private makeCountAggregateInputSchema(model: GetModels) { - const modelDef = requireModel(this.schema, model); - return z.union([ - z.literal(true), - z.strictObject({ - _all: z.literal(true).optional(), - ...Object.keys(modelDef.fields).reduce( - (acc, field) => { - acc[field] = z.literal(true).optional(); - return acc; - }, - {} as Record, - ), - }), - ]); + return this.cached( + { + type: 'countAggregateInput', + model, + }, + () => { + const modelDef = requireModel(this.schema, model); + return z.union([ + z.literal(true), + z.strictObject({ + _all: z.literal(true).optional(), + ...Object.keys(modelDef.fields).reduce( + (acc, field) => { + acc[field] = z.literal(true).optional(); + return acc; + }, + {} as Record, + ), + }), + ]); + }, + ); } // #endregion @@ -1732,121 +2047,154 @@ export class InputValidator { // #region Aggregate makeAggregateSchema(model: GetModels) { - return this.mergePluginArgsSchema( - z.strictObject({ - where: this.makeWhereSchema(model, false).optional(), - skip: this.makeSkipSchema().optional(), - take: this.makeTakeSchema().optional(), - orderBy: this.orArray(this.makeOrderBySchema(model, true, false), true).optional(), - _count: this.makeCountAggregateInputSchema(model).optional(), - _avg: this.makeSumAvgInputSchema(model).optional(), - _sum: this.makeSumAvgInputSchema(model).optional(), - _min: this.makeMinMaxInputSchema(model).optional(), - _max: this.makeMinMaxInputSchema(model).optional(), - }), - 'aggregate', - ).optional(); + return this.cached( + { + type: 'aggregate', + model, + }, + () => + this.mergePluginArgsSchema( + z.strictObject({ + where: this.makeWhereSchema(model, false).optional(), + skip: this.makeSkipSchema().optional(), + take: this.makeTakeSchema().optional(), + orderBy: this.orArray(this.makeOrderBySchema(model, true, false), true).optional(), + _count: this.makeCountAggregateInputSchema(model).optional(), + _avg: this.makeSumAvgInputSchema(model).optional(), + _sum: this.makeSumAvgInputSchema(model).optional(), + _min: this.makeMinMaxInputSchema(model).optional(), + _max: this.makeMinMaxInputSchema(model).optional(), + }), + 'aggregate', + ).optional(), + ); } makeSumAvgInputSchema(model: GetModels) { - const modelDef = requireModel(this.schema, model); - return z.strictObject( - Object.keys(modelDef.fields).reduce( - (acc, field) => { - const fieldDef = requireField(this.schema, model, field); - if (this.isNumericField(fieldDef)) { - acc[field] = z.literal(true).optional(); - } - return acc; - }, - {} as Record, - ), + return this.cached( + { + type: 'sumAvgInput', + model, + }, + () => { + const modelDef = requireModel(this.schema, model); + return z.strictObject( + Object.keys(modelDef.fields).reduce( + (acc, field) => { + const fieldDef = requireField(this.schema, model, field); + if (this.isNumericField(fieldDef)) { + acc[field] = z.literal(true).optional(); + } + return acc; + }, + {} as Record, + ), + ); + }, ); } makeMinMaxInputSchema(model: GetModels) { - const modelDef = requireModel(this.schema, model); - return z.strictObject( - Object.keys(modelDef.fields).reduce( - (acc, field) => { - const fieldDef = requireField(this.schema, model, field); - if (!fieldDef.relation && !fieldDef.array) { - acc[field] = z.literal(true).optional(); - } - return acc; - }, - {} as Record, - ), + return this.cached( + { + type: 'minMaxInput', + model, + }, + () => { + const modelDef = requireModel(this.schema, model); + return z.strictObject( + Object.keys(modelDef.fields).reduce( + (acc, field) => { + const fieldDef = requireField(this.schema, model, field); + if (!fieldDef.relation && !fieldDef.array) { + acc[field] = z.literal(true).optional(); + } + return acc; + }, + {} as Record, + ), + ); + }, ); } private makeGroupBySchema(model: GetModels) { - const modelDef = requireModel(this.schema, model); - const nonRelationFields = Object.keys(modelDef.fields).filter((field) => !modelDef.fields[field]?.relation); - const bySchema = - nonRelationFields.length > 0 - ? this.orArray(z.enum(nonRelationFields as [string, ...string[]]), true) - : z.never(); - - const baseSchema = z.strictObject({ - where: this.makeWhereSchema(model, false).optional(), - orderBy: this.orArray(this.makeOrderBySchema(model, false, true), true).optional(), - by: bySchema, - having: this.makeHavingSchema(model).optional(), - skip: this.makeSkipSchema().optional(), - take: this.makeTakeSchema().optional(), - _count: this.makeCountAggregateInputSchema(model).optional(), - _avg: this.makeSumAvgInputSchema(model).optional(), - _sum: this.makeSumAvgInputSchema(model).optional(), - _min: this.makeMinMaxInputSchema(model).optional(), - _max: this.makeMinMaxInputSchema(model).optional(), - }); - - let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'groupBy'); - - // fields used in `having` must be either in the `by` list, or aggregations - schema = schema.refine((value: any) => { - const bys = typeof value.by === 'string' ? [value.by] : value.by; - if (value.having && typeof value.having === 'object') { - for (const [key, val] of Object.entries(value.having)) { - if (AGGREGATE_OPERATORS.includes(key as any)) { - continue; - } - if (bys.includes(key)) { - continue; - } - // we have a key not mentioned in `by`, in this case it must only use - // aggregations in the condition - - // 1. payload must be an object - if (!val || typeof val !== 'object') { - return false; + return this.cached( + { + type: 'groupBy', + model, + }, + () => { + const modelDef = requireModel(this.schema, model); + const nonRelationFields = Object.keys(modelDef.fields).filter( + (field) => !modelDef.fields[field]?.relation, + ); + const bySchema = + nonRelationFields.length > 0 + ? this.orArray(z.enum(nonRelationFields as [string, ...string[]]), true) + : z.never(); + + const baseSchema = z.strictObject({ + where: this.makeWhereSchema(model, false).optional(), + orderBy: this.orArray(this.makeOrderBySchema(model, false, true), true).optional(), + by: bySchema, + having: this.makeHavingSchema(model).optional(), + skip: this.makeSkipSchema().optional(), + take: this.makeTakeSchema().optional(), + _count: this.makeCountAggregateInputSchema(model).optional(), + _avg: this.makeSumAvgInputSchema(model).optional(), + _sum: this.makeSumAvgInputSchema(model).optional(), + _min: this.makeMinMaxInputSchema(model).optional(), + _max: this.makeMinMaxInputSchema(model).optional(), + }); + + let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'groupBy'); + + // fields used in `having` must be either in the `by` list, or aggregations + schema = schema.refine((value: any) => { + const bys = typeof value.by === 'string' ? [value.by] : value.by; + if (value.having && typeof value.having === 'object') { + for (const [key, val] of Object.entries(value.having)) { + if (AGGREGATE_OPERATORS.includes(key as any)) { + continue; + } + if (bys.includes(key)) { + continue; + } + // we have a key not mentioned in `by`, in this case it must only use + // aggregations in the condition + + // 1. payload must be an object + if (!val || typeof val !== 'object') { + return false; + } + // 2. payload must only contain aggregations + if (!this.onlyAggregationFields(val)) { + return false; + } + } } - // 2. payload must only contain aggregations - if (!this.onlyAggregationFields(val)) { + return true; + }, 'fields in "having" must be in "by"'); + + // fields used in `orderBy` must be either in the `by` list, or aggregations + schema = schema.refine((value: any) => { + const bys = typeof value.by === 'string' ? [value.by] : value.by; + if ( + value.orderBy && + Object.keys(value.orderBy) + .filter((f) => !AGGREGATE_OPERATORS.includes(f as AGGREGATE_OPERATORS)) + .some((key) => !bys.includes(key)) + ) { return false; + } else { + return true; } - } - } - return true; - }, 'fields in "having" must be in "by"'); - - // fields used in `orderBy` must be either in the `by` list, or aggregations - schema = schema.refine((value: any) => { - const bys = typeof value.by === 'string' ? [value.by] : value.by; - if ( - value.orderBy && - Object.keys(value.orderBy) - .filter((f) => !AGGREGATE_OPERATORS.includes(f as AGGREGATE_OPERATORS)) - .some((key) => !bys.includes(key)) - ) { - return false; - } else { - return true; - } - }, 'fields in "orderBy" must be in "by"'); + }, 'fields in "orderBy" must be in "by"'); - return schema; + return schema; + }, + ); } private onlyAggregationFields(val: object) { @@ -1867,19 +2215,96 @@ export class InputValidator { } private makeHavingSchema(model: GetModels) { + // `makeWhereSchema` is cached return this.makeWhereSchema(model, false, true, true); } // #endregion + // #region Procedures + + private makeProcedureParamSchema(param: { type: string; array?: boolean; optional?: boolean }): z.ZodType { + return this.cached( + { + type: 'procedureParam', + param, + }, + () => { + let schema: z.ZodType; + + if (isTypeDef(this.schema, param.type)) { + schema = this.makeTypeDefSchema(param.type); + } else if (isEnum(this.schema, param.type)) { + schema = this.makeEnumSchema(param.type); + } else if (param.type in (this.schema.models ?? {})) { + // For model-typed values, accept any object (no deep shape validation). + schema = z.record(z.string(), z.unknown()); + } else { + // Builtin scalar types. + schema = this.makeScalarSchema(param.type as BuiltinType); + + // If a type isn't recognized by any of the above branches, `makeScalarSchema` returns `unknown`. + // Treat it as configuration/schema error. + if (schema instanceof z.ZodUnknown) { + throw createInternalError(`Unsupported procedure parameter type: ${param.type}`); + } + } + + if (param.array) { + schema = schema.array(); + } + if (param.optional) { + schema = schema.optional(); + } + + return schema; + }, + ); + } + + // #endregion + + // #region Cache Management + + private cached(key: Record, factory: () => T): T { + const cacheKey = stableStringify(key); + let schema = this.getSchemaCache(cacheKey!); + if (schema) { + return schema as T; + } + schema = factory(); + this.setSchemaCache(cacheKey!, schema); + return schema as T; + } + + private getSchemaCache(cacheKey: string) { + return this.schemaCache.get(cacheKey); + } + + private setSchemaCache(cacheKey: string, schema: ZodType) { + return this.schemaCache.set(cacheKey, schema); + } + + // @ts-ignore + private printCacheStats(detailed = false) { + console.log('Schema cache size:', this.schemaCache.size); + if (detailed) { + for (const key of this.schemaCache.keys()) { + console.log(`\t${key}`); + } + } + } + + // #endregion + // #region Helpers private makeSkipSchema() { - return z.number().int().nonnegative(); + return this.cached({ type: 'skip' }, () => z.number().int().nonnegative()); } private makeTakeSchema() { - return z.number().int(); + return this.cached({ type: 'take' }, () => z.number().int()); } private refineForSelectIncludeMutuallyExclusive(schema: ZodType) { @@ -1911,5 +2336,6 @@ export class InputValidator { private get providerSupportsCaseSensitivity() { return this.schema.provider.type === 'postgresql'; } + // #endregion } diff --git a/packages/schema/src/schema.ts b/packages/schema/src/schema.ts index 58fc1bc5b..08b0726f6 100644 --- a/packages/schema/src/schema.ts +++ b/packages/schema/src/schema.ts @@ -106,6 +106,7 @@ export type EnumField = { }; export type EnumDef = { + name: string; fields?: Record; values: Record; attributes?: readonly AttributeApplication[]; diff --git a/packages/sdk/src/ts-schema-generator.ts b/packages/sdk/src/ts-schema-generator.ts index 4a5e0a548..16f81bae2 100644 --- a/packages/sdk/src/ts-schema-generator.ts +++ b/packages/sdk/src/ts-schema-generator.ts @@ -1030,6 +1030,8 @@ export class TsSchemaGenerator { private createEnumObject(e: Enum) { return ts.factory.createObjectLiteralExpression( [ + ts.factory.createPropertyAssignment('name', ts.factory.createStringLiteral(e.name)), + ts.factory.createPropertyAssignment( 'values', ts.factory.createObjectLiteralExpression( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c202a4423..02aef9843 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1120,6 +1120,9 @@ importers: '@zenstackhq/orm': specifier: workspace:* version: link:../../packages/orm + '@zenstackhq/plugin-policy': + specifier: workspace:* + version: link:../../packages/plugins/policy '@zenstackhq/sdk': specifier: workspace:* version: link:../../packages/sdk @@ -13073,8 +13076,8 @@ snapshots: '@babel/parser': 7.28.5 eslint: 9.29.0(jiti@2.6.1) hermes-parser: 0.25.1 - zod: 4.1.12 - zod-validation-error: 4.0.1(zod@4.1.12) + zod: 4.3.6 + zod-validation-error: 4.0.1(zod@4.3.6) transitivePeerDependencies: - supports-color @@ -17131,6 +17134,10 @@ snapshots: dependencies: zod: 4.1.12 + zod-validation-error@4.0.1(zod@4.3.6): + dependencies: + zod: 4.3.6 + zod@4.1.12: {} zod@4.3.6: {} diff --git a/samples/orm/zenstack/schema.ts b/samples/orm/zenstack/schema.ts index e3c02e5c6..908df1e71 100644 --- a/samples/orm/zenstack/schema.ts +++ b/samples/orm/zenstack/schema.ts @@ -232,6 +232,7 @@ export class SchemaType implements SchemaDef { } as const; enums = { Role: { + name: "Role", values: { ADMIN: "ADMIN", USER: "USER" diff --git a/tests/e2e/apps/rally/zenstack/schema.ts b/tests/e2e/apps/rally/zenstack/schema.ts index 7d20facea..d9ddfdb78 100644 --- a/tests/e2e/apps/rally/zenstack/schema.ts +++ b/tests/e2e/apps/rally/zenstack/schema.ts @@ -2391,6 +2391,7 @@ export class SchemaType implements SchemaDef { } as const; enums = { TimeFormat: { + name: "TimeFormat", values: { hours12: "hours12", hours24: "hours24" @@ -2400,6 +2401,7 @@ export class SchemaType implements SchemaDef { ] }, UserRole: { + name: "UserRole", values: { admin: "admin", user: "user" @@ -2409,6 +2411,7 @@ export class SchemaType implements SchemaDef { ] }, ParticipantVisibility: { + name: "ParticipantVisibility", values: { full: "full", scoresOnly: "scoresOnly", @@ -2419,6 +2422,7 @@ export class SchemaType implements SchemaDef { ] }, PollStatus: { + name: "PollStatus", values: { live: "live", paused: "paused", @@ -2429,6 +2433,7 @@ export class SchemaType implements SchemaDef { ] }, VoteType: { + name: "VoteType", values: { yes: "yes", no: "no", @@ -2439,12 +2444,14 @@ export class SchemaType implements SchemaDef { ] }, SpaceMemberRole: { + name: "SpaceMemberRole", values: { ADMIN: "ADMIN", MEMBER: "MEMBER" } }, SpaceTier: { + name: "SpaceTier", values: { hobby: "hobby", pro: "pro" @@ -2454,6 +2461,7 @@ export class SchemaType implements SchemaDef { ] }, SubscriptionStatus: { + name: "SubscriptionStatus", values: { incomplete: "incomplete", incomplete_expired: "incomplete_expired", @@ -2469,6 +2477,7 @@ export class SchemaType implements SchemaDef { ] }, SubscriptionInterval: { + name: "SubscriptionInterval", values: { month: "month", year: "year" @@ -2478,6 +2487,7 @@ export class SchemaType implements SchemaDef { ] }, ScheduledEventStatus: { + name: "ScheduledEventStatus", values: { confirmed: "confirmed", canceled: "canceled", @@ -2488,6 +2498,7 @@ export class SchemaType implements SchemaDef { ] }, ScheduledEventInviteStatus: { + name: "ScheduledEventInviteStatus", values: { pending: "pending", accepted: "accepted", @@ -2499,11 +2510,13 @@ export class SchemaType implements SchemaDef { ] }, CredentialType: { + name: "CredentialType", values: { OAUTH: "OAUTH" } }, LicenseType: { + name: "LicenseType", values: { PLUS: "PLUS", ORGANIZATION: "ORGANIZATION", @@ -2511,6 +2524,7 @@ export class SchemaType implements SchemaDef { } }, LicenseStatus: { + name: "LicenseStatus", values: { ACTIVE: "ACTIVE", REVOKED: "REVOKED" diff --git a/tests/e2e/github-repos/cal.com/schema.ts b/tests/e2e/github-repos/cal.com/schema.ts index 7715a296c..47bb801a4 100644 --- a/tests/e2e/github-repos/cal.com/schema.ts +++ b/tests/e2e/github-repos/cal.com/schema.ts @@ -9185,6 +9185,7 @@ export class SchemaType implements SchemaDef { } as const; enums = { SchedulingType: { + name: "SchedulingType", values: { ROUND_ROBIN: "ROUND_ROBIN", COLLECTIVE: "COLLECTIVE", @@ -9212,6 +9213,7 @@ export class SchemaType implements SchemaDef { } }, PeriodType: { + name: "PeriodType", values: { UNLIMITED: "UNLIMITED", ROLLING: "ROLLING", @@ -9246,6 +9248,7 @@ export class SchemaType implements SchemaDef { } }, CreationSource: { + name: "CreationSource", values: { API_V1: "API_V1", API_V2: "API_V2", @@ -9273,6 +9276,7 @@ export class SchemaType implements SchemaDef { } }, IdentityProvider: { + name: "IdentityProvider", values: { CAL: "CAL", GOOGLE: "GOOGLE", @@ -9280,18 +9284,21 @@ export class SchemaType implements SchemaDef { } }, UserPermissionRole: { + name: "UserPermissionRole", values: { USER: "USER", ADMIN: "ADMIN" } }, CreditType: { + name: "CreditType", values: { MONTHLY: "MONTHLY", ADDITIONAL: "ADDITIONAL" } }, MembershipRole: { + name: "MembershipRole", values: { MEMBER: "MEMBER", ADMIN: "ADMIN", @@ -9299,6 +9306,7 @@ export class SchemaType implements SchemaDef { } }, BookingStatus: { + name: "BookingStatus", values: { CANCELLED: "CANCELLED", ACCEPTED: "ACCEPTED", @@ -9340,6 +9348,7 @@ export class SchemaType implements SchemaDef { } }, EventTypeCustomInputType: { + name: "EventTypeCustomInputType", values: { TEXT: "TEXT", TEXTLONG: "TEXTLONG", @@ -9388,17 +9397,20 @@ export class SchemaType implements SchemaDef { } }, ReminderType: { + name: "ReminderType", values: { PENDING_BOOKING_CONFIRMATION: "PENDING_BOOKING_CONFIRMATION" } }, PaymentOption: { + name: "PaymentOption", values: { ON_BOOKING: "ON_BOOKING", HOLD: "HOLD" } }, WebhookTriggerEvents: { + name: "WebhookTriggerEvents", values: { BOOKING_CREATED: "BOOKING_CREATED", BOOKING_PAYMENT_INITIATED: "BOOKING_PAYMENT_INITIATED", @@ -9421,6 +9433,7 @@ export class SchemaType implements SchemaDef { } }, AppCategories: { + name: "AppCategories", values: { calendar: "calendar", messaging: "messaging", @@ -9435,6 +9448,7 @@ export class SchemaType implements SchemaDef { } }, WorkflowTriggerEvents: { + name: "WorkflowTriggerEvents", values: { BEFORE_EVENT: "BEFORE_EVENT", EVENT_CANCELLED: "EVENT_CANCELLED", @@ -9446,6 +9460,7 @@ export class SchemaType implements SchemaDef { } }, WorkflowActions: { + name: "WorkflowActions", values: { EMAIL_HOST: "EMAIL_HOST", EMAIL_ATTENDEE: "EMAIL_ATTENDEE", @@ -9457,6 +9472,7 @@ export class SchemaType implements SchemaDef { } }, TimeUnit: { + name: "TimeUnit", values: { DAY: "DAY", HOUR: "HOUR", @@ -9484,6 +9500,7 @@ export class SchemaType implements SchemaDef { } }, WorkflowTemplates: { + name: "WorkflowTemplates", values: { REMINDER: "REMINDER", CUSTOM: "CUSTOM", @@ -9494,6 +9511,7 @@ export class SchemaType implements SchemaDef { } }, WorkflowMethods: { + name: "WorkflowMethods", values: { EMAIL: "EMAIL", SMS: "SMS", @@ -9501,6 +9519,7 @@ export class SchemaType implements SchemaDef { } }, FeatureType: { + name: "FeatureType", values: { RELEASE: "RELEASE", EXPERIMENT: "EXPERIMENT", @@ -9510,24 +9529,28 @@ export class SchemaType implements SchemaDef { } }, RRResetInterval: { + name: "RRResetInterval", values: { MONTH: "MONTH", DAY: "DAY" } }, RRTimestampBasis: { + name: "RRTimestampBasis", values: { CREATED_AT: "CREATED_AT", START_TIME: "START_TIME" } }, AccessScope: { + name: "AccessScope", values: { READ_BOOKING: "READ_BOOKING", READ_PROFILE: "READ_PROFILE" } }, RedirectType: { + name: "RedirectType", values: { UserEventType: "UserEventType", TeamEventType: "TeamEventType", @@ -9562,6 +9585,7 @@ export class SchemaType implements SchemaDef { } }, SMSLockState: { + name: "SMSLockState", values: { LOCKED: "LOCKED", UNLOCKED: "UNLOCKED", @@ -9569,6 +9593,7 @@ export class SchemaType implements SchemaDef { } }, AttributeType: { + name: "AttributeType", values: { TEXT: "TEXT", NUMBER: "NUMBER", @@ -9577,6 +9602,7 @@ export class SchemaType implements SchemaDef { } }, AssignmentReasonEnum: { + name: "AssignmentReasonEnum", values: { ROUTING_FORM_ROUTING: "ROUTING_FORM_ROUTING", ROUTING_FORM_ROUTING_FALLBACK: "ROUTING_FORM_ROUTING_FALLBACK", @@ -9587,12 +9613,14 @@ export class SchemaType implements SchemaDef { } }, EventTypeAutoTranslatedField: { + name: "EventTypeAutoTranslatedField", values: { DESCRIPTION: "DESCRIPTION", TITLE: "TITLE" } }, WatchlistType: { + name: "WatchlistType", values: { EMAIL: "EMAIL", DOMAIN: "DOMAIN", @@ -9600,6 +9628,7 @@ export class SchemaType implements SchemaDef { } }, WatchlistSeverity: { + name: "WatchlistSeverity", values: { LOW: "LOW", MEDIUM: "MEDIUM", @@ -9608,29 +9637,34 @@ export class SchemaType implements SchemaDef { } }, BillingPeriod: { + name: "BillingPeriod", values: { MONTHLY: "MONTHLY", ANNUALLY: "ANNUALLY" } }, IncompleteBookingActionType: { + name: "IncompleteBookingActionType", values: { SALESFORCE: "SALESFORCE" } }, FilterSegmentScope: { + name: "FilterSegmentScope", values: { USER: "USER", TEAM: "TEAM" } }, WorkflowContactType: { + name: "WorkflowContactType", values: { PHONE: "PHONE", EMAIL: "EMAIL" } }, RoleType: { + name: "RoleType", values: { SYSTEM: "SYSTEM", CUSTOM: "CUSTOM" diff --git a/tests/e2e/github-repos/formbricks/schema.ts b/tests/e2e/github-repos/formbricks/schema.ts index 935b8320b..f3e478760 100644 --- a/tests/e2e/github-repos/formbricks/schema.ts +++ b/tests/e2e/github-repos/formbricks/schema.ts @@ -2834,6 +2834,7 @@ export class SchemaType implements SchemaDef { } as const; enums = { PipelineTriggers: { + name: "PipelineTriggers", values: { responseCreated: "responseCreated", responseUpdated: "responseUpdated", @@ -2841,6 +2842,7 @@ export class SchemaType implements SchemaDef { } }, WebhookSource: { + name: "WebhookSource", values: { user: "user", zapier: "zapier", @@ -2850,12 +2852,14 @@ export class SchemaType implements SchemaDef { } }, ContactAttributeType: { + name: "ContactAttributeType", values: { default: "default", custom: "custom" } }, SurveyStatus: { + name: "SurveyStatus", values: { draft: "draft", scheduled: "scheduled", @@ -2865,18 +2869,21 @@ export class SchemaType implements SchemaDef { } }, DisplayStatus: { + name: "DisplayStatus", values: { seen: "seen", responded: "responded" } }, SurveyAttributeFilterCondition: { + name: "SurveyAttributeFilterCondition", values: { equals: "equals", notEquals: "notEquals" } }, SurveyType: { + name: "SurveyType", values: { link: "link", web: "web", @@ -2885,6 +2892,7 @@ export class SchemaType implements SchemaDef { } }, displayOptions: { + name: "displayOptions", values: { displayOnce: "displayOnce", displayMultiple: "displayMultiple", @@ -2893,18 +2901,21 @@ export class SchemaType implements SchemaDef { } }, ActionType: { + name: "ActionType", values: { code: "code", noCode: "noCode" } }, EnvironmentType: { + name: "EnvironmentType", values: { production: "production", development: "development" } }, IntegrationType: { + name: "IntegrationType", values: { googleSheets: "googleSheets", notion: "notion", @@ -2913,6 +2924,7 @@ export class SchemaType implements SchemaDef { } }, DataMigrationStatus: { + name: "DataMigrationStatus", values: { pending: "pending", applied: "applied", @@ -2920,6 +2932,7 @@ export class SchemaType implements SchemaDef { } }, WidgetPlacement: { + name: "WidgetPlacement", values: { bottomLeft: "bottomLeft", bottomRight: "bottomRight", @@ -2929,6 +2942,7 @@ export class SchemaType implements SchemaDef { } }, OrganizationRole: { + name: "OrganizationRole", values: { owner: "owner", manager: "manager", @@ -2937,6 +2951,7 @@ export class SchemaType implements SchemaDef { } }, MembershipRole: { + name: "MembershipRole", values: { owner: "owner", admin: "admin", @@ -2946,6 +2961,7 @@ export class SchemaType implements SchemaDef { } }, ApiKeyPermission: { + name: "ApiKeyPermission", values: { read: "read", write: "write", @@ -2953,6 +2969,7 @@ export class SchemaType implements SchemaDef { } }, IdentityProvider: { + name: "IdentityProvider", values: { email: "email", github: "github", @@ -2963,6 +2980,7 @@ export class SchemaType implements SchemaDef { } }, Role: { + name: "Role", values: { project_manager: "project_manager", engineer: "engineer", @@ -2972,6 +2990,7 @@ export class SchemaType implements SchemaDef { } }, Objective: { + name: "Objective", values: { increase_conversion: "increase_conversion", improve_user_retention: "improve_user_retention", @@ -2982,6 +3001,7 @@ export class SchemaType implements SchemaDef { } }, Intention: { + name: "Intention", values: { survey_user_segments: "survey_user_segments", survey_at_specific_point_in_user_journey: "survey_at_specific_point_in_user_journey", @@ -2991,6 +3011,7 @@ export class SchemaType implements SchemaDef { } }, InsightCategory: { + name: "InsightCategory", values: { featureRequest: "featureRequest", complaint: "complaint", @@ -2999,6 +3020,7 @@ export class SchemaType implements SchemaDef { } }, Sentiment: { + name: "Sentiment", values: { positive: "positive", negative: "negative", @@ -3006,12 +3028,14 @@ export class SchemaType implements SchemaDef { } }, TeamUserRole: { + name: "TeamUserRole", values: { admin: "admin", contributor: "contributor" } }, ProjectTeamPermission: { + name: "ProjectTeamPermission", values: { read: "read", readWrite: "readWrite", diff --git a/tests/e2e/github-repos/trigger.dev/schema.ts b/tests/e2e/github-repos/trigger.dev/schema.ts index 9b884d078..12dab9e1b 100644 --- a/tests/e2e/github-repos/trigger.dev/schema.ts +++ b/tests/e2e/github-repos/trigger.dev/schema.ts @@ -6081,18 +6081,21 @@ export class SchemaType implements SchemaDef { } as const; enums = { AuthenticationMethod: { + name: "AuthenticationMethod", values: { GITHUB: "GITHUB", MAGIC_LINK: "MAGIC_LINK" } }, OrgMemberRole: { + name: "OrgMemberRole", values: { ADMIN: "ADMIN", MEMBER: "MEMBER" } }, RuntimeEnvironmentType: { + name: "RuntimeEnvironmentType", values: { PRODUCTION: "PRODUCTION", STAGING: "STAGING", @@ -6101,24 +6104,28 @@ export class SchemaType implements SchemaDef { } }, ProjectVersion: { + name: "ProjectVersion", values: { V2: "V2", V3: "V3" } }, SecretStoreProvider: { + name: "SecretStoreProvider", values: { DATABASE: "DATABASE", AWS_PARAM_STORE: "AWS_PARAM_STORE" } }, TaskTriggerSource: { + name: "TaskTriggerSource", values: { STANDARD: "STANDARD", SCHEDULED: "SCHEDULED" } }, TaskRunStatus: { + name: "TaskRunStatus", values: { DELAYED: "DELAYED", PENDING: "PENDING", @@ -6139,12 +6146,14 @@ export class SchemaType implements SchemaDef { } }, RunEngineVersion: { + name: "RunEngineVersion", values: { V1: "V1", V2: "V2" } }, TaskRunExecutionStatus: { + name: "TaskRunExecutionStatus", values: { RUN_CREATED: "RUN_CREATED", QUEUED: "QUEUED", @@ -6158,12 +6167,14 @@ export class SchemaType implements SchemaDef { } }, TaskRunCheckpointType: { + name: "TaskRunCheckpointType", values: { DOCKER: "DOCKER", KUBERNETES: "KUBERNETES" } }, WaitpointType: { + name: "WaitpointType", values: { RUN: "RUN", DATETIME: "DATETIME", @@ -6172,18 +6183,21 @@ export class SchemaType implements SchemaDef { } }, WaitpointStatus: { + name: "WaitpointStatus", values: { PENDING: "PENDING", COMPLETED: "COMPLETED" } }, WorkerInstanceGroupType: { + name: "WorkerInstanceGroupType", values: { MANAGED: "MANAGED", UNMANAGED: "UNMANAGED" } }, TaskRunAttemptStatus: { + name: "TaskRunAttemptStatus", values: { PENDING: "PENDING", EXECUTING: "EXECUTING", @@ -6194,6 +6208,7 @@ export class SchemaType implements SchemaDef { } }, TaskEventLevel: { + name: "TaskEventLevel", values: { TRACE: "TRACE", DEBUG: "DEBUG", @@ -6204,6 +6219,7 @@ export class SchemaType implements SchemaDef { } }, TaskEventKind: { + name: "TaskEventKind", values: { UNSPECIFIED: "UNSPECIFIED", INTERNAL: "INTERNAL", @@ -6216,6 +6232,7 @@ export class SchemaType implements SchemaDef { } }, TaskEventStatus: { + name: "TaskEventStatus", values: { UNSET: "UNSET", OK: "OK", @@ -6224,18 +6241,21 @@ export class SchemaType implements SchemaDef { } }, TaskQueueType: { + name: "TaskQueueType", values: { VIRTUAL: "VIRTUAL", NAMED: "NAMED" } }, TaskQueueVersion: { + name: "TaskQueueVersion", values: { V1: "V1", V2: "V2" } }, BatchTaskRunStatus: { + name: "BatchTaskRunStatus", values: { PENDING: "PENDING", COMPLETED: "COMPLETED", @@ -6243,6 +6263,7 @@ export class SchemaType implements SchemaDef { } }, BatchTaskRunItemStatus: { + name: "BatchTaskRunItemStatus", values: { PENDING: "PENDING", FAILED: "FAILED", @@ -6251,18 +6272,21 @@ export class SchemaType implements SchemaDef { } }, CheckpointType: { + name: "CheckpointType", values: { DOCKER: "DOCKER", KUBERNETES: "KUBERNETES" } }, CheckpointRestoreEventType: { + name: "CheckpointRestoreEventType", values: { CHECKPOINT: "CHECKPOINT", RESTORE: "RESTORE" } }, WorkerDeploymentType: { + name: "WorkerDeploymentType", values: { MANAGED: "MANAGED", UNMANAGED: "UNMANAGED", @@ -6270,6 +6294,7 @@ export class SchemaType implements SchemaDef { } }, WorkerDeploymentStatus: { + name: "WorkerDeploymentStatus", values: { PENDING: "PENDING", BUILDING: "BUILDING", @@ -6281,17 +6306,20 @@ export class SchemaType implements SchemaDef { } }, ScheduleType: { + name: "ScheduleType", values: { DECLARATIVE: "DECLARATIVE", IMPERATIVE: "IMPERATIVE" } }, ScheduleGeneratorType: { + name: "ScheduleGeneratorType", values: { CRON: "CRON" } }, ProjectAlertChannelType: { + name: "ProjectAlertChannelType", values: { EMAIL: "EMAIL", SLACK: "SLACK", @@ -6299,6 +6327,7 @@ export class SchemaType implements SchemaDef { } }, ProjectAlertType: { + name: "ProjectAlertType", values: { TASK_RUN: "TASK_RUN", TASK_RUN_ATTEMPT: "TASK_RUN_ATTEMPT", @@ -6307,6 +6336,7 @@ export class SchemaType implements SchemaDef { } }, ProjectAlertStatus: { + name: "ProjectAlertStatus", values: { PENDING: "PENDING", SENT: "SENT", @@ -6314,23 +6344,27 @@ export class SchemaType implements SchemaDef { } }, IntegrationService: { + name: "IntegrationService", values: { SLACK: "SLACK" } }, BulkActionType: { + name: "BulkActionType", values: { CANCEL: "CANCEL", REPLAY: "REPLAY" } }, BulkActionStatus: { + name: "BulkActionStatus", values: { PENDING: "PENDING", COMPLETED: "COMPLETED" } }, BulkActionItemStatus: { + name: "BulkActionItemStatus", values: { PENDING: "PENDING", COMPLETED: "COMPLETED", diff --git a/tests/e2e/orm/schemas/basic/schema.ts b/tests/e2e/orm/schemas/basic/schema.ts index 1e559cc79..9d6cb1982 100644 --- a/tests/e2e/orm/schemas/basic/schema.ts +++ b/tests/e2e/orm/schemas/basic/schema.ts @@ -295,6 +295,7 @@ export class SchemaType implements SchemaDef { } as const; enums = { Role: { + name: "Role", values: { ADMIN: "ADMIN", USER: "USER" diff --git a/tests/e2e/orm/schemas/name-mapping/schema.ts b/tests/e2e/orm/schemas/name-mapping/schema.ts index 8e610ffd0..b92cd14b4 100644 --- a/tests/e2e/orm/schemas/name-mapping/schema.ts +++ b/tests/e2e/orm/schemas/name-mapping/schema.ts @@ -90,6 +90,7 @@ export class SchemaType implements SchemaDef { } as const; enums = { Role: { + name: "Role", values: { USER: "USER", ADMIN: "ADMIN", diff --git a/tests/e2e/orm/schemas/procedures/schema.ts b/tests/e2e/orm/schemas/procedures/schema.ts index f5a9044e5..bd2b10018 100644 --- a/tests/e2e/orm/schemas/procedures/schema.ts +++ b/tests/e2e/orm/schemas/procedures/schema.ts @@ -69,6 +69,7 @@ export class SchemaType implements SchemaDef { } as const; enums = { Role: { + name: "Role", values: { ADMIN: "ADMIN", USER: "USER" diff --git a/tests/e2e/orm/schemas/typed-json/schema.ts b/tests/e2e/orm/schemas/typed-json/schema.ts index 11be6f901..d99f97b5d 100644 --- a/tests/e2e/orm/schemas/typed-json/schema.ts +++ b/tests/e2e/orm/schemas/typed-json/schema.ts @@ -94,6 +94,7 @@ export class SchemaType implements SchemaDef { } as const; enums = { Gender: { + name: "Gender", values: { MALE: "MALE", FEMALE: "FEMALE" diff --git a/tests/e2e/orm/schemas/typing/schema.ts b/tests/e2e/orm/schemas/typing/schema.ts index a558c23c6..4c82da67d 100644 --- a/tests/e2e/orm/schemas/typing/schema.ts +++ b/tests/e2e/orm/schemas/typing/schema.ts @@ -328,12 +328,14 @@ export class SchemaType implements SchemaDef { } as const; enums = { Role: { + name: "Role", values: { ADMIN: "ADMIN", USER: "USER" } }, Status: { + name: "Status", values: { ACTIVE: "ACTIVE", INACTIVE: "INACTIVE", diff --git a/tests/regression/package.json b/tests/regression/package.json index d6b14dac5..12f1f3014 100644 --- a/tests/regression/package.json +++ b/tests/regression/package.json @@ -20,6 +20,7 @@ "@zenstackhq/language": "workspace:*", "@zenstackhq/orm": "workspace:*", "@zenstackhq/sdk": "workspace:*", + "@zenstackhq/plugin-policy": "workspace:*", "@zenstackhq/typescript-config": "workspace:*", "@zenstackhq/vitest-config": "workspace:*", "@types/node": "catalog:" diff --git a/tests/regression/test/issue-204/schema.ts b/tests/regression/test/issue-204/schema.ts index 5b24b3ae7..3c8726b39 100644 --- a/tests/regression/test/issue-204/schema.ts +++ b/tests/regression/test/issue-204/schema.ts @@ -47,6 +47,7 @@ export class SchemaType implements SchemaDef { } as const; enums = { ShirtColor: { + name: "ShirtColor", values: { Black: "Black", White: "White", From 291833b2b61f0de4e6a58d25afe0598614210a11 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 26 Jan 2026 22:31:25 +0800 Subject: [PATCH 2/9] refactor: use a decorator-based approach for caching --- .../client/crud/validator/cache-decorator.ts | 62 + .../orm/src/client/crud/validator/index.ts | 2455 +++++++---------- 2 files changed, 1120 insertions(+), 1397 deletions(-) create mode 100644 packages/orm/src/client/crud/validator/cache-decorator.ts diff --git a/packages/orm/src/client/crud/validator/cache-decorator.ts b/packages/orm/src/client/crud/validator/cache-decorator.ts new file mode 100644 index 000000000..d666c5c46 --- /dev/null +++ b/packages/orm/src/client/crud/validator/cache-decorator.ts @@ -0,0 +1,62 @@ +import stableStringify from 'json-stable-stringify'; + +interface CacheOptions { + /** + * Instance property names to include in the cache key. + * Useful when cache should be invalidated based on instance state. + */ + includeProperties?: string[]; +} + +/** + * Method decorator that caches the return value based on method name and arguments. + * + * Requirements: + * - Class must have a `getCache(key: string)` method + * - Class must have a `setCache(key: string, value: any)` method + */ +export function cache(options: CacheOptions = {}) { + return function (_target: any, propertyKey: string, descriptor: PropertyDescriptor) { + const originalMethod = descriptor.value; + + descriptor.value = function ( + this: { + getCache: (key: string) => unknown; + setCache: (key: string, value: unknown) => void; + } & Record, + ...args: any[] + ) { + // Build cache key object + const cacheKeyObj: Record = { + $call: propertyKey, + ...args, + }; + + // Include specified instance properties + if (options.includeProperties) { + for (const prop of options.includeProperties) { + cacheKeyObj['$' + prop] = this[prop]; + } + } + + // Generate stable string key + const cacheKey = stableStringify(cacheKeyObj)!; + + // Check cache + const cached = this.getCache(cacheKey); + if (cached) { + return cached; + } + + // Execute original method + const result = originalMethod.apply(this, args); + + // Store in cache + this.setCache(cacheKey, result); + + return result; + }; + + return descriptor; + }; +} diff --git a/packages/orm/src/client/crud/validator/index.ts b/packages/orm/src/client/crud/validator/index.ts index e73fa3e39..1c9067cda 100644 --- a/packages/orm/src/client/crud/validator/index.ts +++ b/packages/orm/src/client/crud/validator/index.ts @@ -1,6 +1,5 @@ import { enumerate, invariant } from '@zenstackhq/common-helpers'; import Decimal from 'decimal.js'; -import stableStringify from 'json-stable-stringify'; import { match, P } from 'ts-pattern'; import { z, ZodObject, ZodType } from 'zod'; import { AnyNullClass, DbNullClass, JsonNullClass } from '../../../common-types'; @@ -54,6 +53,7 @@ import { CoreUpdateOperations, type CoreCrudOperations, } from '../operations/base'; +import { cache } from './cache-decorator'; import { addBigIntValidation, addCustomValidation, @@ -431,66 +431,51 @@ export class InputValidator { // #region Find + @cache() private makeFindSchema(model: string, operation: CoreCrudOperations) { - return this.cached( - { - type: 'findArgs', - model, - operation, - }, - () => { - const fields: Record = {}; - const unique = operation === 'findUnique'; - const findOne = operation === 'findUnique' || operation === 'findFirst'; - const where = this.makeWhereSchema(model, unique); - if (unique) { - fields['where'] = where; - } else { - fields['where'] = where.optional(); - } + const fields: Record = {}; + const unique = operation === 'findUnique'; + const findOne = operation === 'findUnique' || operation === 'findFirst'; + const where = this.makeWhereSchema(model, unique); + if (unique) { + fields['where'] = where; + } else { + fields['where'] = where.optional(); + } - fields['select'] = this.makeSelectSchema(model).optional().nullable(); - fields['include'] = this.makeIncludeSchema(model).optional().nullable(); - fields['omit'] = this.makeOmitSchema(model).optional().nullable(); + fields['select'] = this.makeSelectSchema(model).optional().nullable(); + fields['include'] = this.makeIncludeSchema(model).optional().nullable(); + fields['omit'] = this.makeOmitSchema(model).optional().nullable(); - if (!unique) { - fields['skip'] = this.makeSkipSchema().optional(); - if (findOne) { - fields['take'] = z.literal(1).optional(); - } else { - fields['take'] = this.makeTakeSchema().optional(); - } - fields['orderBy'] = this.orArray(this.makeOrderBySchema(model, true, false), true).optional(); - fields['cursor'] = this.makeCursorSchema(model).optional(); - fields['distinct'] = this.makeDistinctSchema(model).optional(); - } + if (!unique) { + fields['skip'] = this.makeSkipSchema().optional(); + if (findOne) { + fields['take'] = z.literal(1).optional(); + } else { + fields['take'] = this.makeTakeSchema().optional(); + } + fields['orderBy'] = this.orArray(this.makeOrderBySchema(model, true, false), true).optional(); + fields['cursor'] = this.makeCursorSchema(model).optional(); + fields['distinct'] = this.makeDistinctSchema(model).optional(); + } - const baseSchema = z.strictObject(fields); - let result: ZodType = this.mergePluginArgsSchema(baseSchema, operation); - result = this.refineForSelectIncludeMutuallyExclusive(result); - result = this.refineForSelectOmitMutuallyExclusive(result); + const baseSchema = z.strictObject(fields); + let result: ZodType = this.mergePluginArgsSchema(baseSchema, operation); + result = this.refineForSelectIncludeMutuallyExclusive(result); + result = this.refineForSelectOmitMutuallyExclusive(result); - if (!unique) { - result = result.optional(); - } - return result; - }, - ); + if (!unique) { + result = result.optional(); + } + return result; } + @cache() private makeExistsSchema(model: string) { - return this.cached( - { - type: 'existsArgs', - model, - }, - () => { - const baseSchema = z.strictObject({ - where: this.makeWhereSchema(model, false).optional(), - }); - return this.mergePluginArgsSchema(baseSchema, 'exists').optional(); - }, - ); + const baseSchema = z.strictObject({ + where: this.makeWhereSchema(model, false).optional(), + }); + return this.mergePluginArgsSchema(baseSchema, 'exists').optional(); } private makeScalarSchema(type: string, attributes?: readonly AttributeApplication[]) { @@ -532,322 +517,287 @@ export class InputValidator { } } + @cache() private makeEnumSchema(type: string) { - return this.cached({ type: 'enum', name: type }, () => { - const enumDef = getEnum(this.schema, type); - invariant(enumDef, `Enum "${type}" not found in schema`); - return z.enum(Object.keys(enumDef.values) as [string, ...string[]]); - }); + const enumDef = getEnum(this.schema, type); + invariant(enumDef, `Enum "${type}" not found in schema`); + return z.enum(Object.keys(enumDef.values) as [string, ...string[]]); } + @cache({ includeProperties: ['extraValidationsEnabled'] }) private makeTypeDefSchema(type: string): z.ZodType { - return this.cached( - { - type: 'typedef', - name: type, - extraValidationsEnabled: this.extraValidationsEnabled, - }, - () => { - const typeDef = getTypeDef(this.schema, type); - invariant(typeDef, `Type definition "${type}" not found in schema`); - const schema = z.looseObject( - Object.fromEntries( - Object.entries(typeDef.fields).map(([field, def]) => { - let fieldSchema = this.makeScalarSchema(def.type); - if (def.array) { - fieldSchema = fieldSchema.array(); - } - if (def.optional) { - fieldSchema = fieldSchema.nullish(); - } - return [field, fieldSchema]; - }), - ), - ); - - // zod doesn't preserve object field order after parsing, here we use a - // validation-only custom schema and use the original data if parsing - // is successful - const finalSchema = z.any().superRefine((value, ctx) => { - const parseResult = schema.safeParse(value); - if (!parseResult.success) { - parseResult.error.issues.forEach((issue) => ctx.addIssue(issue as any)); + const typeDef = getTypeDef(this.schema, type); + invariant(typeDef, `Type definition "${type}" not found in schema`); + const schema = z.looseObject( + Object.fromEntries( + Object.entries(typeDef.fields).map(([field, def]) => { + let fieldSchema = this.makeScalarSchema(def.type); + if (def.array) { + fieldSchema = fieldSchema.array(); } - }); - - return finalSchema; - }, + if (def.optional) { + fieldSchema = fieldSchema.nullish(); + } + return [field, fieldSchema]; + }), + ), ); + + // zod doesn't preserve object field order after parsing, here we use a + // validation-only custom schema and use the original data if parsing + // is successful + const finalSchema = z.any().superRefine((value, ctx) => { + const parseResult = schema.safeParse(value); + if (!parseResult.success) { + parseResult.error.issues.forEach((issue) => ctx.addIssue(issue as any)); + } + }); + + return finalSchema; } + @cache() private makeWhereSchema( model: string, unique: boolean, withoutRelationFields = false, withAggregations = false, ): ZodType { - return this.cached( - { - type: 'where', - model, - unique, - withoutRelationFields, - withAggregations, - }, - () => { - const modelDef = requireModel(this.schema, model); - - const fields: Record = {}; - for (const field of Object.keys(modelDef.fields)) { - const fieldDef = requireField(this.schema, model, field); - let fieldSchema: ZodType | undefined; + const modelDef = requireModel(this.schema, model); - if (fieldDef.relation) { - if (withoutRelationFields) { - continue; - } - fieldSchema = z.lazy(() => this.makeWhereSchema(fieldDef.type, false).optional()); + const fields: Record = {}; + for (const field of Object.keys(modelDef.fields)) { + const fieldDef = requireField(this.schema, model, field); + let fieldSchema: ZodType | undefined; - // optional to-one relation allows null - fieldSchema = this.nullableIf(fieldSchema, !fieldDef.array && !!fieldDef.optional); + if (fieldDef.relation) { + if (withoutRelationFields) { + continue; + } + fieldSchema = z.lazy(() => this.makeWhereSchema(fieldDef.type, false).optional()); - if (fieldDef.array) { - // to-many relation - fieldSchema = z.union([ - fieldSchema, - z.strictObject({ - some: fieldSchema.optional(), - every: fieldSchema.optional(), - none: fieldSchema.optional(), - }), - ]); - } else { - // to-one relation - fieldSchema = z.union([ - fieldSchema, - z.strictObject({ - is: fieldSchema.optional(), - isNot: fieldSchema.optional(), - }), - ]); - } - } else { - const enumDef = getEnum(this.schema, fieldDef.type); - if (enumDef) { - // enum - if (Object.keys(enumDef.values).length > 0) { - fieldSchema = this.makeEnumFilterSchema( - fieldDef.type, - enumDef, - !!fieldDef.optional, - withAggregations, - !!fieldDef.array, - ); - } - } else if (fieldDef.array) { - // array field - fieldSchema = this.makeArrayFilterSchema(fieldDef.type as BuiltinType); - } else if (this.isTypeDefType(fieldDef.type)) { - fieldSchema = this.makeTypedJsonFilterSchema( - fieldDef.type, - !!fieldDef.optional, - !!fieldDef.array, - ); - } else { - // primitive field - fieldSchema = this.makePrimitiveFilterSchema( - fieldDef.type as BuiltinType, - !!fieldDef.optional, - withAggregations, - ); - } - } + // optional to-one relation allows null + fieldSchema = this.nullableIf(fieldSchema, !fieldDef.array && !!fieldDef.optional); - if (fieldSchema) { - fields[field] = fieldSchema.optional(); - } + if (fieldDef.array) { + // to-many relation + fieldSchema = z.union([ + fieldSchema, + z.strictObject({ + some: fieldSchema.optional(), + every: fieldSchema.optional(), + none: fieldSchema.optional(), + }), + ]); + } else { + // to-one relation + fieldSchema = z.union([ + fieldSchema, + z.strictObject({ + is: fieldSchema.optional(), + isNot: fieldSchema.optional(), + }), + ]); } - - if (unique) { - // add compound unique fields, e.g. `{ id1_id2: { id1: 1, id2: 1 } }` - const uniqueFields = getUniqueFields(this.schema, model); - for (const uniqueField of uniqueFields) { - if ('defs' in uniqueField) { - fields[uniqueField.name] = z - .object( - Object.fromEntries( - Object.entries(uniqueField.defs).map(([key, def]) => { - invariant(!def.relation, 'unique field cannot be a relation'); - let fieldSchema: ZodType; - const enumDef = getEnum(this.schema, def.type); - if (enumDef) { - // enum - if (Object.keys(enumDef.values).length > 0) { - fieldSchema = this.makeEnumFilterSchema( - def.type, - enumDef, - !!def.optional, - false, - false, - ); - } else { - fieldSchema = z.never(); - } - } else { - // regular field - fieldSchema = this.makePrimitiveFilterSchema( - def.type as BuiltinType, - !!def.optional, - false, - ); - } - return [key, fieldSchema]; - }), - ), - ) - .optional(); - } + } else { + const enumDef = getEnum(this.schema, fieldDef.type); + if (enumDef) { + // enum + if (Object.keys(enumDef.values).length > 0) { + fieldSchema = this.makeEnumFilterSchema( + fieldDef.type, + enumDef, + !!fieldDef.optional, + withAggregations, + !!fieldDef.array, + ); } + } else if (fieldDef.array) { + // array field + fieldSchema = this.makeArrayFilterSchema(fieldDef.type as BuiltinType); + } else if (this.isTypeDefType(fieldDef.type)) { + fieldSchema = this.makeTypedJsonFilterSchema(fieldDef.type, !!fieldDef.optional, !!fieldDef.array); + } else { + // primitive field + fieldSchema = this.makePrimitiveFilterSchema( + fieldDef.type as BuiltinType, + !!fieldDef.optional, + withAggregations, + ); } + } - // expression builder - fields['$expr'] = z - .custom((v) => typeof v === 'function', { error: '"$expr" must be a function' }) - .optional(); - - // logical operators - fields['AND'] = this.orArray( - z.lazy(() => this.makeWhereSchema(model, false, withoutRelationFields)), - true, - ).optional(); - fields['OR'] = z - .lazy(() => this.makeWhereSchema(model, false, withoutRelationFields)) - .array() - .optional(); - fields['NOT'] = this.orArray( - z.lazy(() => this.makeWhereSchema(model, false, withoutRelationFields)), - true, - ).optional(); + if (fieldSchema) { + fields[field] = fieldSchema.optional(); + } + } - const baseWhere = z.strictObject(fields); - let result: ZodType = baseWhere; + if (unique) { + // add compound unique fields, e.g. `{ id1_id2: { id1: 1, id2: 1 } }` + const uniqueFields = getUniqueFields(this.schema, model); + for (const uniqueField of uniqueFields) { + if ('defs' in uniqueField) { + fields[uniqueField.name] = z + .object( + Object.fromEntries( + Object.entries(uniqueField.defs).map(([key, def]) => { + invariant(!def.relation, 'unique field cannot be a relation'); + let fieldSchema: ZodType; + const enumDef = getEnum(this.schema, def.type); + if (enumDef) { + // enum + if (Object.keys(enumDef.values).length > 0) { + fieldSchema = this.makeEnumFilterSchema( + def.type, + enumDef, + !!def.optional, + false, + false, + ); + } else { + fieldSchema = z.never(); + } + } else { + // regular field + fieldSchema = this.makePrimitiveFilterSchema( + def.type as BuiltinType, + !!def.optional, + false, + ); + } + return [key, fieldSchema]; + }), + ), + ) + .optional(); + } + } + } - if (unique) { - // requires at least one unique field (field set) is required - const uniqueFields = getUniqueFields(this.schema, model); - if (uniqueFields.length === 0) { - throw createInternalError(`Model "${model}" has no unique fields`); - } + // expression builder + fields['$expr'] = z.custom((v) => typeof v === 'function', { error: '"$expr" must be a function' }).optional(); + + // logical operators + fields['AND'] = this.orArray( + z.lazy(() => this.makeWhereSchema(model, false, withoutRelationFields)), + true, + ).optional(); + fields['OR'] = z + .lazy(() => this.makeWhereSchema(model, false, withoutRelationFields)) + .array() + .optional(); + fields['NOT'] = this.orArray( + z.lazy(() => this.makeWhereSchema(model, false, withoutRelationFields)), + true, + ).optional(); + + const baseWhere = z.strictObject(fields); + let result: ZodType = baseWhere; + + if (unique) { + // requires at least one unique field (field set) is required + const uniqueFields = getUniqueFields(this.schema, model); + if (uniqueFields.length === 0) { + throw createInternalError(`Model "${model}" has no unique fields`); + } - if (uniqueFields.length === 1) { - // only one unique field (set), mark the field(s) required - result = baseWhere.required({ - [uniqueFields[0]!.name]: true, - } as any); - } else { - result = baseWhere.refine((value) => { - // check that at least one unique field is set - return uniqueFields.some(({ name }) => value[name] !== undefined); - }, `At least one unique field or field set must be set`); - } - } + if (uniqueFields.length === 1) { + // only one unique field (set), mark the field(s) required + result = baseWhere.required({ + [uniqueFields[0]!.name]: true, + } as any); + } else { + result = baseWhere.refine((value) => { + // check that at least one unique field is set + return uniqueFields.some(({ name }) => value[name] !== undefined); + }, `At least one unique field or field set must be set`); + } + } - return result; - }, - ); + return result; } + @cache() private makeTypedJsonFilterSchema(type: string, optional: boolean, array: boolean) { - return this.cached( - { - type: 'typedJsonFilter', - dataType: type, - optional, - array, - }, - () => { - const typeDef = getTypeDef(this.schema, type); - invariant(typeDef, `Type definition "${type}" not found in schema`); - - const candidates: z.ZodType[] = []; - - if (!array) { - // fields filter - const fieldSchemas: Record = {}; - for (const [fieldName, fieldDef] of Object.entries(typeDef.fields)) { - if (this.isTypeDefType(fieldDef.type)) { - // recursive typed JSON - fieldSchemas[fieldName] = this.makeTypedJsonFilterSchema( - fieldDef.type, - !!fieldDef.optional, - !!fieldDef.array, - ).optional(); - } else { - // enum, array, primitives - const enumDef = getEnum(this.schema, fieldDef.type); - if (enumDef) { - fieldSchemas[fieldName] = this.makeEnumFilterSchema( - fieldDef.type, - enumDef, - !!fieldDef.optional, - false, - !!fieldDef.array, - ).optional(); - } else if (fieldDef.array) { - fieldSchemas[fieldName] = this.makeArrayFilterSchema( - fieldDef.type as BuiltinType, - ).optional(); - } else { - fieldSchemas[fieldName] = this.makePrimitiveFilterSchema( - fieldDef.type as BuiltinType, - !!fieldDef.optional, - false, - ).optional(); - } - } + const typeDef = getTypeDef(this.schema, type); + invariant(typeDef, `Type definition "${type}" not found in schema`); + + const candidates: z.ZodType[] = []; + + if (!array) { + // fields filter + const fieldSchemas: Record = {}; + for (const [fieldName, fieldDef] of Object.entries(typeDef.fields)) { + if (this.isTypeDefType(fieldDef.type)) { + // recursive typed JSON + fieldSchemas[fieldName] = this.makeTypedJsonFilterSchema( + fieldDef.type, + !!fieldDef.optional, + !!fieldDef.array, + ).optional(); + } else { + // enum, array, primitives + const enumDef = getEnum(this.schema, fieldDef.type); + if (enumDef) { + fieldSchemas[fieldName] = this.makeEnumFilterSchema( + fieldDef.type, + enumDef, + !!fieldDef.optional, + false, + !!fieldDef.array, + ).optional(); + } else if (fieldDef.array) { + fieldSchemas[fieldName] = this.makeArrayFilterSchema(fieldDef.type as BuiltinType).optional(); + } else { + fieldSchemas[fieldName] = this.makePrimitiveFilterSchema( + fieldDef.type as BuiltinType, + !!fieldDef.optional, + false, + ).optional(); } - - candidates.push(z.strictObject(fieldSchemas)); } + } - const recursiveSchema = z.lazy(() => this.makeTypedJsonFilterSchema(type, optional, false)).optional(); - if (array) { - // array filter - candidates.push( - z.strictObject({ - some: recursiveSchema, - every: recursiveSchema, - none: recursiveSchema, - }), - ); - } else { - // is / isNot filter - candidates.push( - z.strictObject({ - is: recursiveSchema, - isNot: recursiveSchema, - }), - ); - } + candidates.push(z.strictObject(fieldSchemas)); + } + + const recursiveSchema = z.lazy(() => this.makeTypedJsonFilterSchema(type, optional, false)).optional(); + if (array) { + // array filter + candidates.push( + z.strictObject({ + some: recursiveSchema, + every: recursiveSchema, + none: recursiveSchema, + }), + ); + } else { + // is / isNot filter + candidates.push( + z.strictObject({ + is: recursiveSchema, + isNot: recursiveSchema, + }), + ); + } - // plain json filter - candidates.push(this.makeJsonFilterSchema(optional)); + // plain json filter + candidates.push(this.makeJsonFilterSchema(optional)); - if (optional) { - // allow null as well - candidates.push(z.null()); - } + if (optional) { + // allow null as well + candidates.push(z.null()); + } - // either plain json filter or field filters - return z.union(candidates); - }, - ); + // either plain json filter or field filters + return z.union(candidates); } private isTypeDefType(type: string) { return this.schema.typeDefs && type in this.schema.typeDefs; } + @cache() private makeEnumFilterSchema( enumName: string, enumDef: EnumDef, @@ -855,39 +805,23 @@ export class InputValidator { withAggregations: boolean, array: boolean, ) { - return this.cached( - { - type: 'enumFilter', - enum: enumName, - optional, - array, - withAggregations, - }, - () => { - const baseSchema = z.enum(Object.keys(enumDef.values) as [string, ...string[]]); - if (array) { - return this.internalMakeArrayFilterSchema(baseSchema); - } - const components = this.makeCommonPrimitiveFilterComponents( - baseSchema, - optional, - () => z.lazy(() => this.makeEnumFilterSchema(enumName, enumDef, optional, withAggregations, array)), - ['equals', 'in', 'notIn', 'not'], - withAggregations ? ['_count', '_min', '_max'] : undefined, - ); - return z.union([this.nullableIf(baseSchema, optional), z.strictObject(components)]); - }, + const baseSchema = z.enum(Object.keys(enumDef.values) as [string, ...string[]]); + if (array) { + return this.internalMakeArrayFilterSchema(baseSchema); + } + const components = this.makeCommonPrimitiveFilterComponents( + baseSchema, + optional, + () => z.lazy(() => this.makeEnumFilterSchema(enumName, enumDef, optional, withAggregations, array)), + ['equals', 'in', 'notIn', 'not'], + withAggregations ? ['_count', '_min', '_max'] : undefined, ); + return z.union([this.nullableIf(baseSchema, optional), z.strictObject(components)]); } + @cache() private makeArrayFilterSchema(type: BuiltinType) { - return this.cached( - { - type: 'arrayFilter', - dataType: type, - }, - () => this.internalMakeArrayFilterSchema(this.makeScalarSchema(type)), - ); + return this.internalMakeArrayFilterSchema(this.makeScalarSchema(type)); } private internalMakeArrayFilterSchema(elementSchema: ZodType) { @@ -900,27 +834,19 @@ export class InputValidator { }); } + @cache() private makePrimitiveFilterSchema(type: BuiltinType, optional: boolean, withAggregations: boolean) { - return this.cached( - { - type: 'primitiveFilter', - dataType: type, - optional, - withAggregations, - }, - () => - match(type) - .with('String', () => this.makeStringFilterSchema(optional, withAggregations)) - .with(P.union('Int', 'Float', 'Decimal', 'BigInt'), (type) => - this.makeNumberFilterSchema(this.makeScalarSchema(type), optional, withAggregations), - ) - .with('Boolean', () => this.makeBooleanFilterSchema(optional, withAggregations)) - .with('DateTime', () => this.makeDateTimeFilterSchema(optional, withAggregations)) - .with('Bytes', () => this.makeBytesFilterSchema(optional, withAggregations)) - .with('Json', () => this.makeJsonFilterSchema(optional)) - .with('Unsupported', () => z.never()) - .exhaustive(), - ); + return match(type) + .with('String', () => this.makeStringFilterSchema(optional, withAggregations)) + .with(P.union('Int', 'Float', 'Decimal', 'BigInt'), (type) => + this.makeNumberFilterSchema(this.makeScalarSchema(type), optional, withAggregations), + ) + .with('Boolean', () => this.makeBooleanFilterSchema(optional, withAggregations)) + .with('DateTime', () => this.makeDateTimeFilterSchema(optional, withAggregations)) + .with('Bytes', () => this.makeBytesFilterSchema(optional, withAggregations)) + .with('Json', () => this.makeJsonFilterSchema(optional)) + .with('Unsupported', () => z.never()) + .exhaustive(); } private makeJsonValueSchema(nullable: boolean, forFilter: boolean): z.ZodType { @@ -950,85 +876,56 @@ export class InputValidator { return this.nullableIf(schema, nullable); } + @cache() private makeJsonFilterSchema(optional: boolean) { - return this.cached( - { - type: 'jsonFilter', - optional, - }, - () => { - const valueSchema = this.makeJsonValueSchema(optional, true); - return z.strictObject({ - path: z.string().optional(), - equals: valueSchema.optional(), - not: valueSchema.optional(), - string_contains: z.string().optional(), - string_starts_with: z.string().optional(), - string_ends_with: z.string().optional(), - mode: this.makeStringModeSchema().optional(), - array_contains: valueSchema.optional(), - array_starts_with: valueSchema.optional(), - array_ends_with: valueSchema.optional(), - }); - }, - ); + const valueSchema = this.makeJsonValueSchema(optional, true); + return z.strictObject({ + path: z.string().optional(), + equals: valueSchema.optional(), + not: valueSchema.optional(), + string_contains: z.string().optional(), + string_starts_with: z.string().optional(), + string_ends_with: z.string().optional(), + mode: this.makeStringModeSchema().optional(), + array_contains: valueSchema.optional(), + array_starts_with: valueSchema.optional(), + array_ends_with: valueSchema.optional(), + }); } + @cache() private makeDateTimeFilterSchema(optional: boolean, withAggregations: boolean): ZodType { - return this.cached( - { - type: 'dateTimeFilter', - optional, - withAggregations, - }, - () => - this.makeCommonPrimitiveFilterSchema( - z.union([z.iso.datetime(), z.date()]), - optional, - () => z.lazy(() => this.makeDateTimeFilterSchema(optional, withAggregations)), - withAggregations ? ['_count', '_min', '_max'] : undefined, - ), + return this.makeCommonPrimitiveFilterSchema( + z.union([z.iso.datetime(), z.date()]), + optional, + () => z.lazy(() => this.makeDateTimeFilterSchema(optional, withAggregations)), + withAggregations ? ['_count', '_min', '_max'] : undefined, ); } + @cache() private makeBooleanFilterSchema(optional: boolean, withAggregations: boolean): ZodType { - return this.cached( - { - type: 'booleanFilter', - optional, - withAggregations, - }, - () => { - const components = this.makeCommonPrimitiveFilterComponents( - z.boolean(), - optional, - () => z.lazy(() => this.makeBooleanFilterSchema(optional, withAggregations)), - ['equals', 'not'], - withAggregations ? ['_count', '_min', '_max'] : undefined, - ); - return z.union([this.nullableIf(z.boolean(), optional), z.strictObject(components)]); - }, + const components = this.makeCommonPrimitiveFilterComponents( + z.boolean(), + optional, + () => z.lazy(() => this.makeBooleanFilterSchema(optional, withAggregations)), + ['equals', 'not'], + withAggregations ? ['_count', '_min', '_max'] : undefined, ); + return z.union([this.nullableIf(z.boolean(), optional), z.strictObject(components)]); } + @cache() private makeBytesFilterSchema(optional: boolean, withAggregations: boolean): ZodType { - return this.cached( - { - type: 'bytesFilter', - withAggregations, - }, - () => { - const baseSchema = z.instanceof(Uint8Array); - const components = this.makeCommonPrimitiveFilterComponents( - baseSchema, - optional, - () => z.instanceof(Uint8Array), - ['equals', 'in', 'notIn', 'not'], - withAggregations ? ['_count', '_min', '_max'] : undefined, - ); - return z.union([this.nullableIf(baseSchema, optional), z.strictObject(components)]); - }, + const baseSchema = z.instanceof(Uint8Array); + const components = this.makeCommonPrimitiveFilterComponents( + baseSchema, + optional, + () => z.instanceof(Uint8Array), + ['equals', 'in', 'notIn', 'not'], + withAggregations ? ['_count', '_min', '_max'] : undefined, ); + return z.union([this.nullableIf(baseSchema, optional), z.strictObject(components)]); } private makeCommonPrimitiveFilterComponents( @@ -1116,250 +1013,192 @@ export class InputValidator { return z.union([z.literal('default'), z.literal('insensitive')]); } + @cache() private makeSelectSchema(model: string) { - return this.cached( - { - type: 'select', - model, - }, - () => { - const modelDef = requireModel(this.schema, model); - const fields: Record = {}; - for (const field of Object.keys(modelDef.fields)) { - const fieldDef = requireField(this.schema, model, field); - if (fieldDef.relation) { - fields[field] = this.makeRelationSelectIncludeSchema(fieldDef).optional(); - } else { - fields[field] = z.boolean().optional(); - } - } + const modelDef = requireModel(this.schema, model); + const fields: Record = {}; + for (const field of Object.keys(modelDef.fields)) { + const fieldDef = requireField(this.schema, model, field); + if (fieldDef.relation) { + fields[field] = this.makeRelationSelectIncludeSchema(fieldDef).optional(); + } else { + fields[field] = z.boolean().optional(); + } + } - const _countSchema = this.makeCountSelectionSchema(modelDef); - if (!(_countSchema instanceof z.ZodNever)) { - fields['_count'] = _countSchema; - } + const _countSchema = this.makeCountSelectionSchema(modelDef); + if (!(_countSchema instanceof z.ZodNever)) { + fields['_count'] = _countSchema; + } - return z.strictObject(fields); - }, - ); + return z.strictObject(fields); } + @cache() private makeCountSelectionSchema(modelDef: ModelDef) { - return this.cached( - { - type: 'countSelection', - model: modelDef.name, - }, - () => { - const toManyRelations = Object.values(modelDef.fields).filter((def) => def.relation && def.array); - if (toManyRelations.length > 0) { - return z - .union([ - z.literal(true), - z.strictObject({ - select: z.strictObject( - toManyRelations.reduce( - (acc, fieldDef) => ({ - ...acc, - [fieldDef.name]: z - .union([ - z.boolean(), - z.strictObject({ - where: this.makeWhereSchema(fieldDef.type, false, false), - }), - ]) - .optional(), - }), - {} as Record, - ), - ), - }), - ]) - .optional(); - } else { - return z.never(); - } - }, - ); + const toManyRelations = Object.values(modelDef.fields).filter((def) => def.relation && def.array); + if (toManyRelations.length > 0) { + return z + .union([ + z.literal(true), + z.strictObject({ + select: z.strictObject( + toManyRelations.reduce( + (acc, fieldDef) => ({ + ...acc, + [fieldDef.name]: z + .union([ + z.boolean(), + z.strictObject({ + where: this.makeWhereSchema(fieldDef.type, false, false), + }), + ]) + .optional(), + }), + {} as Record, + ), + ), + }), + ]) + .optional(); + } else { + return z.never(); + } } + @cache() private makeRelationSelectIncludeSchema(fieldDef: FieldDef) { - return this.cached( - { - type: 'relationSelectInclude', - model: fieldDef.type, - field: fieldDef.name, - }, - () => { - let objSchema: z.ZodType = z.strictObject({ - ...(fieldDef.array || fieldDef.optional - ? { - // to-many relations and optional to-one relations are filterable - where: z.lazy(() => this.makeWhereSchema(fieldDef.type, false)).optional(), - } - : {}), - select: z - .lazy(() => this.makeSelectSchema(fieldDef.type)) - .optional() - .nullable(), - include: z - .lazy(() => this.makeIncludeSchema(fieldDef.type)) - .optional() - .nullable(), - omit: z - .lazy(() => this.makeOmitSchema(fieldDef.type)) - .optional() - .nullable(), - ...(fieldDef.array - ? { - // to-many relations can be ordered, skipped, taken, and cursor-located - orderBy: z - .lazy(() => this.orArray(this.makeOrderBySchema(fieldDef.type, true, false), true)) - .optional(), - skip: this.makeSkipSchema().optional(), - take: this.makeTakeSchema().optional(), - cursor: this.makeCursorSchema(fieldDef.type).optional(), - distinct: this.makeDistinctSchema(fieldDef.type).optional(), - } - : {}), - }); - - objSchema = this.refineForSelectIncludeMutuallyExclusive(objSchema); - objSchema = this.refineForSelectOmitMutuallyExclusive(objSchema); - - return z.union([z.boolean(), objSchema]); - }, - ); + let objSchema: z.ZodType = z.strictObject({ + ...(fieldDef.array || fieldDef.optional + ? { + // to-many relations and optional to-one relations are filterable + where: z.lazy(() => this.makeWhereSchema(fieldDef.type, false)).optional(), + } + : {}), + select: z + .lazy(() => this.makeSelectSchema(fieldDef.type)) + .optional() + .nullable(), + include: z + .lazy(() => this.makeIncludeSchema(fieldDef.type)) + .optional() + .nullable(), + omit: z + .lazy(() => this.makeOmitSchema(fieldDef.type)) + .optional() + .nullable(), + ...(fieldDef.array + ? { + // to-many relations can be ordered, skipped, taken, and cursor-located + orderBy: z + .lazy(() => this.orArray(this.makeOrderBySchema(fieldDef.type, true, false), true)) + .optional(), + skip: this.makeSkipSchema().optional(), + take: this.makeTakeSchema().optional(), + cursor: this.makeCursorSchema(fieldDef.type).optional(), + distinct: this.makeDistinctSchema(fieldDef.type).optional(), + } + : {}), + }); + + objSchema = this.refineForSelectIncludeMutuallyExclusive(objSchema); + objSchema = this.refineForSelectOmitMutuallyExclusive(objSchema); + + return z.union([z.boolean(), objSchema]); } + @cache() private makeOmitSchema(model: string) { - return this.cached( - { - type: 'omit', - model, - }, - () => { - const modelDef = requireModel(this.schema, model); - const fields: Record = {}; - for (const field of Object.keys(modelDef.fields)) { - const fieldDef = requireField(this.schema, model, field); - if (!fieldDef.relation) { - if (this.options.allowQueryTimeOmitOverride !== false) { - // if override is allowed, use boolean - fields[field] = z.boolean().optional(); - } else { - // otherwise only allow true - fields[field] = z.literal(true).optional(); - } - } + const modelDef = requireModel(this.schema, model); + const fields: Record = {}; + for (const field of Object.keys(modelDef.fields)) { + const fieldDef = requireField(this.schema, model, field); + if (!fieldDef.relation) { + if (this.options.allowQueryTimeOmitOverride !== false) { + // if override is allowed, use boolean + fields[field] = z.boolean().optional(); + } else { + // otherwise only allow true + fields[field] = z.literal(true).optional(); } - return z.strictObject(fields); - }, - ); + } + } + return z.strictObject(fields); } + @cache() private makeIncludeSchema(model: string) { - return this.cached( - { - type: 'include', - model, - }, - () => { - const modelDef = requireModel(this.schema, model); - const fields: Record = {}; - for (const field of Object.keys(modelDef.fields)) { - const fieldDef = requireField(this.schema, model, field); - if (fieldDef.relation) { - fields[field] = this.makeRelationSelectIncludeSchema(fieldDef).optional(); - } - } + const modelDef = requireModel(this.schema, model); + const fields: Record = {}; + for (const field of Object.keys(modelDef.fields)) { + const fieldDef = requireField(this.schema, model, field); + if (fieldDef.relation) { + fields[field] = this.makeRelationSelectIncludeSchema(fieldDef).optional(); + } + } - const _countSchema = this.makeCountSelectionSchema(modelDef); - if (!(_countSchema instanceof z.ZodNever)) { - fields['_count'] = _countSchema; - } + const _countSchema = this.makeCountSelectionSchema(modelDef); + if (!(_countSchema instanceof z.ZodNever)) { + fields['_count'] = _countSchema; + } - return z.strictObject(fields); - }, - ); + return z.strictObject(fields); } + @cache() private makeOrderBySchema(model: string, withRelation: boolean, WithAggregation: boolean) { - return this.cached( - { - type: 'orderBy', - model, - withRelation, - WithAggregation, - }, - () => { - const modelDef = requireModel(this.schema, model); - 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); - if (fieldDef.relation) { - // relations - if (withRelation) { - fields[field] = z.lazy(() => { - let relationOrderBy = this.makeOrderBySchema( - fieldDef.type, - withRelation, - WithAggregation, - ); - if (fieldDef.array) { - relationOrderBy = relationOrderBy.extend({ - _count: sort, - }); - } - return relationOrderBy.optional(); + const modelDef = requireModel(this.schema, model); + 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); + if (fieldDef.relation) { + // relations + if (withRelation) { + fields[field] = z.lazy(() => { + let relationOrderBy = this.makeOrderBySchema(fieldDef.type, withRelation, WithAggregation); + if (fieldDef.array) { + relationOrderBy = relationOrderBy.extend({ + _count: sort, }); } - } else { - // scalars - if (fieldDef.optional) { - fields[field] = z - .union([ - sort, - z.strictObject({ - sort, - nulls: z.union([z.literal('first'), z.literal('last')]), - }), - ]) - .optional(); - } else { - fields[field] = sort.optional(); - } - } + return relationOrderBy.optional(); + }); } - - // aggregations - if (WithAggregation) { - const aggregationFields = ['_count', '_avg', '_sum', '_min', '_max']; - for (const agg of aggregationFields) { - fields[agg] = z.lazy(() => this.makeOrderBySchema(model, true, false).optional()); - } + } else { + // scalars + if (fieldDef.optional) { + fields[field] = z + .union([ + sort, + z.strictObject({ + sort, + nulls: z.union([z.literal('first'), z.literal('last')]), + }), + ]) + .optional(); + } else { + fields[field] = sort.optional(); } + } + } - return z.strictObject(fields); - }, - ); + // aggregations + if (WithAggregation) { + const aggregationFields = ['_count', '_avg', '_sum', '_min', '_max']; + for (const agg of aggregationFields) { + fields[agg] = z.lazy(() => this.makeOrderBySchema(model, true, false).optional()); + } + } + + return z.strictObject(fields); } + @cache() private makeDistinctSchema(model: string) { - return this.cached( - { - type: 'distinct', - model, - }, - () => { - const modelDef = requireModel(this.schema, model); - const nonRelationFields = Object.keys(modelDef.fields).filter( - (field) => !modelDef.fields[field]?.relation, - ); - return this.orArray(z.enum(nonRelationFields as any), true); - }, - ); + const modelDef = requireModel(this.schema, model); + const nonRelationFields = Object.keys(modelDef.fields).filter((field) => !modelDef.fields[field]?.relation); + return this.orArray(z.enum(nonRelationFields as any), true); } private makeCursorSchema(model: string) { @@ -1371,191 +1210,165 @@ export class InputValidator { // #region Create + @cache() private makeCreateSchema(model: string) { - return this.cached( - { - type: 'create', - model, - }, - () => { - const dataSchema = this.makeCreateDataSchema(model, false); - const baseSchema = z.strictObject({ - data: dataSchema, - select: this.makeSelectSchema(model).optional().nullable(), - include: this.makeIncludeSchema(model).optional().nullable(), - omit: this.makeOmitSchema(model).optional().nullable(), - }); - let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'create'); - schema = this.refineForSelectIncludeMutuallyExclusive(schema); - schema = this.refineForSelectOmitMutuallyExclusive(schema); - return schema; - }, - ); + const dataSchema = this.makeCreateDataSchema(model, false); + const baseSchema = z.strictObject({ + data: dataSchema, + select: this.makeSelectSchema(model).optional().nullable(), + include: this.makeIncludeSchema(model).optional().nullable(), + omit: this.makeOmitSchema(model).optional().nullable(), + }); + let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'create'); + schema = this.refineForSelectIncludeMutuallyExclusive(schema); + schema = this.refineForSelectOmitMutuallyExclusive(schema); + return schema; } + @cache() private makeCreateManySchema(model: string) { - return this.cached( - { - type: 'createMany', - model, - }, - () => this.mergePluginArgsSchema(this.makeCreateManyDataSchema(model, []), 'createMany').optional(), - ); + return this.mergePluginArgsSchema(this.makeCreateManyDataSchema(model, []), 'createMany').optional(); } + @cache() private makeCreateManyAndReturnSchema(model: string) { - return this.cached( - { - type: 'createManyAndReturn', - model, - }, - () => { - const base = this.makeCreateManyDataSchema(model, []); - let result: ZodObject = base.extend({ - select: this.makeSelectSchema(model).optional().nullable(), - omit: this.makeOmitSchema(model).optional().nullable(), - }); - result = this.mergePluginArgsSchema(result, 'createManyAndReturn'); - return this.refineForSelectOmitMutuallyExclusive(result).optional(); - }, - ); + const base = this.makeCreateManyDataSchema(model, []); + let result: ZodObject = base.extend({ + select: this.makeSelectSchema(model).optional().nullable(), + omit: this.makeOmitSchema(model).optional().nullable(), + }); + result = this.mergePluginArgsSchema(result, 'createManyAndReturn'); + return this.refineForSelectOmitMutuallyExclusive(result).optional(); } + @cache({ includeProperties: ['extraValidationsEnabled'] }) private makeCreateDataSchema( model: string, canBeArray: boolean, withoutFields: string[] = [], withoutRelationFields = false, ) { - return this.cached( - { - type: 'createData', - model, - canBeArray, - withoutFields: [...withoutFields].sort(), - withoutRelationFields, - }, - () => { - const uncheckedVariantFields: Record = {}; - const checkedVariantFields: Record = {}; - const modelDef = requireModel(this.schema, model); - const hasRelation = - !withoutRelationFields && - Object.entries(modelDef.fields).some(([f, def]) => !withoutFields.includes(f) && def.relation); - - Object.keys(modelDef.fields).forEach((field) => { - if (withoutFields.includes(field)) { - return; - } - const fieldDef = requireField(this.schema, model, field); - if (fieldDef.computed) { - return; - } - - if (this.isDelegateDiscriminator(fieldDef)) { - // discriminator field is auto-assigned - return; - } + // Normalize array argument for consistent cache keys + withoutFields = [...withoutFields].sort(); + + const uncheckedVariantFields: Record = {}; + const checkedVariantFields: Record = {}; + const modelDef = requireModel(this.schema, model); + const hasRelation = + !withoutRelationFields && + Object.entries(modelDef.fields).some(([f, def]) => !withoutFields.includes(f) && def.relation); + + Object.keys(modelDef.fields).forEach((field) => { + if (withoutFields.includes(field)) { + return; + } + const fieldDef = requireField(this.schema, model, field); + if (fieldDef.computed) { + return; + } - if (fieldDef.relation) { - if (withoutRelationFields) { - return; - } - const excludeFields: string[] = []; - const oppositeField = fieldDef.relation.opposite; - if (oppositeField) { - excludeFields.push(oppositeField); - const oppositeFieldDef = requireField(this.schema, fieldDef.type, oppositeField); - if (oppositeFieldDef.relation?.fields) { - excludeFields.push(...oppositeFieldDef.relation.fields); - } - } + if (this.isDelegateDiscriminator(fieldDef)) { + // discriminator field is auto-assigned + return; + } - let fieldSchema: ZodType = z.lazy(() => - this.makeRelationManipulationSchema(fieldDef, excludeFields, 'create'), - ); + if (fieldDef.relation) { + if (withoutRelationFields) { + return; + } + const excludeFields: string[] = []; + const oppositeField = fieldDef.relation.opposite; + if (oppositeField) { + excludeFields.push(oppositeField); + const oppositeFieldDef = requireField(this.schema, fieldDef.type, oppositeField); + if (oppositeFieldDef.relation?.fields) { + excludeFields.push(...oppositeFieldDef.relation.fields); + } + } - if (fieldDef.optional || fieldDef.array) { - // optional or array relations are optional - fieldSchema = fieldSchema.optional(); - } else { - // if all fk fields are optional, the relation is optional - let allFksOptional = false; - if (fieldDef.relation.fields) { - allFksOptional = fieldDef.relation.fields.every((f) => { - const fkDef = requireField(this.schema, model, f); - return fkDef.optional || fieldHasDefaultValue(fkDef); - }); - } - if (allFksOptional) { - fieldSchema = fieldSchema.optional(); - } - } + let fieldSchema: ZodType = z.lazy(() => + this.makeRelationManipulationSchema(fieldDef, excludeFields, 'create'), + ); - // optional to-one relation can be null - if (fieldDef.optional && !fieldDef.array) { - fieldSchema = fieldSchema.nullable(); - } - checkedVariantFields[field] = fieldSchema; - if (fieldDef.array || !fieldDef.relation.references) { - // non-owned relation - uncheckedVariantFields[field] = fieldSchema; - } - } else { - let fieldSchema = this.makeScalarSchema(fieldDef.type, fieldDef.attributes); + if (fieldDef.optional || fieldDef.array) { + // optional or array relations are optional + fieldSchema = fieldSchema.optional(); + } else { + // if all fk fields are optional, the relation is optional + let allFksOptional = false; + if (fieldDef.relation.fields) { + allFksOptional = fieldDef.relation.fields.every((f) => { + const fkDef = requireField(this.schema, model, f); + return fkDef.optional || fieldHasDefaultValue(fkDef); + }); + } + if (allFksOptional) { + fieldSchema = fieldSchema.optional(); + } + } - if (fieldDef.array) { - fieldSchema = addListValidation(fieldSchema.array(), fieldDef.attributes); - fieldSchema = z - .union([ - fieldSchema, - z.strictObject({ - set: fieldSchema, - }), - ]) - .optional(); - } + // optional to-one relation can be null + if (fieldDef.optional && !fieldDef.array) { + fieldSchema = fieldSchema.nullable(); + } + checkedVariantFields[field] = fieldSchema; + if (fieldDef.array || !fieldDef.relation.references) { + // non-owned relation + uncheckedVariantFields[field] = fieldSchema; + } + } else { + let fieldSchema = this.makeScalarSchema(fieldDef.type, fieldDef.attributes); - if (fieldDef.optional || fieldHasDefaultValue(fieldDef)) { - fieldSchema = fieldSchema.optional(); - } + if (fieldDef.array) { + fieldSchema = addListValidation(fieldSchema.array(), fieldDef.attributes); + fieldSchema = z + .union([ + fieldSchema, + z.strictObject({ + set: fieldSchema, + }), + ]) + .optional(); + } - if (fieldDef.optional) { - if (fieldDef.type === 'Json') { - // DbNull for Json fields - fieldSchema = z.union([fieldSchema, z.instanceof(DbNullClass)]); - } else { - fieldSchema = fieldSchema.nullable(); - } - } + if (fieldDef.optional || fieldHasDefaultValue(fieldDef)) { + fieldSchema = fieldSchema.optional(); + } - uncheckedVariantFields[field] = fieldSchema; - if (!fieldDef.foreignKeyFor) { - // non-fk field - checkedVariantFields[field] = fieldSchema; - } + if (fieldDef.optional) { + if (fieldDef.type === 'Json') { + // DbNull for Json fields + fieldSchema = z.union([fieldSchema, z.instanceof(DbNullClass)]); + } else { + fieldSchema = fieldSchema.nullable(); } - }); - - const uncheckedCreateSchema = this.extraValidationsEnabled - ? addCustomValidation(z.strictObject(uncheckedVariantFields), modelDef.attributes) - : z.strictObject(uncheckedVariantFields); - const checkedCreateSchema = this.extraValidationsEnabled - ? addCustomValidation(z.strictObject(checkedVariantFields), modelDef.attributes) - : z.strictObject(checkedVariantFields); + } - if (!hasRelation) { - return this.orArray(uncheckedCreateSchema, canBeArray); - } else { - return z.union([ - uncheckedCreateSchema, - checkedCreateSchema, - ...(canBeArray ? [z.array(uncheckedCreateSchema)] : []), - ...(canBeArray ? [z.array(checkedCreateSchema)] : []), - ]); + uncheckedVariantFields[field] = fieldSchema; + if (!fieldDef.foreignKeyFor) { + // non-fk field + checkedVariantFields[field] = fieldSchema; } - }, - ); + } + }); + + const uncheckedCreateSchema = this.extraValidationsEnabled + ? addCustomValidation(z.strictObject(uncheckedVariantFields), modelDef.attributes) + : z.strictObject(uncheckedVariantFields); + const checkedCreateSchema = this.extraValidationsEnabled + ? addCustomValidation(z.strictObject(checkedVariantFields), modelDef.attributes) + : z.strictObject(checkedVariantFields); + + if (!hasRelation) { + return this.orArray(uncheckedCreateSchema, canBeArray); + } else { + return z.union([ + uncheckedCreateSchema, + checkedCreateSchema, + ...(canBeArray ? [z.array(uncheckedCreateSchema)] : []), + ...(canBeArray ? [z.array(checkedCreateSchema)] : []), + ]); + } } private isDelegateDiscriminator(fieldDef: FieldDef) { @@ -1567,634 +1380,498 @@ export class InputValidator { return discriminatorField === fieldDef.name; } + @cache() private makeRelationManipulationSchema(fieldDef: FieldDef, withoutFields: string[], mode: 'create' | 'update') { - return this.cached( - { - type: 'relationManipulation', - model: fieldDef.type, - field: fieldDef.name, - withoutFields: [...withoutFields].sort(), - mode, - }, - () => { - const fieldType = fieldDef.type; - const array = !!fieldDef.array; - const fields: Record = { - create: this.makeCreateDataSchema(fieldDef.type, !!fieldDef.array, withoutFields).optional(), - - connect: this.makeConnectDataSchema(fieldType, array).optional(), - - connectOrCreate: this.makeConnectOrCreateDataSchema(fieldType, array, withoutFields).optional(), - }; - - if (array) { - fields['createMany'] = this.makeCreateManyDataSchema(fieldType, withoutFields).optional(); - } + // Normalize array argument for consistent cache keys + withoutFields = [...withoutFields].sort(); - if (mode === 'update') { - if (fieldDef.optional || fieldDef.array) { - // disconnect and delete are only available for optional/to-many relations - fields['disconnect'] = this.makeDisconnectDataSchema(fieldType, array).optional(); + const fieldType = fieldDef.type; + const array = !!fieldDef.array; + const fields: Record = { + create: this.makeCreateDataSchema(fieldDef.type, !!fieldDef.array, withoutFields).optional(), - fields['delete'] = this.makeDeleteRelationDataSchema(fieldType, array, true).optional(); - } + connect: this.makeConnectDataSchema(fieldType, array).optional(), - fields['update'] = array - ? this.orArray( - z.strictObject({ - where: this.makeWhereSchema(fieldType, true), - data: this.makeUpdateDataSchema(fieldType, withoutFields), - }), - true, - ).optional() - : z - .union([ - z.strictObject({ - where: this.makeWhereSchema(fieldType, false).optional(), - data: this.makeUpdateDataSchema(fieldType, withoutFields), - }), - this.makeUpdateDataSchema(fieldType, withoutFields), - ]) - .optional(); - - let upsertWhere = this.makeWhereSchema(fieldType, true); - if (!fieldDef.array) { - // to-one relation, can upsert without where clause - upsertWhere = upsertWhere.optional(); - } - fields['upsert'] = this.orArray( - z.strictObject({ - where: upsertWhere, - create: this.makeCreateDataSchema(fieldType, false, withoutFields), - update: this.makeUpdateDataSchema(fieldType, withoutFields), - }), - true, - ).optional(); + connectOrCreate: this.makeConnectOrCreateDataSchema(fieldType, array, withoutFields).optional(), + }; - if (array) { - // to-many relation specifics - fields['set'] = this.makeSetDataSchema(fieldType, true).optional(); + if (array) { + fields['createMany'] = this.makeCreateManyDataSchema(fieldType, withoutFields).optional(); + } - fields['updateMany'] = this.orArray( - z.strictObject({ - where: this.makeWhereSchema(fieldType, false, true), - data: this.makeUpdateDataSchema(fieldType, withoutFields), - }), - true, - ).optional(); + if (mode === 'update') { + if (fieldDef.optional || fieldDef.array) { + // disconnect and delete are only available for optional/to-many relations + fields['disconnect'] = this.makeDisconnectDataSchema(fieldType, array).optional(); - fields['deleteMany'] = this.makeDeleteRelationDataSchema(fieldType, true, false).optional(); - } - } + fields['delete'] = this.makeDeleteRelationDataSchema(fieldType, array, true).optional(); + } - return z.strictObject(fields); - }, - ); + fields['update'] = array + ? this.orArray( + z.strictObject({ + where: this.makeWhereSchema(fieldType, true), + data: this.makeUpdateDataSchema(fieldType, withoutFields), + }), + true, + ).optional() + : z + .union([ + z.strictObject({ + where: this.makeWhereSchema(fieldType, false).optional(), + data: this.makeUpdateDataSchema(fieldType, withoutFields), + }), + this.makeUpdateDataSchema(fieldType, withoutFields), + ]) + .optional(); + + let upsertWhere = this.makeWhereSchema(fieldType, true); + if (!fieldDef.array) { + // to-one relation, can upsert without where clause + upsertWhere = upsertWhere.optional(); + } + fields['upsert'] = this.orArray( + z.strictObject({ + where: upsertWhere, + create: this.makeCreateDataSchema(fieldType, false, withoutFields), + update: this.makeUpdateDataSchema(fieldType, withoutFields), + }), + true, + ).optional(); + + if (array) { + // to-many relation specifics + fields['set'] = this.makeSetDataSchema(fieldType, true).optional(); + + fields['updateMany'] = this.orArray( + z.strictObject({ + where: this.makeWhereSchema(fieldType, false, true), + data: this.makeUpdateDataSchema(fieldType, withoutFields), + }), + true, + ).optional(); + + fields['deleteMany'] = this.makeDeleteRelationDataSchema(fieldType, true, false).optional(); + } + } + + return z.strictObject(fields); } + @cache() private makeSetDataSchema(model: string, canBeArray: boolean) { - return this.cached( - { - type: 'setData', - model, - canBeArray, - }, - () => this.orArray(this.makeWhereSchema(model, true), canBeArray), - ); + return this.orArray(this.makeWhereSchema(model, true), canBeArray); } + @cache() private makeConnectDataSchema(model: string, canBeArray: boolean) { - return this.cached( - { - type: 'connectData', - model, - canBeArray, - }, - () => this.orArray(this.makeWhereSchema(model, true), canBeArray), - ); + return this.orArray(this.makeWhereSchema(model, true), canBeArray); } + @cache() private makeDisconnectDataSchema(model: string, canBeArray: boolean) { - return this.cached( - { - type: 'disconnectData', - model, - canBeArray, - }, - () => { - if (canBeArray) { - // to-many relation, must be unique filters - return this.orArray(this.makeWhereSchema(model, true), canBeArray); - } else { - // to-one relation, can be boolean or a regular filter - the entity - // being disconnected is already uniquely identified by its parent - return z.union([z.boolean(), this.makeWhereSchema(model, false)]); - } - }, - ); + if (canBeArray) { + // to-many relation, must be unique filters + return this.orArray(this.makeWhereSchema(model, true), canBeArray); + } else { + // to-one relation, can be boolean or a regular filter - the entity + // being disconnected is already uniquely identified by its parent + return z.union([z.boolean(), this.makeWhereSchema(model, false)]); + } } + @cache() private makeDeleteRelationDataSchema(model: string, toManyRelation: boolean, uniqueFilter: boolean) { - return this.cached( - { - type: 'deleteRelationData', - model, - toManyRelation, - uniqueFilter, - }, - () => - toManyRelation - ? this.orArray(this.makeWhereSchema(model, uniqueFilter), true) - : z.union([z.boolean(), this.makeWhereSchema(model, uniqueFilter)]), - ); + return toManyRelation + ? this.orArray(this.makeWhereSchema(model, uniqueFilter), true) + : z.union([z.boolean(), this.makeWhereSchema(model, uniqueFilter)]); } + @cache() private makeConnectOrCreateDataSchema(model: string, canBeArray: boolean, withoutFields: string[]) { - return this.cached( - { - type: 'connectOrCreateData', - model, - canBeArray, - withoutFields: [...withoutFields].sort(), - }, - () => { - const whereSchema = this.makeWhereSchema(model, true); - const createSchema = this.makeCreateDataSchema(model, false, withoutFields); - return this.orArray( - z.strictObject({ - where: whereSchema, - create: createSchema, - }), - canBeArray, - ); - }, + // Normalize array argument for consistent cache keys + withoutFields = [...withoutFields].sort(); + + const whereSchema = this.makeWhereSchema(model, true); + const createSchema = this.makeCreateDataSchema(model, false, withoutFields); + return this.orArray( + z.strictObject({ + where: whereSchema, + create: createSchema, + }), + canBeArray, ); } + @cache() private makeCreateManyDataSchema(model: string, withoutFields: string[]) { - return this.cached( - { - type: 'createManyData', - model, - withoutFields: [...withoutFields].sort(), - }, - () => - z.strictObject({ - data: this.makeCreateDataSchema(model, true, withoutFields, true), - skipDuplicates: z.boolean().optional(), - }), - ); + // Normalize array argument for consistent cache keys + withoutFields = [...withoutFields].sort(); + + return z.strictObject({ + data: this.makeCreateDataSchema(model, true, withoutFields, true), + skipDuplicates: z.boolean().optional(), + }); } // #endregion // #region Update + @cache() private makeUpdateSchema(model: string) { - return this.cached( - { - type: 'update', - model, - }, - () => { - const baseSchema = z.strictObject({ - where: this.makeWhereSchema(model, true), - data: this.makeUpdateDataSchema(model), - select: this.makeSelectSchema(model).optional().nullable(), - include: this.makeIncludeSchema(model).optional().nullable(), - omit: this.makeOmitSchema(model).optional().nullable(), - }); - let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'update'); - schema = this.refineForSelectIncludeMutuallyExclusive(schema); - schema = this.refineForSelectOmitMutuallyExclusive(schema); - return schema; - }, - ); + const baseSchema = z.strictObject({ + where: this.makeWhereSchema(model, true), + data: this.makeUpdateDataSchema(model), + select: this.makeSelectSchema(model).optional().nullable(), + include: this.makeIncludeSchema(model).optional().nullable(), + omit: this.makeOmitSchema(model).optional().nullable(), + }); + let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'update'); + schema = this.refineForSelectIncludeMutuallyExclusive(schema); + schema = this.refineForSelectOmitMutuallyExclusive(schema); + return schema; } + @cache() private makeUpdateManySchema(model: string) { - return this.cached( - { - type: 'updateMany', - model, - }, - () => - this.mergePluginArgsSchema( - z.strictObject({ - where: this.makeWhereSchema(model, false).optional(), - data: this.makeUpdateDataSchema(model, [], true), - limit: z.number().int().nonnegative().optional(), - }), - 'updateMany', - ), + return this.mergePluginArgsSchema( + z.strictObject({ + where: this.makeWhereSchema(model, false).optional(), + data: this.makeUpdateDataSchema(model, [], true), + limit: z.number().int().nonnegative().optional(), + }), + 'updateMany', ); } + @cache() private makeUpdateManyAndReturnSchema(model: string) { - return this.cached( - { - type: 'updateManyAndReturn', - model, - }, - () => { - // plugin extended args schema is merged in `makeUpdateManySchema` - const baseSchema: ZodObject = this.makeUpdateManySchema(model); - let schema: ZodType = baseSchema.extend({ - select: this.makeSelectSchema(model).optional().nullable(), - omit: this.makeOmitSchema(model).optional().nullable(), - }); - schema = this.refineForSelectOmitMutuallyExclusive(schema); - return schema; - }, - ); + // plugin extended args schema is merged in `makeUpdateManySchema` + const baseSchema: ZodObject = this.makeUpdateManySchema(model); + let schema: ZodType = baseSchema.extend({ + select: this.makeSelectSchema(model).optional().nullable(), + omit: this.makeOmitSchema(model).optional().nullable(), + }); + schema = this.refineForSelectOmitMutuallyExclusive(schema); + return schema; } + @cache() private makeUpsertSchema(model: string) { - return this.cached( - { - type: 'upsert', - model, - }, - () => { - const baseSchema = z.strictObject({ - where: this.makeWhereSchema(model, true), - create: this.makeCreateDataSchema(model, false), - update: this.makeUpdateDataSchema(model), - select: this.makeSelectSchema(model).optional().nullable(), - include: this.makeIncludeSchema(model).optional().nullable(), - omit: this.makeOmitSchema(model).optional().nullable(), - }); - let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'upsert'); - schema = this.refineForSelectIncludeMutuallyExclusive(schema); - schema = this.refineForSelectOmitMutuallyExclusive(schema); - return schema; - }, - ); + const baseSchema = z.strictObject({ + where: this.makeWhereSchema(model, true), + create: this.makeCreateDataSchema(model, false), + update: this.makeUpdateDataSchema(model), + select: this.makeSelectSchema(model).optional().nullable(), + include: this.makeIncludeSchema(model).optional().nullable(), + omit: this.makeOmitSchema(model).optional().nullable(), + }); + let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'upsert'); + schema = this.refineForSelectIncludeMutuallyExclusive(schema); + schema = this.refineForSelectOmitMutuallyExclusive(schema); + return schema; } + @cache({ includeProperties: ['extraValidationsEnabled'] }) private makeUpdateDataSchema(model: string, withoutFields: string[] = [], withoutRelationFields = false) { - return this.cached( - { - type: 'updateData', - model, - withoutFields: [...withoutFields].sort(), - withoutRelationFields, - }, - () => { - const uncheckedVariantFields: Record = {}; - const checkedVariantFields: Record = {}; - const modelDef = requireModel(this.schema, model); - const hasRelation = Object.entries(modelDef.fields).some( - ([key, value]) => value.relation && !withoutFields.includes(key), - ); + // Normalize array argument for consistent cache keys + withoutFields = [...withoutFields].sort(); + + const uncheckedVariantFields: Record = {}; + const checkedVariantFields: Record = {}; + const modelDef = requireModel(this.schema, model); + const hasRelation = Object.entries(modelDef.fields).some( + ([key, value]) => value.relation && !withoutFields.includes(key), + ); - Object.keys(modelDef.fields).forEach((field) => { - if (withoutFields.includes(field)) { - return; - } - const fieldDef = requireField(this.schema, model, field); + Object.keys(modelDef.fields).forEach((field) => { + if (withoutFields.includes(field)) { + return; + } + const fieldDef = requireField(this.schema, model, field); - if (fieldDef.relation) { - if (withoutRelationFields) { - return; - } - const excludeFields: string[] = []; - const oppositeField = fieldDef.relation.opposite; - if (oppositeField) { - excludeFields.push(oppositeField); - const oppositeFieldDef = requireField(this.schema, fieldDef.type, oppositeField); - if (oppositeFieldDef.relation?.fields) { - excludeFields.push(...oppositeFieldDef.relation.fields); - } - } - let fieldSchema: ZodType = z - .lazy(() => this.makeRelationManipulationSchema(fieldDef, excludeFields, 'update')) - .optional(); - // optional to-one relation can be null - if (fieldDef.optional && !fieldDef.array) { - fieldSchema = fieldSchema.nullable(); - } - checkedVariantFields[field] = fieldSchema; - if (fieldDef.array || !fieldDef.relation.references) { - // non-owned relation - uncheckedVariantFields[field] = fieldSchema; - } - } else { - let fieldSchema = this.makeScalarSchema(fieldDef.type, fieldDef.attributes); - - if (this.isNumericField(fieldDef)) { - fieldSchema = z.union([ - fieldSchema, - z - .object({ - set: this.nullableIf(z.number().optional(), !!fieldDef.optional).optional(), - increment: z.number().optional(), - decrement: z.number().optional(), - multiply: z.number().optional(), - divide: z.number().optional(), - }) - .refine( - (v) => Object.keys(v).length === 1, - 'Only one of "set", "increment", "decrement", "multiply", or "divide" can be provided', - ), - ]); - } + if (fieldDef.relation) { + if (withoutRelationFields) { + return; + } + const excludeFields: string[] = []; + const oppositeField = fieldDef.relation.opposite; + if (oppositeField) { + excludeFields.push(oppositeField); + const oppositeFieldDef = requireField(this.schema, fieldDef.type, oppositeField); + if (oppositeFieldDef.relation?.fields) { + excludeFields.push(...oppositeFieldDef.relation.fields); + } + } + let fieldSchema: ZodType = z + .lazy(() => this.makeRelationManipulationSchema(fieldDef, excludeFields, 'update')) + .optional(); + // optional to-one relation can be null + if (fieldDef.optional && !fieldDef.array) { + fieldSchema = fieldSchema.nullable(); + } + checkedVariantFields[field] = fieldSchema; + if (fieldDef.array || !fieldDef.relation.references) { + // non-owned relation + uncheckedVariantFields[field] = fieldSchema; + } + } else { + let fieldSchema = this.makeScalarSchema(fieldDef.type, fieldDef.attributes); + + if (this.isNumericField(fieldDef)) { + fieldSchema = z.union([ + fieldSchema, + z + .object({ + set: this.nullableIf(z.number().optional(), !!fieldDef.optional).optional(), + increment: z.number().optional(), + decrement: z.number().optional(), + multiply: z.number().optional(), + divide: z.number().optional(), + }) + .refine( + (v) => Object.keys(v).length === 1, + 'Only one of "set", "increment", "decrement", "multiply", or "divide" can be provided', + ), + ]); + } - if (fieldDef.array) { - const arraySchema = addListValidation(fieldSchema.array(), fieldDef.attributes); - fieldSchema = z.union([ - arraySchema, - z - .object({ - set: arraySchema.optional(), - push: z.union([fieldSchema, fieldSchema.array()]).optional(), - }) - .refine( - (v) => Object.keys(v).length === 1, - 'Only one of "set", "push" can be provided', - ), - ]); - } + if (fieldDef.array) { + const arraySchema = addListValidation(fieldSchema.array(), fieldDef.attributes); + fieldSchema = z.union([ + arraySchema, + z + .object({ + set: arraySchema.optional(), + push: z.union([fieldSchema, fieldSchema.array()]).optional(), + }) + .refine((v) => Object.keys(v).length === 1, 'Only one of "set", "push" can be provided'), + ]); + } - if (fieldDef.optional) { - if (fieldDef.type === 'Json') { - // DbNull for Json fields - fieldSchema = z.union([fieldSchema, z.instanceof(DbNullClass)]); - } else { - fieldSchema = fieldSchema.nullable(); - } - } + if (fieldDef.optional) { + if (fieldDef.type === 'Json') { + // DbNull for Json fields + fieldSchema = z.union([fieldSchema, z.instanceof(DbNullClass)]); + } else { + fieldSchema = fieldSchema.nullable(); + } + } - // all fields are optional in update - fieldSchema = fieldSchema.optional(); + // all fields are optional in update + fieldSchema = fieldSchema.optional(); - uncheckedVariantFields[field] = fieldSchema; - if (!fieldDef.foreignKeyFor) { - // non-fk field - checkedVariantFields[field] = fieldSchema; - } - } - }); - - const uncheckedUpdateSchema = this.extraValidationsEnabled - ? addCustomValidation(z.strictObject(uncheckedVariantFields), modelDef.attributes) - : z.strictObject(uncheckedVariantFields); - const checkedUpdateSchema = this.extraValidationsEnabled - ? addCustomValidation(z.strictObject(checkedVariantFields), modelDef.attributes) - : z.strictObject(checkedVariantFields); - if (!hasRelation) { - return uncheckedUpdateSchema; - } else { - return z.union([uncheckedUpdateSchema, checkedUpdateSchema]); + uncheckedVariantFields[field] = fieldSchema; + if (!fieldDef.foreignKeyFor) { + // non-fk field + checkedVariantFields[field] = fieldSchema; } - }, - ); + } + }); + + const uncheckedUpdateSchema = this.extraValidationsEnabled + ? addCustomValidation(z.strictObject(uncheckedVariantFields), modelDef.attributes) + : z.strictObject(uncheckedVariantFields); + const checkedUpdateSchema = this.extraValidationsEnabled + ? addCustomValidation(z.strictObject(checkedVariantFields), modelDef.attributes) + : z.strictObject(checkedVariantFields); + if (!hasRelation) { + return uncheckedUpdateSchema; + } else { + return z.union([uncheckedUpdateSchema, checkedUpdateSchema]); + } } // #endregion // #region Delete + @cache() private makeDeleteSchema(model: GetModels) { - return this.cached( - { - type: 'delete', - model, - }, - () => { - const baseSchema = z.strictObject({ - where: this.makeWhereSchema(model, true), - select: this.makeSelectSchema(model).optional().nullable(), - include: this.makeIncludeSchema(model).optional().nullable(), - omit: this.makeOmitSchema(model).optional().nullable(), - }); - let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'delete'); - schema = this.refineForSelectIncludeMutuallyExclusive(schema); - schema = this.refineForSelectOmitMutuallyExclusive(schema); - return schema; - }, - ); + const baseSchema = z.strictObject({ + where: this.makeWhereSchema(model, true), + select: this.makeSelectSchema(model).optional().nullable(), + include: this.makeIncludeSchema(model).optional().nullable(), + omit: this.makeOmitSchema(model).optional().nullable(), + }); + let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'delete'); + schema = this.refineForSelectIncludeMutuallyExclusive(schema); + schema = this.refineForSelectOmitMutuallyExclusive(schema); + return schema; } + @cache() private makeDeleteManySchema(model: GetModels) { - return this.cached( - { - type: 'deleteMany', - model, - }, - () => - this.mergePluginArgsSchema( - z.strictObject({ - where: this.makeWhereSchema(model, false).optional(), - limit: z.number().int().nonnegative().optional(), - }), - 'deleteMany', - ).optional(), - ); + return this.mergePluginArgsSchema( + z.strictObject({ + where: this.makeWhereSchema(model, false).optional(), + limit: z.number().int().nonnegative().optional(), + }), + 'deleteMany', + ).optional(); } // #endregion // #region Count + @cache() makeCountSchema(model: GetModels) { - return this.cached( - { - type: 'count', - model, - }, - () => - this.mergePluginArgsSchema( - z.strictObject({ - where: this.makeWhereSchema(model, false).optional(), - skip: this.makeSkipSchema().optional(), - take: this.makeTakeSchema().optional(), - orderBy: this.orArray(this.makeOrderBySchema(model, true, false), true).optional(), - select: this.makeCountAggregateInputSchema(model).optional(), - }), - 'count', - ).optional(), - ); + return this.mergePluginArgsSchema( + z.strictObject({ + where: this.makeWhereSchema(model, false).optional(), + skip: this.makeSkipSchema().optional(), + take: this.makeTakeSchema().optional(), + orderBy: this.orArray(this.makeOrderBySchema(model, true, false), true).optional(), + select: this.makeCountAggregateInputSchema(model).optional(), + }), + 'count', + ).optional(); } + @cache() private makeCountAggregateInputSchema(model: GetModels) { - return this.cached( - { - type: 'countAggregateInput', - model, - }, - () => { - const modelDef = requireModel(this.schema, model); - return z.union([ - z.literal(true), - z.strictObject({ - _all: z.literal(true).optional(), - ...Object.keys(modelDef.fields).reduce( - (acc, field) => { - acc[field] = z.literal(true).optional(); - return acc; - }, - {} as Record, - ), - }), - ]); - }, - ); + const modelDef = requireModel(this.schema, model); + return z.union([ + z.literal(true), + z.strictObject({ + _all: z.literal(true).optional(), + ...Object.keys(modelDef.fields).reduce( + (acc, field) => { + acc[field] = z.literal(true).optional(); + return acc; + }, + {} as Record, + ), + }), + ]); } // #endregion // #region Aggregate + @cache() makeAggregateSchema(model: GetModels) { - return this.cached( - { - type: 'aggregate', - model, - }, - () => - this.mergePluginArgsSchema( - z.strictObject({ - where: this.makeWhereSchema(model, false).optional(), - skip: this.makeSkipSchema().optional(), - take: this.makeTakeSchema().optional(), - orderBy: this.orArray(this.makeOrderBySchema(model, true, false), true).optional(), - _count: this.makeCountAggregateInputSchema(model).optional(), - _avg: this.makeSumAvgInputSchema(model).optional(), - _sum: this.makeSumAvgInputSchema(model).optional(), - _min: this.makeMinMaxInputSchema(model).optional(), - _max: this.makeMinMaxInputSchema(model).optional(), - }), - 'aggregate', - ).optional(), - ); + return this.mergePluginArgsSchema( + z.strictObject({ + where: this.makeWhereSchema(model, false).optional(), + skip: this.makeSkipSchema().optional(), + take: this.makeTakeSchema().optional(), + orderBy: this.orArray(this.makeOrderBySchema(model, true, false), true).optional(), + _count: this.makeCountAggregateInputSchema(model).optional(), + _avg: this.makeSumAvgInputSchema(model).optional(), + _sum: this.makeSumAvgInputSchema(model).optional(), + _min: this.makeMinMaxInputSchema(model).optional(), + _max: this.makeMinMaxInputSchema(model).optional(), + }), + 'aggregate', + ).optional(); } + @cache() makeSumAvgInputSchema(model: GetModels) { - return this.cached( - { - type: 'sumAvgInput', - model, - }, - () => { - const modelDef = requireModel(this.schema, model); - return z.strictObject( - Object.keys(modelDef.fields).reduce( - (acc, field) => { - const fieldDef = requireField(this.schema, model, field); - if (this.isNumericField(fieldDef)) { - acc[field] = z.literal(true).optional(); - } - return acc; - }, - {} as Record, - ), - ); - }, + const modelDef = requireModel(this.schema, model); + return z.strictObject( + Object.keys(modelDef.fields).reduce( + (acc, field) => { + const fieldDef = requireField(this.schema, model, field); + if (this.isNumericField(fieldDef)) { + acc[field] = z.literal(true).optional(); + } + return acc; + }, + {} as Record, + ), ); } + @cache() makeMinMaxInputSchema(model: GetModels) { - return this.cached( - { - type: 'minMaxInput', - model, - }, - () => { - const modelDef = requireModel(this.schema, model); - return z.strictObject( - Object.keys(modelDef.fields).reduce( - (acc, field) => { - const fieldDef = requireField(this.schema, model, field); - if (!fieldDef.relation && !fieldDef.array) { - acc[field] = z.literal(true).optional(); - } - return acc; - }, - {} as Record, - ), - ); - }, + const modelDef = requireModel(this.schema, model); + return z.strictObject( + Object.keys(modelDef.fields).reduce( + (acc, field) => { + const fieldDef = requireField(this.schema, model, field); + if (!fieldDef.relation && !fieldDef.array) { + acc[field] = z.literal(true).optional(); + } + return acc; + }, + {} as Record, + ), ); } + @cache() private makeGroupBySchema(model: GetModels) { - return this.cached( - { - type: 'groupBy', - model, - }, - () => { - const modelDef = requireModel(this.schema, model); - const nonRelationFields = Object.keys(modelDef.fields).filter( - (field) => !modelDef.fields[field]?.relation, - ); - const bySchema = - nonRelationFields.length > 0 - ? this.orArray(z.enum(nonRelationFields as [string, ...string[]]), true) - : z.never(); - - const baseSchema = z.strictObject({ - where: this.makeWhereSchema(model, false).optional(), - orderBy: this.orArray(this.makeOrderBySchema(model, false, true), true).optional(), - by: bySchema, - having: this.makeHavingSchema(model).optional(), - skip: this.makeSkipSchema().optional(), - take: this.makeTakeSchema().optional(), - _count: this.makeCountAggregateInputSchema(model).optional(), - _avg: this.makeSumAvgInputSchema(model).optional(), - _sum: this.makeSumAvgInputSchema(model).optional(), - _min: this.makeMinMaxInputSchema(model).optional(), - _max: this.makeMinMaxInputSchema(model).optional(), - }); - - let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'groupBy'); - - // fields used in `having` must be either in the `by` list, or aggregations - schema = schema.refine((value: any) => { - const bys = typeof value.by === 'string' ? [value.by] : value.by; - if (value.having && typeof value.having === 'object') { - for (const [key, val] of Object.entries(value.having)) { - if (AGGREGATE_OPERATORS.includes(key as any)) { - continue; - } - if (bys.includes(key)) { - continue; - } - // we have a key not mentioned in `by`, in this case it must only use - // aggregations in the condition - - // 1. payload must be an object - if (!val || typeof val !== 'object') { - return false; - } - // 2. payload must only contain aggregations - if (!this.onlyAggregationFields(val)) { - return false; - } - } + const modelDef = requireModel(this.schema, model); + const nonRelationFields = Object.keys(modelDef.fields).filter((field) => !modelDef.fields[field]?.relation); + const bySchema = + nonRelationFields.length > 0 + ? this.orArray(z.enum(nonRelationFields as [string, ...string[]]), true) + : z.never(); + + const baseSchema = z.strictObject({ + where: this.makeWhereSchema(model, false).optional(), + orderBy: this.orArray(this.makeOrderBySchema(model, false, true), true).optional(), + by: bySchema, + having: this.makeHavingSchema(model).optional(), + skip: this.makeSkipSchema().optional(), + take: this.makeTakeSchema().optional(), + _count: this.makeCountAggregateInputSchema(model).optional(), + _avg: this.makeSumAvgInputSchema(model).optional(), + _sum: this.makeSumAvgInputSchema(model).optional(), + _min: this.makeMinMaxInputSchema(model).optional(), + _max: this.makeMinMaxInputSchema(model).optional(), + }); + + let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'groupBy'); + + // fields used in `having` must be either in the `by` list, or aggregations + schema = schema.refine((value: any) => { + const bys = typeof value.by === 'string' ? [value.by] : value.by; + if (value.having && typeof value.having === 'object') { + for (const [key, val] of Object.entries(value.having)) { + if (AGGREGATE_OPERATORS.includes(key as any)) { + continue; } - return true; - }, 'fields in "having" must be in "by"'); - - // fields used in `orderBy` must be either in the `by` list, or aggregations - schema = schema.refine((value: any) => { - const bys = typeof value.by === 'string' ? [value.by] : value.by; - if ( - value.orderBy && - Object.keys(value.orderBy) - .filter((f) => !AGGREGATE_OPERATORS.includes(f as AGGREGATE_OPERATORS)) - .some((key) => !bys.includes(key)) - ) { + if (bys.includes(key)) { + continue; + } + // we have a key not mentioned in `by`, in this case it must only use + // aggregations in the condition + + // 1. payload must be an object + if (!val || typeof val !== 'object') { return false; - } else { - return true; } - }, 'fields in "orderBy" must be in "by"'); + // 2. payload must only contain aggregations + if (!this.onlyAggregationFields(val)) { + return false; + } + } + } + return true; + }, 'fields in "having" must be in "by"'); + + // fields used in `orderBy` must be either in the `by` list, or aggregations + schema = schema.refine((value: any) => { + const bys = typeof value.by === 'string' ? [value.by] : value.by; + if ( + value.orderBy && + Object.keys(value.orderBy) + .filter((f) => !AGGREGATE_OPERATORS.includes(f as AGGREGATE_OPERATORS)) + .some((key) => !bys.includes(key)) + ) { + return false; + } else { + return true; + } + }, 'fields in "orderBy" must be in "by"'); - return schema; - }, - ); + return schema; } private onlyAggregationFields(val: object) { @@ -2223,65 +1900,47 @@ export class InputValidator { // #region Procedures + @cache() private makeProcedureParamSchema(param: { type: string; array?: boolean; optional?: boolean }): z.ZodType { - return this.cached( - { - type: 'procedureParam', - param, - }, - () => { - let schema: z.ZodType; - - if (isTypeDef(this.schema, param.type)) { - schema = this.makeTypeDefSchema(param.type); - } else if (isEnum(this.schema, param.type)) { - schema = this.makeEnumSchema(param.type); - } else if (param.type in (this.schema.models ?? {})) { - // For model-typed values, accept any object (no deep shape validation). - schema = z.record(z.string(), z.unknown()); - } else { - // Builtin scalar types. - schema = this.makeScalarSchema(param.type as BuiltinType); + let schema: z.ZodType; + + if (isTypeDef(this.schema, param.type)) { + schema = this.makeTypeDefSchema(param.type); + } else if (isEnum(this.schema, param.type)) { + schema = this.makeEnumSchema(param.type); + } else if (param.type in (this.schema.models ?? {})) { + // For model-typed values, accept any object (no deep shape validation). + schema = z.record(z.string(), z.unknown()); + } else { + // Builtin scalar types. + schema = this.makeScalarSchema(param.type as BuiltinType); - // If a type isn't recognized by any of the above branches, `makeScalarSchema` returns `unknown`. - // Treat it as configuration/schema error. - if (schema instanceof z.ZodUnknown) { - throw createInternalError(`Unsupported procedure parameter type: ${param.type}`); - } - } + // If a type isn't recognized by any of the above branches, `makeScalarSchema` returns `unknown`. + // Treat it as configuration/schema error. + if (schema instanceof z.ZodUnknown) { + throw createInternalError(`Unsupported procedure parameter type: ${param.type}`); + } + } - if (param.array) { - schema = schema.array(); - } - if (param.optional) { - schema = schema.optional(); - } + if (param.array) { + schema = schema.array(); + } + if (param.optional) { + schema = schema.optional(); + } - return schema; - }, - ); + return schema; } // #endregion // #region Cache Management - private cached(key: Record, factory: () => T): T { - const cacheKey = stableStringify(key); - let schema = this.getSchemaCache(cacheKey!); - if (schema) { - return schema as T; - } - schema = factory(); - this.setSchemaCache(cacheKey!, schema); - return schema as T; - } - - private getSchemaCache(cacheKey: string) { + getCache(cacheKey: string) { return this.schemaCache.get(cacheKey); } - private setSchemaCache(cacheKey: string, schema: ZodType) { + setCache(cacheKey: string, schema: ZodType) { return this.schemaCache.set(cacheKey, schema); } @@ -2299,12 +1958,14 @@ export class InputValidator { // #region Helpers + @cache() private makeSkipSchema() { - return this.cached({ type: 'skip' }, () => z.number().int().nonnegative()); + return z.number().int().nonnegative(); } + @cache() private makeTakeSchema() { - return this.cached({ type: 'take' }, () => z.number().int()); + return z.number().int(); } private refineForSelectIncludeMutuallyExclusive(schema: ZodType) { From 1a038be4701d20b52b92141eb631d63b43fc1197 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 26 Jan 2026 22:43:49 +0800 Subject: [PATCH 3/9] update --- .../orm/src/client/crud/validator/index.ts | 57 ++++++++----------- 1 file changed, 23 insertions(+), 34 deletions(-) diff --git a/packages/orm/src/client/crud/validator/index.ts b/packages/orm/src/client/crud/validator/index.ts index 1c9067cda..0301dfc96 100644 --- a/packages/orm/src/client/crud/validator/index.ts +++ b/packages/orm/src/client/crud/validator/index.ts @@ -65,6 +65,12 @@ import { type GetSchemaFunc = (model: GetModels) => ZodType; +/** + * Helper decorator that caches schema builders with class's state included + * as part of the key (here the `extraValidationsEnabled` property). + */ +const cacheWithState = () => cache({ includeProperties: ['extraValidationsEnabled'] }); + export class InputValidator { private readonly schemaCache = new Map(); @@ -312,25 +318,8 @@ export class InputValidator { // #region Validation helpers private validate(model: GetModels, operation: string, getSchema: GetSchemaFunc, args: unknown) { - // const schema = this.cached( - // { - // type: 'model', - // model, - // operation, - // extraValidationsEnabled: this.extraValidationsEnabled, - // }, - // () => getSchema(model), - // ); - const schema = getSchema(model); - const { error, data } = schema.safeParse(args); - - // const start1 = new Date(); - // schema.safeParse(args); - // const took1 = new Date().getTime() - start1.getTime(); - // console.log('Validation took:', took1); - if (error) { throw createInvalidInputError( `Invalid ${operation} args for model "${model}": ${formatError(error)}`, @@ -431,7 +420,7 @@ export class InputValidator { // #region Find - @cache() + @cacheWithState() private makeFindSchema(model: string, operation: CoreCrudOperations) { const fields: Record = {}; const unique = operation === 'findUnique'; @@ -470,7 +459,7 @@ export class InputValidator { return result; } - @cache() + @cacheWithState() private makeExistsSchema(model: string) { const baseSchema = z.strictObject({ where: this.makeWhereSchema(model, false).optional(), @@ -524,7 +513,7 @@ export class InputValidator { return z.enum(Object.keys(enumDef.values) as [string, ...string[]]); } - @cache({ includeProperties: ['extraValidationsEnabled'] }) + @cacheWithState() private makeTypeDefSchema(type: string): z.ZodType { const typeDef = getTypeDef(this.schema, type); invariant(typeDef, `Type definition "${type}" not found in schema`); @@ -1210,7 +1199,7 @@ export class InputValidator { // #region Create - @cache() + @cacheWithState() private makeCreateSchema(model: string) { const dataSchema = this.makeCreateDataSchema(model, false); const baseSchema = z.strictObject({ @@ -1225,12 +1214,12 @@ export class InputValidator { return schema; } - @cache() + @cacheWithState() private makeCreateManySchema(model: string) { return this.mergePluginArgsSchema(this.makeCreateManyDataSchema(model, []), 'createMany').optional(); } - @cache() + @cacheWithState() private makeCreateManyAndReturnSchema(model: string) { const base = this.makeCreateManyDataSchema(model, []); let result: ZodObject = base.extend({ @@ -1241,7 +1230,7 @@ export class InputValidator { return this.refineForSelectOmitMutuallyExclusive(result).optional(); } - @cache({ includeProperties: ['extraValidationsEnabled'] }) + @cacheWithState() private makeCreateDataSchema( model: string, canBeArray: boolean, @@ -1518,7 +1507,7 @@ export class InputValidator { // #region Update - @cache() + @cacheWithState() private makeUpdateSchema(model: string) { const baseSchema = z.strictObject({ where: this.makeWhereSchema(model, true), @@ -1533,7 +1522,7 @@ export class InputValidator { return schema; } - @cache() + @cacheWithState() private makeUpdateManySchema(model: string) { return this.mergePluginArgsSchema( z.strictObject({ @@ -1545,7 +1534,7 @@ export class InputValidator { ); } - @cache() + @cacheWithState() private makeUpdateManyAndReturnSchema(model: string) { // plugin extended args schema is merged in `makeUpdateManySchema` const baseSchema: ZodObject = this.makeUpdateManySchema(model); @@ -1557,7 +1546,7 @@ export class InputValidator { return schema; } - @cache() + @cacheWithState() private makeUpsertSchema(model: string) { const baseSchema = z.strictObject({ where: this.makeWhereSchema(model, true), @@ -1573,7 +1562,7 @@ export class InputValidator { return schema; } - @cache({ includeProperties: ['extraValidationsEnabled'] }) + @cacheWithState() private makeUpdateDataSchema(model: string, withoutFields: string[] = [], withoutRelationFields = false) { // Normalize array argument for consistent cache keys withoutFields = [...withoutFields].sort(); @@ -1687,7 +1676,7 @@ export class InputValidator { // #region Delete - @cache() + @cacheWithState() private makeDeleteSchema(model: GetModels) { const baseSchema = z.strictObject({ where: this.makeWhereSchema(model, true), @@ -1701,7 +1690,7 @@ export class InputValidator { return schema; } - @cache() + @cacheWithState() private makeDeleteManySchema(model: GetModels) { return this.mergePluginArgsSchema( z.strictObject({ @@ -1716,7 +1705,7 @@ export class InputValidator { // #region Count - @cache() + @cacheWithState() makeCountSchema(model: GetModels) { return this.mergePluginArgsSchema( z.strictObject({ @@ -1752,7 +1741,7 @@ export class InputValidator { // #region Aggregate - @cache() + @cacheWithState() makeAggregateSchema(model: GetModels) { return this.mergePluginArgsSchema( z.strictObject({ @@ -1804,7 +1793,7 @@ export class InputValidator { ); } - @cache() + @cacheWithState() private makeGroupBySchema(model: GetModels) { const modelDef = requireModel(this.schema, model); const nonRelationFields = Object.keys(modelDef.fields).filter((field) => !modelDef.fields[field]?.relation); From d0826eada62a7d73eb5f02e5de6f9d0ccf18e4e1 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 26 Jan 2026 22:51:16 +0800 Subject: [PATCH 4/9] update --- .../client/crud/validator/cache-decorator.ts | 17 +------- .../orm/src/client/crud/validator/index.ts | 42 ++++++++----------- 2 files changed, 19 insertions(+), 40 deletions(-) diff --git a/packages/orm/src/client/crud/validator/cache-decorator.ts b/packages/orm/src/client/crud/validator/cache-decorator.ts index d666c5c46..c7d81cc59 100644 --- a/packages/orm/src/client/crud/validator/cache-decorator.ts +++ b/packages/orm/src/client/crud/validator/cache-decorator.ts @@ -1,13 +1,5 @@ import stableStringify from 'json-stable-stringify'; -interface CacheOptions { - /** - * Instance property names to include in the cache key. - * Useful when cache should be invalidated based on instance state. - */ - includeProperties?: string[]; -} - /** * Method decorator that caches the return value based on method name and arguments. * @@ -15,7 +7,7 @@ interface CacheOptions { * - Class must have a `getCache(key: string)` method * - Class must have a `setCache(key: string, value: any)` method */ -export function cache(options: CacheOptions = {}) { +export function cache() { return function (_target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; @@ -32,13 +24,6 @@ export function cache(options: CacheOptions = {}) { ...args, }; - // Include specified instance properties - if (options.includeProperties) { - for (const prop of options.includeProperties) { - cacheKeyObj['$' + prop] = this[prop]; - } - } - // Generate stable string key const cacheKey = stableStringify(cacheKeyObj)!; diff --git a/packages/orm/src/client/crud/validator/index.ts b/packages/orm/src/client/crud/validator/index.ts index 0301dfc96..6af8ad2d3 100644 --- a/packages/orm/src/client/crud/validator/index.ts +++ b/packages/orm/src/client/crud/validator/index.ts @@ -65,12 +65,6 @@ import { type GetSchemaFunc = (model: GetModels) => ZodType; -/** - * Helper decorator that caches schema builders with class's state included - * as part of the key (here the `extraValidationsEnabled` property). - */ -const cacheWithState = () => cache({ includeProperties: ['extraValidationsEnabled'] }); - export class InputValidator { private readonly schemaCache = new Map(); @@ -247,7 +241,7 @@ export class InputValidator { throw createInvalidInputError('Missing procedure arguments', `$procs.${proc}`); } - if (typeof input !== 'object') { + if (typeof input !== 'object' || input === null || Array.isArray(input)) { throw createInvalidInputError('Procedure input must be an object', `$procs.${proc}`); } @@ -420,7 +414,7 @@ export class InputValidator { // #region Find - @cacheWithState() + @cache() private makeFindSchema(model: string, operation: CoreCrudOperations) { const fields: Record = {}; const unique = operation === 'findUnique'; @@ -459,7 +453,7 @@ export class InputValidator { return result; } - @cacheWithState() + @cache() private makeExistsSchema(model: string) { const baseSchema = z.strictObject({ where: this.makeWhereSchema(model, false).optional(), @@ -513,7 +507,7 @@ export class InputValidator { return z.enum(Object.keys(enumDef.values) as [string, ...string[]]); } - @cacheWithState() + @cache() private makeTypeDefSchema(type: string): z.ZodType { const typeDef = getTypeDef(this.schema, type); invariant(typeDef, `Type definition "${type}" not found in schema`); @@ -1199,7 +1193,7 @@ export class InputValidator { // #region Create - @cacheWithState() + @cache() private makeCreateSchema(model: string) { const dataSchema = this.makeCreateDataSchema(model, false); const baseSchema = z.strictObject({ @@ -1214,12 +1208,12 @@ export class InputValidator { return schema; } - @cacheWithState() + @cache() private makeCreateManySchema(model: string) { return this.mergePluginArgsSchema(this.makeCreateManyDataSchema(model, []), 'createMany').optional(); } - @cacheWithState() + @cache() private makeCreateManyAndReturnSchema(model: string) { const base = this.makeCreateManyDataSchema(model, []); let result: ZodObject = base.extend({ @@ -1230,7 +1224,7 @@ export class InputValidator { return this.refineForSelectOmitMutuallyExclusive(result).optional(); } - @cacheWithState() + @cache() private makeCreateDataSchema( model: string, canBeArray: boolean, @@ -1507,7 +1501,7 @@ export class InputValidator { // #region Update - @cacheWithState() + @cache() private makeUpdateSchema(model: string) { const baseSchema = z.strictObject({ where: this.makeWhereSchema(model, true), @@ -1522,7 +1516,7 @@ export class InputValidator { return schema; } - @cacheWithState() + @cache() private makeUpdateManySchema(model: string) { return this.mergePluginArgsSchema( z.strictObject({ @@ -1534,7 +1528,7 @@ export class InputValidator { ); } - @cacheWithState() + @cache() private makeUpdateManyAndReturnSchema(model: string) { // plugin extended args schema is merged in `makeUpdateManySchema` const baseSchema: ZodObject = this.makeUpdateManySchema(model); @@ -1546,7 +1540,7 @@ export class InputValidator { return schema; } - @cacheWithState() + @cache() private makeUpsertSchema(model: string) { const baseSchema = z.strictObject({ where: this.makeWhereSchema(model, true), @@ -1562,7 +1556,7 @@ export class InputValidator { return schema; } - @cacheWithState() + @cache() private makeUpdateDataSchema(model: string, withoutFields: string[] = [], withoutRelationFields = false) { // Normalize array argument for consistent cache keys withoutFields = [...withoutFields].sort(); @@ -1676,7 +1670,7 @@ export class InputValidator { // #region Delete - @cacheWithState() + @cache() private makeDeleteSchema(model: GetModels) { const baseSchema = z.strictObject({ where: this.makeWhereSchema(model, true), @@ -1690,7 +1684,7 @@ export class InputValidator { return schema; } - @cacheWithState() + @cache() private makeDeleteManySchema(model: GetModels) { return this.mergePluginArgsSchema( z.strictObject({ @@ -1705,7 +1699,7 @@ export class InputValidator { // #region Count - @cacheWithState() + @cache() makeCountSchema(model: GetModels) { return this.mergePluginArgsSchema( z.strictObject({ @@ -1741,7 +1735,7 @@ export class InputValidator { // #region Aggregate - @cacheWithState() + @cache() makeAggregateSchema(model: GetModels) { return this.mergePluginArgsSchema( z.strictObject({ @@ -1793,7 +1787,7 @@ export class InputValidator { ); } - @cacheWithState() + @cache() private makeGroupBySchema(model: GetModels) { const modelDef = requireModel(this.schema, model); const nonRelationFields = Object.keys(modelDef.fields).filter((field) => !modelDef.fields[field]?.relation); From 51fb17cd00e467ca9b6a553ca27e0a6a3fd8ee96 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 26 Jan 2026 22:52:14 +0800 Subject: [PATCH 5/9] update --- packages/orm/src/client/crud/validator/cache-decorator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/orm/src/client/crud/validator/cache-decorator.ts b/packages/orm/src/client/crud/validator/cache-decorator.ts index c7d81cc59..8f04d0f64 100644 --- a/packages/orm/src/client/crud/validator/cache-decorator.ts +++ b/packages/orm/src/client/crud/validator/cache-decorator.ts @@ -29,7 +29,7 @@ export function cache() { // Check cache const cached = this.getCache(cacheKey); - if (cached) { + if (cached === undefined) { return cached; } From ec22acdd60cf227fe29af65ae94ce656069ac73c Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 26 Jan 2026 22:52:42 +0800 Subject: [PATCH 6/9] update --- packages/orm/src/client/crud/validator/cache-decorator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/orm/src/client/crud/validator/cache-decorator.ts b/packages/orm/src/client/crud/validator/cache-decorator.ts index 8f04d0f64..d70d607ac 100644 --- a/packages/orm/src/client/crud/validator/cache-decorator.ts +++ b/packages/orm/src/client/crud/validator/cache-decorator.ts @@ -29,7 +29,7 @@ export function cache() { // Check cache const cached = this.getCache(cacheKey); - if (cached === undefined) { + if (cached !== undefined) { return cached; } From d14e1ba9fbcf3edcbd1b0c9dea2b59fc5ed289ff Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 26 Jan 2026 22:56:37 +0800 Subject: [PATCH 7/9] update --- .../src/client/crud/validator/cache-decorator.ts | 9 ++++++++- packages/orm/src/client/crud/validator/index.ts | 15 --------------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/packages/orm/src/client/crud/validator/cache-decorator.ts b/packages/orm/src/client/crud/validator/cache-decorator.ts index d70d607ac..8bd3e5a87 100644 --- a/packages/orm/src/client/crud/validator/cache-decorator.ts +++ b/packages/orm/src/client/crud/validator/cache-decorator.ts @@ -21,7 +21,14 @@ export function cache() { // Build cache key object const cacheKeyObj: Record = { $call: propertyKey, - ...args, + ...args.map((arg) => { + if (Array.isArray(arg)) { + // sort array arguments for consistent cache keys + return [...arg].sort(); + } else { + return arg; + } + }), }; // Generate stable string key diff --git a/packages/orm/src/client/crud/validator/index.ts b/packages/orm/src/client/crud/validator/index.ts index 6af8ad2d3..fca59017f 100644 --- a/packages/orm/src/client/crud/validator/index.ts +++ b/packages/orm/src/client/crud/validator/index.ts @@ -1231,9 +1231,6 @@ export class InputValidator { withoutFields: string[] = [], withoutRelationFields = false, ) { - // Normalize array argument for consistent cache keys - withoutFields = [...withoutFields].sort(); - const uncheckedVariantFields: Record = {}; const checkedVariantFields: Record = {}; const modelDef = requireModel(this.schema, model); @@ -1365,9 +1362,6 @@ export class InputValidator { @cache() private makeRelationManipulationSchema(fieldDef: FieldDef, withoutFields: string[], mode: 'create' | 'update') { - // Normalize array argument for consistent cache keys - withoutFields = [...withoutFields].sort(); - const fieldType = fieldDef.type; const array = !!fieldDef.array; const fields: Record = { @@ -1472,9 +1466,6 @@ export class InputValidator { @cache() private makeConnectOrCreateDataSchema(model: string, canBeArray: boolean, withoutFields: string[]) { - // Normalize array argument for consistent cache keys - withoutFields = [...withoutFields].sort(); - const whereSchema = this.makeWhereSchema(model, true); const createSchema = this.makeCreateDataSchema(model, false, withoutFields); return this.orArray( @@ -1488,9 +1479,6 @@ export class InputValidator { @cache() private makeCreateManyDataSchema(model: string, withoutFields: string[]) { - // Normalize array argument for consistent cache keys - withoutFields = [...withoutFields].sort(); - return z.strictObject({ data: this.makeCreateDataSchema(model, true, withoutFields, true), skipDuplicates: z.boolean().optional(), @@ -1558,9 +1546,6 @@ export class InputValidator { @cache() private makeUpdateDataSchema(model: string, withoutFields: string[] = [], withoutRelationFields = false) { - // Normalize array argument for consistent cache keys - withoutFields = [...withoutFields].sort(); - const uncheckedVariantFields: Record = {}; const checkedVariantFields: Record = {}; const modelDef = requireModel(this.schema, model); From 4812270c5fde6470d585c2a2f1b511c25cb8e879 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 26 Jan 2026 23:06:16 +0800 Subject: [PATCH 8/9] remove object-type args from cache key --- .../orm/src/client/crud/validator/index.ts | 61 +++++++++---------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/packages/orm/src/client/crud/validator/index.ts b/packages/orm/src/client/crud/validator/index.ts index fca59017f..8cad792e9 100644 --- a/packages/orm/src/client/crud/validator/index.ts +++ b/packages/orm/src/client/crud/validator/index.ts @@ -6,10 +6,8 @@ import { AnyNullClass, DbNullClass, JsonNullClass } from '../../../common-types' import { type AttributeApplication, type BuiltinType, - type EnumDef, type FieldDef, type GetModels, - type ModelDef, type ProcedureDef, type SchemaDef, } from '../../../schema'; @@ -589,7 +587,6 @@ export class InputValidator { if (Object.keys(enumDef.values).length > 0) { fieldSchema = this.makeEnumFilterSchema( fieldDef.type, - enumDef, !!fieldDef.optional, withAggregations, !!fieldDef.array, @@ -632,7 +629,6 @@ export class InputValidator { if (Object.keys(enumDef.values).length > 0) { fieldSchema = this.makeEnumFilterSchema( def.type, - enumDef, !!def.optional, false, false, @@ -724,7 +720,6 @@ export class InputValidator { if (enumDef) { fieldSchemas[fieldName] = this.makeEnumFilterSchema( fieldDef.type, - enumDef, !!fieldDef.optional, false, !!fieldDef.array, @@ -781,13 +776,9 @@ export class InputValidator { } @cache() - private makeEnumFilterSchema( - enumName: string, - enumDef: EnumDef, - optional: boolean, - withAggregations: boolean, - array: boolean, - ) { + private makeEnumFilterSchema(enumName: string, optional: boolean, withAggregations: boolean, array: boolean) { + const enumDef = getEnum(this.schema, enumName); + invariant(enumDef, `Enum "${enumName}" not found in schema`); const baseSchema = z.enum(Object.keys(enumDef.values) as [string, ...string[]]); if (array) { return this.internalMakeArrayFilterSchema(baseSchema); @@ -795,7 +786,7 @@ export class InputValidator { const components = this.makeCommonPrimitiveFilterComponents( baseSchema, optional, - () => z.lazy(() => this.makeEnumFilterSchema(enumName, enumDef, optional, withAggregations, array)), + () => z.lazy(() => this.makeEnumFilterSchema(enumName, optional, withAggregations, array)), ['equals', 'in', 'notIn', 'not'], withAggregations ? ['_count', '_min', '_max'] : undefined, ); @@ -1003,13 +994,13 @@ export class InputValidator { for (const field of Object.keys(modelDef.fields)) { const fieldDef = requireField(this.schema, model, field); if (fieldDef.relation) { - fields[field] = this.makeRelationSelectIncludeSchema(fieldDef).optional(); + fields[field] = this.makeRelationSelectIncludeSchema(model, field).optional(); } else { fields[field] = z.boolean().optional(); } } - const _countSchema = this.makeCountSelectionSchema(modelDef); + const _countSchema = this.makeCountSelectionSchema(model); if (!(_countSchema instanceof z.ZodNever)) { fields['_count'] = _countSchema; } @@ -1018,7 +1009,8 @@ export class InputValidator { } @cache() - private makeCountSelectionSchema(modelDef: ModelDef) { + private makeCountSelectionSchema(model: string) { + const modelDef = requireModel(this.schema, model); const toManyRelations = Object.values(modelDef.fields).filter((def) => def.relation && def.array); if (toManyRelations.length > 0) { return z @@ -1050,7 +1042,8 @@ export class InputValidator { } @cache() - private makeRelationSelectIncludeSchema(fieldDef: FieldDef) { + private makeRelationSelectIncludeSchema(model: string, field: string) { + const fieldDef = requireField(this.schema, model, field); let objSchema: z.ZodType = z.strictObject({ ...(fieldDef.array || fieldDef.optional ? { @@ -1116,11 +1109,11 @@ export class InputValidator { for (const field of Object.keys(modelDef.fields)) { const fieldDef = requireField(this.schema, model, field); if (fieldDef.relation) { - fields[field] = this.makeRelationSelectIncludeSchema(fieldDef).optional(); + fields[field] = this.makeRelationSelectIncludeSchema(model, field).optional(); } } - const _countSchema = this.makeCountSelectionSchema(modelDef); + const _countSchema = this.makeCountSelectionSchema(model); if (!(_countSchema instanceof z.ZodNever)) { fields['_count'] = _countSchema; } @@ -1267,7 +1260,7 @@ export class InputValidator { } let fieldSchema: ZodType = z.lazy(() => - this.makeRelationManipulationSchema(fieldDef, excludeFields, 'create'), + this.makeRelationManipulationSchema(model, field, excludeFields, 'create'), ); if (fieldDef.optional || fieldDef.array) { @@ -1361,7 +1354,13 @@ export class InputValidator { } @cache() - private makeRelationManipulationSchema(fieldDef: FieldDef, withoutFields: string[], mode: 'create' | 'update') { + private makeRelationManipulationSchema( + model: string, + field: string, + withoutFields: string[], + mode: 'create' | 'update', + ) { + const fieldDef = requireField(this.schema, model, field); const fieldType = fieldDef.type; const array = !!fieldDef.array; const fields: Record = { @@ -1573,7 +1572,7 @@ export class InputValidator { } } let fieldSchema: ZodType = z - .lazy(() => this.makeRelationManipulationSchema(fieldDef, excludeFields, 'update')) + .lazy(() => this.makeRelationManipulationSchema(model, field, excludeFields, 'update')) .optional(); // optional to-one relation can be null if (fieldDef.optional && !fieldDef.array) { @@ -1656,7 +1655,7 @@ export class InputValidator { // #region Delete @cache() - private makeDeleteSchema(model: GetModels) { + private makeDeleteSchema(model: string) { const baseSchema = z.strictObject({ where: this.makeWhereSchema(model, true), select: this.makeSelectSchema(model).optional().nullable(), @@ -1670,7 +1669,7 @@ export class InputValidator { } @cache() - private makeDeleteManySchema(model: GetModels) { + private makeDeleteManySchema(model: string) { return this.mergePluginArgsSchema( z.strictObject({ where: this.makeWhereSchema(model, false).optional(), @@ -1685,7 +1684,7 @@ export class InputValidator { // #region Count @cache() - makeCountSchema(model: GetModels) { + makeCountSchema(model: string) { return this.mergePluginArgsSchema( z.strictObject({ where: this.makeWhereSchema(model, false).optional(), @@ -1699,7 +1698,7 @@ export class InputValidator { } @cache() - private makeCountAggregateInputSchema(model: GetModels) { + private makeCountAggregateInputSchema(model: string) { const modelDef = requireModel(this.schema, model); return z.union([ z.literal(true), @@ -1721,7 +1720,7 @@ export class InputValidator { // #region Aggregate @cache() - makeAggregateSchema(model: GetModels) { + makeAggregateSchema(model: string) { return this.mergePluginArgsSchema( z.strictObject({ where: this.makeWhereSchema(model, false).optional(), @@ -1739,7 +1738,7 @@ export class InputValidator { } @cache() - makeSumAvgInputSchema(model: GetModels) { + makeSumAvgInputSchema(model: string) { const modelDef = requireModel(this.schema, model); return z.strictObject( Object.keys(modelDef.fields).reduce( @@ -1756,7 +1755,7 @@ export class InputValidator { } @cache() - makeMinMaxInputSchema(model: GetModels) { + makeMinMaxInputSchema(model: string) { const modelDef = requireModel(this.schema, model); return z.strictObject( Object.keys(modelDef.fields).reduce( @@ -1773,7 +1772,7 @@ export class InputValidator { } @cache() - private makeGroupBySchema(model: GetModels) { + private makeGroupBySchema(model: string) { const modelDef = requireModel(this.schema, model); const nonRelationFields = Object.keys(modelDef.fields).filter((field) => !modelDef.fields[field]?.relation); const bySchema = @@ -1859,7 +1858,7 @@ export class InputValidator { return true; } - private makeHavingSchema(model: GetModels) { + private makeHavingSchema(model: string) { // `makeWhereSchema` is cached return this.makeWhereSchema(model, false, true, true); } From c413572845841232745221e54bd1129bea2dcfb5 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 26 Jan 2026 23:10:04 +0800 Subject: [PATCH 9/9] update cache key --- packages/orm/src/client/crud/validator/cache-decorator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/orm/src/client/crud/validator/cache-decorator.ts b/packages/orm/src/client/crud/validator/cache-decorator.ts index 8bd3e5a87..bd8dd452f 100644 --- a/packages/orm/src/client/crud/validator/cache-decorator.ts +++ b/packages/orm/src/client/crud/validator/cache-decorator.ts @@ -21,7 +21,7 @@ export function cache() { // Build cache key object const cacheKeyObj: Record = { $call: propertyKey, - ...args.map((arg) => { + $args: args.map((arg) => { if (Array.isArray(arg)) { // sort array arguments for consistent cache keys return [...arg].sort();