@travetto/schema 2.1.5 → 2.2.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,18 +2,19 @@ import { Class, ClassInstance, Util } from '@travetto/base';
2
2
 
3
3
  import { FieldConfig, SchemaConfig } from '../service/types';
4
4
  import { SchemaRegistry } from '../service/registry';
5
- import { ValidationError, ValidationKind, ValidationResult } from './types';
5
+ import { ValidationError, ValidationKindCore, ValidationResult } from './types';
6
6
  import { Messages } from './messages';
7
- import { TypeMismatchError, ValidationResultError } from './error';
7
+ import { isValidationError, TypeMismatchError, ValidationResultError } from './error';
8
8
 
9
9
  /**
10
10
  * Get the schema config for Class/Schema config, including support for polymorphism
11
11
  * @param base The starting type or config
12
12
  * @param o The value to use for the polymorphic check
13
13
  */
14
- function resolveSchema<T>(base: Class<T>, o: T, view?: string) {
14
+ function resolveSchema<T>(base: Class<T>, o: T, view?: string): SchemaConfig {
15
15
  return SchemaRegistry.getViewSchema(
16
- SchemaRegistry.resolveSubTypeForInstance(base, o), view).schema;
16
+ SchemaRegistry.resolveSubTypeForInstance(base, o), view
17
+ ).schema;
17
18
  }
18
19
 
