@@ -10,35 +10,47 @@ import ValidationError, { FieldErrors } from './errors/ValidationError'
1010import SchemaField , { FieldProperties } from './SchemaField'
1111import FieldResolutionError from './errors/FieldResolutionError'
1212
13- // todo allow type inference (autocomplete schema fields from fields definition)
14- interface FieldsDefinition {
15- [ key : string ] : FieldProperties < unknown > ;
13+ type Fields < K extends string | number | symbol = string > = Record < K , FieldProperties >
14+
15+ type SchemaFields < F extends Fields > = { [ K in keyof F ] : SchemaField < F [ K ] > }
16+
17+ /**
18+ * Makes fields partial. (required: false)
19+ */
20+ export type PartialFields < F extends Fields > = {
21+ [ K in keyof F ] : Omit < F [ K ] , 'required' > & { required : false }
22+ }
23+
24+ /**
25+ * Makes fields mandatory. (required: true)
26+ */
27+ export type RequiredFields < F extends Fields > = {
28+ [ K in keyof F ] : Omit < F [ K ] , 'required' > & { required : true }
1629}
1730
1831interface ValidateOptions {
1932 clean ?: boolean ;
20- context ?: Record < string , unknown > ,
33+ context ?: Record < string , unknown > ;
2134 ignoreMissing ?: boolean ;
2235 ignoreUnknown ?: boolean ;
2336 parse ?: boolean ;
2437 path ?: string ;
2538 removeUnknown ?: boolean ;
2639}
2740
28- class Schema {
29- public fields : { [ key : string ] : SchemaField < unknown > }
30-
31- constructor ( fields : FieldsDefinition ) {
32- this . fields = { }
41+ class Schema < F extends Fields = Fields > {
42+ public fields : SchemaFields < F > = { } as any
3343
44+ constructor ( fields : F ) {
3445 // Set fields.
35- Object . keys ( fields ) . forEach ( ( name : string ) : void => {
36- this . fields [ name ] = new SchemaField ( name , fields [ name ] )
46+ Object . keys ( fields ) . forEach ( ( name : keyof F ) : void => {
47+ this . fields [ name ] = new SchemaField ( String ( name ) , fields [ name ] )
3748 } )
3849 }
3950
4051 /**
4152 * Returns a clean copy of the object.
53+ * todo move to util functions
4254 * @param object
4355 * @param options
4456 */
@@ -66,21 +78,26 @@ class Schema {
6678 /**
6779 * Returns a clone of the schema.
6880 */
69- clone ( ) : Schema {
70- return this . pick ( Object . keys ( this . fields ) )
81+ clone ( ) : Schema < F > {
82+ const fields : Fields = { }
83+
84+ Object . keys ( this . fields ) . forEach ( ( name ) => {
85+ fields [ name ] = this . fields [ name ] . getProperties ( )
86+ } )
87+ return new Schema ( deepExtend ( { } , fields ) )
7188 }
7289
7390 /**
7491 * Returns a new schema based on current schema.
75- * @param fields
92+ * @param newFields
7693 */
77- extend ( fields : FieldsDefinition ) : Schema {
78- const fieldsDefinition : FieldsDefinition = { }
94+ extend < NF extends Fields > ( newFields : NF ) : Schema < F & NF > {
95+ const fields : Fields = { }
7996
80- Object . keys ( this . fields ) . forEach ( ( name : string ) : void => {
81- fieldsDefinition [ name ] = this . fields [ name ] . getProperties ( )
97+ Object . keys ( this . fields ) . forEach ( ( name ) => {
98+ fields [ name ] = this . fields [ name ] . getProperties ( )
8299 } )
83- return new Schema ( deepExtend ( { } , fieldsDefinition , fields ) )
100+ return new Schema ( deepExtend ( { } , fields , newFields ) )
84101 }
85102
86103 /**
@@ -120,14 +137,14 @@ class Schema {
120137 * Returns a field.
121138 * @param name
122139 */
123- getField ( name : string ) : SchemaField < unknown > {
124- return this . resolveField ( name )
140+ getField < N extends keyof F > ( name : N ) : SchemaFields < F > [ N ] {
141+ return this . fields [ name ]
125142 }
126143
127144 /**
128145 * Returns all fields.
129146 */
130- getFields ( ) : { [ key : string ] : SchemaField < unknown > } {
147+ getFields ( ) : SchemaFields < F > {
131148 return this . fields
132149 }
133150
@@ -149,11 +166,11 @@ class Schema {
149166 * Returns a sub schema without some fields.
150167 * @param fieldNames
151168 */
152- omit ( fieldNames : string [ ] ) : Schema {
153- const fields : FieldsDefinition = { }
169+ omit < K extends keyof F > ( fieldNames : K [ ] ) : Schema < Omit < F , K > > {
170+ const fields : Fields = { }
154171
155- Object . keys ( this . fields ) . forEach ( ( name : string ) : void => {
156- if ( fieldNames . indexOf ( name ) === - 1 ) {
172+ Object . keys ( this . fields ) . forEach ( ( name ) => {
173+ if ( ! fieldNames . includes ( name as K ) ) {
157174 fields [ name ] = this . fields [ name ] . getProperties ( )
158175 }
159176 } )
@@ -178,39 +195,26 @@ class Schema {
178195 /**
179196 * Returns a copy of the schema where all fields are not required.
180197 */
181- partial ( ) {
182- const fields : FieldsDefinition = { }
198+ partial ( ) : Schema < PartialFields < F > > {
199+ const fields : Fields = { }
183200
184- Object . keys ( this . fields ) . forEach ( ( name : string ) : void => {
201+ Object . keys ( this . fields ) . forEach ( ( name ) => {
185202 fields [ name ] = deepExtend ( { } , this . fields [ name ] . getProperties ( ) )
186203 fields [ name ] . required = false
187204 } )
188205 return new Schema ( deepExtend ( { } , fields ) )
189206 }
190207
191- /**
192- * Returns a copy of the schema where all fields are required.
193- */
194- required ( ) {
195- const fields : FieldsDefinition = { }
196-
197- Object . keys ( this . fields ) . forEach ( ( name : string ) : void => {
198- fields [ name ] = deepExtend ( { } , this . fields [ name ] . getProperties ( ) )
199- fields [ name ] . required = true
200- } )
201- return new Schema ( deepExtend ( { } , fields ) )
202- }
203-
204208 /**
205209 * Returns a sub schema from selected fields.
206210 * @param fieldNames
207211 */
208- pick ( fieldNames : string [ ] ) : Schema {
209- const fields : FieldsDefinition = { }
212+ pick < K extends keyof F > ( fieldNames : K [ ] ) : Schema < Pick < F , K > > {
213+ const fields : Fields = { }
210214
211- fieldNames . forEach ( ( name : string ) : void => {
215+ fieldNames . forEach ( ( name ) => {
212216 if ( typeof this . fields [ name ] !== 'undefined' ) {
213- fields [ name ] = this . fields [ name ] . getProperties ( )
217+ fields [ String ( name ) ] = this . fields [ name ] . getProperties ( )
214218 }
215219 } )
216220 return new Schema ( deepExtend ( { } , fields ) )
@@ -220,39 +224,52 @@ class Schema {
220224 * Returns a copy of the object without unknown fields.
221225 * @param object
222226 */
223- removeUnknownFields < T > ( object : Record < string , unknown > ) : T {
227+ removeUnknownFields ( object : Record < string , unknown > ) : Schema < F > {
224228 if ( object == null ) {
225229 return object
226230 }
227231 const clone = deepExtend ( { } , object )
228232
229233 Object . keys ( clone ) . forEach ( ( name : string ) : void => {
230- const field : SchemaField < unknown > = this . fields [ name ]
234+ const field = this . fields [ name ]
231235
232236 if ( typeof field === 'undefined' ) {
233237 delete clone [ name ]
234238 } else if ( field . getType ( ) instanceof Schema ) {
235239 clone [ name ] = ( field . getType ( ) as Schema ) . removeUnknownFields ( clone [ name ] )
236240 } else if ( field . getItems ( ) ?. type instanceof Schema ) {
237241 if ( clone [ name ] instanceof Array ) {
238- clone [ name ] = clone [ name ] . map ( ( item : any ) => (
239- ( field . getItems ( ) . type as Schema ) . removeUnknownFields ( item )
242+ clone [ name ] = clone [ name ] . map ( ( item ) => (
243+ ( field . getItems ( ) ? .type as Schema ) . removeUnknownFields ( item )
240244 ) )
241245 }
242246 }
243247 } )
244248 return clone
245249 }
246250
251+ /**
252+ * Returns a copy of the schema where all fields are required.
253+ */
254+ required ( ) : Schema < RequiredFields < F > > {
255+ const fields = { } as Fields
256+
257+ Object . keys ( this . fields ) . forEach ( ( name : string ) : void => {
258+ fields [ name ] = deepExtend ( { } , this . fields [ name ] . getProperties ( ) )
259+ fields [ name ] . required = true
260+ } )
261+ return new Schema ( deepExtend ( { } , fields ) )
262+ }
263+
247264 /**
248265 * Builds an object from a string (ex: [colors][0][code]).
249266 * @param path (ex: address[country][code])
250267 * @param syntaxChecked
251- * @throws {SyntaxError|TypeError }
252268 */
253- resolveField ( path : string , syntaxChecked = false ) : SchemaField < unknown > {
269+ resolveField < T extends SchemaField < FieldProperties > > ( path : keyof F | string , syntaxChecked = false ) : T {
270+ const p = path . toString ( )
254271 // Removes array indexes from path because we want to resolve field and not data.
255- const realPath = path . replace ( / \[ \d + ] / g, '' )
272+ const realPath = p . replace ( / \[ \d + ] / g, '' )
256273
257274 const bracketIndex = realPath . indexOf ( '[' )
258275 const bracketEnd = realPath . indexOf ( ']' )
@@ -262,31 +279,31 @@ class Schema {
262279 if ( ! syntaxChecked ) {
263280 // Check for extra space.
264281 if ( realPath . indexOf ( ' ' ) !== - 1 ) {
265- throw new SyntaxError ( `path "${ path } " is not valid` )
282+ throw new SyntaxError ( `path "${ p } " is not valid` )
266283 }
267284 // Check if key is not defined (ex: []).
268285 if ( realPath . indexOf ( '[]' ) !== - 1 ) {
269- throw new SyntaxError ( `missing array index or object attribute in "${ path } "` )
286+ throw new SyntaxError ( `missing array index or object attribute in "${ p } "` )
270287 }
271288 // Check for missing object attribute.
272289 if ( dotIndex + 1 === realPath . length ) {
273- throw new SyntaxError ( `missing object attribute in "${ path } "` )
290+ throw new SyntaxError ( `missing object attribute in "${ p } "` )
274291 }
275292
276293 const closingBrackets = realPath . split ( ']' ) . length
277294 const openingBrackets = realPath . split ( '[' ) . length
278295
279296 // Check for missing opening bracket.
280297 if ( openingBrackets < closingBrackets ) {
281- throw new SyntaxError ( `missing opening bracket "[" in "${ path } "` )
298+ throw new SyntaxError ( `missing opening bracket "[" in "${ p } "` )
282299 }
283300 // Check for missing closing bracket.
284301 if ( closingBrackets < openingBrackets ) {
285- throw new SyntaxError ( `missing closing bracket "]" in "${ path } "` )
302+ throw new SyntaxError ( `missing closing bracket "]" in "${ p } "` )
286303 }
287304 }
288305
289- let name = realPath
306+ let name : keyof F = realPath
290307 let subPath
291308
292309 // Resolve dot "." path.
@@ -313,26 +330,29 @@ class Schema {
313330 }
314331
315332 if ( typeof this . fields [ name ] === 'undefined' ) {
316- throw new FieldResolutionError ( path )
333+ throw new FieldResolutionError ( p )
317334 }
318335
319- let field : SchemaField < unknown > = this . fields [ name ]
336+ const field = this . fields [ name ]
320337
338+ // Get nested field
321339 if ( typeof subPath === 'string' && subPath . length > 0 ) {
322340 const type = field . getType ( )
323341 const props = field . getProperties ( )
324342
325343 if ( type instanceof Schema ) {
326- field = type . resolveField ( subPath , true )
327- } else if ( typeof props . items !== 'undefined' &&
344+ return type . resolveField ( subPath , true )
345+ }
346+ if ( typeof props . items !== 'undefined' &&
328347 typeof props . items . type !== 'undefined' &&
329348 props . items . type instanceof Schema ) {
330- field = props . items . type . resolveField ( subPath , true )
331- } else {
332- throw new FieldResolutionError ( path )
349+ return props . items . type . resolveField ( subPath , true )
333350 }
351+ } else if ( name in this . fields ) {
352+ // @ts -ignore fixme TS error
353+ return field
334354 }
335- return field
355+ throw new FieldResolutionError ( p )
336356 }
337357
338358 /**
@@ -420,15 +440,6 @@ class Schema {
420440 }
421441 return clone
422442 }
423-
424- /**
425- * Returns a sub schema without some fields.
426- * @deprecated use `omit()` instead
427- * @param fieldNames
428- */
429- without ( fieldNames : string [ ] ) : Schema {
430- return this . omit ( fieldNames )
431- }
432443}
433444
434445export default Schema
0 commit comments