@@ -15,8 +15,10 @@ import { SchemaFactoryError } from './error';
1515import type {
1616 GetModelCreateFieldsShape ,
1717 GetModelFieldsShape ,
18+ GetModelSchemaShapeWithOptions ,
1819 GetModelUpdateFieldsShape ,
1920 GetTypeDefFieldsShape ,
21+ ModelSchemaOptions ,
2022} from './types' ;
2123import {
2224 addBigIntValidation ,
@@ -30,6 +32,13 @@ export function createSchemaFactory<Schema extends SchemaDef>(schema: Schema) {
3032 return new SchemaFactory ( schema ) ;
3133}
3234
35+ /** Internal untyped representation of the options object used at runtime. */
36+ type RawOptions = {
37+ select ?: Record < string , unknown > ;
38+ include ?: Record < string , unknown > ;
39+ omit ?: Record < string , unknown > ;
40+ } ;
41+
3342class SchemaFactory < Schema extends SchemaDef > {
3443 private readonly schema : SchemaAccessor < Schema > ;
3544
@@ -39,29 +48,63 @@ class SchemaFactory<Schema extends SchemaDef> {
3948
4049 makeModelSchema < Model extends GetModels < Schema > > (
4150 model : Model ,
42- ) : z . ZodObject < GetModelFieldsShape < Schema , Model > , z . core . $strict > {
51+ ) : z . ZodObject < GetModelFieldsShape < Schema , Model > , z . core . $strict > ;
52+
53+ makeModelSchema < Model extends GetModels < Schema > , Options extends ModelSchemaOptions < Schema , Model > > (
54+ model : Model ,
55+ options : Options ,
56+ ) : z . ZodObject < GetModelSchemaShapeWithOptions < Schema , Model , Options > , z . core . $strict > ;
57+
58+ makeModelSchema < Model extends GetModels < Schema > , Options extends ModelSchemaOptions < Schema , Model > > (
59+ model : Model ,
60+ options ?: Options ,
61+ ) : z . ZodObject < Record < string , z . ZodType > , z . core . $strict > {
4362 const modelDef = this . schema . requireModel ( model ) ;
44- const fields : Record < string , z . ZodType > = { } ;
4563
46- for ( const [ fieldName , fieldDef ] of Object . entries ( modelDef . fields ) ) {
47- if ( fieldDef . relation ) {
48- const relatedModelName = fieldDef . type ;
49- const lazySchema : z . ZodType = z . lazy ( ( ) => this . makeModelSchema ( relatedModelName as GetModels < Schema > ) ) ;
50- // relation fields are always optional
51- fields [ fieldName ] = this . applyDescription (
52- this . applyCardinality ( lazySchema , fieldDef ) . optional ( ) ,
53- fieldDef . attributes ,
54- ) ;
55- } else {
56- fields [ fieldName ] = this . applyDescription ( this . makeScalarFieldSchema ( fieldDef ) , fieldDef . attributes ) ;
64+ if ( ! options ) {
65+ // ── No-options path (original behaviour) ─────────────────────────
66+ const fields : Record < string , z . ZodType > = { } ;
67+
68+ for ( const [ fieldName , fieldDef ] of Object . entries ( modelDef . fields ) ) {
69+ if ( fieldDef . relation ) {
70+ const relatedModelName = fieldDef . type ;
71+ const lazySchema : z . ZodType = z . lazy ( ( ) =>
72+ this . makeModelSchema ( relatedModelName as GetModels < Schema > ) ,
73+ ) ;
74+ // relation fields are always optional
75+ fields [ fieldName ] = this . applyDescription (
76+ this . applyCardinality ( lazySchema , fieldDef ) . optional ( ) ,
77+ fieldDef . attributes ,
78+ ) ;
79+ } else {
80+ fields [ fieldName ] = this . applyDescription (
81+ this . makeScalarFieldSchema ( fieldDef ) ,
82+ fieldDef . attributes ,
83+ ) ;
84+ }
5785 }
86+
87+ const shape = z . strictObject ( fields ) ;
88+ return this . applyDescription (
89+ addCustomValidation ( shape , modelDef . attributes ) ,
90+ modelDef . attributes ,
91+ ) as unknown as z . ZodObject < GetModelFieldsShape < Schema , Model > , z . core . $strict > ;
5892 }
5993
94+ // ── Options path ─────────────────────────────────────────────────────
95+ const rawOptions = options as unknown as RawOptions ;
96+ const fields = this . buildFieldsWithOptions ( model as string , rawOptions ) ;
6097 const shape = z . strictObject ( fields ) ;
61- return this . applyDescription (
62- addCustomValidation ( shape , modelDef . attributes ) ,
63- modelDef . attributes ,
64- ) as unknown as z . ZodObject < GetModelFieldsShape < Schema , Model > , z . core . $strict > ;
98+ // @@validate expressions reference fields by name — when `select` is
99+ // used only a subset of fields is present, so running @@validate would
100+ // silently evaluate missing fields as null and produce false negatives.
101+ // We therefore only apply model-level custom validation on the
102+ // include/omit path where all scalar fields are still present.
103+ const withValidation = rawOptions . select ? shape : addCustomValidation ( shape , modelDef . attributes ) ;
104+ return this . applyDescription ( withValidation , modelDef . attributes ) as unknown as z . ZodObject <
105+ GetModelSchemaShapeWithOptions < Schema , Model , Options > ,
106+ z . core . $strict
107+ > ;
65108 }
66109
67110 makeModelCreateSchema < Model extends GetModels < Schema > > (
@@ -114,6 +157,90 @@ class SchemaFactory<Schema extends SchemaDef> {
114157 ) as unknown as z . ZodObject < GetModelUpdateFieldsShape < Schema , Model > , z . core . $strict > ;
115158 }
116159
160+ // -------------------------------------------------------------------------
161+ // Options-aware field building
162+ // -------------------------------------------------------------------------
163+
164+ /**
165+ * Internal loose options shape used at runtime (we've already validated the
166+ * type-level constraints via the overload signatures).
167+ */
168+ private buildFieldsWithOptions ( model : string , options : RawOptions ) : Record < string , z . ZodType > {
169+ const { select, include, omit } = options ;
170+ const modelDef = this . schema . requireModel ( model ) ;
171+ const fields : Record < string , z . ZodType > = { } ;
172+
173+ if ( select ) {
174+ // ── select branch ────────────────────────────────────────────────
175+ // Only include fields that are explicitly listed with a truthy value.
176+ for ( const [ key , value ] of Object . entries ( select ) ) {
177+ if ( ! value ) continue ; // false → skip
178+
179+ const fieldDef = modelDef . fields [ key ] ;
180+ if ( ! fieldDef ) continue ;
181+
182+ if ( fieldDef . relation ) {
183+ // Relation field: recurse if value is a nested options object,
184+ // otherwise use the default lazy schema.
185+ const subOptions = typeof value === 'object' ? ( value as RawOptions ) : undefined ;
186+ const relSchema = this . makeRelationFieldSchema ( fieldDef , subOptions ) ;
187+ fields [ key ] = this . applyDescription (
188+ this . applyCardinality ( relSchema , fieldDef ) . optional ( ) ,
189+ fieldDef . attributes ,
190+ ) ;
191+ } else {
192+ fields [ key ] = this . applyDescription ( this . makeScalarFieldSchema ( fieldDef ) , fieldDef . attributes ) ;
193+ }
194+ }
195+ } else {
196+ // ── include + omit branch ────────────────────────────────────────
197+ // Start with all scalar fields, applying omit exclusions.
198+ for ( const [ fieldName , fieldDef ] of Object . entries ( modelDef . fields ) ) {
199+ if ( fieldDef . relation ) continue ; // relations handled below
200+
201+ // Skip if this field is explicitly omitted.
202+ if ( omit && ( omit as Record < string , unknown > ) [ fieldName ] === true ) continue ;
203+
204+ fields [ fieldName ] = this . applyDescription ( this . makeScalarFieldSchema ( fieldDef ) , fieldDef . attributes ) ;
205+ }
206+
207+ // Add included relation fields.
208+ if ( include ) {
209+ for ( const [ key , value ] of Object . entries ( include ) ) {
210+ if ( ! value ) continue ; // false → skip
211+
212+ const fieldDef = modelDef . fields [ key ] ;
213+ if ( ! fieldDef ?. relation ) continue ;
214+
215+ const subOptions = typeof value === 'object' ? ( value as RawOptions ) : undefined ;
216+ const relSchema = this . makeRelationFieldSchema ( fieldDef , subOptions ) ;
217+ fields [ key ] = this . applyDescription (
218+ this . applyCardinality ( relSchema , fieldDef ) . optional ( ) ,
219+ fieldDef . attributes ,
220+ ) ;
221+ }
222+ }
223+ }
224+
225+ return fields ;
226+ }
227+
228+ /**
229+ * Build the inner Zod schema for a relation field, optionally with nested
230+ * query options. Does NOT apply cardinality/optional wrappers — the caller
231+ * does that.
232+ */
233+ private makeRelationFieldSchema ( fieldDef : FieldDef , subOptions ?: RawOptions ) : z . ZodType {
234+ const relatedModelName = fieldDef . type as GetModels < Schema > ;
235+ if ( subOptions ) {
236+ // Recurse: build the related model's schema with its own options.
237+ return this . makeModelSchema ( relatedModelName , subOptions as ModelSchemaOptions < Schema , GetModels < Schema > > ) ;
238+ }
239+ // No sub-options: use a lazy reference to the default schema so that
240+ // circular models don't cause infinite recursion at build time.
241+ return z . lazy ( ( ) => this . makeModelSchema ( relatedModelName ) ) ;
242+ }
243+
117244 private makeScalarFieldSchema ( fieldDef : FieldDef ) : z . ZodType {
118245 const { type, attributes } = fieldDef ;
119246
0 commit comments