diff --git a/packages/orm/src/client/zod/factory.ts b/packages/orm/src/client/zod/factory.ts index 8ad67de6b..8ef8446ab 100644 --- a/packages/orm/src/client/zod/factory.ts +++ b/packages/orm/src/client/zod/factory.ts @@ -87,6 +87,16 @@ export function createQuerySchemaFactory(clientOrSchema: any, options?: any) { return new ZodSchemaFactory(clientOrSchema, options); } +/** + * Options for creating Zod schemas. + */ +export type CreateSchemaOptions = { + /** + * Controls the depth of relation nesting in the generated schema. Default is unlimited. + */ + relationDepth?: number; +}; + /** * Factory class responsible for creating and caching Zod schemas for ORM input validation. */ @@ -120,6 +130,16 @@ export class ZodSchemaFactory< return this.options.validateInput !== false; } + private shouldIncludeRelations(options?: CreateSchemaOptions): boolean { + return options?.relationDepth === undefined || options.relationDepth > 0; + } + + private nextOptions(options?: CreateSchemaOptions): CreateSchemaOptions | undefined { + if (!options) return undefined; + if (options.relationDepth === undefined) return options; + return { ...options, relationDepth: options.relationDepth - 1 }; + } + // #region Cache Management // @ts-ignore @@ -148,42 +168,45 @@ export class ZodSchemaFactory< makeFindUniqueSchema>( model: Model, + options?: CreateSchemaOptions, ): ZodType> { - return this.makeFindSchema(model, 'findUnique') as ZodType< + return this.makeFindSchema(model, 'findUnique', options) as ZodType< FindUniqueArgs >; } makeFindFirstSchema>( model: Model, + options?: CreateSchemaOptions, ): ZodType | undefined> { - return this.makeFindSchema(model, 'findFirst') as ZodType< + return this.makeFindSchema(model, 'findFirst', options) as ZodType< FindFirstArgs | undefined >; } makeFindManySchema>( model: Model, + options?: CreateSchemaOptions, ): ZodType | undefined> { - return this.makeFindSchema(model, 'findMany') as ZodType< + return this.makeFindSchema(model, 'findMany', options) as ZodType< FindManyArgs | undefined >; } @cache() - private makeFindSchema(model: string, operation: CoreCrudOperations) { + private makeFindSchema(model: string, operation: CoreCrudOperations, options?: CreateSchemaOptions) { const fields: Record = {}; const unique = operation === 'findUnique'; const findOne = operation === 'findUnique' || operation === 'findFirst'; - const where = this.makeWhereSchema(model, unique); + const where = this.makeWhereSchema(model, unique, false, false, options); 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['select'] = this.makeSelectSchema(model, options).optional().nullable(); + fields['include'] = this.makeIncludeSchema(model, options).optional().nullable(); fields['omit'] = this.makeOmitSchema(model).optional().nullable(); if (!unique) { @@ -193,8 +216,11 @@ export class ZodSchemaFactory< } else { fields['take'] = this.makeTakeSchema().optional(); } - fields['orderBy'] = this.orArray(this.makeOrderBySchema(model, true, false), true).optional(); - fields['cursor'] = this.makeCursorSchema(model).optional(); + fields['orderBy'] = this.orArray( + this.makeOrderBySchema(model, true, false, options), + true, + ).optional(); + fields['cursor'] = this.makeCursorSchema(model, options).optional(); fields['distinct'] = this.makeDistinctSchema(model).optional(); } @@ -212,9 +238,10 @@ export class ZodSchemaFactory< @cache() makeExistsSchema>( model: Model, + options?: CreateSchemaOptions, ): ZodType | undefined> { const baseSchema = z.strictObject({ - where: this.makeWhereSchema(model, false).optional(), + where: this.makeWhereSchema(model, false, false, false, options).optional(), }); return this.mergePluginArgsSchema(baseSchema, 'exists').optional() as ZodType< ExistsArgs | undefined @@ -306,7 +333,13 @@ export class ZodSchemaFactory< } @cache() - makeWhereSchema(model: string, unique: boolean, withoutRelationFields = false, withAggregations = false): ZodType { + makeWhereSchema( + model: string, + unique: boolean, + withoutRelationFields = false, + withAggregations = false, + options?: CreateSchemaOptions, + ): ZodType { const modelDef = requireModel(this.schema, model); // unique field used in unique filters bypass filter slicing @@ -320,13 +353,15 @@ export class ZodSchemaFactory< .map((uf) => uf.name) : undefined; + const nextOpts = this.nextOptions(options); + 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) { + if (withoutRelationFields || !this.shouldIncludeRelations(options)) { continue; } @@ -336,7 +371,9 @@ export class ZodSchemaFactory< // Relation filters are not allowed for this field - use z.never() fieldSchema = z.never(); } else { - fieldSchema = z.lazy(() => this.makeWhereSchema(fieldDef.type, false).optional()); + fieldSchema = z.lazy(() => + this.makeWhereSchema(fieldDef.type, false, false, false, nextOpts).optional(), + ); // optional to-one relation allows null fieldSchema = this.nullableIf(fieldSchema, !fieldDef.array && !!fieldDef.optional); @@ -424,15 +461,15 @@ export class ZodSchemaFactory< // logical operators fields['AND'] = this.orArray( - z.lazy(() => this.makeWhereSchema(model, false, withoutRelationFields)), + z.lazy(() => this.makeWhereSchema(model, false, withoutRelationFields, false, options)), true, ).optional(); fields['OR'] = z - .lazy(() => this.makeWhereSchema(model, false, withoutRelationFields)) + .lazy(() => this.makeWhereSchema(model, false, withoutRelationFields, false, options)) .array() .optional(); fields['NOT'] = this.orArray( - z.lazy(() => this.makeWhereSchema(model, false, withoutRelationFields)), + z.lazy(() => this.makeWhereSchema(model, false, withoutRelationFields, false, options)), true, ).optional(); @@ -842,34 +879,40 @@ export class ZodSchemaFactory< } @cache() - private makeSelectSchema(model: string) { + private makeSelectSchema(model: string, options?: CreateSchemaOptions) { 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.shouldIncludeRelations(options)) { + continue; + } // Check if the target model is allowed by slicing configuration if (this.isModelAllowed(fieldDef.type)) { - fields[field] = this.makeRelationSelectIncludeSchema(model, field).optional(); + fields[field] = this.makeRelationSelectIncludeSchema(model, field, options).optional(); } } else { fields[field] = z.boolean().optional(); } } - const _countSchema = this.makeCountSelectionSchema(model); - if (!(_countSchema instanceof z.ZodNever)) { - fields['_count'] = _countSchema; + if (this.shouldIncludeRelations(options)) { + const _countSchema = this.makeCountSelectionSchema(model, options); + if (!(_countSchema instanceof z.ZodNever)) { + fields['_count'] = _countSchema; + } } return z.strictObject(fields); } @cache() - private makeCountSelectionSchema(model: string) { + private makeCountSelectionSchema(model: string, options?: CreateSchemaOptions) { const modelDef = requireModel(this.schema, model); const toManyRelations = Object.values(modelDef.fields).filter((def) => def.relation && def.array); if (toManyRelations.length > 0) { + const nextOpts = this.nextOptions(options); return z .union([ z.literal(true), @@ -882,7 +925,13 @@ export class ZodSchemaFactory< .union([ z.boolean(), z.strictObject({ - where: this.makeWhereSchema(fieldDef.type, false, false), + where: this.makeWhereSchema( + fieldDef.type, + false, + false, + false, + nextOpts, + ), }), ]) .optional(), @@ -899,21 +948,24 @@ export class ZodSchemaFactory< } @cache() - private makeRelationSelectIncludeSchema(model: string, field: string) { + private makeRelationSelectIncludeSchema(model: string, field: string, options?: CreateSchemaOptions) { const fieldDef = requireField(this.schema, model, field); + const nextOpts = this.nextOptions(options); let objSchema: 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(), + where: z + .lazy(() => this.makeWhereSchema(fieldDef.type, false, false, false, nextOpts)) + .optional(), } : {}), select: z - .lazy(() => this.makeSelectSchema(fieldDef.type)) + .lazy(() => this.makeSelectSchema(fieldDef.type, nextOpts)) .optional() .nullable(), include: z - .lazy(() => this.makeIncludeSchema(fieldDef.type)) + .lazy(() => this.makeIncludeSchema(fieldDef.type, nextOpts)) .optional() .nullable(), omit: z @@ -924,11 +976,11 @@ export class ZodSchemaFactory< ? { // to-many relations can be ordered, skipped, taken, and cursor-located orderBy: z - .lazy(() => this.orArray(this.makeOrderBySchema(fieldDef.type, true, false), true)) + .lazy(() => this.orArray(this.makeOrderBySchema(fieldDef.type, true, false, nextOpts), true)) .optional(), skip: this.makeSkipSchema().optional(), take: this.makeTakeSchema().optional(), - cursor: this.makeCursorSchema(fieldDef.type).optional(), + cursor: this.makeCursorSchema(fieldDef.type, nextOpts).optional(), distinct: this.makeDistinctSchema(fieldDef.type).optional(), } : {}), @@ -960,39 +1012,45 @@ export class ZodSchemaFactory< } @cache() - private makeIncludeSchema(model: string) { + private makeIncludeSchema(model: string, options?: CreateSchemaOptions) { 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.shouldIncludeRelations(options)) { + continue; + } // Check if the target model is allowed by slicing configuration if (this.isModelAllowed(fieldDef.type)) { - fields[field] = this.makeRelationSelectIncludeSchema(model, field).optional(); + fields[field] = this.makeRelationSelectIncludeSchema(model, field, options).optional(); } } } - const _countSchema = this.makeCountSelectionSchema(model); - if (!(_countSchema instanceof z.ZodNever)) { - fields['_count'] = _countSchema; + if (this.shouldIncludeRelations(options)) { + const _countSchema = this.makeCountSelectionSchema(model, options); + if (!(_countSchema instanceof z.ZodNever)) { + fields['_count'] = _countSchema; + } } return z.strictObject(fields); } @cache() - private makeOrderBySchema(model: string, withRelation: boolean, WithAggregation: boolean) { + private makeOrderBySchema(model: string, withRelation: boolean, WithAggregation: boolean, options?: CreateSchemaOptions) { const modelDef = requireModel(this.schema, model); const fields: Record = {}; const sort = z.union([z.literal('asc'), z.literal('desc')]); + const nextOpts = this.nextOptions(options); for (const field of Object.keys(modelDef.fields)) { const fieldDef = requireField(this.schema, model, field); if (fieldDef.relation) { // relations - if (withRelation) { + if (withRelation && this.shouldIncludeRelations(options)) { fields[field] = z.lazy(() => { - let relationOrderBy = this.makeOrderBySchema(fieldDef.type, withRelation, WithAggregation); + let relationOrderBy = this.makeOrderBySchema(fieldDef.type, withRelation, WithAggregation, nextOpts); if (fieldDef.array) { relationOrderBy = relationOrderBy.extend({ _count: sort, @@ -1023,7 +1081,7 @@ export class ZodSchemaFactory< if (WithAggregation) { const aggregationFields = ['_count', '_avg', '_sum', '_min', '_max']; for (const agg of aggregationFields) { - fields[agg] = z.lazy(() => this.makeOrderBySchema(model, true, false).optional()); + fields[agg] = z.lazy(() => this.makeOrderBySchema(model, true, false, options).optional()); } } @@ -1037,9 +1095,9 @@ export class ZodSchemaFactory< return nonRelationFields.length > 0 ? this.orArray(z.enum(nonRelationFields as any), true) : z.never(); } - private makeCursorSchema(model: string) { + private makeCursorSchema(model: string, options?: CreateSchemaOptions) { // `makeWhereSchema` is already cached - return this.makeWhereSchema(model, true, true).optional(); + return this.makeWhereSchema(model, true, true, false, options).optional(); } // #endregion @@ -1049,12 +1107,13 @@ export class ZodSchemaFactory< @cache() makeCreateSchema>( model: Model, + options?: CreateSchemaOptions, ): ZodType> { - const dataSchema = this.makeCreateDataSchema(model, false); + const dataSchema = this.makeCreateDataSchema(model, false, [], false, options); const baseSchema = z.strictObject({ data: dataSchema, - select: this.makeSelectSchema(model).optional().nullable(), - include: this.makeIncludeSchema(model).optional().nullable(), + select: this.makeSelectSchema(model, options).optional().nullable(), + include: this.makeIncludeSchema(model, options).optional().nullable(), omit: this.makeOmitSchema(model).optional().nullable(), }); let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'create'); @@ -1066,9 +1125,10 @@ export class ZodSchemaFactory< @cache() makeCreateManySchema>( model: Model, + options?: CreateSchemaOptions, ): ZodType> { return this.mergePluginArgsSchema( - this.makeCreateManyPayloadSchema(model, []), + this.makeCreateManyPayloadSchema(model, [], options), 'createMany', ) as unknown as ZodType>; } @@ -1076,10 +1136,11 @@ export class ZodSchemaFactory< @cache() makeCreateManyAndReturnSchema>( model: Model, + options?: CreateSchemaOptions, ): ZodType> { - const base = this.makeCreateManyPayloadSchema(model, []); + const base = this.makeCreateManyPayloadSchema(model, [], options); let result: ZodObject = base.extend({ - select: this.makeSelectSchema(model).optional().nullable(), + select: this.makeSelectSchema(model, options).optional().nullable(), omit: this.makeOmitSchema(model).optional().nullable(), }); result = this.mergePluginArgsSchema(result, 'createManyAndReturn'); @@ -1094,14 +1155,18 @@ export class ZodSchemaFactory< canBeArray: boolean, withoutFields: string[] = [], withoutRelationFields = false, + options?: CreateSchemaOptions, ) { + const skipRelations = withoutRelationFields || !this.shouldIncludeRelations(options); const uncheckedVariantFields: Record = {}; const checkedVariantFields: Record = {}; const modelDef = requireModel(this.schema, model); const hasRelation = - !withoutRelationFields && + !skipRelations && Object.entries(modelDef.fields).some(([f, def]) => !withoutFields.includes(f) && def.relation); + const nextOpts = this.nextOptions(options); + Object.keys(modelDef.fields).forEach((field) => { if (withoutFields.includes(field)) { return; @@ -1117,7 +1182,7 @@ export class ZodSchemaFactory< } if (fieldDef.relation) { - if (withoutRelationFields) { + if (skipRelations) { return; } // Check if the target model is allowed by slicing configuration @@ -1135,7 +1200,7 @@ export class ZodSchemaFactory< } let fieldSchema: ZodType = z.lazy(() => - this.makeRelationManipulationSchema(model, field, excludeFields, 'create'), + this.makeRelationManipulationSchema(model, field, excludeFields, 'create', nextOpts), ); if (fieldDef.optional || fieldDef.array) { @@ -1234,49 +1299,61 @@ export class ZodSchemaFactory< field: string, withoutFields: string[], mode: 'create' | 'update', + options?: CreateSchemaOptions, ) { const fieldDef = requireField(this.schema, model, field); 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(), + create: this.makeCreateDataSchema( + fieldDef.type, + !!fieldDef.array, + withoutFields, + false, + options, + ).optional(), + + connect: this.makeConnectDataSchema(fieldType, array, options).optional(), + + connectOrCreate: this.makeConnectOrCreateDataSchema( + fieldType, + array, + withoutFields, + options, + ).optional(), }; if (array) { - fields['createMany'] = this.makeCreateManyPayloadSchema(fieldType, withoutFields).optional(); + fields['createMany'] = this.makeCreateManyPayloadSchema(fieldType, withoutFields, options).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['disconnect'] = this.makeDisconnectDataSchema(fieldType, array, options).optional(); - fields['delete'] = this.makeDeleteRelationDataSchema(fieldType, array, true).optional(); + fields['delete'] = this.makeDeleteRelationDataSchema(fieldType, array, true, options).optional(); } fields['update'] = array ? this.orArray( z.strictObject({ - where: this.makeWhereSchema(fieldType, true), - data: this.makeUpdateDataSchema(fieldType, withoutFields), + where: this.makeWhereSchema(fieldType, true, false, false, options), + data: this.makeUpdateDataSchema(fieldType, withoutFields, false, options), }), true, ).optional() : z .union([ z.strictObject({ - where: this.makeWhereSchema(fieldType, false).optional(), - data: this.makeUpdateDataSchema(fieldType, withoutFields), + where: this.makeWhereSchema(fieldType, false, false, false, options).optional(), + data: this.makeUpdateDataSchema(fieldType, withoutFields, false, options), }), - this.makeUpdateDataSchema(fieldType, withoutFields), + this.makeUpdateDataSchema(fieldType, withoutFields, false, options), ]) .optional(); - let upsertWhere = this.makeWhereSchema(fieldType, true); + let upsertWhere = this.makeWhereSchema(fieldType, true, false, false, options); if (!fieldDef.array) { // to-one relation, can upsert without where clause upsertWhere = upsertWhere.optional(); @@ -1284,25 +1361,30 @@ export class ZodSchemaFactory< fields['upsert'] = this.orArray( z.strictObject({ where: upsertWhere, - create: this.makeCreateDataSchema(fieldType, false, withoutFields), - update: this.makeUpdateDataSchema(fieldType, withoutFields), + create: this.makeCreateDataSchema(fieldType, false, withoutFields, false, options), + update: this.makeUpdateDataSchema(fieldType, withoutFields, false, options), }), true, ).optional(); if (array) { // to-many relation specifics - fields['set'] = this.makeSetDataSchema(fieldType, true).optional(); + fields['set'] = this.makeSetDataSchema(fieldType, true, options).optional(); fields['updateMany'] = this.orArray( z.strictObject({ - where: this.makeWhereSchema(fieldType, false, true), - data: this.makeUpdateDataSchema(fieldType, withoutFields), + where: this.makeWhereSchema(fieldType, false, true, false, options), + data: this.makeUpdateDataSchema(fieldType, withoutFields, false, options), }), true, ).optional(); - fields['deleteMany'] = this.makeDeleteRelationDataSchema(fieldType, true, false).optional(); + fields['deleteMany'] = this.makeDeleteRelationDataSchema( + fieldType, + true, + false, + options, + ).optional(); } } @@ -1310,38 +1392,54 @@ export class ZodSchemaFactory< } @cache() - private makeSetDataSchema(model: string, canBeArray: boolean) { - return this.orArray(this.makeWhereSchema(model, true), canBeArray); + private makeSetDataSchema(model: string, canBeArray: boolean, options?: CreateSchemaOptions) { + return this.orArray( + this.makeWhereSchema(model, true, false, false, options), + canBeArray, + ); } @cache() - private makeConnectDataSchema(model: string, canBeArray: boolean) { - return this.orArray(this.makeWhereSchema(model, true), canBeArray); + private makeConnectDataSchema(model: string, canBeArray: boolean, options?: CreateSchemaOptions) { + return this.orArray( + this.makeWhereSchema(model, true, false, false, options), + canBeArray, + ); } @cache() - private makeDisconnectDataSchema(model: string, canBeArray: boolean) { + private makeDisconnectDataSchema(model: string, canBeArray: boolean, options?: CreateSchemaOptions) { if (canBeArray) { // to-many relation, must be unique filters - return this.orArray(this.makeWhereSchema(model, true), canBeArray); + return this.orArray(this.makeWhereSchema(model, true, false, false, options), 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 z.union([z.boolean(), this.makeWhereSchema(model, false, false, false, options)]); } } @cache() - private makeDeleteRelationDataSchema(model: string, toManyRelation: boolean, uniqueFilter: boolean) { + private makeDeleteRelationDataSchema( + model: string, + toManyRelation: boolean, + uniqueFilter: boolean, + options?: CreateSchemaOptions, + ) { return toManyRelation - ? this.orArray(this.makeWhereSchema(model, uniqueFilter), true) - : z.union([z.boolean(), this.makeWhereSchema(model, uniqueFilter)]); + ? this.orArray(this.makeWhereSchema(model, uniqueFilter, false, false, options), true) + : z.union([z.boolean(), this.makeWhereSchema(model, uniqueFilter, false, false, options)]); } @cache() - private makeConnectOrCreateDataSchema(model: string, canBeArray: boolean, withoutFields: string[]) { - const whereSchema = this.makeWhereSchema(model, true); - const createSchema = this.makeCreateDataSchema(model, false, withoutFields); + private makeConnectOrCreateDataSchema( + model: string, + canBeArray: boolean, + withoutFields: string[], + options?: CreateSchemaOptions, + ) { + const whereSchema = this.makeWhereSchema(model, true, false, false, options); + const createSchema = this.makeCreateDataSchema(model, false, withoutFields, false, options); return this.orArray( z.strictObject({ where: whereSchema, @@ -1352,9 +1450,9 @@ export class ZodSchemaFactory< } @cache() - private makeCreateManyPayloadSchema(model: string, withoutFields: string[]) { + private makeCreateManyPayloadSchema(model: string, withoutFields: string[], options?: CreateSchemaOptions) { return z.strictObject({ - data: this.makeCreateDataSchema(model, true, withoutFields, true), + data: this.makeCreateDataSchema(model, true, withoutFields, true, options), skipDuplicates: z.boolean().optional(), }); } @@ -1366,12 +1464,13 @@ export class ZodSchemaFactory< @cache() makeUpdateSchema>( model: Model, + options?: CreateSchemaOptions, ): ZodType> { 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(), + where: this.makeWhereSchema(model, true, false, false, options), + data: this.makeUpdateDataSchema(model, [], false, options), + select: this.makeSelectSchema(model, options).optional().nullable(), + include: this.makeIncludeSchema(model, options).optional().nullable(), omit: this.makeOmitSchema(model).optional().nullable(), }); let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'update'); @@ -1383,11 +1482,12 @@ export class ZodSchemaFactory< @cache() makeUpdateManySchema>( model: Model, + options?: CreateSchemaOptions, ): ZodType> { return this.mergePluginArgsSchema( z.strictObject({ - where: this.makeWhereSchema(model, false).optional(), - data: this.makeUpdateDataSchema(model, [], true), + where: this.makeWhereSchema(model, false, false, false, options).optional(), + data: this.makeUpdateDataSchema(model, [], true, options), limit: z.number().int().nonnegative().optional(), }), 'updateMany', @@ -1397,11 +1497,12 @@ export class ZodSchemaFactory< @cache() makeUpdateManyAndReturnSchema>( model: Model, + options?: CreateSchemaOptions, ): ZodType> { // plugin extended args schema is merged in `makeUpdateManySchema` - const baseSchema = this.makeUpdateManySchema(model) as unknown as ZodObject; + const baseSchema = this.makeUpdateManySchema(model, options) as unknown as ZodObject; let schema: ZodType = baseSchema.extend({ - select: this.makeSelectSchema(model).optional().nullable(), + select: this.makeSelectSchema(model, options).optional().nullable(), omit: this.makeOmitSchema(model).optional().nullable(), }); schema = this.refineForSelectOmitMutuallyExclusive(schema); @@ -1411,13 +1512,14 @@ export class ZodSchemaFactory< @cache() makeUpsertSchema>( model: Model, + options?: CreateSchemaOptions, ): ZodType> { 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(), + where: this.makeWhereSchema(model, true, false, false, options), + create: this.makeCreateDataSchema(model, false, [], false, options), + update: this.makeUpdateDataSchema(model, [], false, options), + select: this.makeSelectSchema(model, options).optional().nullable(), + include: this.makeIncludeSchema(model, options).optional().nullable(), omit: this.makeOmitSchema(model).optional().nullable(), }); let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'upsert'); @@ -1427,13 +1529,21 @@ export class ZodSchemaFactory< } @cache() - private makeUpdateDataSchema(model: string, withoutFields: string[] = [], withoutRelationFields = false) { + private makeUpdateDataSchema( + model: string, + withoutFields: string[] = [], + withoutRelationFields = false, + options?: CreateSchemaOptions, + ) { + const skipRelations = withoutRelationFields || !this.shouldIncludeRelations(options); 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), - ); + const hasRelation = + !skipRelations && + Object.entries(modelDef.fields).some(([key, value]) => value.relation && !withoutFields.includes(key)); + + const nextOpts = this.nextOptions(options); Object.keys(modelDef.fields).forEach((field) => { if (withoutFields.includes(field)) { @@ -1442,7 +1552,7 @@ export class ZodSchemaFactory< const fieldDef = requireField(this.schema, model, field); if (fieldDef.relation) { - if (withoutRelationFields) { + if (skipRelations) { return; } // Check if the target model is allowed by slicing configuration @@ -1459,7 +1569,7 @@ export class ZodSchemaFactory< } } let fieldSchema: ZodType = z - .lazy(() => this.makeRelationManipulationSchema(model, field, excludeFields, 'update')) + .lazy(() => this.makeRelationManipulationSchema(model, field, excludeFields, 'update', nextOpts)) .optional(); // optional to-one relation can be null if (fieldDef.optional && !fieldDef.array) { @@ -1545,11 +1655,12 @@ export class ZodSchemaFactory< @cache() makeDeleteSchema>( model: Model, + options?: CreateSchemaOptions, ): ZodType> { const baseSchema = z.strictObject({ - where: this.makeWhereSchema(model, true), - select: this.makeSelectSchema(model).optional().nullable(), - include: this.makeIncludeSchema(model).optional().nullable(), + where: this.makeWhereSchema(model, true, false, false, options), + select: this.makeSelectSchema(model, options).optional().nullable(), + include: this.makeIncludeSchema(model, options).optional().nullable(), omit: this.makeOmitSchema(model).optional().nullable(), }); let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'delete'); @@ -1561,10 +1672,11 @@ export class ZodSchemaFactory< @cache() makeDeleteManySchema>( model: Model, + options?: CreateSchemaOptions, ): ZodType | undefined> { return this.mergePluginArgsSchema( z.strictObject({ - where: this.makeWhereSchema(model, false).optional(), + where: this.makeWhereSchema(model, false, false, false, options).optional(), limit: z.number().int().nonnegative().optional(), }), 'deleteMany', @@ -1578,13 +1690,14 @@ export class ZodSchemaFactory< @cache() makeCountSchema>( model: Model, + options?: CreateSchemaOptions, ): ZodType | undefined> { return this.mergePluginArgsSchema( z.strictObject({ - where: this.makeWhereSchema(model, false).optional(), + where: this.makeWhereSchema(model, false, false, false, options).optional(), skip: this.makeSkipSchema().optional(), take: this.makeTakeSchema().optional(), - orderBy: this.orArray(this.makeOrderBySchema(model, true, false), true).optional(), + orderBy: this.orArray(this.makeOrderBySchema(model, true, false, options), true).optional(), select: this.makeCountAggregateInputSchema(model).optional(), }), 'count', @@ -1616,13 +1729,14 @@ export class ZodSchemaFactory< @cache() makeAggregateSchema>( model: Model, + options?: CreateSchemaOptions, ): ZodType | undefined> { return this.mergePluginArgsSchema( z.strictObject({ - where: this.makeWhereSchema(model, false).optional(), + where: this.makeWhereSchema(model, false, false, false, options).optional(), skip: this.makeSkipSchema().optional(), take: this.makeTakeSchema().optional(), - orderBy: this.orArray(this.makeOrderBySchema(model, true, false), true).optional(), + orderBy: this.orArray(this.makeOrderBySchema(model, true, false, options), true).optional(), _count: this.makeCountAggregateInputSchema(model).optional(), _avg: this.makeSumAvgInputSchema(model).optional(), _sum: this.makeSumAvgInputSchema(model).optional(), @@ -1674,6 +1788,7 @@ export class ZodSchemaFactory< @cache() makeGroupBySchema>( model: Model, + options?: CreateSchemaOptions, ): ZodType> { const modelDef = requireModel(this.schema, model); const nonRelationFields = Object.keys(modelDef.fields).filter((field) => !modelDef.fields[field]?.relation); @@ -1683,10 +1798,10 @@ export class ZodSchemaFactory< : z.never(); const baseSchema = z.strictObject({ - where: this.makeWhereSchema(model, false).optional(), - orderBy: this.orArray(this.makeOrderBySchema(model, false, true), true).optional(), + where: this.makeWhereSchema(model, false, false, false, options).optional(), + orderBy: this.orArray(this.makeOrderBySchema(model, false, true, options), true).optional(), by: bySchema, - having: this.makeHavingSchema(model).optional(), + having: this.makeHavingSchema(model, options).optional(), skip: this.makeSkipSchema().optional(), take: this.makeTakeSchema().optional(), _count: this.makeCountAggregateInputSchema(model).optional(), @@ -1761,9 +1876,9 @@ export class ZodSchemaFactory< return true; } - private makeHavingSchema(model: string) { + private makeHavingSchema(model: string, options?: CreateSchemaOptions) { // `makeWhereSchema` is cached - return this.makeWhereSchema(model, false, true, true); + return this.makeWhereSchema(model, false, true, true, options); } // #endregion @@ -1771,7 +1886,10 @@ export class ZodSchemaFactory< // #region Procedures @cache() - makeProcedureParamSchema(param: { type: string; array?: boolean; optional?: boolean }): ZodType { + makeProcedureParamSchema( + param: { type: string; array?: boolean; optional?: boolean }, + _options?: CreateSchemaOptions, + ): ZodType { let schema: ZodType; if (isTypeDef(this.schema, param.type)) { diff --git a/packages/zod/src/index.ts b/packages/zod/src/index.ts index 667815bbe..55f241ea1 100644 --- a/packages/zod/src/index.ts +++ b/packages/zod/src/index.ts @@ -1,3 +1,2 @@ -export { createSchemaFactory as createModelSchemaFactory } from './factory'; -export type * from './types'; +export { createSchemaFactory } from './factory'; export * as ZodUtils from './utils'; diff --git a/packages/zod/test/factory.test.ts b/packages/zod/test/factory.test.ts index 8ebf2aafc..3cd16af15 100644 --- a/packages/zod/test/factory.test.ts +++ b/packages/zod/test/factory.test.ts @@ -1,10 +1,10 @@ import Decimal from 'decimal.js'; import { describe, expect, expectTypeOf, it } from 'vitest'; -import { createModelSchemaFactory } from '../src/index'; +import { createSchemaFactory } from '../src/index'; import { schema } from './schema/schema'; import z from 'zod'; -const factory = createModelSchemaFactory(schema); +const factory = createSchemaFactory(schema); // A fully valid User object (without relations) const validUser = { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f5a1e0575..a950c4bb5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -949,7 +949,7 @@ importers: version: 2.0.8 nuxt: specifier: 'catalog:' - version: 4.2.2(6ed21839bfb37f6518a04a21db8dd1a5) + version: 4.2.2(8d53318c89c53efd506d8dd328c0edc0) tailwindcss: specifier: ^4.1.18 version: 4.1.18 @@ -966,6 +966,9 @@ importers: '@zenstackhq/cli': specifier: workspace:* version: link:../../packages/cli + vue-tsc: + specifier: ^3.2.5 + version: 3.2.5(typescript@5.9.3) samples/orm: dependencies: @@ -4027,9 +4030,18 @@ packages: '@volar/language-core@2.4.27': resolution: {integrity: sha512-DjmjBWZ4tJKxfNC1F6HyYERNHPYS7L7OPFyCrestykNdUZMFYzI9WTyvwPcaNaHlrEUwESHYsfEw3isInncZxQ==} + '@volar/language-core@2.4.28': + resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} + '@volar/source-map@2.4.27': resolution: {integrity: sha512-ynlcBReMgOZj2i6po+qVswtDUeeBRCTgDurjMGShbm8WYZgJ0PA4RmtebBJ0BCYol1qPv3GQF6jK7C9qoVc7lg==} + '@volar/source-map@2.4.28': + resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==} + + '@volar/typescript@2.4.28': + resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} + '@vue-macros/common@3.1.1': resolution: {integrity: sha512-afW2DMjgCBVs33mWRlz7YsGHzoEEupnl0DK5ZTKsgziAlLh5syc5m+GM7eqeYrgiQpwMaVxa1fk73caCvPxyAw==} engines: {node: '>=20.19.0'} @@ -4096,6 +4108,9 @@ packages: '@vue/language-core@3.2.1': resolution: {integrity: sha512-g6oSenpnGMtpxHGAwKuu7HJJkNZpemK/zg3vZzZbJ6cnnXq1ssxuNrXSsAHYM3NvH8p4IkTw+NLmuxyeYz4r8A==} + '@vue/language-core@3.2.5': + resolution: {integrity: sha512-d3OIxN/+KRedeM5wQ6H6NIpwS3P5gC9nmyaHgBk+rO6dIsjY+tOh4UlPpiZbAh3YtLdCGEX4M16RmsBqPmJV+g==} + '@vue/reactivity@3.5.22': resolution: {integrity: sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==} @@ -8688,6 +8703,12 @@ packages: peerDependencies: vue: ^3.5.0 + vue-tsc@3.2.5: + resolution: {integrity: sha512-/htfTCMluQ+P2FISGAooul8kO4JMheOTCbCy4M6dYnYYjqLe3BExZudAua6MSIKSFYQtFOYAll7XobYwcpokGA==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + vue@3.5.22: resolution: {integrity: sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==} peerDependencies: @@ -10241,6 +10262,70 @@ snapshots: - uploadthing - xml2js + '@nuxt/nitro-server@4.2.2(better-sqlite3@12.5.0)(db0@0.3.4(better-sqlite3@12.5.0)(drizzle-orm@0.41.0(@prisma/client@5.22.0(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3)))(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.5.0)(bun-types@1.3.3)(kysely@0.28.8)(mysql2@3.16.1)(pg@8.16.3)(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3))(sql.js@1.13.0))(mysql2@3.16.1))(drizzle-orm@0.41.0(@prisma/client@5.22.0(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3)))(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.5.0)(bun-types@1.3.3)(kysely@0.28.8)(mysql2@3.16.1)(pg@8.16.3)(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3))(sql.js@1.13.0))(ioredis@5.8.2)(magicast@0.5.1)(mysql2@3.16.1)(nuxt@4.2.2(8d53318c89c53efd506d8dd328c0edc0))(typescript@5.9.3)': + dependencies: + '@nuxt/devalue': 2.0.2 + '@nuxt/kit': 4.2.2(magicast@0.5.1) + '@unhead/vue': 2.0.19(vue@3.5.26(typescript@5.9.3)) + '@vue/shared': 3.5.26 + consola: 3.4.2 + defu: 6.1.4 + destr: 2.0.5 + devalue: 5.6.1 + errx: 0.1.0 + escape-string-regexp: 5.0.0 + exsolve: 1.0.8 + h3: 1.15.4 + impound: 1.0.0 + klona: 2.0.6 + mocked-exports: 0.1.1 + nitropack: 2.12.9(better-sqlite3@12.5.0)(drizzle-orm@0.41.0(@prisma/client@5.22.0(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3)))(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.5.0)(bun-types@1.3.3)(kysely@0.28.8)(mysql2@3.16.1)(pg@8.16.3)(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3))(sql.js@1.13.0))(mysql2@3.16.1) + nuxt: 4.2.2(8d53318c89c53efd506d8dd328c0edc0) + pathe: 2.0.3 + pkg-types: 2.3.0 + radix3: 1.1.2 + std-env: 3.10.0 + ufo: 1.6.1 + unctx: 2.4.1 + unstorage: 1.17.3(db0@0.3.4(better-sqlite3@12.5.0)(drizzle-orm@0.41.0(@prisma/client@5.22.0(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3)))(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.5.0)(bun-types@1.3.3)(kysely@0.28.8)(mysql2@3.16.1)(pg@8.16.3)(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3))(sql.js@1.13.0))(mysql2@3.16.1))(ioredis@5.8.2) + vue: 3.5.26(typescript@5.9.3) + vue-bundle-renderer: 2.2.0 + vue-devtools-stub: 0.1.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@electric-sql/pglite' + - '@libsql/client' + - '@netlify/blobs' + - '@planetscale/database' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bare-abort-controller + - better-sqlite3 + - db0 + - drizzle-orm + - encoding + - idb-keyval + - ioredis + - magicast + - mysql2 + - react-native-b4a + - rolldown + - sqlite3 + - supports-color + - typescript + - uploadthing + - xml2js + '@nuxt/schema@4.2.2': dependencies: '@vue/shared': 3.5.26 @@ -10297,7 +10382,66 @@ snapshots: unenv: 2.0.0-rc.24 vite: 7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2) vite-node: 5.2.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2) - vite-plugin-checker: 0.12.0(eslint@9.29.0(jiti@2.6.1))(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)) + vite-plugin-checker: 0.12.0(eslint@9.29.0(jiti@2.6.1))(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(vue-tsc@3.2.5(typescript@5.9.3)) + vue: 3.5.26(typescript@5.9.3) + vue-bundle-renderer: 2.2.0 + transitivePeerDependencies: + - '@biomejs/biome' + - '@types/node' + - eslint + - less + - lightningcss + - magicast + - meow + - optionator + - oxlint + - rollup + - sass + - sass-embedded + - stylelint + - stylus + - sugarss + - supports-color + - terser + - tsx + - typescript + - vls + - vti + - vue-tsc + - yaml + + '@nuxt/vite-builder@4.2.2(@types/node@20.19.24)(eslint@9.29.0(jiti@2.6.1))(lightningcss@1.30.2)(magicast@0.5.1)(nuxt@4.2.2(8d53318c89c53efd506d8dd328c0edc0))(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vue-tsc@3.2.5(typescript@5.9.3))(vue@3.5.26(typescript@5.9.3))(yaml@2.8.2)': + dependencies: + '@nuxt/kit': 4.2.2(magicast@0.5.1) + '@rollup/plugin-replace': 6.0.3(rollup@4.52.5) + '@vitejs/plugin-vue': 6.0.3(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3)) + '@vitejs/plugin-vue-jsx': 5.1.3(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3)) + autoprefixer: 10.4.23(postcss@8.5.6) + consola: 3.4.2 + cssnano: 7.1.2(postcss@8.5.6) + defu: 6.1.4 + esbuild: 0.27.2 + escape-string-regexp: 5.0.0 + exsolve: 1.0.8 + get-port-please: 3.2.0 + h3: 1.15.4 + jiti: 2.6.1 + knitwork: 1.3.0 + magic-string: 0.30.21 + mlly: 1.8.0 + mocked-exports: 0.1.1 + nuxt: 4.2.2(8d53318c89c53efd506d8dd328c0edc0) + pathe: 2.0.3 + pkg-types: 2.3.0 + postcss: 8.5.6 + rollup-plugin-visualizer: 6.0.5(rollup@4.52.5) + seroval: 1.4.1 + std-env: 3.10.0 + ufo: 1.6.1 + unenv: 2.0.0-rc.24 + vite: 7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2) + vite-node: 5.2.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2) + vite-plugin-checker: 0.12.0(eslint@9.29.0(jiti@2.6.1))(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(vue-tsc@3.2.5(typescript@5.9.3)) vue: 3.5.26(typescript@5.9.3) vue-bundle-renderer: 2.2.0 transitivePeerDependencies: @@ -11706,8 +11850,20 @@ snapshots: dependencies: '@volar/source-map': 2.4.27 + '@volar/language-core@2.4.28': + dependencies: + '@volar/source-map': 2.4.28 + '@volar/source-map@2.4.27': {} + '@volar/source-map@2.4.28': {} + + '@volar/typescript@2.4.28': + dependencies: + '@volar/language-core': 2.4.28 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + '@vue-macros/common@3.1.1(vue@3.5.26(typescript@5.9.3))': dependencies: '@vue/compiler-sfc': 3.5.26 @@ -11845,6 +12001,16 @@ snapshots: path-browserify: 1.0.1 picomatch: 4.0.3 + '@vue/language-core@3.2.5': + dependencies: + '@volar/language-core': 2.4.28 + '@vue/compiler-dom': 3.5.26 + '@vue/shared': 3.5.26 + alien-signals: 3.0.3 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + picomatch: 4.0.3 + '@vue/reactivity@3.5.22': dependencies: '@vue/shared': 3.5.22 @@ -13073,7 +13239,7 @@ snapshots: eslint: 9.29.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.29.0(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.29.0(jiti@2.6.1)) eslint-plugin-react-hooks: 7.0.1(eslint@9.29.0(jiti@2.6.1)) @@ -13106,7 +13272,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -13121,7 +13287,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -14889,6 +15055,129 @@ snapshots: - xml2js - yaml + nuxt@4.2.2(8d53318c89c53efd506d8dd328c0edc0): + dependencies: + '@dxup/nuxt': 0.2.2(magicast@0.5.1) + '@nuxt/cli': 3.31.3(cac@6.7.14)(magicast@0.5.1) + '@nuxt/devtools': 3.1.1(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3)) + '@nuxt/kit': 4.2.2(magicast@0.5.1) + '@nuxt/nitro-server': 4.2.2(better-sqlite3@12.5.0)(db0@0.3.4(better-sqlite3@12.5.0)(drizzle-orm@0.41.0(@prisma/client@5.22.0(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3)))(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.5.0)(bun-types@1.3.3)(kysely@0.28.8)(mysql2@3.16.1)(pg@8.16.3)(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3))(sql.js@1.13.0))(mysql2@3.16.1))(drizzle-orm@0.41.0(@prisma/client@5.22.0(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3)))(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.5.0)(bun-types@1.3.3)(kysely@0.28.8)(mysql2@3.16.1)(pg@8.16.3)(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3))(sql.js@1.13.0))(ioredis@5.8.2)(magicast@0.5.1)(mysql2@3.16.1)(nuxt@4.2.2(8d53318c89c53efd506d8dd328c0edc0))(typescript@5.9.3) + '@nuxt/schema': 4.2.2 + '@nuxt/telemetry': 2.6.6(magicast@0.5.1) + '@nuxt/vite-builder': 4.2.2(@types/node@20.19.24)(eslint@9.29.0(jiti@2.6.1))(lightningcss@1.30.2)(magicast@0.5.1)(nuxt@4.2.2(8d53318c89c53efd506d8dd328c0edc0))(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vue-tsc@3.2.5(typescript@5.9.3))(vue@3.5.26(typescript@5.9.3))(yaml@2.8.2) + '@unhead/vue': 2.0.19(vue@3.5.26(typescript@5.9.3)) + '@vue/shared': 3.5.26 + c12: 3.3.3(magicast@0.5.1) + chokidar: 5.0.0 + compatx: 0.2.0 + consola: 3.4.2 + cookie-es: 2.0.0 + defu: 6.1.4 + destr: 2.0.5 + devalue: 5.6.1 + errx: 0.1.0 + escape-string-regexp: 5.0.0 + exsolve: 1.0.8 + h3: 1.15.4 + hookable: 5.5.3 + ignore: 7.0.5 + impound: 1.0.0 + jiti: 2.6.1 + klona: 2.0.6 + knitwork: 1.3.0 + magic-string: 0.30.21 + mlly: 1.8.0 + nanotar: 0.2.0 + nypm: 0.6.2 + ofetch: 1.5.1 + ohash: 2.0.11 + on-change: 6.0.1 + oxc-minify: 0.102.0 + oxc-parser: 0.102.0 + oxc-transform: 0.102.0 + oxc-walker: 0.6.0(oxc-parser@0.102.0) + pathe: 2.0.3 + perfect-debounce: 2.0.0 + pkg-types: 2.3.0 + radix3: 1.1.2 + scule: 1.3.0 + semver: 7.7.3 + std-env: 3.10.0 + tinyglobby: 0.2.15 + ufo: 1.6.1 + ultrahtml: 1.6.0 + uncrypto: 0.1.3 + unctx: 2.4.1 + unimport: 5.5.0 + unplugin: 2.3.11 + unplugin-vue-router: 0.19.1(@vue/compiler-sfc@3.5.26)(vue-router@4.6.4(vue@3.5.22(typescript@5.9.3)))(vue@3.5.26(typescript@5.9.3)) + untyped: 2.0.0 + vue: 3.5.26(typescript@5.9.3) + vue-router: 4.6.4(vue@3.5.26(typescript@5.9.3)) + optionalDependencies: + '@parcel/watcher': 2.5.1 + '@types/node': 20.19.24 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@biomejs/biome' + - '@capacitor/preferences' + - '@deno/kv' + - '@electric-sql/pglite' + - '@libsql/client' + - '@netlify/blobs' + - '@planetscale/database' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - '@vitejs/devtools' + - '@vue/compiler-sfc' + - aws4fetch + - bare-abort-controller + - better-sqlite3 + - bufferutil + - cac + - commander + - db0 + - drizzle-orm + - encoding + - eslint + - idb-keyval + - ioredis + - less + - lightningcss + - magicast + - meow + - mysql2 + - optionator + - oxlint + - react-native-b4a + - rolldown + - rollup + - sass + - sass-embedded + - sqlite3 + - stylelint + - stylus + - sugarss + - supports-color + - terser + - tsx + - typescript + - uploadthing + - utf-8-validate + - vite + - vls + - vti + - vue-tsc + - xml2js + - yaml + nypm@0.6.2: dependencies: citty: 0.1.6 @@ -16671,6 +16960,31 @@ snapshots: pathe: 2.0.3 picomatch: 4.0.3 + unplugin-vue-router@0.19.1(@vue/compiler-sfc@3.5.26)(vue-router@4.6.4(vue@3.5.22(typescript@5.9.3)))(vue@3.5.26(typescript@5.9.3)): + dependencies: + '@babel/generator': 7.28.5 + '@vue-macros/common': 3.1.1(vue@3.5.26(typescript@5.9.3)) + '@vue/compiler-sfc': 3.5.26 + '@vue/language-core': 3.2.1 + ast-walker-scope: 0.8.3 + chokidar: 5.0.0 + json5: 2.2.3 + local-pkg: 1.1.2 + magic-string: 0.30.21 + mlly: 1.8.0 + muggle-string: 0.4.1 + pathe: 2.0.3 + picomatch: 4.0.3 + scule: 1.3.0 + tinyglobby: 0.2.15 + unplugin: 2.3.11 + unplugin-utils: 0.3.1 + yaml: 2.8.2 + optionalDependencies: + vue-router: 4.6.4(vue@3.5.22(typescript@5.9.3)) + transitivePeerDependencies: + - vue + unplugin-vue-router@0.19.1(@vue/compiler-sfc@3.5.26)(vue-router@4.6.4(vue@3.5.26(typescript@5.9.3)))(vue@3.5.26(typescript@5.9.3)): dependencies: '@babel/generator': 7.28.5 @@ -16825,7 +17139,7 @@ snapshots: - tsx - yaml - vite-plugin-checker@0.12.0(eslint@9.29.0(jiti@2.6.1))(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)): + vite-plugin-checker@0.12.0(eslint@9.29.0(jiti@2.6.1))(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(vue-tsc@3.2.5(typescript@5.9.3)): dependencies: '@babel/code-frame': 7.27.1 chokidar: 4.0.3 @@ -16840,6 +17154,7 @@ snapshots: eslint: 9.29.0(jiti@2.6.1) optionator: 0.9.4 typescript: 5.9.3 + vue-tsc: 3.2.5(typescript@5.9.3) vite-plugin-inspect@11.3.3(@nuxt/kit@4.2.2(magicast@0.5.1))(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)): dependencies: @@ -17032,6 +17347,12 @@ snapshots: '@vue/devtools-api': 6.6.4 vue: 3.5.26(typescript@5.9.3) + vue-tsc@3.2.5(typescript@5.9.3): + dependencies: + '@volar/typescript': 2.4.28 + '@vue/language-core': 3.2.5 + typescript: 5.9.3 + vue@3.5.22(typescript@5.9.3): dependencies: '@vue/compiler-dom': 3.5.22 diff --git a/samples/nuxt/package.json b/samples/nuxt/package.json index 4145dc0aa..2660310cd 100644 --- a/samples/nuxt/package.json +++ b/samples/nuxt/package.json @@ -6,15 +6,15 @@ "generate": "zen generate --lite", "db:init": "pnpm generate && zen db push && npx tsx zenstack/seed.ts", "dev": "nuxt dev --port 3302", - "build": "pnpm generate && nuxt build", + "build": "pnpm generate && nuxt typecheck", "preview": "nuxt preview", "postinstall": "nuxt prepare" }, "dependencies": { "@tailwindcss/vite": "^4.1.18", "@tanstack/vue-query": "catalog:", - "@zenstackhq/schema": "workspace:*", "@zenstackhq/orm": "workspace:*", + "@zenstackhq/schema": "workspace:*", "@zenstackhq/server": "workspace:*", "@zenstackhq/tanstack-query": "workspace:*", "better-sqlite3": "catalog:", @@ -26,6 +26,7 @@ }, "devDependencies": { "@types/better-sqlite3": "catalog:", - "@zenstackhq/cli": "workspace:*" + "@zenstackhq/cli": "workspace:*", + "vue-tsc": "^3.2.5" } } diff --git a/tests/e2e/orm/client-api/zod.test.ts b/tests/e2e/orm/client-api/zod.test.ts index 377b68743..586782f80 100644 --- a/tests/e2e/orm/client-api/zod.test.ts +++ b/tests/e2e/orm/client-api/zod.test.ts @@ -1060,4 +1060,319 @@ describe('Zod schema factory test', () => { }); // #endregion + + // #region CreateSchemaOptions depth + + describe('CreateSchemaOptions depth', () => { + // --- Find schemas --- + + describe('makeFindManySchema with depth', () => { + it('relationDepth: 0 rejects relation where filters', () => { + const s = client.$zod.makeFindManySchema('User', { relationDepth: 0 }); + // scalar where still works + expect(s.safeParse({ where: { email: 'u@test.com' } }).success).toBe(true); + // relation filter rejected + expect(s.safeParse({ where: { posts: { some: { published: true } } } }).success).toBe(false); + }); + + it('relationDepth: 0 rejects relation in select', () => { + const s = client.$zod.makeFindManySchema('User', { relationDepth: 0 }); + // scalar select works + expect(s.safeParse({ select: { id: true, email: true } }).success).toBe(true); + // relation select rejected + expect(s.safeParse({ select: { posts: true } }).success).toBe(false); + }); + + it('relationDepth: 0 rejects relation in include', () => { + const s = client.$zod.makeFindManySchema('User', { relationDepth: 0 }); + // relation include rejected + expect(s.safeParse({ include: { posts: true } }).success).toBe(false); + }); + + it('relationDepth: 0 rejects relation in orderBy', () => { + const s = client.$zod.makeFindManySchema('User', { relationDepth: 0 }); + // scalar orderBy works + expect(s.safeParse({ orderBy: { email: 'asc' } }).success).toBe(true); + // relation orderBy rejected + expect(s.safeParse({ orderBy: { posts: { _count: 'desc' } } }).success).toBe(false); + }); + + it('relationDepth: 1 allows one level of relation nesting', () => { + const s = client.$zod.makeFindManySchema('User', { relationDepth: 1 }); + // relation where allowed + expect(s.safeParse({ where: { posts: { some: { published: true } } } }).success).toBe(true); + // relation select allowed + expect(s.safeParse({ select: { id: true, posts: true } }).success).toBe(true); + // relation include allowed + expect(s.safeParse({ include: { posts: true } }).success).toBe(true); + }); + + it('relationDepth: 1 rejects two levels of relation nesting in where', () => { + const s = client.$zod.makeFindManySchema('User', { relationDepth: 1 }); + // User -> posts -> comments (2 levels) rejected + expect( + s.safeParse({ + where: { posts: { some: { comments: { some: { content: 'hi' } } } } }, + }).success, + ).toBe(false); + }); + + it('relationDepth: 1 rejects two levels of relation nesting in select', () => { + const s = client.$zod.makeFindManySchema('User', { relationDepth: 1 }); + // nested relation in select rejected - can select posts but posts cannot select comments + expect( + s.safeParse({ + select: { posts: { select: { comments: true } } }, + }).success, + ).toBe(false); + }); + + it('relationDepth: 2 allows two levels of relation nesting', () => { + const s = client.$zod.makeFindManySchema('User', { relationDepth: 2 }); + // User -> posts -> comments (2 levels) allowed + expect( + s.safeParse({ + where: { posts: { some: { comments: { some: { content: 'hi' } } } } }, + }).success, + ).toBe(true); + // nested relation in select allowed + expect( + s.safeParse({ + select: { posts: { select: { comments: true } } }, + }).success, + ).toBe(true); + }); + + it('no options behaves as unlimited depth', () => { + const s = client.$zod.makeFindManySchema('User'); + // deep nesting works + expect( + s.safeParse({ + where: { posts: { some: { comments: { some: { content: 'hi' } } } } }, + }).success, + ).toBe(true); + expect( + s.safeParse({ + select: { posts: { select: { comments: true } } }, + }).success, + ).toBe(true); + }); + }); + + describe('makeFindUniqueSchema with depth', () => { + it('relationDepth: 0 limits to scalar where', () => { + const s = client.$zod.makeFindUniqueSchema('User', { relationDepth: 0 }); + expect(s.safeParse({ where: { id: 'abc' } }).success).toBe(true); + expect(s.safeParse({ where: { id: 'abc' }, select: { posts: true } }).success).toBe(false); + }); + }); + + describe('makeFindFirstSchema with depth', () => { + it('relationDepth: 0 limits to scalar fields', () => { + const s = client.$zod.makeFindFirstSchema('User', { relationDepth: 0 }); + expect(s.safeParse({ where: { email: 'u@test.com' } }).success).toBe(true); + expect(s.safeParse({ where: { posts: { some: { published: true } } } }).success).toBe(false); + }); + }); + + // --- Exists schema --- + + describe('makeExistsSchema with depth', () => { + it('relationDepth: 0 rejects relation filters', () => { + const s = client.$zod.makeExistsSchema('User', { relationDepth: 0 }); + expect(s.safeParse({ where: { email: 'u@test.com' } }).success).toBe(true); + expect(s.safeParse({ where: { posts: { some: { published: true } } } }).success).toBe(false); + }); + }); + + // --- Create schemas --- + + describe('makeCreateSchema with depth', () => { + it('relationDepth: 0 rejects nested relation create', () => { + const s = client.$zod.makeCreateSchema('User', { relationDepth: 0 }); + // scalar-only create works + expect(s.safeParse({ data: { email: 'u@test.com' } }).success).toBe(true); + // nested relation create rejected + expect( + s.safeParse({ + data: { + email: 'u@test.com', + posts: { create: { title: 'Post' } }, + }, + }).success, + ).toBe(false); + }); + + it('relationDepth: 1 allows one level of nested create', () => { + const s = client.$zod.makeCreateSchema('User', { relationDepth: 1 }); + // one level nested create allowed + expect( + s.safeParse({ + data: { + email: 'u@test.com', + posts: { create: { title: 'Post' } }, + }, + }).success, + ).toBe(true); + }); + + it('relationDepth: 1 rejects two levels of nested create', () => { + const s = client.$zod.makeCreateSchema('User', { relationDepth: 1 }); + // two levels nested create rejected (posts -> comments) + expect( + s.safeParse({ + data: { + email: 'u@test.com', + posts: { + create: { + title: 'Post', + comments: { create: { content: 'Comment' } }, + }, + }, + }, + }).success, + ).toBe(false); + }); + + it('relationDepth: 0 rejects relation in select', () => { + const s = client.$zod.makeCreateSchema('User', { relationDepth: 0 }); + expect( + s.safeParse({ + data: { email: 'u@test.com' }, + select: { posts: true }, + }).success, + ).toBe(false); + }); + }); + + describe('makeCreateManySchema with depth', () => { + it('relationDepth: 0 accepts scalar-only data', () => { + const s = client.$zod.makeCreateManySchema('User', { relationDepth: 0 }); + expect(s.safeParse({ data: [{ email: 'u@test.com' }] }).success).toBe(true); + }); + }); + + // --- Update schemas --- + + describe('makeUpdateSchema with depth', () => { + it('relationDepth: 0 rejects nested relation update', () => { + const s = client.$zod.makeUpdateSchema('User', { relationDepth: 0 }); + // scalar-only update works + expect(s.safeParse({ where: { id: 'abc' }, data: { name: 'New Name' } }).success).toBe(true); + // nested relation update rejected + expect( + s.safeParse({ + where: { id: 'abc' }, + data: { posts: { create: { title: 'Post' } } }, + }).success, + ).toBe(false); + }); + + it('relationDepth: 1 allows one level of nested update', () => { + const s = client.$zod.makeUpdateSchema('User', { relationDepth: 1 }); + expect( + s.safeParse({ + where: { id: 'abc' }, + data: { posts: { create: { title: 'Post' } } }, + }).success, + ).toBe(true); + }); + }); + + describe('makeUpsertSchema with depth', () => { + it('relationDepth: 0 rejects nested relations in create/update data', () => { + const s = client.$zod.makeUpsertSchema('User', { relationDepth: 0 }); + // scalar upsert works + expect( + s.safeParse({ + where: { id: 'abc' }, + create: { email: 'u@test.com' }, + update: { name: 'New' }, + }).success, + ).toBe(true); + // nested relation in create rejected + expect( + s.safeParse({ + where: { id: 'abc' }, + create: { email: 'u@test.com', posts: { create: { title: 'Post' } } }, + update: { name: 'New' }, + }).success, + ).toBe(false); + }); + }); + + // --- Delete schemas --- + + describe('makeDeleteSchema with depth', () => { + it('relationDepth: 0 rejects relation in select/include', () => { + const s = client.$zod.makeDeleteSchema('User', { relationDepth: 0 }); + expect(s.safeParse({ where: { id: 'abc' } }).success).toBe(true); + expect(s.safeParse({ where: { id: 'abc' }, select: { posts: true } }).success).toBe(false); + expect(s.safeParse({ where: { id: 'abc' }, include: { posts: true } }).success).toBe(false); + }); + }); + + describe('makeDeleteManySchema with depth', () => { + it('relationDepth: 0 rejects relation where filters', () => { + const s = client.$zod.makeDeleteManySchema('User', { relationDepth: 0 }); + expect(s.safeParse({ where: { email: 'u@test.com' } }).success).toBe(true); + expect(s.safeParse({ where: { posts: { some: { published: true } } } }).success).toBe(false); + }); + }); + + // --- Count / Aggregate / GroupBy --- + + describe('makeCountSchema with depth', () => { + it('relationDepth: 0 rejects relation where and orderBy', () => { + const s = client.$zod.makeCountSchema('User', { relationDepth: 0 }); + expect(s.safeParse({ where: { email: 'u@test.com' } }).success).toBe(true); + expect(s.safeParse({ where: { posts: { some: { published: true } } } }).success).toBe(false); + }); + }); + + describe('makeAggregateSchema with depth', () => { + it('relationDepth: 0 rejects relation where', () => { + const s = client.$zod.makeAggregateSchema('User', { relationDepth: 0 }); + expect(s.safeParse({ where: { email: 'u@test.com' } }).success).toBe(true); + expect(s.safeParse({ where: { posts: { some: { published: true } } } }).success).toBe(false); + }); + }); + + describe('makeGroupBySchema with depth', () => { + it('relationDepth: 0 rejects relation where', () => { + const s = client.$zod.makeGroupBySchema('User', { relationDepth: 0 }); + expect(s.safeParse({ by: ['email'], where: { email: 'u@test.com' } }).success).toBe(true); + expect(s.safeParse({ by: ['email'], where: { posts: { some: { published: true } } } }).success).toBe( + false, + ); + }); + }); + + // --- Where schema directly --- + + describe('makeWhereSchema with depth', () => { + it('relationDepth: 0 produces scalar-only where', () => { + const s = client.$zod.makeWhereSchema('User', false, false, false, { relationDepth: 0 }); + expect(s.safeParse({ email: 'u@test.com' }).success).toBe(true); + expect(s.safeParse({ posts: { some: { published: true } } }).success).toBe(false); + }); + + it('relationDepth: 0 still allows logical operators', () => { + const s = client.$zod.makeWhereSchema('User', false, false, false, { relationDepth: 0 }); + expect(s.safeParse({ AND: [{ email: 'a' }, { name: 'b' }] }).success).toBe(true); + expect(s.safeParse({ OR: [{ email: 'a' }] }).success).toBe(true); + expect(s.safeParse({ NOT: { email: 'a' } }).success).toBe(true); + }); + + it('relationDepth: 1 allows relation filters but not nested relation filters', () => { + const s = client.$zod.makeWhereSchema('Post', false, false, false, { relationDepth: 1 }); + // Post -> author (1 level) allowed + expect(s.safeParse({ author: { email: 'u@test.com' } }).success).toBe(true); + // Post -> author -> posts (2 levels) rejected + expect(s.safeParse({ author: { posts: { some: { published: true } } } }).success).toBe(false); + }); + }); + }); + + // #endregion });