@travetto/schema 3.1.2 → 3.1.4
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 +1 -1
- package/src/decorator/schema.ts +4 -4
- package/src/service/registry.ts +69 -75
- package/src/service/types.ts +10 -2
package/package.json
CHANGED
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, 'subTypeName' | '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
|
}
|
|
@@ -51,6 +51,6 @@ export function View<T>(name: string, fields: ViewFieldsConfig<Partial<T>>) {
|
|
|
51
51
|
*/
|
|
52
52
|
export function SubType<T>(name: string) {
|
|
53
53
|
return (target: Class<Partial<T>>): void => {
|
|
54
|
-
SchemaRegistry.
|
|
54
|
+
SchemaRegistry.register(target, { subTypeName: name });
|
|
55
55
|
};
|
|
56
56
|
}
|
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,31 +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
|
-
#computeSubTypeName(cls: Class): string {
|
|
37
|
-
if (!this.#typeKeys.has(cls)) {
|
|
38
|
-
this.#typeKeys.set(cls, cls.name
|
|
39
|
-
.replace(/([A-Z])([A-Z][a-z])/g, (all, l, r) => `${l}_${r.toLowerCase()}`)
|
|
40
|
-
.replace(/([a-z]|\b)([A-Z])/g, (all, l, r) => l ? `${l}_${r.toLowerCase()}` : r.toLowerCase())
|
|
41
|
-
.toLowerCase());
|
|
42
|
-
}
|
|
43
|
-
return this.#typeKeys.get(cls)!;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
28
|
/**
|
|
47
|
-
*
|
|
48
|
-
* @param cls Base class
|
|
29
|
+
* Find base schema class for a given class
|
|
49
30
|
*/
|
|
50
|
-
|
|
51
|
-
if (this.
|
|
52
|
-
|
|
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);
|
|
53
42
|
}
|
|
43
|
+
return this.#baseSchema.get(cls)!;
|
|
54
44
|
}
|
|
55
45
|
|
|
56
46
|
/**
|
|
@@ -83,8 +73,12 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
83
73
|
* Ensure type is set properly
|
|
84
74
|
*/
|
|
85
75
|
ensureInstanceTypeField<T>(cls: Class, o: T): void {
|
|
86
|
-
|
|
87
|
-
|
|
76
|
+
const schema = this.get(cls);
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
78
|
+
const typeField = (schema.subTypeField) as keyof T;
|
|
79
|
+
if (schema.subTypeName && typeField in schema.views[AllViewⲐ].schema && !o[typeField]) { // Do we have a type field defined
|
|
80
|
+
// @ts-expect-error
|
|
81
|
+
o[typeField] = schema.subTypeName; // Assign if missing
|
|
88
82
|
}
|
|
89
83
|
}
|
|
90
84
|
|
|
@@ -112,64 +106,59 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
112
106
|
* @param o Actual instance
|
|
113
107
|
*/
|
|
114
108
|
resolveSubTypeForInstance<T>(cls: Class<T>, o: T): Class {
|
|
115
|
-
|
|
116
|
-
|
|
109
|
+
const base = this.getBaseSchema(cls);
|
|
110
|
+
const clsSchema = this.get(cls);
|
|
111
|
+
const baseSchema = this.get(base);
|
|
117
112
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const typeId = type && (typeof type === 'string' ? type : type.Ⲑid);
|
|
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');
|
|
113
|
+
if (clsSchema.subTypeName || baseSchema.baseType) { // We have a sub type
|
|
114
|
+
// @ts-expect-error
|
|
115
|
+
const type = o[baseSchema.subTypeField] ?? clsSchema.subTypeName ?? baseSchema.subTypeName;
|
|
116
|
+
const ret = this.#subTypes.get(base)!.get(type)!;
|
|
117
|
+
// @ts-expect-error
|
|
118
|
+
if (ret && !(new ret() instanceof cls)) {
|
|
119
|
+
throw new AppError(`Resolved class ${ret.name} is not assignable to ${cls.name}`);
|
|
133
120
|
}
|
|
121
|
+
return ret;
|
|
122
|
+
} else {
|
|
123
|
+
return cls;
|
|
134
124
|
}
|
|
135
|
-
return cls;
|
|
136
125
|
}
|
|
137
126
|
|
|
138
127
|
/**
|
|
139
128
|
* Return all subtypes by discriminator for a given class
|
|
140
129
|
* @param cls The base class to resolve from
|
|
141
130
|
*/
|
|
142
|
-
getSubTypesForClass(cls: Class):
|
|
143
|
-
|
|
131
|
+
getSubTypesForClass(cls: Class): Class[] | undefined {
|
|
132
|
+
const res = this.#subTypes.get(cls)?.values();
|
|
133
|
+
return res ? [...res] : undefined;
|
|
144
134
|
}
|
|
145
135
|
|
|
146
136
|
/**
|
|
147
137
|
* Register sub types for a class
|
|
148
138
|
* @param cls The class to register against
|
|
149
|
-
* @param
|
|
139
|
+
* @param name The subtype name
|
|
150
140
|
*/
|
|
151
|
-
registerSubTypes(cls: Class,
|
|
141
|
+
registerSubTypes(cls: Class, name?: string): void {
|
|
152
142
|
// Mark as subtype
|
|
153
|
-
(this.get(cls) ?? this.getOrCreatePending(cls))
|
|
143
|
+
const config = (this.get(cls) ?? this.getOrCreatePending(cls));
|
|
144
|
+
let base: Class | undefined = this.getBaseSchema(cls);
|
|
154
145
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
let parent = this.getParentClass(cls)!;
|
|
160
|
-
let parentConfig = this.get(parent);
|
|
146
|
+
if (!this.#subTypes.has(base)) {
|
|
147
|
+
this.#subTypes.set(base, new Map());
|
|
148
|
+
}
|
|
161
149
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
150
|
+
if (base !== cls || config.baseType) {
|
|
151
|
+
config.subTypeField = (this.get(base) ?? this.getOrCreatePending(base)).subTypeField;
|
|
152
|
+
config.subTypeName = name ?? config.subTypeName ?? classToSubTypeName(cls);
|
|
153
|
+
this.#subTypes.get(base)!.set(config.subTypeName!, cls);
|
|
154
|
+
}
|
|
155
|
+
if (base !== cls) {
|
|
156
|
+
while (base && ('Ⲑid' in base)) {
|
|
157
|
+
this.#subTypes.get(base)!.set(config.subTypeName!, cls);
|
|
158
|
+
const parent = this.getParentClass(base);
|
|
159
|
+
base = parent ? this.getBaseSchema(parent) : undefined;
|
|
165
160
|
}
|
|
166
|
-
this.#subTypes.get(parent)!.set(type, cls);
|
|
167
|
-
this.#subTypes.get(parent)!.set(cls.Ⲑid, cls);
|
|
168
|
-
parent = this.getParentClass(parent!)!;
|
|
169
|
-
parentConfig = this.get(parent);
|
|
170
161
|
}
|
|
171
|
-
|
|
172
|
-
return type;
|
|
173
162
|
}
|
|
174
163
|
|
|
175
164
|
/**
|
|
@@ -196,7 +185,8 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
196
185
|
return {
|
|
197
186
|
class: cls,
|
|
198
187
|
validators: [],
|
|
199
|
-
|
|
188
|
+
subTypeField: 'type',
|
|
189
|
+
baseType: RootIndex.getFunctionMetadata(cls)?.abstract,
|
|
200
190
|
metadata: {},
|
|
201
191
|
methods: {},
|
|
202
192
|
views: {
|
|
@@ -347,14 +337,18 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
347
337
|
* @param dest Target config
|
|
348
338
|
* @param src Source config
|
|
349
339
|
*/
|
|
350
|
-
mergeConfigs(dest: ClassConfig, src: Partial<ClassConfig
|
|
340
|
+
mergeConfigs(dest: ClassConfig, src: Partial<ClassConfig>, inherited = false): ClassConfig {
|
|
351
341
|
dest.views[AllViewⲐ] = {
|
|
352
342
|
schema: { ...dest.views[AllViewⲐ].schema, ...src.views?.[AllViewⲐ].schema },
|
|
353
343
|
fields: [...dest.views[AllViewⲐ].fields, ...src.views?.[AllViewⲐ].fields ?? []]
|
|
354
344
|
};
|
|
345
|
+
if (!inherited) {
|
|
346
|
+
dest.baseType = src.baseType ?? dest.baseType;
|
|
347
|
+
dest.subTypeName = src.subTypeName ?? dest.subTypeName;
|
|
348
|
+
}
|
|
355
349
|
dest.methods = { ...src.methods ?? {}, ...dest.methods ?? {} };
|
|
356
350
|
dest.metadata = { ...src.metadata ?? {}, ...dest.metadata ?? {} };
|
|
357
|
-
dest.
|
|
351
|
+
dest.subTypeField = src.subTypeField ?? dest.subTypeField;
|
|
358
352
|
dest.title = src.title || dest.title;
|
|
359
353
|
dest.validators = [...src.validators ?? [], ...dest.validators];
|
|
360
354
|
return dest;
|
|
@@ -397,7 +391,7 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
397
391
|
if (parent) {
|
|
398
392
|
const parentConfig = this.get(parent);
|
|
399
393
|
if (parentConfig) {
|
|
400
|
-
config = this.mergeConfigs(config, parentConfig);
|
|
394
|
+
config = this.mergeConfigs(config, parentConfig, true);
|
|
401
395
|
}
|
|
402
396
|
}
|
|
403
397
|
|
|
@@ -428,7 +422,7 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
428
422
|
if (e.type === 'removing' && this.hasExpired(cls)) {
|
|
429
423
|
// Recompute subtypes
|
|
430
424
|
this.#subTypes.clear();
|
|
431
|
-
this.#
|
|
425
|
+
this.#baseSchema.delete(cls);
|
|
432
426
|
this.#accessorDescriptors.delete(cls);
|
|
433
427
|
|
|
434
428
|
// 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
|
*/
|