19
20
  declare global {
@@ -34,11 +35,13 @@ export class SchemaValidator {
34
35
  * @param o The object to validate
35
36
  * @param relative The relative path as the validation recurses
36
37
  */
37
- static #validateSchema<T>(schema: SchemaConfig, o: T, relative: string) {
38
+ static #validateSchema<T>(schema: SchemaConfig, o: T, relative: string): ValidationError[] {
38
39
  let errors: ValidationError[] = [];
39
40
 
40
- for (const field of Object.keys(schema)) {
41
+ const fields: (keyof SchemaConfig)[] = Object.keys(schema);
42
+ for (const field of fields) {
41
43
  if (schema[field].access !== 'readonly') { // Do not validate readonly fields
44
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
42
45
  errors = errors.concat(this.#validateFieldSchema(schema[field], o[field as keyof T], relative));
43
46
  }
44
47
  }
@@ -52,7 +55,7 @@ export class SchemaValidator {
52
55
  * @param val The raw value, could be an array or not
53
56
  * @param relative The relative path of object traversal
54
57
  */
55
- static #validateFieldSchema(fieldSchema: FieldConfig, val: unknown, relative: string = '') {
58
+ static #validateFieldSchema(fieldSchema: FieldConfig, val: unknown, relative: string = ''): ValidationError[] {
56
59
  const path = `${relative}${relative && '.'}${fieldSchema.name}`;
57
60
 
58
61
  const hasValue = !(val === undefined || val === null || (typeof val === 'string' && val === '') || (Array.isArray(val) && val.length === 0));
@@ -101,11 +104,15 @@ export class SchemaValidator {
101
104
  * @param key The bounds to check
102
105
  * @param value The value to validate
103
106
  */
104
- static #validateRange(field: FieldConfig, key: 'min' | 'max', value: string | number | Date) {
107
+ static #validateRange(field: FieldConfig, key: 'min' | 'max', rawValue: string | number | Date | unknown): boolean {
108
+
109
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
110
+ let value = rawValue as string | number | Date;
111
+
105
112
  const f = field[key]!;
106
113
  if (typeof f.n === 'number') {
107
- if (typeof value !== 'number') {
108
- value = parseInt(value as string, 10);
114
+ if (typeof value === 'string') {
115
+ value = parseInt(value, 10);
109
116
  }
110
117
  if (field.type === Date) {
111
118
  value = new Date(value);
@@ -132,7 +139,7 @@ export class SchemaValidator {
132
139
  * @param value The actual value
133
140
  */
134
141
  static #validateField(field: FieldConfig, value: unknown): ValidationResult[] {
135
- const criteria: ValidationKind[] = [];
142
+ const criteria: ([string, FieldConfig[ValidationKindCore]] | [string])[] = [];
136
143
 
137
144
  if (
138
145
  (field.type === String && (typeof value !== 'string')) ||
@@ -140,7 +147,7 @@ export class SchemaValidator {
140
147
  (field.type === Date && (!(value instanceof Date) || Number.isNaN(value.getTime()))) ||
141
148
  (field.type === Boolean && typeof value !== 'boolean')
142
149
  ) {
143
- criteria.push('type');
150
+ criteria.push(['type']);
144
151
  return [{ kind: 'type', type: field.type.name.toLowerCase() }];
145
152
  }
146
153
 
@@ -150,38 +157,37 @@ export class SchemaValidator {
150
157
  case undefined: break;
151
158
  case 'type': return [{ kind, type: field.type.name }];
152
159
  default:
153
- criteria.push(kind as 'type');
160
+ criteria.push([kind]);
154
161
  }
155
162
  }
156
163
 
157
164
  if (field.match && !field.match.re.test(`${value}`)) {
158
- criteria.push('match');
165
+ criteria.push(['match', field.match]);
159
166
  }
160
167
 
161
168
  if (field.minlength && `${value}`.length < field.minlength.n) {
162
- criteria.push('minlength');
169
+ criteria.push(['minlength', field.minlength]);
163
170
  }
164
171
 
165
172
  if (field.maxlength && `${value}`.length > field.maxlength.n) {
166
- criteria.push('maxlength');
173
+ criteria.push(['maxlength', field.maxlength]);
167
174
  }
168
175
 
176
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
169
177
  if (field.enum && !field.enum.values.includes(value as string)) {
170
- criteria.push('enum');
178
+ criteria.push(['enum', field.enum]);
171
179
  }
172
180
 
173
- if (field.min && this.#validateRange(field, 'min', value as number)) {
174
- criteria.push('min');
181
+ if (field.min && this.#validateRange(field, 'min', value)) {
182
+ criteria.push(['min', field.min]);
175
183
  }
176
184
 
177
- if (field.max && this.#validateRange(field, 'max', value as number)) {
178
- criteria.push('max');
185
+ if (field.max && this.#validateRange(field, 'max', value)) {
186
+ criteria.push(['max', field.max]);
179
187
  }
180
188
 
181
189
  const errors: ValidationResult[] = [];
182
- for (const key of criteria) {
183
- const block = field[key as keyof FieldConfig];
184
- // @ts-expect-error
190
+ for (const [key, block] of criteria) {
185
191
  errors.push({ ...block, kind: key, value });
186
192
  }
187
193
 
@@ -196,24 +202,29 @@ export class SchemaValidator {
196
202
  static #prepareErrors(path: string, results: ValidationResult[]): ValidationError[] {
197
203
  const out: ValidationError[] = [];
198
204
  for (const res of results) {
199
- const err: Partial<ValidationError> = {
200
- ...(res as ValidationError),
201
- path
205
+ const err: ValidationError = {
206
+ kind: res.kind,
207
+ value: res.value,
208
+ message: '',
209
+ re: res.re?.name ?? res.re?.source ?? '',
210
+ path,
211
+ type: (typeof res.type === 'function' ? res.type.name : res.type)
202
212
  };
203
213
 
204
- const msg = res.message ??
205
- Messages.get(res.re?.name ?? res.re?.source ?? '') ??
206
- Messages.get(res.kind) ??
207
- Messages.get('default')!;
208
-
209
- if (res.re) {
210
- err.re = res.re?.name ?? res.re?.source ?? '';
214
+ if (!err.re) {
215
+ delete err.re;
211
216
  }
212
217
 
218
+ const msg = res.message ?? (
219
+ Messages.get(err.re ?? '') ??
220
+ Messages.get(err.kind) ??
221
+ Messages.get('default')!
222
+ );
223
+
213
224
  err.message = msg
214
- .replace(/\{([^}]+)\}/g, (a: string, k: string) => `${err[k as (keyof ValidationError)]}`);
225
+ .replace(/\{([^}]+)\}/g, (_, k: (keyof ValidationError)) => `${err[k]}`);
215
226
 
216
- out.push(err as ValidationError);
227
+ out.push(err);
217
228
  }
218
229
  return out;
219
230
  }
@@ -225,8 +236,10 @@ export class SchemaValidator {
225
236
  * @param view The optional view to limit the scope to
226
237
  */
227
238
  static async validate<T>(cls: Class<T>, o: T, view?: string): Promise<T> {
239
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
228
240
  if (!Util.isPlainObject(o) && !(o instanceof cls || cls.ᚕid === (o as ClassInstance<T>).constructor.ᚕid)) {
229
- throw new TypeMismatchError(cls.name, (o as unknown as ClassInstance).constructor.name);
241
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
242
+ throw new TypeMismatchError(cls.name, (o as ClassInstance).constructor.name);
230
243
  }
231
244
  cls = SchemaRegistry.resolveSubTypeForInstance(cls, o);
232
245
 
@@ -243,8 +256,12 @@ export class SchemaValidator {
243
256
  if (res) {
244
257
  errors.push(res);
245
258
  }
246
- } catch (err) {
247
- errors.push(err);
259
+ } catch (err: unknown) {
260
+ if (isValidationError(err)) {
261
+ errors.push(err);
262
+ } else {
263
+ throw err;
264
+ }
248
265
  }
249
266
  }
250
267
 
@@ -276,12 +293,12 @@ export class SchemaValidator {
276
293
  static async validatePartial<T>(cls: Class<T>, o: T, view?: string): Promise<T> {
277
294
  try {
278
295
  await this.validate(cls, o, view);
279
- } catch (e) {
280
- if (e instanceof ValidationResultError) { // Don't check required fields
281
- const errs = e.errors.filter(x => x.kind !== 'required');
296
+ } catch (err) {
297
+ if (err instanceof ValidationResultError) { // Don't check required fields
298
+ const errs = err.errors.filter(x => x.kind !== 'required');
282
299
  if (errs.length) {
283
- e.errors = errs;
284
- throw e;
300
+ err.errors = errs;
301
+ throw err;
285
302
  }
286
303
  }
287
304
  }
@@ -295,7 +312,7 @@ export class SchemaValidator {
295
312
  * @param method The method being invoked
296
313
  * @param params The params to validate
297
314
  */
298
- static validateMethod<T>(cls: Class<T>, method: string, params: unknown[]) {
315
+ static validateMethod<T>(cls: Class<T>, method: string, params: unknown[]): void {
299
316
  const errors: ValidationError[] = [];
300
317
  for (const field of SchemaRegistry.getMethodSchema(cls, method)) {
301
318
  errors.push(...this.#validateFieldSchema(field, params[field.index!]));
@@ -4,7 +4,7 @@
4
4
  export const init = {
5
5
  key: '@trv:schema/init',
6
6
  after: ['@trv:registry/init'], // Should be global
7
- action: async () => {
7
+ action: async (): Promise<void> => {
8
8
  const { BindUtil } = await import('../src/bind-util');
9
9
  BindUtil.register();
10
10
  }
@@ -48,7 +48,7 @@ export class SchemaTransformUtil {
48
48
  ), { type: v, root })
49
49
  )
50
50
  );
51
- cls.getText = () => '';
51
+ cls.getText = (): string => '';
52
52
  state.addStatement(cls, root || node);
53
53
  }
54
54
  return id;
@@ -57,6 +57,7 @@ export class SchemaTransformUtil {
57
57
  if (type.commonType) {
58
58
  return this.toConcreteType(state, type.commonType, node, root);
59
59
  }
60
+ break;
60
61
  }
61
62
  case 'unknown':
62
63
  default: {
@@ -119,7 +120,7 @@ export class SchemaTransformUtil {
119
120
 
120
121
  if (ts.isParameter(node)) {
121
122
  const comments = DocUtil.describeDocs(node.parent);
122
- const commentConfig = (comments.params ?? []).find(x => x.name === node.name.getText()) || {} as Partial<ParamDocumentation>;
123
+ const commentConfig: Partial<ParamDocumentation> = (comments.params ?? []).find(x => x.name === node.name.getText()) || {};
123
124
  if (commentConfig.description) {
124
125
  attrs.push(state.factory.createPropertyAssignment('description', state.fromLiteral(commentConfig.description)));
125
126
  }
@@ -152,15 +153,19 @@ export class SchemaTransformUtil {
152
153
  })));
153
154
  }
154
155
 
156
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
155
157
  return state.factory.updatePropertyDeclaration(node as Exclude<typeof node, T>,
156
158
  newDecs, node.modifiers, node.name, node.questionToken, node.type, node.initializer) as T;
157
159
  } else if (ts.isParameter(node)) {
160
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
158
161
  return state.factory.updateParameterDeclaration(node as Exclude<typeof node, T>,
159
162
  newDecs, node.modifiers, node.dotDotDotToken, node.name, node.questionToken, node.type, node.initializer) as T;
160
163
  } else if (ts.isGetAccessorDeclaration(node)) {
164
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
161
165
  return state.factory.updateGetAccessorDeclaration(node as Exclude<typeof node, T>,
162
166
  newDecs, node.modifiers, node.name, node.parameters, node.type, node.body) as T;
163
167
  } else {
168
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
164
169
  return state.factory.updateSetAccessorDeclaration(node as Exclude<typeof node, T>,
165
170
  newDecs, node.modifiers, node.name, node.parameters, node.body) as T;
166
171
  }
@@ -169,7 +174,7 @@ export class SchemaTransformUtil {
169
174
  /**
170
175
  * Unwrap type
171
176
  */
172
- static unwrapType(type: AnyType) {
177
+ static unwrapType(type: AnyType): { out: Record<string, unknown>, type: AnyType } {
173
178
  const out: Record<string, unknown> = {};
174
179
 
175
180
  while (type?.key === 'literal' && type.typeArguments?.length) {
@@ -209,7 +214,7 @@ export class SchemaTransformUtil {
209
214
  * @param methodName
210
215
  * @returns
211
216
  */
212
- static findInnerReturnMethod(state: TransformerState, node: ts.MethodDeclaration, methodName: string) {
217
+ static findInnerReturnMethod(state: TransformerState, node: ts.MethodDeclaration, methodName: string): ts.MethodDeclaration | undefined {
213
218
  // Process returnType
214
219
  const { type } = this.unwrapType(state.resolveReturnType(node));
215
220
  let cls;
@@ -28,7 +28,7 @@ export class SchemaTransformer {
28
28
  * Track schema on start
29
29
  */
30
30
  @OnClass('Schema')
31
- static startSchema(state: AutoState & TransformerState, node: ts.ClassDeclaration, dec?: DecoratorMeta) {
31
+ static startSchema(state: AutoState & TransformerState, node: ts.ClassDeclaration, dec?: DecoratorMeta): ts.ClassDeclaration {
32
32
  state[inSchema] = true;
33
33
  state[accessors] = new Set();
34
34
  return node;
@@ -38,7 +38,7 @@ export class SchemaTransformer {
38
38
  * Mark the end of the schema, document
39
39
  */
40
40
  @AfterClass('Schema')
41
- static finalizeSchema(state: AutoState & TransformerState, node: ts.ClassDeclaration) {
41
+ static finalizeSchema(state: AutoState & TransformerState, node: ts.ClassDeclaration): ts.ClassDeclaration {
42
42
  const decls = [...(node.decorators ?? [])];
43
43
 
44
44
  const comments = DocUtil.describeDocs(node);
@@ -71,7 +71,7 @@ export class SchemaTransformer {
71
71
  * Handle all properties, while in schema
72
72
  */
73
73
  @OnProperty()
74
- static processSchemaField(state: TransformerState & AutoState, node: ts.PropertyDeclaration) {
74
+ static processSchemaField(state: TransformerState & AutoState, node: ts.PropertyDeclaration): ts.PropertyDeclaration {
75
75
  const ignore = state.findDecorator(this, node, 'Ignore');
76
76
  return state[inSchema] && !ignore && DeclarationUtil.isPublic(node) ?
77
77
  SchemaTransformUtil.computeField(state, node) : node;
@@ -81,7 +81,7 @@ export class SchemaTransformer {
81
81
  * Handle getters
82
82
  */
83
83
  @OnGetter()
84
- static processSchemaGetter(state: TransformerState & AutoState, node: ts.GetAccessorDeclaration) {
84
+ static processSchemaGetter(state: TransformerState & AutoState, node: ts.GetAccessorDeclaration): ts.GetAccessorDeclaration {
85
85
  const ignore = state.findDecorator(this, node, 'Ignore');
86
86
  if (state[inSchema] && !ignore && DeclarationUtil.isPublic(node) && !state[accessors]?.has(node.name.getText())) {
87
87
  state[accessors]?.add(node.name.getText());
@@ -94,7 +94,7 @@ export class SchemaTransformer {
94
94
  * Handle setters
95
95
  */
96
96
  @OnSetter()
97
- static processSchemaSetter(state: TransformerState & AutoState, node: ts.SetAccessorDeclaration) {
97
+ static processSchemaSetter(state: TransformerState & AutoState, node: ts.SetAccessorDeclaration): ts.SetAccessorDeclaration {
98
98
  const ignore = state.findDecorator(this, node, 'Ignore');
99
99
  if (state[inSchema] && !ignore && DeclarationUtil.isPublic(node) && !state[accessors]?.has(node.name.getText())) {
100
100
  state[accessors]?.add(node.name.getText());