11import {
2+ ExpressionUtils ,
23 SchemaAccessor ,
3- type BuiltinType ,
4+ type AttributeApplication ,
45 type FieldDef ,
5- type FieldIsArray ,
6- type FieldIsRelation ,
76 type GetEnum ,
87 type GetEnums ,
9- type GetModelFields ,
10- type GetModelFieldType ,
118 type GetModels ,
12- type GetTypeDefFields ,
13- type GetTypeDefFieldType ,
149 type GetTypeDefs ,
15- type ModelFieldIsOptional ,
1610 type SchemaDef ,
17- type TypeDefFieldIsOptional ,
1811} from '@zenstackhq/schema' ;
1912import Decimal from 'decimal.js' ;
20- import { match } from 'ts-pattern' ;
2113import z from 'zod' ;
14+ import { SchemaFactoryError } from './error' ;
15+ import type {
16+ GetModelCreateFieldsShape ,
17+ GetModelFieldsShape ,
18+ GetModelUpdateFieldsShape ,
19+ GetTypeDefFieldsShape ,
20+ } from './types' ;
2221import {
2322 addBigIntValidation ,
2423 addCustomValidation ,
@@ -41,28 +40,76 @@ class SchemaFactory<Schema extends SchemaDef> {
4140 makeModelSchema < Model extends GetModels < Schema > > (
4241 model : Model ,
4342 ) : z . ZodObject < GetModelFieldsShape < Schema , Model > , z . core . $strict > {
44- const modelDef = this . schema . models [ model ] ;
45- if ( ! modelDef ) {
46- throw new Error ( `Model "${ model } " not found in schema` ) ;
47- }
43+ const modelDef = this . schema . requireModel ( model ) ;
4844 const fields : Record < string , z . ZodType > = { } ;
4945
5046 for ( const [ fieldName , fieldDef ] of Object . entries ( modelDef . fields ) ) {
5147 if ( fieldDef . relation ) {
5248 const relatedModelName = fieldDef . type ;
5349 const lazySchema : z . ZodType = z . lazy ( ( ) => this . makeModelSchema ( relatedModelName as GetModels < Schema > ) ) ;
5450 // relation fields are always optional
55- fields [ fieldName ] = this . applyCardinality ( lazySchema , fieldDef ) . optional ( ) ;
51+ fields [ fieldName ] = this . applyDescription (
52+ this . applyCardinality ( lazySchema , fieldDef ) . optional ( ) ,
53+ fieldDef . attributes ,
54+ ) ;
5655 } else {
57- fields [ fieldName ] = this . makeScalarFieldSchema ( fieldDef ) ;
56+ fields [ fieldName ] = this . applyDescription ( this . makeScalarFieldSchema ( fieldDef ) , fieldDef . attributes ) ;
57+ }
58+ }
59+
60+ 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 > ;
65+ }
66+
67+ makeModelCreateSchema < Model extends GetModels < Schema > > (
68+ model : Model ,
69+ ) : z . ZodObject < GetModelCreateFieldsShape < Schema , Model > , z . core . $strict > {
70+ const modelDef = this . schema . requireModel ( model ) ;
71+ const fields : Record < string , z . ZodType > = { } ;
72+
73+ for ( const [ fieldName , fieldDef ] of Object . entries ( modelDef . fields ) ) {
74+ if ( fieldDef . relation ) {
75+ continue ;
76+ }
77+
78+ let fieldSchema = this . makeScalarFieldSchema ( fieldDef ) ;
79+ if ( fieldDef . optional || fieldDef . default !== undefined || fieldDef . updatedAt ) {
80+ fieldSchema = fieldSchema . optional ( ) ;
81+ }
82+ fields [ fieldName ] = this . applyDescription ( fieldSchema , fieldDef . attributes ) ;
83+ }
84+
85+ const shape = z . strictObject ( fields ) ;
86+ return this . applyDescription (
87+ addCustomValidation ( shape , modelDef . attributes ) ,
88+ modelDef . attributes ,
89+ ) as unknown as z . ZodObject < GetModelCreateFieldsShape < Schema , Model > , z . core . $strict > ;
90+ }
91+
92+ makeModelUpdateSchema < Model extends GetModels < Schema > > (
93+ model : Model ,
94+ ) : z . ZodObject < GetModelUpdateFieldsShape < Schema , Model > , z . core . $strict > {
95+ const modelDef = this . schema . requireModel ( model ) ;
96+ const fields : Record < string , z . ZodType > = { } ;
97+
98+ for ( const [ fieldName , fieldDef ] of Object . entries ( modelDef . fields ) ) {
99+ if ( fieldDef . relation ) {
100+ continue ;
58101 }
102+
103+ let fieldSchema = this . makeScalarFieldSchema ( fieldDef ) ;
104+ fieldSchema = fieldSchema . optional ( ) ;
105+ fields [ fieldName ] = this . applyDescription ( fieldSchema , fieldDef . attributes ) ;
59106 }
60107
61108 const shape = z . strictObject ( fields ) ;
62- return addCustomValidation ( shape , modelDef . attributes ) as unknown as z . ZodObject <
63- GetModelFieldsShape < Schema , Model > ,
64- z . core . $strict
65- > ;
109+ return this . applyDescription (
110+ addCustomValidation ( shape , modelDef . attributes ) ,
111+ modelDef . attributes ,
112+ ) as unknown as z . ZodObject < GetModelUpdateFieldsShape < Schema , Model > , z . core . $strict > ;
66113 }
67114
68115 private makeScalarFieldSchema ( fieldDef : FieldDef ) : z . ZodType {
@@ -80,24 +127,47 @@ class SchemaFactory<Schema extends SchemaDef> {
80127 return this . applyCardinality ( this . makeTypeSchema ( type as GetTypeDefs < Schema > ) , fieldDef ) ;
81128 }
82129
83- const base = match < BuiltinType > ( type as BuiltinType )
84- . with ( 'String' , ( ) => addStringValidation ( z . string ( ) , attributes ) )
85- . with ( 'Int' , ( ) => addNumberValidation ( z . number ( ) . int ( ) , attributes ) )
86- . with ( 'Float' , ( ) => addNumberValidation ( z . number ( ) , attributes ) )
87- . with ( 'Boolean' , ( ) => z . boolean ( ) )
88- . with ( 'BigInt' , ( ) => addBigIntValidation ( z . bigint ( ) , attributes ) )
89- . with ( 'Decimal' , ( ) =>
90- z . union ( [
130+ let base : z . ZodType ;
131+ switch ( type ) {
132+ case 'String' :
133+ base = addStringValidation ( z . string ( ) , attributes ) ;
134+ break ;
135+ case 'Int' :
136+ base = addNumberValidation ( z . number ( ) . int ( ) , attributes ) ;
137+ break ;
138+ case 'Float' :
139+ base = addNumberValidation ( z . number ( ) , attributes ) ;
140+ break ;
141+ case 'Boolean' :
142+ base = z . boolean ( ) ;
143+ break ;
144+ case 'BigInt' :
145+ base = addBigIntValidation ( z . bigint ( ) , attributes ) ;
146+ break ;
147+ case 'Decimal' :
148+ base = z . union ( [
91149 addNumberValidation ( z . number ( ) , attributes ) as z . ZodNumber ,
92150 addDecimalValidation ( z . string ( ) , attributes , true ) as z . ZodString ,
93151 addDecimalValidation ( z . instanceof ( Decimal ) , attributes , true ) ,
94- ] ) ,
95- )
96- . with ( 'DateTime' , ( ) => z . union ( [ z . date ( ) , z . iso . datetime ( ) ] ) )
97- . with ( 'Bytes' , ( ) => z . instanceof ( Uint8Array ) )
98- . with ( 'Json' , ( ) => this . makeJsonSchema ( ) )
99- . with ( 'Unsupported' , ( ) => z . unknown ( ) )
100- . exhaustive ( ) ;
152+ ] ) ;
153+ break ;
154+ case 'DateTime' :
155+ base = z . union ( [ z . date ( ) , z . iso . datetime ( ) ] ) ;
156+ break ;
157+ case 'Bytes' :
158+ base = z . instanceof ( Uint8Array ) ;
159+ break ;
160+ case 'Json' :
161+ base = this . makeJsonSchema ( ) ;
162+ break ;
163+ case 'Unsupported' :
164+ base = z . unknown ( ) ;
165+ break ;
166+ default : {
167+ const _exhaustive : never = type as never ;
168+ throw new SchemaFactoryError ( `Unsupported field type: ${ _exhaustive } ` ) ;
169+ }
170+ }
101171
102172 return this . applyCardinality ( base , fieldDef ) ;
103173 }
@@ -131,113 +201,48 @@ class SchemaFactory<Schema extends SchemaDef> {
131201 const fields : Record < string , z . ZodType > = { } ;
132202
133203 for ( const [ fieldName , fieldDef ] of Object . entries ( typeDef . fields ) ) {
134- fields [ fieldName ] = this . makeScalarFieldSchema ( fieldDef ) ;
204+ fields [ fieldName ] = this . applyDescription ( this . makeScalarFieldSchema ( fieldDef ) , fieldDef . attributes ) ;
135205 }
136206
137207 const shape = z . strictObject ( fields ) ;
138- return addCustomValidation ( shape , typeDef . attributes ) as unknown as z . ZodObject <
139- GetTypeDefFieldsShape < Schema , Type > ,
140- z . core . $strict
141- > ;
208+ return this . applyDescription (
209+ addCustomValidation ( shape , typeDef . attributes ) ,
210+ typeDef . attributes ,
211+ ) as unknown as z . ZodObject < GetTypeDefFieldsShape < Schema , Type > , z . core . $strict > ;
142212 }
143213
144214 makeEnumSchema < Enum extends GetEnums < Schema > > (
145215 _enum : Enum ,
146216 ) : z . ZodEnum < { [ Key in keyof GetEnum < Schema , Enum > ] : GetEnum < Schema , Enum > [ Key ] } > {
147217 const enumDef = this . schema . requireEnum ( _enum ) ;
148- return z . enum ( Object . keys ( enumDef . values ) as [ string , ...string [ ] ] ) as unknown as z . ZodEnum < {
218+ const schema = z . enum ( Object . keys ( enumDef . values ) as [ string , ...string [ ] ] ) ;
219+ return this . applyDescription ( schema , enumDef . attributes ) as unknown as z . ZodEnum < {
149220 [ Key in keyof GetEnum < Schema , Enum > ] : GetEnum < Schema , Enum > [ Key ] ;
150221 } > ;
151222 }
152- }
153223
154- type GetModelFieldsShape < Schema extends SchemaDef , Model extends GetModels < Schema > > = {
155- // scalar fields
156- [ Field in GetModelFields < Schema , Model > as FieldIsRelation < Schema , Model , Field > extends true
157- ? never
158- : Field ] : ZodOptionalAndNullableIf <
159- MapModelFieldToZod < Schema , Model , Field > ,
160- ModelFieldIsOptional < Schema , Model , Field >
161- > ;
162- } & {
163- // relation fields, always optional
164- [ Field in GetModelFields < Schema , Model > as FieldIsRelation < Schema , Model , Field > extends true
165- ? Field
166- : never ] : ZodNullableIf <
167- z . ZodOptional <
168- ZodArrayIf <
169- z . ZodObject <
170- GetModelFieldsShape <
171- Schema ,
172- GetModelFieldType < Schema , Model , Field > extends GetModels < Schema >
173- ? GetModelFieldType < Schema , Model , Field >
174- : never
175- > ,
176- z . core . $strict
177- > ,
178- FieldIsArray < Schema , Model , Field >
179- >
180- > ,
181- ModelFieldIsOptional < Schema , Model , Field >
182- > ;
183- } ;
184-
185- type GetTypeDefFieldsShape < Schema extends SchemaDef , Type extends GetTypeDefs < Schema > > = {
186- [ Field in GetTypeDefFields < Schema , Type > ] : ZodOptionalAndNullableIf <
187- MapTypeDefFieldToZod < Schema , Type , Field > ,
188- TypeDefFieldIsOptional < Schema , Type , Field >
189- > ;
190- } ;
191-
192- type FieldTypeZodMap = {
193- String : z . ZodString ;
194- Int : z . ZodNumber ;
195- BigInt : z . ZodBigInt ;
196- Float : z . ZodNumber ;
197- Decimal : z . ZodType < Decimal > ;
198- Boolean : z . ZodBoolean ;
199- DateTime : z . ZodType < Date > ;
200- Bytes : z . ZodType < Uint8Array > ;
201- Json : JsonZodType ;
202- } ;
203-
204- type MapModelFieldToZod <
205- Schema extends SchemaDef ,
206- Model extends GetModels < Schema > ,
207- Field extends GetModelFields < Schema , Model > ,
208- FieldType = GetModelFieldType < Schema , Model , Field > ,
209- > = MapFieldTypeToZod < Schema , FieldType > ;
210-
211- type MapTypeDefFieldToZod <
212- Schema extends SchemaDef ,
213- Type extends GetTypeDefs < Schema > ,
214- Field extends GetTypeDefFields < Schema , Type > ,
215- FieldType = GetTypeDefFieldType < Schema , Type , Field > ,
216- > = MapFieldTypeToZod < Schema , FieldType > ;
217-
218- type MapFieldTypeToZod < Schema extends SchemaDef , FieldType > = FieldType extends keyof FieldTypeZodMap
219- ? FieldTypeZodMap [ FieldType ]
220- : FieldType extends GetEnums < Schema >
221- ? EnumZodType < Schema , FieldType >
222- : FieldType extends GetTypeDefs < Schema >
223- ? z . ZodObject < GetTypeDefFieldsShape < Schema , FieldType > , z . core . $strict >
224- : z . ZodUnknown ;
225-
226- type JsonZodType =
227- | z . ZodObject < Record < string , z . ZodType > , z . core . $loose >
228- | z . ZodArray < z . ZodType >
229- | z . ZodString
230- | z . ZodNumber
231- | z . ZodBoolean
232- | z . ZodNull ;
233-
234- type EnumZodType < Schema extends SchemaDef , EnumName extends GetEnums < Schema > > = z . ZodEnum < {
235- [ Key in keyof GetEnum < Schema , EnumName > ] : GetEnum < Schema , EnumName > [ Key ] ;
236- } > ;
237-
238- type ZodOptionalAndNullableIf < T extends z . ZodType , Condition extends boolean > = Condition extends true
239- ? z . ZodOptional < z . ZodNullable < T > >
240- : T ;
241-
242- type ZodNullableIf < T extends z . ZodType , Condition extends boolean > = Condition extends true ? z . ZodNullable < T > : T ;
243- type ZodArrayIf < T extends z . ZodType , Condition extends boolean > = Condition extends true ? z . ZodArray < T > : T ;
224+ private getMetaDescription ( attributes : readonly AttributeApplication [ ] | undefined ) : string | undefined {
225+ if ( ! attributes ) return undefined ;
226+ for ( const attr of attributes ) {
227+ if ( attr . name !== '@meta' && attr . name !== '@@meta' ) continue ;
228+ const nameExpr = attr . args ?. [ 0 ] ?. value ;
229+ if ( ! nameExpr || ExpressionUtils . getLiteralValue ( nameExpr ) !== 'description' ) continue ;
230+ const valueExpr = attr . args ?. [ 1 ] ?. value ;
231+ if ( valueExpr ) {
232+ return ExpressionUtils . getLiteralValue ( valueExpr ) as string | undefined ;
233+ }
234+ }
235+ return undefined ;
236+ }
237+
238+ private applyDescription < T extends z . ZodType > (
239+ schema : T ,
240+ attributes : readonly AttributeApplication [ ] | undefined ,
241+ ) : T {
242+ const description = this . getMetaDescription ( attributes ) ;
243+ if ( description ) {
244+ return schema . meta ( { description } ) as T ;
245+ }
246+ return schema ;
247+ }
248+ }
0 commit comments