@travetto/schema 6.0.1 → 7.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 +30 -26
- package/__index__.ts +4 -2
- package/package.json +3 -3
- package/src/bind-util.ts +40 -37
- package/src/decorator/common.ts +32 -16
- package/src/decorator/field.ts +21 -184
- package/src/decorator/input.ts +207 -0
- package/src/decorator/method.ts +28 -0
- package/src/decorator/schema.ts +36 -29
- package/src/name.ts +2 -2
- package/src/service/changes.ts +27 -30
- package/src/service/registry-adapter.ts +340 -0
- package/src/service/registry-index.ts +240 -0
- package/src/service/types.ts +113 -63
- package/src/validate/types.ts +4 -0
- package/src/validate/validator.ts +70 -64
- package/support/transformer/util.ts +147 -61
- package/support/transformer.schema.ts +137 -49
- package/src/service/registry.ts +0 -501
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import type { RegistryAdapter } from '@travetto/registry';
|
|
2
|
+
import { AppError, castKey, castTo, Class, describeFunction, safeAssign } from '@travetto/runtime';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
SchemaClassConfig, SchemaMethodConfig, SchemaFieldConfig,
|
|
6
|
+
SchemaParameterConfig, SchemaInputConfig, SchemaFieldMap, SchemaCoreConfig,
|
|
7
|
+
CONSTRUCTOR_PROPERTY
|
|
8
|
+
} from './types';
|
|
9
|
+
|
|
10
|
+
const classToDiscriminatedType = (cls: Class): string => cls.name
|
|
11
|
+
.replace(/([A-Z])([A-Z][a-z])/g, (all, l, r) => `${l}_${r.toLowerCase()}`)
|
|
12
|
+
.replace(/([a-z]|\b)([A-Z])/g, (all, l, r) => l ? `${l}_${r.toLowerCase()}` : r.toLowerCase())
|
|
13
|
+
.toLowerCase();
|
|
14
|
+
|
|
15
|
+
function assignMetadata<T>(key: symbol, base: SchemaCoreConfig, data: Partial<T>[]): T {
|
|
16
|
+
const md = base.metadata ??= {};
|
|
17
|
+
const out = md[key] ??= {};
|
|
18
|
+
for (const d of data) {
|
|
19
|
+
safeAssign(out, d);
|
|
20
|
+
}
|
|
21
|
+
return castTo(out);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function combineCore<T extends SchemaCoreConfig>(base: T, config: Partial<T>): T {
|
|
25
|
+
return safeAssign(base, {
|
|
26
|
+
...config.metadata ? { metadata: { ...base.metadata, ...config.metadata } } : {},
|
|
27
|
+
...config.private ? { private: config.private ?? base.private } : {},
|
|
28
|
+
...config.description ? { description: config.description || base.description } : {},
|
|
29
|
+
...config.examples ? { examples: [...(base.examples ?? []), ...(config.examples ?? [])] } : {},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function combineInputs<T extends SchemaInputConfig>(base: T, configs: Partial<T>[]): T {
|
|
34
|
+
for (const config of configs) {
|
|
35
|
+
safeAssign(base, {
|
|
36
|
+
...config,
|
|
37
|
+
...config.aliases ? { aliases: [...base.aliases ?? [], ...config.aliases ?? []] } : {},
|
|
38
|
+
...config.specifiers ? { specifiers: [...base.specifiers ?? [], ...config.specifiers ?? []] } : {},
|
|
39
|
+
...config.enum ? {
|
|
40
|
+
enum: {
|
|
41
|
+
message: config.enum?.message ?? base.enum?.message,
|
|
42
|
+
values: (config.enum?.values ?? base.enum?.values ?? []).toSorted()
|
|
43
|
+
}
|
|
44
|
+
} : {},
|
|
45
|
+
});
|
|
46
|
+
combineCore(base, config);
|
|
47
|
+
}
|
|
48
|
+
return base;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function combineMethods<T extends SchemaMethodConfig>(base: T, configs: Partial<T>[]): T {
|
|
52
|
+
for (const config of configs) {
|
|
53
|
+
safeAssign(base, {
|
|
54
|
+
...config,
|
|
55
|
+
parameters: config.parameters ?? base.parameters,
|
|
56
|
+
validators: [...base.validators, ...(config.validators ?? [])],
|
|
57
|
+
});
|
|
58
|
+
combineCore(base, config);
|
|
59
|
+
if (config.parameters) {
|
|
60
|
+
for (const param of config.parameters) {
|
|
61
|
+
safeAssign(base.parameters[param.index], param);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return base;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getConstructorConfig<T extends SchemaClassConfig>(base: Partial<T>, parent?: Partial<T>): SchemaMethodConfig {
|
|
69
|
+
const parentCons = parent?.methods?.[CONSTRUCTOR_PROPERTY];
|
|
70
|
+
const baseCons = base.methods?.[CONSTRUCTOR_PROPERTY];
|
|
71
|
+
return {
|
|
72
|
+
parameters: [],
|
|
73
|
+
validators: [],
|
|
74
|
+
...parentCons,
|
|
75
|
+
...baseCons,
|
|
76
|
+
returnType: { type: base.class! }
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function combineClassWithParent<T extends SchemaClassConfig>(base: T, parent: T): T {
|
|
81
|
+
safeAssign(base, {
|
|
82
|
+
...base.views ? { views: { ...parent.views, ...base.views } } : {},
|
|
83
|
+
...base.validators ? { validators: [...parent.validators, ...base.validators] } : {},
|
|
84
|
+
...base.metadata ? { metadata: { ...parent.metadata, ...base.metadata } } : {},
|
|
85
|
+
interfaces: [...parent.interfaces, ...base.interfaces],
|
|
86
|
+
methods: { ...parent.methods, ...base.methods },
|
|
87
|
+
description: base.description || parent.description,
|
|
88
|
+
examples: [...(parent.examples ?? []), ...(base.examples ?? [])],
|
|
89
|
+
discriminatedField: base.discriminatedField ?? parent.discriminatedField,
|
|
90
|
+
});
|
|
91
|
+
switch (base.mappedOperation) {
|
|
92
|
+
case 'Required':
|
|
93
|
+
case 'Partial': {
|
|
94
|
+
base.fields = Object.fromEntries(
|
|
95
|
+
Object.entries(base.fields).map(([k, v]) => [k, {
|
|
96
|
+
...v,
|
|
97
|
+
required: {
|
|
98
|
+
active: base.mappedOperation === 'Required'
|
|
99
|
+
}
|
|
100
|
+
}])
|
|
101
|
+
);
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
case 'Pick':
|
|
105
|
+
case 'Omit': {
|
|
106
|
+
const keys = new Set<string>(base.mappedFields ?? []);
|
|
107
|
+
base.fields = Object.fromEntries(
|
|
108
|
+
Object.entries(parent.fields).filter(([k]) =>
|
|
109
|
+
base.mappedOperation === 'Pick' ? keys.has(k) : !keys.has(k)
|
|
110
|
+
)
|
|
111
|
+
);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
default: {
|
|
115
|
+
base.fields = { ...parent.fields, ...base.fields };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return base;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function combineClasses<T extends SchemaClassConfig>(base: T, configs: Partial<T>[]): T {
|
|
122
|
+
for (const config of configs) {
|
|
123
|
+
Object.assign(base, {
|
|
124
|
+
...config,
|
|
125
|
+
...config.views ? { views: { ...base.views, ...config.views } } : {},
|
|
126
|
+
...config.validators ? { validators: [...base.validators, ...config.validators] } : {},
|
|
127
|
+
interfaces: [...base.interfaces, ...(config.interfaces ?? [])],
|
|
128
|
+
methods: { ...base.methods, ...config.methods },
|
|
129
|
+
fields: { ...base.fields, ...config.fields },
|
|
130
|
+
});
|
|
131
|
+
combineCore(base, config);
|
|
132
|
+
}
|
|
133
|
+
return base;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export class SchemaRegistryAdapter implements RegistryAdapter<SchemaClassConfig> {
|
|
137
|
+
|
|
138
|
+
#cls: Class;
|
|
139
|
+
#config: SchemaClassConfig;
|
|
140
|
+
#views: Map<string, SchemaFieldMap> = new Map();
|
|
141
|
+
#accessorDescriptors: Map<string, PropertyDescriptor> = new Map();
|
|
142
|
+
|
|
143
|
+
constructor(cls: Class) {
|
|
144
|
+
this.#cls = cls;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
register(...data: Partial<SchemaClassConfig>[]): SchemaClassConfig {
|
|
148
|
+
const cfg = this.#config ??= {
|
|
149
|
+
methods: {},
|
|
150
|
+
class: this.#cls,
|
|
151
|
+
views: {},
|
|
152
|
+
validators: [],
|
|
153
|
+
interfaces: [],
|
|
154
|
+
fields: {},
|
|
155
|
+
};
|
|
156
|
+
return combineClasses(cfg, data);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
registerMetadata<T>(key: symbol, ...data: Partial<T>[]): T {
|
|
160
|
+
const cfg = this.register({});
|
|
161
|
+
return assignMetadata(key, cfg, data);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
getMetadata<T>(key: symbol): T | undefined {
|
|
165
|
+
const md = this.#config?.metadata;
|
|
166
|
+
return castTo<T>(md?.[key]);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
registerField(field: string | symbol, ...data: Partial<SchemaFieldConfig>[]): SchemaFieldConfig {
|
|
170
|
+
const config = this.register({});
|
|
171
|
+
const cfg = config.fields[field] ??= { name: field, owner: this.#cls, type: null! };
|
|
172
|
+
const combined = combineInputs(cfg, data);
|
|
173
|
+
return combined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
registerFieldMetadata<T>(field: string | symbol, key: symbol, ...data: Partial<T>[]): T {
|
|
177
|
+
const cfg = this.registerField(field);
|
|
178
|
+
return assignMetadata(key, cfg, data);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
getFieldMetadata<T>(field: string | symbol, key: symbol): T | undefined {
|
|
182
|
+
const md = this.#config?.fields[field]?.metadata;
|
|
183
|
+
return castTo<T>(md?.[key]);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
registerClass({ methods, ...cfg }: Partial<SchemaClassConfig> = {}): SchemaClassConfig {
|
|
187
|
+
this.register({ ...cfg });
|
|
188
|
+
if (methods?.[CONSTRUCTOR_PROPERTY]) {
|
|
189
|
+
const { parameters, ...rest } = methods[CONSTRUCTOR_PROPERTY];
|
|
190
|
+
this.registerMethod(CONSTRUCTOR_PROPERTY, rest);
|
|
191
|
+
for (const param of parameters ?? []) {
|
|
192
|
+
this.registerParameter(CONSTRUCTOR_PROPERTY, param.index!, param);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return this.#config;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
registerMethod(method: string | symbol, ...data: Partial<SchemaMethodConfig>[]): SchemaMethodConfig {
|
|
199
|
+
const config = this.register();
|
|
200
|
+
const cfg = config.methods[method] ??= { parameters: [], validators: [] };
|
|
201
|
+
return combineMethods(cfg, data);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
registerMethodMetadata<T>(method: string | symbol, key: symbol, ...data: Partial<T>[]): T {
|
|
205
|
+
const cfg = this.registerMethod(method);
|
|
206
|
+
return assignMetadata(key, cfg, data);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
getMethodMetadata<T>(method: string | symbol, key: symbol): T | undefined {
|
|
210
|
+
const md = this.#config?.methods[method]?.metadata;
|
|
211
|
+
return castTo<T>(md?.[key]);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
registerParameter(method: string | symbol, idx: number, ...data: Partial<SchemaParameterConfig>[]): SchemaParameterConfig {
|
|
215
|
+
const params = this.registerMethod(method, {}).parameters;
|
|
216
|
+
const cfg = params[idx] ??= { method, index: idx, owner: this.#cls, array: false, type: null! };
|
|
217
|
+
return combineInputs(cfg, data);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
registerParameterMetadata<T>(method: string | symbol, idx: number, key: symbol, ...data: Partial<T>[]): T {
|
|
221
|
+
const cfg = this.registerParameter(method, idx);
|
|
222
|
+
return assignMetadata(key, cfg, data);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
getParameterMetadata<T>(method: string | symbol, idx: number, key: symbol): T | undefined {
|
|
226
|
+
const md = this.#config?.methods[method]?.parameters[idx]?.metadata;
|
|
227
|
+
return castTo<T>(md?.[key]);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
finalize(parent?: SchemaClassConfig): void {
|
|
231
|
+
const config = this.#config;
|
|
232
|
+
|
|
233
|
+
if (parent) {
|
|
234
|
+
combineClassWithParent(config, parent);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (config.discriminatedField && !config.discriminatedType && !describeFunction(this.#cls).abstract) {
|
|
238
|
+
config.discriminatedType = classToDiscriminatedType(this.#cls);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (config.discriminatedField && config.discriminatedType) {
|
|
242
|
+
config.fields[config.discriminatedField] = {
|
|
243
|
+
...config.fields[config.discriminatedField], // Make a full copy
|
|
244
|
+
required: {
|
|
245
|
+
active: false
|
|
246
|
+
},
|
|
247
|
+
enum: {
|
|
248
|
+
values: [config.discriminatedType],
|
|
249
|
+
message: `${config.discriminatedField} can only be '${config.discriminatedType}'`,
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Compute views on install
|
|
255
|
+
for (const view of Object.keys(config.views)) {
|
|
256
|
+
const fields = config.views[view];
|
|
257
|
+
const withoutSet = 'without' in fields ? new Set<string>(fields.without) : undefined;
|
|
258
|
+
const fieldList = withoutSet ?
|
|
259
|
+
Object.keys(config.fields).filter(x => !withoutSet.has(x)) :
|
|
260
|
+
('with' in fields ? fields.with : []);
|
|
261
|
+
|
|
262
|
+
this.#views.set(view,
|
|
263
|
+
fieldList.reduce<SchemaFieldMap>((acc, v) => {
|
|
264
|
+
acc[v] = config.fields[v];
|
|
265
|
+
return acc;
|
|
266
|
+
}, {})
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
config.methods[CONSTRUCTOR_PROPERTY] = getConstructorConfig(config, parent);
|
|
271
|
+
|
|
272
|
+
for (const method of Object.values(config.methods)) {
|
|
273
|
+
method.parameters = method.parameters.toSorted((a, b) => (a.index! - b.index!));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
get(): SchemaClassConfig {
|
|
278
|
+
return this.#config;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
getField(field: string | symbol): SchemaFieldConfig {
|
|
282
|
+
return this.#config.fields[field];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
getMethod(method: string | symbol): SchemaMethodConfig {
|
|
286
|
+
const res = this.#config.methods[method];
|
|
287
|
+
if (!res) {
|
|
288
|
+
throw new AppError(`Unknown method ${String(method)} on class ${this.#cls.Ⲑid}`);
|
|
289
|
+
}
|
|
290
|
+
return res;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
getMethodReturnType(method: string | symbol): Class {
|
|
294
|
+
return this.getMethod(method).returnType!.type;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
getSchema(view?: string): SchemaFieldMap {
|
|
298
|
+
if (!view) {
|
|
299
|
+
return this.#config.fields;
|
|
300
|
+
}
|
|
301
|
+
if (!this.#views.has(view)) {
|
|
302
|
+
throw new AppError(`Unknown view ${view} for class ${this.#cls.Ⲑid}`);
|
|
303
|
+
}
|
|
304
|
+
return this.#views.get(view)!;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Provides the prototype-derived descriptor for a property
|
|
309
|
+
*/
|
|
310
|
+
getAccessorDescriptor(field: string): PropertyDescriptor {
|
|
311
|
+
if (!this.#accessorDescriptors.has(field)) {
|
|
312
|
+
let proto = this.#cls.prototype;
|
|
313
|
+
while (proto && !Object.hasOwn(proto, field)) {
|
|
314
|
+
proto = proto.prototype;
|
|
315
|
+
}
|
|
316
|
+
this.#accessorDescriptors.set(field, Object.getOwnPropertyDescriptor(proto, field)!);
|
|
317
|
+
}
|
|
318
|
+
return this.#accessorDescriptors.get(field)!;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Ensure type is set properly
|
|
323
|
+
*/
|
|
324
|
+
ensureInstanceTypeField<T>(o: T): T {
|
|
325
|
+
const config = this.getDiscriminatedConfig();
|
|
326
|
+
if (config) {
|
|
327
|
+
const typeField = castKey<T>(config.discriminatedField);
|
|
328
|
+
o[typeField] ??= castTo(config.discriminatedType); // Assign if missing
|
|
329
|
+
}
|
|
330
|
+
return o;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
getDiscriminatedConfig(): Required<Pick<SchemaClassConfig, 'discriminatedType' | 'discriminatedField' | 'discriminatedBase'>> | undefined {
|
|
334
|
+
const { discriminatedField, discriminatedType, discriminatedBase } = this.#config;
|
|
335
|
+
if (discriminatedType && discriminatedField) {
|
|
336
|
+
return { discriminatedType, discriminatedField, discriminatedBase: !!discriminatedBase };
|
|
337
|
+
}
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { ChangeEvent, RegistrationMethods, RegistryIndex, RegistryIndexStore, Registry } from '@travetto/registry';
|
|
2
|
+
import { AppError, castKey, castTo, Class, classConstruct, getParentClass, Util } from '@travetto/runtime';
|
|
3
|
+
|
|
4
|
+
import { SchemaFieldConfig, SchemaClassConfig, SchemaFieldMap, SchemaMethodConfig } from './types.ts';
|
|
5
|
+
import { SchemaRegistryAdapter } from './registry-adapter.ts';
|
|
6
|
+
import { SchemaChangeListener } from './changes.ts';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Schema registry index for managing schema configurations across classes
|
|
10
|
+
*/
|
|
11
|
+
export class SchemaRegistryIndex implements RegistryIndex {
|
|
12
|
+
|
|
13
|
+
static #instance = Registry.registerIndex(SchemaRegistryIndex);
|
|
14
|
+
|
|
15
|
+
static getForRegister(cls: Class, allowFinalized = false): SchemaRegistryAdapter {
|
|
16
|
+
return this.#instance.store.getForRegister(cls, allowFinalized);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static getConfig(cls: Class): SchemaClassConfig {
|
|
20
|
+
return this.#instance.store.get(cls).get();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static getDiscriminatedConfig<T>(cls: Class<T>): Required<Pick<SchemaClassConfig, 'discriminatedType' | 'discriminatedField' | 'discriminatedBase'>> | undefined {
|
|
24
|
+
return this.#instance.store.get(cls).getDiscriminatedConfig();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static getFieldMap(cls: Class, view?: string): SchemaFieldMap {
|
|
28
|
+
return this.#instance.store.get(cls).getSchema(view);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static getMethodConfig(cls: Class, method: string | symbol): SchemaMethodConfig {
|
|
32
|
+
return this.#instance.store.get(cls).getMethod(method);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static has(cls: Class): boolean {
|
|
36
|
+
return this.#instance.store.has(cls);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static getClassById(classId: string): Class {
|
|
40
|
+
return this.#instance.store.getClassById(classId);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static getDiscriminatedTypes(cls: Class): string[] | undefined {
|
|
44
|
+
return this.#instance.getDiscriminatedTypes(cls);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static resolveInstanceType<T>(cls: Class<T>, o: T): Class {
|
|
48
|
+
return this.#instance.resolveInstanceType(cls, o);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static visitFields<T>(cls: Class<T>, onField: (field: SchemaFieldConfig, path: SchemaFieldConfig[]) => void): void {
|
|
52
|
+
return this.#instance.visitFields(cls, onField);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static getDiscriminatedClasses(cls: Class): Class[] {
|
|
56
|
+
return this.#instance.getDiscriminatedClasses(cls);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static getBaseClass(cls: Class): Class {
|
|
60
|
+
return this.#instance.getBaseClass(cls);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
static get(cls: Class): Omit<SchemaRegistryAdapter, RegistrationMethods> {
|
|
64
|
+
return this.#instance.store.get(cls);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static getOptionalConfig(cls: Class): SchemaClassConfig | undefined {
|
|
68
|
+
return this.#instance.store.getOptional(cls)?.get();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
static getClasses(): Class[] {
|
|
72
|
+
return this.#instance.store.getClasses();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
store = new RegistryIndexStore(SchemaRegistryAdapter);
|
|
76
|
+
#baseSchema = new Map<Class, Class>();
|
|
77
|
+
#byDiscriminatedTypes = new Map<Class, Map<string, Class>>();
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Register discriminated types for a class
|
|
81
|
+
*/
|
|
82
|
+
#registerDiscriminatedTypes(cls: Class): void {
|
|
83
|
+
// Mark as subtype
|
|
84
|
+
const config = this.getClassConfig(cls);
|
|
85
|
+
if (!config.discriminatedType) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const base = this.getBaseClass(cls);
|
|
89
|
+
if (!this.#byDiscriminatedTypes.has(base)) {
|
|
90
|
+
this.#byDiscriminatedTypes.set(base, new Map());
|
|
91
|
+
}
|
|
92
|
+
this.#byDiscriminatedTypes.get(base)!.set(config.discriminatedType, cls);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#onChanged(event: ChangeEvent<Class> & { type: 'changed' }): void {
|
|
96
|
+
Util.queueMacroTask().then(() => {
|
|
97
|
+
SchemaChangeListener.emitFieldChanges({
|
|
98
|
+
type: 'changed',
|
|
99
|
+
curr: this.getClassConfig(event.curr),
|
|
100
|
+
prev: this.getClassConfig(event.prev)
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#onRemoving(event: ChangeEvent<Class> & { type: 'removing' }): void {
|
|
106
|
+
SchemaChangeListener.clearSchemaDependency(event.prev);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#onAdded(event: ChangeEvent<Class> & { type: 'added' }): void {
|
|
110
|
+
Util.queueMacroTask().then(() => {
|
|
111
|
+
SchemaChangeListener.emitFieldChanges({
|
|
112
|
+
type: 'added',
|
|
113
|
+
curr: this.getClassConfig(event.curr)
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
process(events: ChangeEvent<Class>[]): void {
|
|
119
|
+
for (const event of events) {
|
|
120
|
+
if (event.type === 'changed') {
|
|
121
|
+
this.#onChanged(event);
|
|
122
|
+
} else if (event.type === 'removing') {
|
|
123
|
+
this.#onRemoving(event);
|
|
124
|
+
} else if (event.type === 'added') {
|
|
125
|
+
this.#onAdded(event);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Rebuild indices after every "process" batch
|
|
130
|
+
this.#byDiscriminatedTypes.clear();
|
|
131
|
+
for (const el of this.store.getClasses()) {
|
|
132
|
+
this.#registerDiscriminatedTypes(el);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
getClassConfig(cls: Class): SchemaClassConfig {
|
|
137
|
+
return this.store.get(cls).get();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Find base schema class for a given class
|
|
142
|
+
*/
|
|
143
|
+
getBaseClass(cls: Class): Class {
|
|
144
|
+
if (!this.#baseSchema.has(cls)) {
|
|
145
|
+
let conf = this.getClassConfig(cls);
|
|
146
|
+
let parent: Class | undefined = cls;
|
|
147
|
+
while (parent && conf.discriminatedType && !conf.discriminatedBase) {
|
|
148
|
+
parent = getParentClass(parent);
|
|
149
|
+
if (parent) {
|
|
150
|
+
conf = this.store.getOptional(parent)?.get() ?? conf;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
this.#baseSchema.set(cls, conf.class);
|
|
154
|
+
}
|
|
155
|
+
return this.#baseSchema.get(cls)!;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Find the resolved type for a given instance
|
|
160
|
+
* @param cls Class for instance
|
|
161
|
+
* @param o Actual instance
|
|
162
|
+
*/
|
|
163
|
+
resolveInstanceType<T>(requestedCls: Class<T>, o: T): Class {
|
|
164
|
+
const cls = this.store.getClassById(requestedCls.Ⲑid); // Resolve by id to handle any stale references
|
|
165
|
+
const adapter = this.store.get(cls);
|
|
166
|
+
const { discriminatedField, discriminatedType } = adapter.get();
|
|
167
|
+
if (!discriminatedField) {
|
|
168
|
+
return cls;
|
|
169
|
+
} else {
|
|
170
|
+
const base = this.getBaseClass(cls);
|
|
171
|
+
const map = this.#byDiscriminatedTypes.get(base);
|
|
172
|
+
const type = castTo<string>(o[castKey<T>(discriminatedField)]) ?? discriminatedType;
|
|
173
|
+
if (!type) {
|
|
174
|
+
throw new AppError(`Unable to resolve discriminated type for class ${base.name} without a type`);
|
|
175
|
+
}
|
|
176
|
+
if (!map?.has(type)) {
|
|
177
|
+
throw new AppError(`Unable to resolve discriminated type '${type}' for class ${base.name}`);
|
|
178
|
+
}
|
|
179
|
+
const requested = map.get(type)!;
|
|
180
|
+
if (!(classConstruct(requested) instanceof requestedCls)) {
|
|
181
|
+
throw new AppError(`Resolved discriminated type '${type}' for class ${base.name} is not an instance of requested type ${requestedCls.name}`);
|
|
182
|
+
}
|
|
183
|
+
return requested;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Track changes to schemas, and track the dependent changes
|
|
189
|
+
* @param cls The root class of the hierarchy
|
|
190
|
+
* @param curr The new class
|
|
191
|
+
* @param path The path within the object hierarchy
|
|
192
|
+
*/
|
|
193
|
+
trackSchemaDependencies(cls: Class, curr: Class = cls, path: SchemaFieldConfig[] = []): void {
|
|
194
|
+
const config = this.getClassConfig(curr);
|
|
195
|
+
|
|
196
|
+
SchemaChangeListener.trackSchemaDependency(curr, cls, path, this.getClassConfig(cls));
|
|
197
|
+
|
|
198
|
+
// Read children
|
|
199
|
+
for (const field of Object.values(config.fields)) {
|
|
200
|
+
if (SchemaRegistryIndex.has(field.type) && field.type !== cls) {
|
|
201
|
+
this.trackSchemaDependencies(cls, field.type, [...path, field]);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Visit fields recursively
|
|
208
|
+
*/
|
|
209
|
+
visitFields<T>(cls: Class<T>, onField: (field: SchemaFieldConfig, path: SchemaFieldConfig[]) => void, _path: SchemaFieldConfig[] = [], root = cls): void {
|
|
210
|
+
const fields = SchemaRegistryIndex.has(cls) ?
|
|
211
|
+
Object.values(this.getClassConfig(cls).fields) :
|
|
212
|
+
[];
|
|
213
|
+
for (const field of fields) {
|
|
214
|
+
if (SchemaRegistryIndex.has(field.type)) {
|
|
215
|
+
this.visitFields(field.type, onField, [..._path, field], root);
|
|
216
|
+
} else {
|
|
217
|
+
onField(field, _path);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Return all subtypes by discriminator for a given class
|
|
224
|
+
* @param cls The base class to resolve from
|
|
225
|
+
*/
|
|
226
|
+
getDiscriminatedClasses(cls: Class): Class[] {
|
|
227
|
+
return [...this.#byDiscriminatedTypes.get(cls)?.values() ?? []];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get all discriminated types for a given class
|
|
232
|
+
*/
|
|
233
|
+
getDiscriminatedTypes(cls: Class): string[] | undefined {
|
|
234
|
+
const map = this.#byDiscriminatedTypes.get(cls);
|
|
235
|
+
if (map) {
|
|
236
|
+
return [...map.keys()];
|
|
237
|
+
}
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
}
|