@travetto/schema 4.1.0 → 5.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.
- package/README.md +1 -1
- package/package.json +3 -3
- package/src/bind-util.ts +7 -7
- package/src/data.ts +48 -9
- package/src/service/registry.ts +11 -0
- package/src/service/types.ts +2 -1
- package/src/types.ts +3 -1
- package/src/validate/validator.ts +3 -2
package/README.md
CHANGED
|
@@ -379,7 +379,7 @@ Validation Failed {
|
|
|
379
379
|
```
|
|
380
380
|
|
|
381
381
|
## Data Utilities
|
|
382
|
-
Data utilities for binding values, and type conversion. Currently [DataUtil](https://github.com/travetto/travetto/tree/main/module/schema/src/data.ts#
|
|
382
|
+
Data utilities for binding values, and type conversion. Currently [DataUtil](https://github.com/travetto/travetto/tree/main/module/schema/src/data.ts#L10) includes:
|
|
383
383
|
* `deepAssign(a, b, mode ?)` which allows for deep assignment of `b` onto `a`, the `mode` determines how aggressive the assignment is, and how flexible it is. `mode` can have any of the following values:
|
|
384
384
|
* `loose`, which is the default is the most lenient. It will not error out, and overwrites will always happen
|
|
385
385
|
* `coerce`, will attempt to force values from `b` to fit the types of `a`, and if it can't it will error out
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/schema",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0-rc.0",
|
|
4
4
|
"description": "Data type registry for runtime validation, reflection and binding.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"schema",
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
"directory": "module/schema"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@travetto/registry": "^
|
|
30
|
+
"@travetto/registry": "^5.0.0-rc.0"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@travetto/transformer": "^
|
|
33
|
+
"@travetto/transformer": "^5.0.0-rc.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependenciesMeta": {
|
|
36
36
|
"@travetto/transformer": {
|
package/src/bind-util.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Class, ConcreteClass, TypedObject
|
|
1
|
+
import { Class, ConcreteClass, TypedObject } from '@travetto/base';
|
|
2
2
|
|
|
3
3
|
import { DataUtil } from './data';
|
|
4
4
|
import { AllViewⲐ } from './internal/types';
|
|
@@ -50,7 +50,7 @@ export class BindUtil {
|
|
|
50
50
|
const out: Record<string, unknown> = {};
|
|
51
51
|
for (const k of Object.keys(obj)) {
|
|
52
52
|
const objK = obj[k];
|
|
53
|
-
const val =
|
|
53
|
+
const val = DataUtil.isPlainObject(objK) ? this.expandPaths(objK) : objK;
|
|
54
54
|
const parts = k.split('.');
|
|
55
55
|
const last = parts.pop()!;
|
|
56
56
|
let sub = out;
|
|
@@ -77,7 +77,7 @@ export class BindUtil {
|
|
|
77
77
|
const arr = last.indexOf('[') > 0;
|
|
78
78
|
|
|
79
79
|
if (!arr) {
|
|
80
|
-
if (sub[last] &&
|
|
80
|
+
if (sub[last] && DataUtil.isPlainObject(val)) {
|
|
81
81
|
sub[last] = DataUtil.deepAssign(sub[last], val, 'coerce');
|
|
82
82
|
} else {
|
|
83
83
|
sub[last] = val;
|
|
@@ -95,7 +95,7 @@ export class BindUtil {
|
|
|
95
95
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
96
96
|
key = arrSub.length as number;
|
|
97
97
|
}
|
|
98
|
-
if (arrSub[key] &&
|
|
98
|
+
if (arrSub[key] && DataUtil.isPlainObject(val) && DataUtil.isPlainObject(arrSub[key])) {
|
|
99
99
|
arrSub[key] = DataUtil.deepAssign(arrSub[key], val, 'coerce');
|
|
100
100
|
} else {
|
|
101
101
|
arrSub[key] = val;
|
|
@@ -114,13 +114,13 @@ export class BindUtil {
|
|
|
114
114
|
const out: Record<string, V> = {};
|
|
115
115
|
for (const [key, value] of Object.entries(data)) {
|
|
116
116
|
const pre = `${prefix}${key}`;
|
|
117
|
-
if (
|
|
117
|
+
if (DataUtil.isPlainObject(value)) {
|
|
118
118
|
Object.assign(out, this.flattenPaths(value, `${pre}.`)
|
|
119
119
|
);
|
|
120
120
|
} else if (Array.isArray(value)) {
|
|
121
121
|
for (let i = 0; i < value.length; i++) {
|
|
122
122
|
const v = value[i];
|
|
123
|
-
if (
|
|
123
|
+
if (DataUtil.isPlainObject(v)) {
|
|
124
124
|
Object.assign(out, this.flattenPaths(v, `${pre}[${i}].`));
|
|
125
125
|
} else {
|
|
126
126
|
out[`${pre}[${i}]`] = v ?? '';
|
|
@@ -179,7 +179,7 @@ export class BindUtil {
|
|
|
179
179
|
const view = cfg.view ?? AllViewⲐ; // Does not convey
|
|
180
180
|
delete cfg.view;
|
|
181
181
|
|
|
182
|
-
if (!!data && !
|
|
182
|
+
if (!!data && !DataUtil.isPrimitive(data)) {
|
|
183
183
|
const conf = SchemaRegistry.get(cons);
|
|
184
184
|
|
|
185
185
|
// If no configuration
|
package/src/data.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isNumberObject as isNum, isBooleanObject as isBool, isStringObject as isStr } from 'node:util/types';
|
|
2
|
+
|
|
3
|
+
import { Class, ClassInstance, TypedObject } from '@travetto/base';
|
|
2
4
|
|
|
3
5
|
const REGEX_PAT = /[\/](.*)[\/](i|g|m|s)?/;
|
|
4
6
|
|
|
@@ -7,13 +9,44 @@ const REGEX_PAT = /[\/](.*)[\/](i|g|m|s)?/;
|
|
|
7
9
|
*/
|
|
8
10
|
export class DataUtil {
|
|
9
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Is a value a plain JS object, created using {}
|
|
14
|
+
* @param obj Object to check
|
|
15
|
+
*/
|
|
16
|
+
static isPlainObject(obj: unknown): obj is Record<string, unknown> {
|
|
17
|
+
return typeof obj === 'object' // separate from primitives
|
|
18
|
+
&& obj !== undefined
|
|
19
|
+
&& obj !== null // is obvious
|
|
20
|
+
&& obj.constructor === Object // separate instances (Array, DOM, ...)
|
|
21
|
+
&& Object.prototype.toString.call(obj) === '[object Object]'; // separate build-in like Math
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Is a value of primitive type
|
|
26
|
+
* @param el Value to check
|
|
27
|
+
*/
|
|
28
|
+
static isPrimitive(el: unknown): el is (string | boolean | number | RegExp) {
|
|
29
|
+
switch (typeof el) {
|
|
30
|
+
case 'string': case 'boolean': case 'number': case 'bigint': return true;
|
|
31
|
+
case 'object': return !!el && (el instanceof RegExp || el instanceof Date || isStr(el) || isNum(el) || isBool(el));
|
|
32
|
+
default: return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Is simple, as a primitive, function or class
|
|
38
|
+
*/
|
|
39
|
+
static isSimpleValue(a: unknown): a is Function | Class | string | number | RegExp | Date {
|
|
40
|
+
return this.isPrimitive(a) || typeof a === 'function';
|
|
41
|
+
}
|
|
42
|
+
|
|
10
43
|
static #deepAssignRaw(a: unknown, b: unknown, mode: 'replace' | 'loose' | 'strict' | 'coerce' = 'loose'): unknown {
|
|
11
44
|
const isEmptyA = a === undefined || a === null;
|
|
12
45
|
const isEmptyB = b === undefined || b === null;
|
|
13
46
|
const isArrA = Array.isArray(a);
|
|
14
47
|
const isArrB = Array.isArray(b);
|
|
15
|
-
const isSimpA = !isEmptyA &&
|
|
16
|
-
const isSimpB = !isEmptyB &&
|
|
48
|
+
const isSimpA = !isEmptyA && this.isSimpleValue(a);
|
|
49
|
+
const isSimpB = !isEmptyB && this.isSimpleValue(b);
|
|
17
50
|
|
|
18
51
|
let ret: unknown;
|
|
19
52
|
|
|
@@ -108,9 +141,15 @@ export class DataUtil {
|
|
|
108
141
|
|
|
109
142
|
switch (type) {
|
|
110
143
|
case Date: {
|
|
111
|
-
|
|
144
|
+
let res: Date | undefined;
|
|
145
|
+
if (typeof input === 'object' && 'toDate' in input && typeof input.toDate === 'function') {
|
|
112
146
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
113
|
-
|
|
147
|
+
res = input.toDate() as Date;
|
|
148
|
+
} else {
|
|
149
|
+
res = typeof input === 'number' || /^[-]?\d+$/.test(`${input}`) ?
|
|
150
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
151
|
+
new Date(parseInt(input as string, 10)) : new Date(input as Date);
|
|
152
|
+
}
|
|
114
153
|
if (strict && Number.isNaN(res.getTime())) {
|
|
115
154
|
throw new Error(`Invalid date value: ${input}`);
|
|
116
155
|
}
|
|
@@ -148,7 +187,7 @@ export class DataUtil {
|
|
|
148
187
|
}
|
|
149
188
|
}
|
|
150
189
|
case Object: {
|
|
151
|
-
if (!strict ||
|
|
190
|
+
if (!strict || this.isPlainObject(input)) {
|
|
152
191
|
return input;
|
|
153
192
|
} else {
|
|
154
193
|
throw new Error('Invalid object type');
|
|
@@ -157,7 +196,7 @@ export class DataUtil {
|
|
|
157
196
|
case undefined:
|
|
158
197
|
case String: return `${input}`;
|
|
159
198
|
}
|
|
160
|
-
if (!strict ||
|
|
199
|
+
if (!strict || this.isPlainObject(input)) {
|
|
161
200
|
return input;
|
|
162
201
|
} else {
|
|
163
202
|
throw new Error(`Unknown type ${type.name}`);
|
|
@@ -170,7 +209,7 @@ export class DataUtil {
|
|
|
170
209
|
*/
|
|
171
210
|
static shallowClone<T = unknown>(a: T): T {
|
|
172
211
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
173
|
-
return (Array.isArray(a) ? a.slice(0) : (
|
|
212
|
+
return (Array.isArray(a) ? a.slice(0) : (this.isSimpleValue(a) ? a : { ...(a as {}) })) as T;
|
|
174
213
|
}
|
|
175
214
|
|
|
176
215
|
/**
|
|
@@ -180,7 +219,7 @@ export class DataUtil {
|
|
|
180
219
|
* @param mode How the assignment should be handled
|
|
181
220
|
*/
|
|
182
221
|
static deepAssign<T, U>(a: T, b: U, mode: | 'replace' | 'loose' | 'strict' | 'coerce' = 'loose'): T & U {
|
|
183
|
-
if (!a ||
|
|
222
|
+
if (!a || this.isSimpleValue(a)) {
|
|
184
223
|
throw new Error(`Cannot merge onto a simple value, ${a}`);
|
|
185
224
|
}
|
|
186
225
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
package/src/service/registry.ts
CHANGED
|
@@ -411,6 +411,17 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
411
411
|
// Write views out
|
|
412
412
|
config = this.finalizeViews(cls, config);
|
|
413
413
|
|
|
414
|
+
if (config.subTypeName && config.subTypeField in config.views[AllViewⲐ].schema) {
|
|
415
|
+
const field = config.views[AllViewⲐ].schema[config.subTypeField];
|
|
416
|
+
config.views[AllViewⲐ].schema[config.subTypeField] = {
|
|
417
|
+
...field,
|
|
418
|
+
enum: {
|
|
419
|
+
values: [config.subTypeName],
|
|
420
|
+
message: `${config.subTypeField} can only be '${config.subTypeName}'`,
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
414
425
|
return config;
|
|
415
426
|
}
|
|
416
427
|
|
package/src/service/types.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
export type Primitive = number | bigint | boolean | string | Date;
|
|
2
|
+
|
|
1
3
|
export type DeepPartial<T> = {
|
|
2
|
-
[P in keyof T]?: (T[P] extends (
|
|
4
|
+
[P in keyof T]?: (T[P] extends (Primitive | undefined) ? (T[P] | undefined) :
|
|
3
5
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4
6
|
(T[P] extends any[] ? (DeepPartial<T[P][number]> | null | undefined)[] : DeepPartial<T[P]>));
|
|
5
7
|
};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { Class, ClassInstance, TypedObject
|
|
1
|
+
import { Class, ClassInstance, TypedObject } from '@travetto/base';
|
|
2
2
|
|
|
3
3
|
import { FieldConfig, SchemaConfig } from '../service/types';
|
|
4
4
|
import { SchemaRegistry } from '../service/registry';
|
|
5
5
|
import { ValidationError, ValidationKindCore, ValidationResult } from './types';
|
|
6
6
|
import { Messages } from './messages';
|
|
7
7
|
import { isValidationError, TypeMismatchError, ValidationResultError } from './error';
|
|
8
|
+
import { DataUtil } from '../data';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Get the schema config for Class/Schema config, including support for polymorphism
|
|
@@ -268,7 +269,7 @@ export class SchemaValidator {
|
|
|
268
269
|
*/
|
|
269
270
|
static async validate<T>(cls: Class<T>, o: T, view?: string): Promise<T> {
|
|
270
271
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
271
|
-
if (!
|
|
272
|
+
if (!DataUtil.isPlainObject(o) && !(o instanceof cls || cls.Ⲑid === (o as ClassInstance<T>).constructor.Ⲑid)) {
|
|
272
273
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
273
274
|
throw new TypeMismatchError(cls.name, (o as ClassInstance).constructor.name);
|
|
274
275
|
}
|