@travetto/schema 3.1.1 → 3.1.3
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/package.json +2 -2
- package/src/decorator/schema.ts +4 -4
- package/src/service/registry.ts +77 -67
- package/src/service/types.ts +10 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/schema",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.3",
|
|
4
4
|
"description": "Data type registry for runtime validation, reflection and binding.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"schema",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"@travetto/registry": "^3.1.1"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@travetto/transformer": "^3.1.
|
|
33
|
+
"@travetto/transformer": "^3.1.2"
|
|
34
34
|
},
|
|
35
35
|
"peerDependenciesMeta": {
|
|
36
36
|
"@travetto/transformer": {
|
package/src/decorator/schema.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Class } from '@travetto/base';
|
|
|
2
2
|
|
|
3
3
|
import { BindUtil } from '../bind-util';
|
|
4
4
|
import { SchemaRegistry } from '../service/registry';
|
|
5
|
-
import { ViewFieldsConfig } from '../service/types';
|
|
5
|
+
import { ClassConfig, ViewFieldsConfig } from '../service/types';
|
|
6
6
|
import { DeepPartial } from '../types';
|
|
7
7
|
import { ValidatorFn } from '../validate/types';
|
|
8
8
|
|
|
@@ -11,12 +11,12 @@ import { ValidatorFn } from '../validate/types';
|
|
|
11
11
|
*
|
|
12
12
|
* @augments `@travetto/schema:Schema`
|
|
13
13
|
*/
|
|
14
|
-
export function Schema() { // Auto is used during compilation
|
|
14
|
+
export function Schema(cfg?: Partial<Pick<ClassConfig, 'subTypeField' | 'baseType'>>) { // Auto is used during compilation
|
|
15
15
|
return <T, U extends Class<T>>(target: U): U => {
|
|
16
16
|
target.from ??= function <V>(this: Class<V>, data: DeepPartial<V>, view?: string): V {
|
|
17
17
|
return BindUtil.bindSchema(this, data, { view });
|
|
18
18
|
};
|
|
19
|
-
SchemaRegistry.
|
|
19
|
+
SchemaRegistry.register(target, cfg);
|
|
20
20
|
return target;
|
|
21
21
|
};
|
|
22
22
|
}
|
|
@@ -49,7 +49,7 @@ export function View<T>(name: string, fields: ViewFieldsConfig<Partial<T>>) {
|
|
|
49
49
|
* @param name
|
|
50
50
|
* @returns
|
|
51
51
|
*/
|
|
52
|
-
export function SubType<T>(name
|
|
52
|
+
export function SubType<T>(name?: string) {
|
|
53
53
|
return (target: Class<Partial<T>>): void => {
|
|
54
54
|
SchemaRegistry.registerSubTypes(target, name);
|
|
55
55
|
};
|
package/src/service/registry.ts
CHANGED
|
@@ -1,23 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RootIndex } from '@travetto/manifest';
|
|
2
|
+
import { Class, AppError } from '@travetto/base';
|
|
2
3
|
import { MetadataRegistry, RootRegistry, ChangeEvent } from '@travetto/registry';
|
|
3
4
|
|
|
4
5
|
import { ClassList, FieldConfig, ClassConfig, SchemaConfig, ViewFieldsConfig, ViewConfig } from './types';
|
|
5
6
|
import { SchemaChangeListener } from './changes';
|
|
6
7
|
import { AllViewⲐ } from '../internal/types';
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
function isWithType<T>(o: T, cfg: ClassConfig | undefined): o is T & { type?: string } {
|
|
14
|
-
return !!cfg && !!cfg.subType && 'type' in cfg.views[AllViewⲐ].schema;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function getConstructor<T>(o: T): ConcreteClass<T> {
|
|
18
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
19
|
-
return (o as unknown as ClassInstance<T>).constructor;
|
|
20
|
-
}
|
|
9
|
+
const classToSubTypeName = (cls: Class): string => cls.name
|
|
10
|
+
.replace(/([A-Z])([A-Z][a-z])/g, (all, l, r) => `${l}_${r.toLowerCase()}`)
|
|
11
|
+
.replace(/([a-z]|\b)([A-Z])/g, (all, l, r) => l ? `${l}_${r.toLowerCase()}` : r.toLowerCase())
|
|
12
|
+
.toLowerCase();
|
|
21
13
|
|
|
22
14
|
/**
|
|
23
15
|
* Schema registry for listening to changes
|
|
@@ -26,21 +18,29 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
26
18
|
|
|
27
19
|
#accessorDescriptors = new Map<Class, Map<string, PropertyDescriptor>>();
|
|
28
20
|
#subTypes = new Map<Class, Map<string, Class>>();
|
|
29
|
-
#typeKeys = new Map<Class, string>();
|
|
30
21
|
#pendingViews = new Map<Class, Map<string, ViewFieldsConfig<unknown>>>();
|
|
22
|
+
#baseSchema = new Map<Class, Class>();
|
|
31
23
|
|
|
32
24
|
constructor() {
|
|
33
25
|
super(RootRegistry);
|
|
34
26
|
}
|
|
35
27
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Find base schema class for a given class
|
|
30
|
+
*/
|
|
31
|
+
getBaseSchema(cls: Class): Class {
|
|
32
|
+
if (!this.#baseSchema.has(cls)) {
|
|
33
|
+
let conf = this.get(cls) ?? this.getOrCreatePending(cls);
|
|
34
|
+
let parent = cls;
|
|
35
|
+
|
|
36
|
+
while (conf && !conf.baseType) {
|
|
37
|
+
parent = this.getParentClass(parent)!;
|
|
38
|
+
conf = this.get(parent) ?? this.pending.get(MetadataRegistry.id(parent));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.#baseSchema.set(cls, conf ? parent : cls);
|
|
42
42
|
}
|
|
43
|
-
return this.#
|
|
43
|
+
return this.#baseSchema.get(cls)!;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
@@ -48,9 +48,7 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
48
48
|
* @param cls Base class
|
|
49
49
|
*/
|
|
50
50
|
getSubTypeName(cls: Class): string | undefined {
|
|
51
|
-
|
|
52
|
-
return this.#computeSubTypeName(cls);
|
|
53
|
-
}
|
|
51
|
+
return this.get(cls).subTypeName;
|
|
54
52
|
}
|
|
55
53
|
|
|
56
54
|
/**
|
|
@@ -83,8 +81,12 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
83
81
|
* Ensure type is set properly
|
|
84
82
|
*/
|
|
85
83
|
ensureInstanceTypeField<T>(cls: Class, o: T): void {
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
const schema = this.get(cls);
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
86
|
+
const typeField = (schema.subTypeField) as keyof T;
|
|
87
|
+
if (schema.subTypeName && typeField in schema.views[AllViewⲐ].schema && !o[typeField]) { // Do we have a type field defined
|
|
88
|
+
// @ts-expect-error
|
|
89
|
+
o[typeField] = schema.subTypeName; // Assign if missing
|
|
88
90
|
}
|
|
89
91
|
}
|
|
90
92
|
|
|
@@ -112,56 +114,59 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
112
114
|
* @param o Actual instance
|
|
113
115
|
*/
|
|
114
116
|
resolveSubTypeForInstance<T>(cls: Class<T>, o: T): Class {
|
|
115
|
-
|
|
117
|
+
const base = this.getBaseSchema(cls);
|
|
118
|
+
const clsSchema = this.get(cls);
|
|
119
|
+
const baseSchema = this.get(base);
|
|
120
|
+
|
|
121
|
+
if (clsSchema.subTypeName || baseSchema.baseType) { // We have a sub type
|
|
122
|
+
// @ts-expect-error
|
|
123
|
+
const type = o[baseSchema.subTypeField] ?? clsSchema.subTypeName ?? baseSchema.subTypeName;
|
|
124
|
+
const ret = this.#subTypes.get(base)!.get(type)!;
|
|
125
|
+
// @ts-expect-error
|
|
126
|
+
if (ret && !(new ret() instanceof cls)) {
|
|
127
|
+
throw new AppError(`Resolved class ${ret.name} is not assignable to ${cls.name}`);
|
|
128
|
+
}
|
|
129
|
+
return ret;
|
|
130
|
+
} else {
|
|
131
|
+
return cls;
|
|
132
|
+
}
|
|
116
133
|
}
|
|
117
134
|
|
|
118
135
|
/**
|
|
119
|
-
*
|
|
120
|
-
* @param cls The base class
|
|
121
|
-
* @param type The sub tye value
|
|
136
|
+
* Return all subtypes by discriminator for a given class
|
|
137
|
+
* @param cls The base class to resolve from
|
|
122
138
|
*/
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (type) {
|
|
127
|
-
return this.#subTypes.get(cls)!.get(typeId) ?? cls;
|
|
128
|
-
}
|
|
129
|
-
} else if (this.get(cls)?.subType) {
|
|
130
|
-
const expectedType = this.#typeKeys.get(cls);
|
|
131
|
-
if (expectedType && typeof type === 'string' && expectedType !== type) {
|
|
132
|
-
throw new AppError(`Data of type ${type} does not match expected class type ${expectedType}`, 'data');
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return cls;
|
|
139
|
+
getSubTypesForClass(cls: Class): Class[] | undefined {
|
|
140
|
+
const res = this.#subTypes.get(cls)?.values();
|
|
141
|
+
return res ? [...res] : undefined;
|
|
136
142
|
}
|
|
137
143
|
|
|
138
144
|
/**
|
|
139
145
|
* Register sub types for a class
|
|
140
146
|
* @param cls The class to register against
|
|
141
|
-
* @param
|
|
147
|
+
* @param name The subtype name
|
|
142
148
|
*/
|
|
143
|
-
registerSubTypes(cls: Class,
|
|
149
|
+
registerSubTypes(cls: Class, name?: string): void {
|
|
144
150
|
// Mark as subtype
|
|
145
|
-
(this.get(cls) ?? this.getOrCreatePending(cls))
|
|
151
|
+
const config = (this.get(cls) ?? this.getOrCreatePending(cls));
|
|
152
|
+
let base: Class | undefined = this.getBaseSchema(cls);
|
|
146
153
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
let parent = this.getParentClass(cls)!;
|
|
152
|
-
let parentConfig = this.get(parent);
|
|
154
|
+
if (!this.#subTypes.has(base)) {
|
|
155
|
+
this.#subTypes.set(base, new Map());
|
|
156
|
+
}
|
|
153
157
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
158
|
+
if (base !== cls || config.baseType) {
|
|
159
|
+
config.subTypeField = (this.get(base) ?? this.getOrCreatePending(base)).subTypeField;
|
|
160
|
+
config.subTypeName = name ?? config.subTypeName ?? classToSubTypeName(cls);
|
|
161
|
+
this.#subTypes.get(base)!.set(config.subTypeName!, cls);
|
|
162
|
+
}
|
|
163
|
+
if (base !== cls) {
|
|
164
|
+
while (base && ('Ⲑid' in base)) {
|
|
165
|
+
this.#subTypes.get(base)!.set(config.subTypeName!, cls);
|
|
166
|
+
const parent = this.getParentClass(base);
|
|
167
|
+
base = parent ? this.getBaseSchema(parent) : undefined;
|
|
157
168
|
}
|
|
158
|
-
this.#subTypes.get(parent)!.set(type, cls);
|
|
159
|
-
this.#subTypes.get(parent)!.set(cls.Ⲑid, cls);
|
|
160
|
-
parent = this.getParentClass(parent!)!;
|
|
161
|
-
parentConfig = this.get(parent);
|
|
162
169
|
}
|
|
163
|
-
|
|
164
|
-
return type;
|
|
165
170
|
}
|
|
166
171
|
|
|
167
172
|
/**
|
|
@@ -188,7 +193,8 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
188
193
|
return {
|
|
189
194
|
class: cls,
|
|
190
195
|
validators: [],
|
|
191
|
-
|
|
196
|
+
subTypeField: 'type',
|
|
197
|
+
baseType: RootIndex.getFunctionMetadata(cls)?.abstract,
|
|
192
198
|
metadata: {},
|
|
193
199
|
methods: {},
|
|
194
200
|
views: {
|
|
@@ -339,14 +345,18 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
339
345
|
* @param dest Target config
|
|
340
346
|
* @param src Source config
|
|
341
347
|
*/
|
|
342
|
-
mergeConfigs(dest: ClassConfig, src: Partial<ClassConfig
|
|
348
|
+
mergeConfigs(dest: ClassConfig, src: Partial<ClassConfig>, inherited = false): ClassConfig {
|
|
343
349
|
dest.views[AllViewⲐ] = {
|
|
344
350
|
schema: { ...dest.views[AllViewⲐ].schema, ...src.views?.[AllViewⲐ].schema },
|
|
345
351
|
fields: [...dest.views[AllViewⲐ].fields, ...src.views?.[AllViewⲐ].fields ?? []]
|
|
346
352
|
};
|
|
353
|
+
if (!inherited) {
|
|
354
|
+
dest.baseType = src.baseType ?? dest.baseType;
|
|
355
|
+
dest.subTypeName = src.subTypeName ?? dest.subTypeName;
|
|
356
|
+
}
|
|
347
357
|
dest.methods = { ...src.methods ?? {}, ...dest.methods ?? {} };
|
|
348
358
|
dest.metadata = { ...src.metadata ?? {}, ...dest.metadata ?? {} };
|
|
349
|
-
dest.
|
|
359
|
+
dest.subTypeField = src.subTypeField ?? dest.subTypeField;
|
|
350
360
|
dest.title = src.title || dest.title;
|
|
351
361
|
dest.validators = [...src.validators ?? [], ...dest.validators];
|
|
352
362
|
return dest;
|
|
@@ -389,7 +399,7 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
389
399
|
if (parent) {
|
|
390
400
|
const parentConfig = this.get(parent);
|
|
391
401
|
if (parentConfig) {
|
|
392
|
-
config = this.mergeConfigs(config, parentConfig);
|
|
402
|
+
config = this.mergeConfigs(config, parentConfig, true);
|
|
393
403
|
}
|
|
394
404
|
}
|
|
395
405
|
|
|
@@ -420,7 +430,7 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
420
430
|
if (e.type === 'removing' && this.hasExpired(cls)) {
|
|
421
431
|
// Recompute subtypes
|
|
422
432
|
this.#subTypes.clear();
|
|
423
|
-
this.#
|
|
433
|
+
this.#baseSchema.delete(cls);
|
|
424
434
|
this.#accessorDescriptors.delete(cls);
|
|
425
435
|
|
|
426
436
|
// Recompute subtype mappings
|
package/src/service/types.ts
CHANGED
|
@@ -64,9 +64,17 @@ export interface ClassConfig extends DescribableConfig {
|
|
|
64
64
|
*/
|
|
65
65
|
validators: ValidatorFn<unknown, unknown>[];
|
|
66
66
|
/**
|
|
67
|
-
* Is the class a
|
|
67
|
+
* Is the class a base type
|
|
68
68
|
*/
|
|
69
|
-
|
|
69
|
+
baseType?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Sub type name
|
|
72
|
+
*/
|
|
73
|
+
subTypeName?: string;
|
|
74
|
+
/**
|
|
75
|
+
* The field the subtype is determined by
|
|
76
|
+
*/
|
|
77
|
+
subTypeField: string;
|
|
70
78
|
/**
|
|
71
79
|
* Metadata that is related to the schema structure
|
|
72
80
|
*/
|