@travetto/schema 3.1.2 → 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.2",
3
+ "version": "3.1.3",
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, '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,64 +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));
116
- }
117
+ const base = this.getBaseSchema(cls);
118
+ const clsSchema = this.get(cls);
119
+ const baseSchema = this.get(base);
117
120
 
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');
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}`);
133
128
  }
129
+ return ret;
130
+ } else {
131
+ return cls;
134
132
  }
135
- return cls;
136
133
  }
137
134
 
138
135
  /**
139
136
  * Return all subtypes by discriminator for a given class
140
137
  * @param cls The base class to resolve from
141
138
  */
142
- getSubTypesForClass(cls: Class): Map<string, Class> | undefined {
143
- return this.#subTypes.get(cls);
139
+ getSubTypesForClass(cls: Class): Class[] | undefined {
140
+ const res = this.#subTypes.get(cls)?.values();
141
+ return res ? [...res] : undefined;
144
142
  }
145
143
 
146
144
  /**
147
145
  * Register sub types for a class
148
146
  * @param cls The class to register against
149
- * @param type The subtype name
147
+ * @param name The subtype name
150
148
  */
151
- registerSubTypes(cls: Class, type?: string): string {
149
+ registerSubTypes(cls: Class, name?: string): void {
152
150
  // Mark as subtype
153
- (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);
154
153
 
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);
154
+ if (!this.#subTypes.has(base)) {
155
+ this.#subTypes.set(base, new Map());
156
+ }
161
157
 
162
- while (parentConfig) {
163
- if (!this.#subTypes.has(parent)) {
164
- 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;
165
168
  }
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
169
  }
171
-
172
- return type;
173
170
  }
174
171
 
175
172
  /**
@@ -196,7 +193,8 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
196
193
  return {
197
194
  class: cls,
198
195
  validators: [],
199
- subType: false,
196
+ subTypeField: 'type',
197
+ baseType: RootIndex.getFunctionMetadata(cls)?.abstract,
200
198
  metadata: {},
201
199
  methods: {},
202
200
  views: {
@@ -347,14 +345,18 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
347
345
  * @param dest Target config
348
346
  * @param src Source config
349
347
  */
350
- mergeConfigs(dest: ClassConfig, src: Partial<ClassConfig>): ClassConfig {
348
+ mergeConfigs(dest: ClassConfig, src: Partial<ClassConfig>, inherited = false): ClassConfig {
351
349
  dest.views[AllViewⲐ] = {
352
350
  schema: { ...dest.views[AllViewⲐ].schema, ...src.views?.[AllViewⲐ].schema },
353
351
  fields: [...dest.views[AllViewⲐ].fields, ...src.views?.[AllViewⲐ].fields ?? []]
354
352
  };
353
+ if (!inherited) {
354
+ dest.baseType = src.baseType ?? dest.baseType;
355
+ dest.subTypeName = src.subTypeName ?? dest.subTypeName;
356
+ }
355
357
  dest.methods = { ...src.methods ?? {}, ...dest.methods ?? {} };
356
358
  dest.metadata = { ...src.metadata ?? {}, ...dest.metadata ?? {} };
357
- dest.subType = src.subType || dest.subType;
359
+ dest.subTypeField = src.subTypeField ?? dest.subTypeField;
358
360
  dest.title = src.title || dest.title;
359
361
  dest.validators = [...src.validators ?? [], ...dest.validators];
360
362
  return dest;
@@ -397,7 +399,7 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
397
399
  if (parent) {
398
400
  const parentConfig = this.get(parent);
399
401
  if (parentConfig) {
400
- config = this.mergeConfigs(config, parentConfig);
402
+ config = this.mergeConfigs(config, parentConfig, true);
401
403
  }
402
404
  }
403
405
 
@@ -428,7 +430,7 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
428
430
  if (e.type === 'removing' && this.hasExpired(cls)) {
429
431
  // Recompute subtypes
430
432
  this.#subTypes.clear();
431
- this.#typeKeys.delete(cls);
433
+ this.#baseSchema.delete(cls);
432
434
  this.#accessorDescriptors.delete(cls);
433
435
 
434
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
  */