@travetto/schema 6.0.0 → 7.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,19 +2,40 @@ import { Any, Class, Primitive } from '@travetto/runtime';
2
2
 
3
3
  import { MethodValidatorFn, ValidatorFn } from '../validate/types.ts';
4
4
 
5
- export type ClassList = Class | [Class];
6
-
7
5
  type TemplateLiteralPart = string | NumberConstructor | StringConstructor | BooleanConstructor;
8
6
  export type TemplateLiteral = { op: 'and' | 'or', values: (TemplateLiteralPart | TemplateLiteral)[] };
9
7
 
8
+ export const CONSTRUCTOR_PROPERTY = 'CONSTRUCTOR';
9
+
10
10
  /**
11
- * Basic describable configuration
11
+ * Represents a typed item in the schema
12
12
  */
13
- export interface DescribableConfig {
13
+ export type SchemaBasicType = {
14
+ /**
15
+ * Description of the type
16
+ */
17
+ description?: string;
14
18
  /**
15
- * Title
19
+ * Is the type an array
16
20
  */
17
- title?: string;
21
+ array?: boolean;
22
+ /**
23
+ * The class tied to the type
24
+ */
25
+ type: Class & {
26
+ bindSchema?(input: unknown): undefined | unknown;
27
+ validateSchema?(input: unknown): string | undefined;
28
+ };
29
+ /**
30
+ * Is the field a foreign type
31
+ */
32
+ isForeign?: boolean;
33
+ };
34
+
35
+ /**
36
+ * Basic schema configuration
37
+ */
38
+ export interface SchemaCoreConfig {
18
39
  /**
19
40
  * Description
20
41
  */
@@ -23,44 +44,52 @@ export interface DescribableConfig {
23
44
  * List of examples
24
45
  */
25
46
  examples?: string[];
47
+ /**
48
+ * Is the field/method/private
49
+ */
50
+ private?: boolean;
51
+ /**
52
+ * Metadata that is related to the schema structure
53
+ */
54
+ metadata?: Record<symbol, unknown>;
26
55
  }
27
56
 
28
57
  /**
29
58
  * Basic structure for a method configuration
30
59
  */
31
- export interface SchemaMethodConfig {
32
- fields: FieldConfig[];
60
+ export interface SchemaMethodConfig extends SchemaCoreConfig {
61
+ /**
62
+ * Is the method static
63
+ */
64
+ isStatic?: boolean;
65
+ /**
66
+ * The parameters of the method
67
+ */
68
+ parameters: SchemaParameterConfig[];
69
+ /**
70
+ * Validators to run for th method
71
+ */
33
72
  validators: MethodValidatorFn<unknown[]>[];
34
- }
35
-
36
- /**
37
- * Schema configuration
38
- */
39
- export interface SchemaConfig {
40
73
  /**
41
- * List of all fields
74
+ * The return type configuration
42
75
  */
43
- [key: string]: FieldConfig;
76
+ returnType?: SchemaBasicType;
44
77
  }
45
78
 
46
79
  /**
47
- * Specific view configuration for a schema
80
+ * Schema configuration
48
81
  */
49
- export interface ViewConfig {
50
- /**
51
- * The schema config
52
- */
53
- schema: SchemaConfig;
82
+ export interface SchemaFieldMap {
54
83
  /**
55
- * The list of all fields in the view
84
+ * List of all fields
56
85
  */
57
- fields: string[];
86
+ [key: string | symbol]: SchemaFieldConfig;
58
87
  }
59
88
 
60
89
  /**
61
90
  * Class configuration
62
91
  */
63
- export interface ClassConfig extends DescribableConfig {
92
+ export interface SchemaClassConfig extends SchemaCoreConfig {
64
93
  /**
65
94
  * Target class
66
95
  */
@@ -68,72 +97,61 @@ export interface ClassConfig extends DescribableConfig {
68
97
  /**
69
98
  * List of all views
70
99
  */
71
- views: Record<string, ViewConfig>;
100
+ views: Record<string, ViewFieldsConfig<Any>>;
72
101
  /**
73
- * Composite of all views
102
+ * Field configurations
74
103
  */
75
- totalView: ViewConfig;
104
+ fields: SchemaFieldMap;
76
105
  /**
77
106
  * Global validators
78
107
  */
79
108
  validators: ValidatorFn<Any, unknown>[];
80
109
  /**
81
- * Is the class a base type
110
+ * Is this a base class for discrimination
82
111
  */
83
- baseType?: boolean;
112
+ discriminatedBase?: boolean;
84
113
  /**
85
- * Sub type name
114
+ * Do we have a discriminator field
86
115
  */
87
- subTypeName?: string;
116
+ discriminatedField?: string;
88
117
  /**
89
- * The field the subtype is determined by
118
+ * Discriminated type name
90
119
  */
91
- subTypeField: string;
120
+ discriminatedType?: string;
92
121
  /**
93
- * Metadata that is related to the schema structure
122
+ * Method configs
94
123
  */
95
- metadata?: Record<symbol, unknown>;
124
+ methods: Record<string | symbol, SchemaMethodConfig>;
125
+ /**
126
+ * Interfaces that the class implements
127
+ */
128
+ interfaces: Class[];
96
129
  /**
97
- * Method parameter configs
130
+ * Is this class derived from another via a mapped type
98
131
  */
99
- methods: Record<string, SchemaMethodConfig>;
132
+ mappedOperation?: 'Omit' | 'Pick' | 'Partial' | 'Required';
133
+ /**
134
+ * Are there any restrictions in the mapped type
135
+ */
136
+ mappedFields?: string[];
100
137
  }
101
138
 
102
139
  /**
103
- * Field configuration
140
+ * Shared base type for all input-related fields
104
141
  */
105
- export interface FieldConfig extends DescribableConfig {
142
+ export interface SchemaInputConfig extends SchemaCoreConfig, SchemaBasicType {
106
143
  /**
107
- * Owner class
144
+ * Key name for validation when dealing with complex types
108
145
  */
109
- owner?: Class;
146
+ view?: string;
110
147
  /**
111
- * Field name
148
+ * Owner class
112
149
  */
113
- name: string;
150
+ owner: Class;
114
151
  /**
115
152
  * List of aliases
116
153
  */
117
154
  aliases?: string[];
118
- /**
119
- * Specific type for the field, with optional binding/validation support
120
- */
121
- type: Class & {
122
- bindSchema?(input: unknown): undefined | unknown;
123
- validateSchema?(input: unknown): string | undefined;
124
- };
125
- /**
126
- * View name for validation when dealing with complex types
127
- */
128
- view?: string;
129
- /**
130
- * The position of the field if ordered
131
- */
132
- index?: number;
133
- /**
134
- * Is the field an array
135
- */
136
- array: boolean;
137
155
  /**
138
156
  * Does the field have a specialization
139
157
  */
@@ -174,6 +192,38 @@ export interface FieldConfig extends DescribableConfig {
174
192
  * Default value
175
193
  */
176
194
  default?: Primitive | [];
195
+ }
196
+
197
+ /**
198
+ * Parameter configuration for methods
199
+ */
200
+ export interface SchemaParameterConfig extends SchemaInputConfig {
201
+ /**
202
+ * Parameter name
203
+ */
204
+ name?: string;
205
+ /**
206
+ * The position of the field if ordered
207
+ */
208
+ index: number;
209
+ /**
210
+ * Method the parameter belongs to
211
+ */
212
+ method: string | symbol;
213
+ /**
214
+ * Source text for the parameter
215
+ */
216
+ sourceText?: string;
217
+ }
218
+
219
+ /**
220
+ * Field configuration
221
+ */
222
+ export interface SchemaFieldConfig extends SchemaInputConfig {
223
+ /**
224
+ * Field name
225
+ */
226
+ name: string | symbol;
177
227
  /**
178
228
  * Is the field readonly, or write only?, defaults to no restrictions
179
229
  */
@@ -34,6 +34,10 @@ export interface ValidationError {
34
34
  * The type of the field
35
35
  */
36
36
  type?: string;
37
+ /**
38
+ * Source of the error
39
+ */
40
+ source?: string;
37
41
  }
38
42
 
39
43
  /**
@@ -1,22 +1,21 @@
1
1
  import { castKey, castTo, Class, ClassInstance, TypedObject } from '@travetto/runtime';
2
2
 
3
- import { FieldConfig, SchemaConfig } from '../service/types.ts';
4
- import { SchemaRegistry } from '../service/registry.ts';
3
+ import { SchemaInputConfig, SchemaFieldMap } from '../service/types.ts';
5
4
  import { ValidationError, ValidationKindCore, ValidationResult } from './types.ts';
6
5
  import { Messages } from './messages.ts';
7
6
  import { isValidationError, TypeMismatchError, ValidationResultError } from './error.ts';
8
7
  import { DataUtil } from '../data.ts';
9
8
  import { CommonRegExpToName } from './regexp.ts';
9
+ import { SchemaRegistryIndex } from '../service/registry-index.ts';
10
10
 
11
11
  /**
12
12
  * Get the schema config for Class/Schema config, including support for polymorphism
13
13
  * @param base The starting type or config
14
14
  * @param o The value to use for the polymorphic check
15
15
  */
16
- function resolveSchema<T>(base: Class<T>, o: T, view?: string): SchemaConfig {
17
- return SchemaRegistry.getViewSchema(
18
- SchemaRegistry.resolveInstanceType(base, o), view
19
- ).schema;
16
+ function resolveFieldMap<T>(base: Class<T>, o: T): SchemaFieldMap {
17
+ const target = SchemaRegistryIndex.resolveInstanceType(base, o);
18
+ return SchemaRegistryIndex.getFieldMap(target);
20
19
  }
21
20
 
22
21
  function isClassInstance<T>(o: unknown): o is ClassInstance<T> {
@@ -35,17 +34,16 @@ export class SchemaValidator {
35
34
 
36
35
  /**
37
36
  * Validate the schema for a given object
38
- * @param schema The config to validate against
37
+ * @param fields The config to validate against
39
38
  * @param o The object to validate
40
39
  * @param relative The relative path as the validation recurses
41
40
  */
42
- static #validateSchema<T>(schema: SchemaConfig, o: T, relative: string): ValidationError[] {
41
+ static #validateFields<T>(fields: SchemaFieldMap, o: T, relative: string): ValidationError[] {
43
42
  let errors: ValidationError[] = [];
44
43
 
45
- const fields = TypedObject.keys<SchemaConfig>(schema);
46
- for (const field of fields) {
47
- if (schema[field].access !== 'readonly') { // Do not validate readonly fields
48
- errors = errors.concat(this.#validateFieldSchema(schema[field], o[castKey<T>(field)], relative));
44
+ for (const [field, fieldConfig] of TypedObject.entries(fields)) {
45
+ if (fieldConfig.access !== 'readonly') { // Do not validate readonly fields
46
+ errors = errors.concat(this.#validateInputSchema(fieldConfig, o[castKey<T>(field)], relative));
49
47
  }
50
48
  }
51
49
 
@@ -53,25 +51,26 @@ export class SchemaValidator {
53
51
  }
54
52
 
55
53
  /**
56
- * Validate a single field config against a passed in value
57
- * @param fieldSchema The field schema configuration
54
+ * Validate a single input config against a passed in value
55
+ * @param input The input schema configuration
58
56
  * @param val The raw value, could be an array or not
59
57
  * @param relative The relative path of object traversal
60
58
  */
61
- static #validateFieldSchema(fieldSchema: FieldConfig, val: unknown, relative: string = ''): ValidationError[] {
62
- const path = `${relative}${relative ? '.' : ''}${fieldSchema.name}`;
59
+ static #validateInputSchema(input: SchemaInputConfig, val: unknown, relative: string = ''): ValidationError[] {
60
+ const key = 'name' in input ? input.name : ('index' in input ? input.index : 'unknown');
61
+ const path = `${relative}${relative ? '.' : ''}${key}`;
63
62
  const hasValue = !(val === undefined || val === null || (typeof val === 'string' && val === '') || (Array.isArray(val) && val.length === 0));
64
63
 
65
64
  if (!hasValue) {
66
- if (fieldSchema.required && fieldSchema.required.active) {
67
- return this.#prepareErrors(path, [{ kind: 'required', ...fieldSchema.required }]);
65
+ if (input.required?.active !== false) {
66
+ return this.#prepareErrors(path, [{ kind: 'required', active: true, ...input.required }]);
68
67
  } else {
69
68
  return [];
70
69
  }
71
70
  }
72
71
 
73
- const { type, array, view } = fieldSchema;
74
- const complex = SchemaRegistry.has(type);
72
+ const { type, array } = input;
73
+ const complex = SchemaRegistryIndex.has(type);
75
74
 
76
75
  if (type === Object) {
77
76
  return [];
@@ -82,34 +81,34 @@ export class SchemaValidator {
82
81
  let errors: ValidationError[] = [];
83
82
  if (complex) {
84
83
  for (let i = 0; i < val.length; i++) {
85
- const subErrors = this.#validateSchema(resolveSchema(type, val[i], view), val[i], `${path}[${i}]`);
84
+ const subErrors = this.#validateFields(resolveFieldMap(type, val[i]), val[i], `${path}[${i}]`);
86
85
  errors = errors.concat(subErrors);
87
86
  }
88
87
  } else {
89
88
  for (let i = 0; i < val.length; i++) {
90
- const subErrors = this.#validateField(fieldSchema, val[i]);
89
+ const subErrors = this.#validateInput(input, val[i]);
91
90
  errors.push(...this.#prepareErrors(`${path}[${i}]`, subErrors));
92
91
  }
93
92
  }
94
93
  return errors;
95
94
  } else if (complex) {
96
- return this.#validateSchema(resolveSchema(type, val, view), val, path);
95
+ return this.#validateFields(resolveFieldMap(type, val), val, path);
97
96
  } else {
98
- const fieldErrors = this.#validateField(fieldSchema, val);
97
+ const fieldErrors = this.#validateInput(input, val);
99
98
  return this.#prepareErrors(path, fieldErrors);
100
99
  }
101
100
  }
102
101
 
103
102
  /**
104
103
  * Validate the range for a number, date
105
- * @param field The config to validate against
104
+ * @param input The config to validate against
106
105
  * @param key The bounds to check
107
106
  * @param value The value to validate
108
107
  */
109
- static #validateRange(field: FieldConfig, key: 'min' | 'max', value: string | number | Date): boolean {
110
- const f = field[key]!;
108
+ static #validateRange(input: SchemaInputConfig, key: 'min' | 'max', value: string | number | Date): boolean {
109
+ const f = input[key]!;
111
110
  const valueNum = (typeof value === 'string') ?
112
- (field.type === Date ? Date.parse(value) : parseInt(value, 10)) :
111
+ (input.type === Date ? Date.parse(value) : parseInt(value, 10)) :
113
112
  (value instanceof Date ? value.getTime() : value);
114
113
 
115
114
  const boundary = (typeof f.n === 'number' ? f.n : f.n.getTime());
@@ -119,54 +118,54 @@ export class SchemaValidator {
119
118
  /**
120
119
  * Validate a given field by checking all the appropriate constraints
121
120
  *
122
- * @param field The config of the field to validate
121
+ * @param input The config of the field to validate
123
122
  * @param value The actual value
124
123
  */
125
- static #validateField(field: FieldConfig, value: unknown): ValidationResult[] {
126
- const criteria: ([string, FieldConfig[ValidationKindCore]] | [string])[] = [];
124
+ static #validateInput(input: SchemaInputConfig, value: unknown): ValidationResult[] {
125
+ const criteria: ([string, SchemaInputConfig[ValidationKindCore]] | [string])[] = [];
127
126
 
128
127
  if (
129
- (field.type === String && (typeof value !== 'string')) ||
130
- (field.type === Number && ((typeof value !== 'number') || Number.isNaN(value))) ||
131
- (field.type === Date && (!(value instanceof Date) || Number.isNaN(value.getTime()))) ||
132
- (field.type === Boolean && typeof value !== 'boolean')
128
+ (input.type === String && (typeof value !== 'string')) ||
129
+ (input.type === Number && ((typeof value !== 'number') || Number.isNaN(value))) ||
130
+ (input.type === Date && (!(value instanceof Date) || Number.isNaN(value.getTime()))) ||
131
+ (input.type === Boolean && typeof value !== 'boolean')
133
132
  ) {
134
133
  criteria.push(['type']);
135
- return [{ kind: 'type', type: field.type.name.toLowerCase() }];
134
+ return [{ kind: 'type', type: input.type.name.toLowerCase() }];
136
135
  }
137
136
 
138
- if (field.type?.validateSchema) {
139
- const kind = field.type.validateSchema(value);
137
+ if (input.type?.validateSchema) {
138
+ const kind = input.type.validateSchema(value);
140
139
  switch (kind) {
141
140
  case undefined: break;
142
- case 'type': return [{ kind, type: field.type.name }];
141
+ case 'type': return [{ kind, type: input.type.name }];
143
142
  default:
144
143
  criteria.push([kind]);
145
144
  }
146
145
  }
147
146
 
148
- if (field.match && !field.match.re.test(`${value}`)) {
149
- criteria.push(['match', field.match]);
147
+ if (input.match && !input.match.re.test(`${value}`)) {
148
+ criteria.push(['match', input.match]);
150
149
  }
151
150
 
152
- if (field.minlength && `${value}`.length < field.minlength.n) {
153
- criteria.push(['minlength', field.minlength]);
151
+ if (input.minlength && `${value}`.length < input.minlength.n) {
152
+ criteria.push(['minlength', input.minlength]);
154
153
  }
155
154
 
156
- if (field.maxlength && `${value}`.length > field.maxlength.n) {
157
- criteria.push(['maxlength', field.maxlength]);
155
+ if (input.maxlength && `${value}`.length > input.maxlength.n) {
156
+ criteria.push(['maxlength', input.maxlength]);
158
157
  }
159
158
 
160
- if (field.enum && !field.enum.values.includes(castTo(value))) {
161
- criteria.push(['enum', field.enum]);
159
+ if (input.enum && !input.enum.values.includes(castTo(value))) {
160
+ criteria.push(['enum', input.enum]);
162
161
  }
163
162
 
164
- if (field.min && (!isRangeValue(value) || this.#validateRange(field, 'min', value))) {
165
- criteria.push(['min', field.min]);
163
+ if (input.min && (!isRangeValue(value) || this.#validateRange(input, 'min', value))) {
164
+ criteria.push(['min', input.min]);
166
165
  }
167
166
 
168
- if (field.max && (!isRangeValue(value) || this.#validateRange(field, 'max', value))) {
169
- criteria.push(['max', field.max]);
167
+ if (input.max && (!isRangeValue(value) || this.#validateRange(input, 'max', value))) {
168
+ criteria.push(['max', input.max]);
170
169
  }
171
170
 
172
171
  const errors: ValidationResult[] = [];
@@ -217,14 +216,15 @@ export class SchemaValidator {
217
216
  * Validate the class level validations
218
217
  */
219
218
  static async #validateClassLevel<T>(cls: Class<T>, o: T, view?: string): Promise<ValidationError[]> {
220
- const schema = SchemaRegistry.get(cls);
221
- if (!schema) {
219
+ if (!SchemaRegistryIndex.has(cls)) {
222
220
  return [];
223
221
  }
224
222
 
223
+ const classConfig = SchemaRegistryIndex.getConfig(cls);
225
224
  const errors: ValidationError[] = [];
225
+
226
226
  // Handle class level validators
227
- for (const fn of schema.validators) {
227
+ for (const fn of classConfig.validators) {
228
228
  try {
229
229
  const res = await fn(o, view);
230
230
  if (res) {
@@ -255,13 +255,13 @@ export class SchemaValidator {
255
255
  if (isClassInstance(o) && !(o instanceof cls || cls.Ⲑid === o.constructor.Ⲑid)) {
256
256
  throw new TypeMismatchError(cls.name, o.constructor.name);
257
257
  }
258
- cls = SchemaRegistry.resolveInstanceType(cls, o);
258
+ cls = SchemaRegistryIndex.resolveInstanceType(cls, o);
259
259
 
260
- const config = SchemaRegistry.getViewSchema(cls, view);
260
+ const fields = SchemaRegistryIndex.getFieldMap(cls, view);
261
261
 
262
262
  // Validate using standard behaviors
263
263
  const errors = [
264
- ...this.#validateSchema(config.schema, o, ''),
264
+ ...this.#validateFields(fields, o, ''),
265
265
  ... await this.#validateClassLevel(cls, o, view)
266
266
  ];
267
267
  if (errors.length) {
@@ -311,19 +311,25 @@ export class SchemaValidator {
311
311
  * @param method The method being invoked
312
312
  * @param params The params to validate
313
313
  */
314
- static async validateMethod<T>(cls: Class<T>, method: string, params: unknown[], prefixes: (string | undefined)[] = []): Promise<void> {
314
+ static async validateMethod<T>(cls: Class<T>, method: string | symbol, params: unknown[], prefixes: (string | symbol | undefined)[] = []): Promise<void> {
315
315
  const errors: ValidationError[] = [];
316
- for (const field of SchemaRegistry.getMethodSchema(cls, method)) {
317
- const i = field.index!;
316
+ const config = SchemaRegistryIndex.getMethodConfig(cls, method);
317
+
318
+ for (const param of config.parameters) {
319
+ const i = param.index;
318
320
  errors.push(...[
319
- ... this.#validateFieldSchema(field, params[i]),
320
- ... await this.#validateClassLevel(field.type, params[i])
321
+ ... this.#validateInputSchema(param, params[i]),
322
+ ... await this.#validateClassLevel(param.type, params[i])
321
323
  ].map(x => {
322
- x.path = !prefixes[i] ? x.path.replace(`${field.name}.`, '') : x.path.replace(field.name, prefixes[i]!);
324
+ if (param.name && typeof param.name === 'string') {
325
+ x.path = !prefixes[i] ?
326
+ x.path.replace(`${param.name}.`, '') :
327
+ x.path.replace(param.name, prefixes[i]!.toString());
328
+ }
323
329
  return x;
324
330
  }));
325
331
  }
326
- for (const validator of SchemaRegistry.getMethodValidators(cls, method)) {
332
+ for (const validator of config.validators) {
327
333
  const res = await validator(...params);
328
334
  if (res) {
329
335
  if (Array.isArray(res)) {