@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/schema",
3
- "version": "3.1.2",
3
+ "version": "3.1.4",
4
4
  "description": "Data type registry for runtime validation, reflection and binding.",
5
5
  "keywords": [
6
6
  "schema",
@@ -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.getOrCreatePending(target);
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.registerSubTypes(target, name);
54
+ SchemaRegistry.register(target, { subTypeName: name });
55
55
  };
56
56
  }
@@ -1,23 +1,15 @@
1
- import { Class, AppError, ObjectUtil, ClassInstance, ConcreteClass } from '@travetto/base';
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
- function hasType<T>(o: unknown): o is { type: Class<T> | string } {
9
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
10
- return !!o && !ObjectUtil.isPrimitive(o) && 'type' in (o as object) && !!(o as Record<string, string>)['type'];
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
- * Get subtype name for a class
48
- * @param cls Base class
29
+ * Find base schema class for a given class
49
30
  */
50
- getSubTypeName(cls: Class): string | undefined {
51
- if (this.get(cls).subType) {
52
- return this.#computeSubTypeName(cls);
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
- if (isWithType(o, this.get(cls)) && !o.type) { // Do we have a type field defined
87
- o.type = this.#computeSubTypeName(cls); // Assign if missing
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
- return this.resolveSubType(cls, hasType<T>(o) ? o.type : getConstructor(o));
116
- }
109
+ const base = this.getBaseSchema(cls);
110
+ const clsSchema = this.get(cls);
111
+ const baseSchema = this.get(base);
117
112
 
118
- /**
119
- * Resolve the sub type for a class and a type
120
- * @param cls The base class
121
- * @param type The sub tye value
122
- */
123
- resolveSubType(cls: Class, type: Class | string): Class {
124
- if (this.#subTypes.has(cls)) {
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): Map<string, Class> | undefined {
143
- return this.#subTypes.get(cls);
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 type The subtype name
139
+ * @param name The subtype name
150
140
  */
151
- registerSubTypes(cls: Class, type?: string): string {
141
+ registerSubTypes(cls: Class, name?: string): void {
152
142
  // Mark as subtype
153
- (this.get(cls) ?? this.getOrCreatePending(cls)).subType = true;
143
+ const config = (this.get(cls) ?? this.getOrCreatePending(cls));
144
+ let base: Class | undefined = this.getBaseSchema(cls);
154
145
 
155
- type ??= this.#computeSubTypeName(cls)!;
156
-
157
- this.#typeKeys.set(cls, type);
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
- while (parentConfig) {
163
- if (!this.#subTypes.has(parent)) {
164
- this.#subTypes.set(parent, new Map());
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
- subType: false,
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>): 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.subType = src.subType || dest.subType;
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.#typeKeys.delete(cls);
425
+ this.#baseSchema.delete(cls);
432
426
  this.#accessorDescriptors.delete(cls);
433
427
 
434
428
  // Recompute subtype mappings
@@ -64,9 +64,17 @@ export interface ClassConfig extends DescribableConfig {
64
64
  */
65
65
  validators: ValidatorFn<unknown, unknown>[];
66
66
  /**
67
- * Is the class a sub type
67
+ * Is the class a base type
68
68
  */
69
- subType?: boolean;
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
  */