@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/schema",
3
- "version": "3.1.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.0"
33
+ "@travetto/transformer": "^3.1.2"
34
34
  },
35
35
  "peerDependenciesMeta": {
36
36
  "@travetto/transformer": {
@@ -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.getOrCreatePending(target);
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: string) {
52
+ export function SubType<T>(name?: string) {
53
53
  return (target: Class<Partial<T>>): void => {
54
54
  SchemaRegistry.registerSubTypes(target, name);
55
55
  };
@@ -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,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
- #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());
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.#typeKeys.get(cls)!;
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
- if (this.get(cls).subType) {
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
- if (isWithType(o, this.get(cls)) && !o.type) { // Do we have a type field defined
87
- o.type = this.#computeSubTypeName(cls); // Assign if missing
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
- return this.resolveSubType(cls, hasType<T>(o) ? o.type : getConstructor(o));
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
- * Resolve the sub type for a class and a type
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
- 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');
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 type The subtype name
147
+ * @param name The subtype name
142
148
  */
143
- registerSubTypes(cls: Class, type?: string): string {
149
+ registerSubTypes(cls: Class, name?: string): void {
144
150
  // Mark as subtype
145
- (this.get(cls) ?? this.getOrCreatePending(cls)).subType = true;
151
+ const config = (this.get(cls) ?? this.getOrCreatePending(cls));
152
+ let base: Class | undefined = this.getBaseSchema(cls);
146
153
 
147
- type ??= this.#computeSubTypeName(cls)!;
148
-
149
- this.#typeKeys.set(cls, type);
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
- while (parentConfig) {
155
- if (!this.#subTypes.has(parent)) {
156
- this.#subTypes.set(parent, new Map());
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
- subType: false,
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>): 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.subType = src.subType || dest.subType;
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.#typeKeys.delete(cls);
433
+ this.#baseSchema.delete(cls);
424
434
  this.#accessorDescriptors.delete(cls);
425
435
 
426
436
  // 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
  */