@travetto/schema 2.1.3 → 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.
- package/README.md +58 -56
- package/package.json +3 -3
- package/src/bind-util.ts +28 -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 +49 -43
- package/src/service/types.ts +2 -2
- package/src/typings.d.ts +8 -1
- package/src/validate/error.ts +4 -0
- package/src/validate/regexp.ts +1 -0
- 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 +13 -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,11 @@ export class SchemaTransformUtil {
|
|
|
57
57
|
if (type.commonType) {
|
|
58
58
|
return this.toConcreteType(state, type.commonType, node, root);
|
|
59
59
|
}
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
case 'unknown':
|
|
63
|
+
default: {
|
|
64
|
+
// Object
|
|
60
65
|
}
|
|
61
66
|
}
|
|
62
67
|
return state.createIdentifier('Object');
|
|
@@ -115,7 +120,7 @@ export class SchemaTransformUtil {
|
|
|
115
120
|
|
|
116
121
|
if (ts.isParameter(node)) {
|
|
117
122
|
const comments = DocUtil.describeDocs(node.parent);
|
|
118
|
-
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()) || {};
|
|
119
124
|
if (commentConfig.description) {
|
|
120
125
|
attrs.push(state.factory.createPropertyAssignment('description', state.fromLiteral(commentConfig.description)));
|
|
121
126
|
}
|
|
@@ -148,15 +153,19 @@ export class SchemaTransformUtil {
|
|
|
148
153
|
})));
|
|
149
154
|
}
|
|
150
155
|
|
|
156
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
151
157
|
return state.factory.updatePropertyDeclaration(node as Exclude<typeof node, T>,
|
|
152
158
|
newDecs, node.modifiers, node.name, node.questionToken, node.type, node.initializer) as T;
|
|
153
159
|
} else if (ts.isParameter(node)) {
|
|
160
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
154
161
|
return state.factory.updateParameterDeclaration(node as Exclude<typeof node, T>,
|
|
155
162
|
newDecs, node.modifiers, node.dotDotDotToken, node.name, node.questionToken, node.type, node.initializer) as T;
|
|
156
163
|
} else if (ts.isGetAccessorDeclaration(node)) {
|
|
164
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
157
165
|
return state.factory.updateGetAccessorDeclaration(node as Exclude<typeof node, T>,
|
|
158
166
|
newDecs, node.modifiers, node.name, node.parameters, node.type, node.body) as T;
|
|
159
167
|
} else {
|
|
168
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
160
169
|
return state.factory.updateSetAccessorDeclaration(node as Exclude<typeof node, T>,
|
|
161
170
|
newDecs, node.modifiers, node.name, node.parameters, node.body) as T;
|
|
162
171
|
}
|
|
@@ -165,7 +174,7 @@ export class SchemaTransformUtil {
|
|
|
165
174
|
/**
|
|
166
175
|
* Unwrap type
|
|
167
176
|
*/
|
|
168
|
-
static unwrapType(type: AnyType) {
|
|
177
|
+
static unwrapType(type: AnyType): { out: Record<string, unknown>, type: AnyType } {
|
|
169
178
|
const out: Record<string, unknown> = {};
|
|
170
179
|
|
|
171
180
|
while (type?.key === 'literal' && type.typeArguments?.length) {
|
|
@@ -205,7 +214,7 @@ export class SchemaTransformUtil {
|
|
|
205
214
|
* @param methodName
|
|
206
215
|
* @returns
|
|
207
216
|
*/
|
|
208
|
-
static findInnerReturnMethod(state: TransformerState, node: ts.MethodDeclaration, methodName: string) {
|
|
217
|
+
static findInnerReturnMethod(state: TransformerState, node: ts.MethodDeclaration, methodName: string): ts.MethodDeclaration | undefined {
|
|
209
218
|
// Process returnType
|
|
210
219
|
const { type } = this.unwrapType(state.resolveReturnType(node));
|
|
211
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());
|