@travetto/schema 2.1.5 → 2.2.2
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.
- package/README.md +58 -56
- package/package.json +3 -3
- package/src/bind-util.ts +27 -15
- package/src/decorator/common.ts +8 -4
- package/src/decorator/field.ts +39 -33
- package/src/decorator/schema.ts +4 -3
- package/src/extension/faker.ts +56 -50
- package/src/service/changes.ts +9 -9
- package/src/service/registry.ts +47 -41
- package/src/service/types.ts +3 -3
- package/src/typings.d.ts +8 -1
- package/src/validate/error.ts +4 -0
- package/src/validate/regexp.ts +1 -1
- package/src/validate/types.ts +2 -1
- package/src/validate/validator.ts +62 -45
- package/support/phase.init.ts +1 -1
- package/support/transform-util.ts +9 -4
- package/support/transformer.schema.ts +5 -5
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
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',
|
|
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
|
|
108
|
-
value = parseInt(value
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
205
|
-
|
|
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, (
|
|
225
|
+
.replace(/\{([^}]+)\}/g, (_, k: (keyof ValidationError)) => `${err[k]}`);
|
|
215
226
|
|
|
216
|
-
out.push(err
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
280
|
-
if (
|
|
281
|
-
const errs =
|
|
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
|
-
|
|
284
|
-
throw
|
|
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!]));
|
package/support/phase.init.ts
CHANGED
|
@@ -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()) || {}
|
|
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());
|