@travetto/schema 4.1.0 → 5.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 CHANGED
@@ -379,7 +379,7 @@ Validation Failed {
379
379
  ```
380
380
 
381
381
  ## Data Utilities
382
- Data utilities for binding values, and type conversion. Currently [DataUtil](https://github.com/travetto/travetto/tree/main/module/schema/src/data.ts#L8) includes:
382
+ Data utilities for binding values, and type conversion. Currently [DataUtil](https://github.com/travetto/travetto/tree/main/module/schema/src/data.ts#L10) includes:
383
383
  * `deepAssign(a, b, mode ?)` which allows for deep assignment of `b` onto `a`, the `mode` determines how aggressive the assignment is, and how flexible it is. `mode` can have any of the following values:
384
384
  * `loose`, which is the default is the most lenient. It will not error out, and overwrites will always happen
385
385
  * `coerce`, will attempt to force values from `b` to fit the types of `a`, and if it can't it will error out
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/schema",
3
- "version": "4.1.0",
3
+ "version": "5.0.0-rc.0",
4
4
  "description": "Data type registry for runtime validation, reflection and binding.",
5
5
  "keywords": [
6
6
  "schema",
@@ -27,10 +27,10 @@
27
27
  "directory": "module/schema"
28
28
  },
29
29
  "dependencies": {
30
- "@travetto/registry": "^4.1.0"
30
+ "@travetto/registry": "^5.0.0-rc.0"
31
31
  },
32
32
  "peerDependencies": {
33
- "@travetto/transformer": "^4.1.0"
33
+ "@travetto/transformer": "^5.0.0-rc.0"
34
34
  },
35
35
  "peerDependenciesMeta": {
36
36
  "@travetto/transformer": {
package/src/bind-util.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Class, ConcreteClass, TypedObject, ObjectUtil } from '@travetto/base';
1
+ import { Class, ConcreteClass, TypedObject } from '@travetto/base';
2
2
 
3
3
  import { DataUtil } from './data';
4
4
  import { AllViewⲐ } from './internal/types';
@@ -50,7 +50,7 @@ export class BindUtil {
50
50
  const out: Record<string, unknown> = {};
51
51
  for (const k of Object.keys(obj)) {
52
52
  const objK = obj[k];
53
- const val = ObjectUtil.isPlainObject(objK) ? this.expandPaths(objK) : objK;
53
+ const val = DataUtil.isPlainObject(objK) ? this.expandPaths(objK) : objK;
54
54
  const parts = k.split('.');
55
55
  const last = parts.pop()!;
56
56
  let sub = out;
@@ -77,7 +77,7 @@ export class BindUtil {
77
77
  const arr = last.indexOf('[') > 0;
78
78
 
79
79
  if (!arr) {
80
- if (sub[last] && ObjectUtil.isPlainObject(val)) {
80
+ if (sub[last] && DataUtil.isPlainObject(val)) {
81
81
  sub[last] = DataUtil.deepAssign(sub[last], val, 'coerce');
82
82
  } else {
83
83
  sub[last] = val;
@@ -95,7 +95,7 @@ export class BindUtil {
95
95
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
96
96
  key = arrSub.length as number;
97
97
  }
98
- if (arrSub[key] && ObjectUtil.isPlainObject(val) && ObjectUtil.isPlainObject(arrSub[key])) {
98
+ if (arrSub[key] && DataUtil.isPlainObject(val) && DataUtil.isPlainObject(arrSub[key])) {
99
99
  arrSub[key] = DataUtil.deepAssign(arrSub[key], val, 'coerce');
100
100
  } else {
101
101
  arrSub[key] = val;
@@ -114,13 +114,13 @@ export class BindUtil {
114
114
  const out: Record<string, V> = {};
115
115
  for (const [key, value] of Object.entries(data)) {
116
116
  const pre = `${prefix}${key}`;
117
- if (ObjectUtil.isPlainObject(value)) {
117
+ if (DataUtil.isPlainObject(value)) {
118
118
  Object.assign(out, this.flattenPaths(value, `${pre}.`)
119
119
  );
120
120
  } else if (Array.isArray(value)) {
121
121
  for (let i = 0; i < value.length; i++) {
122
122
  const v = value[i];
123
- if (ObjectUtil.isPlainObject(v)) {
123
+ if (DataUtil.isPlainObject(v)) {
124
124
  Object.assign(out, this.flattenPaths(v, `${pre}[${i}].`));
125
125
  } else {
126
126
  out[`${pre}[${i}]`] = v ?? '';
@@ -179,7 +179,7 @@ export class BindUtil {
179
179
  const view = cfg.view ?? AllViewⲐ; // Does not convey
180
180
  delete cfg.view;
181
181
 
182
- if (!!data && !ObjectUtil.isPrimitive(data)) {
182
+ if (!!data && !DataUtil.isPrimitive(data)) {
183
183
  const conf = SchemaRegistry.get(cons);
184
184
 
185
185
  // If no configuration
package/src/data.ts CHANGED
@@ -1,4 +1,6 @@
1
- import { Class, ClassInstance, TypedObject, ObjectUtil } from '@travetto/base';
1
+ import { isNumberObject as isNum, isBooleanObject as isBool, isStringObject as isStr } from 'node:util/types';
2
+
3
+ import { Class, ClassInstance, TypedObject } from '@travetto/base';
2
4
 
3
5
  const REGEX_PAT = /[\/](.*)[\/](i|g|m|s)?/;
4
6
 
@@ -7,13 +9,44 @@ const REGEX_PAT = /[\/](.*)[\/](i|g|m|s)?/;
7
9
  */
8
10
  export class DataUtil {
9
11
 
12
+ /**
13
+ * Is a value a plain JS object, created using {}
14
+ * @param obj Object to check
15
+ */
16
+ static isPlainObject(obj: unknown): obj is Record<string, unknown> {
17
+ return typeof obj === 'object' // separate from primitives
18
+ && obj !== undefined
19
+ && obj !== null // is obvious
20
+ && obj.constructor === Object // separate instances (Array, DOM, ...)
21
+ && Object.prototype.toString.call(obj) === '[object Object]'; // separate build-in like Math
22
+ }
23
+
24
+ /**
25
+ * Is a value of primitive type
26
+ * @param el Value to check
27
+ */
28
+ static isPrimitive(el: unknown): el is (string | boolean | number | RegExp) {
29
+ switch (typeof el) {
30
+ case 'string': case 'boolean': case 'number': case 'bigint': return true;
31
+ case 'object': return !!el && (el instanceof RegExp || el instanceof Date || isStr(el) || isNum(el) || isBool(el));
32
+ default: return false;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Is simple, as a primitive, function or class
38
+ */
39
+ static isSimpleValue(a: unknown): a is Function | Class | string | number | RegExp | Date {
40
+ return this.isPrimitive(a) || typeof a === 'function';
41
+ }
42
+
10
43
  static #deepAssignRaw(a: unknown, b: unknown, mode: 'replace' | 'loose' | 'strict' | 'coerce' = 'loose'): unknown {
11
44
  const isEmptyA = a === undefined || a === null;
12
45
  const isEmptyB = b === undefined || b === null;
13
46
  const isArrA = Array.isArray(a);
14
47
  const isArrB = Array.isArray(b);
15
- const isSimpA = !isEmptyA && ObjectUtil.isSimple(a);
16
- const isSimpB = !isEmptyB && ObjectUtil.isSimple(b);
48
+ const isSimpA = !isEmptyA && this.isSimpleValue(a);
49
+ const isSimpB = !isEmptyB && this.isSimpleValue(b);
17
50
 
18
51
  let ret: unknown;
19
52
 
@@ -108,9 +141,15 @@ export class DataUtil {
108
141
 
109
142
  switch (type) {
110
143
  case Date: {
111
- const res = typeof input === 'number' || /^[-]?\d+$/.test(`${input}`) ?
144
+ let res: Date | undefined;
145
+ if (typeof input === 'object' && 'toDate' in input && typeof input.toDate === 'function') {
112
146
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
113
- new Date(parseInt(input as string, 10)) : new Date(input as Date);
147
+ res = input.toDate() as Date;
148
+ } else {
149
+ res = typeof input === 'number' || /^[-]?\d+$/.test(`${input}`) ?
150
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
151
+ new Date(parseInt(input as string, 10)) : new Date(input as Date);
152
+ }
114
153
  if (strict && Number.isNaN(res.getTime())) {
115
154
  throw new Error(`Invalid date value: ${input}`);
116
155
  }
@@ -148,7 +187,7 @@ export class DataUtil {
148
187
  }
149
188
  }
150
189
  case Object: {
151
- if (!strict || ObjectUtil.isPlainObject(input)) {
190
+ if (!strict || this.isPlainObject(input)) {
152
191
  return input;
153
192
  } else {
154
193
  throw new Error('Invalid object type');
@@ -157,7 +196,7 @@ export class DataUtil {
157
196
  case undefined:
158
197
  case String: return `${input}`;
159
198
  }
160
- if (!strict || ObjectUtil.isPlainObject(input)) {
199
+ if (!strict || this.isPlainObject(input)) {
161
200
  return input;
162
201
  } else {
163
202
  throw new Error(`Unknown type ${type.name}`);
@@ -170,7 +209,7 @@ export class DataUtil {
170
209
  */
171
210
  static shallowClone<T = unknown>(a: T): T {
172
211
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
173
- return (Array.isArray(a) ? a.slice(0) : (ObjectUtil.isSimple(a) ? a : { ...(a as {}) })) as T;
212
+ return (Array.isArray(a) ? a.slice(0) : (this.isSimpleValue(a) ? a : { ...(a as {}) })) as T;
174
213
  }
175
214
 
176
215
  /**
@@ -180,7 +219,7 @@ export class DataUtil {
180
219
  * @param mode How the assignment should be handled
181
220
  */
182
221
  static deepAssign<T, U>(a: T, b: U, mode: | 'replace' | 'loose' | 'strict' | 'coerce' = 'loose'): T & U {
183
- if (!a || ObjectUtil.isSimple(a)) {
222
+ if (!a || this.isSimpleValue(a)) {
184
223
  throw new Error(`Cannot merge onto a simple value, ${a}`);
185
224
  }
186
225
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
@@ -411,6 +411,17 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
411
411
  // Write views out
412
412
  config = this.finalizeViews(cls, config);
413
413
 
414
+ if (config.subTypeName && config.subTypeField in config.views[AllViewⲐ].schema) {
415
+ const field = config.views[AllViewⲐ].schema[config.subTypeField];
416
+ config.views[AllViewⲐ].schema[config.subTypeField] = {
417
+ ...field,
418
+ enum: {
419
+ values: [config.subTypeName],
420
+ message: `${config.subTypeField} can only be '${config.subTypeName}'`,
421
+ }
422
+ };
423
+ }
424
+
414
425
  return config;
415
426
  }
416
427
 
@@ -1,4 +1,5 @@
1
- import { Primitive, Class } from '@travetto/base';
1
+ import { Class } from '@travetto/base';
2
+ import { Primitive } from '@travetto/schema';
2
3
 
3
4
  import { AllViewⲐ } from '../internal/types';
4
5
  import { ValidatorFn } from '../validate/types';
package/src/types.ts CHANGED
@@ -1,5 +1,7 @@
1
+ export type Primitive = number | bigint | boolean | string | Date;
2
+
1
3
  export type DeepPartial<T> = {
2
- [P in keyof T]?: (T[P] extends (number | string | Date | boolean | undefined) ? (T[P] | undefined) :
4
+ [P in keyof T]?: (T[P] extends (Primitive | undefined) ? (T[P] | undefined) :
3
5
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
6
  (T[P] extends any[] ? (DeepPartial<T[P][number]> | null | undefined)[] : DeepPartial<T[P]>));
5
7
  };
@@ -1,10 +1,11 @@
1
- import { Class, ClassInstance, TypedObject, ObjectUtil } from '@travetto/base';
1
+ import { Class, ClassInstance, TypedObject } from '@travetto/base';
2
2
 
3
3
  import { FieldConfig, SchemaConfig } from '../service/types';
4
4
  import { SchemaRegistry } from '../service/registry';
5
5
  import { ValidationError, ValidationKindCore, ValidationResult } from './types';
6
6
  import { Messages } from './messages';
7
7
  import { isValidationError, TypeMismatchError, ValidationResultError } from './error';
8
+ import { DataUtil } from '../data';
8
9
 
9
10
  /**
10
11
  * Get the schema config for Class/Schema config, including support for polymorphism
@@ -268,7 +269,7 @@ export class SchemaValidator {
268
269
  */
269
270
  static async validate<T>(cls: Class<T>, o: T, view?: string): Promise<T> {
270
271
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
271
- if (!ObjectUtil.isPlainObject(o) && !(o instanceof cls || cls.Ⲑid === (o as ClassInstance<T>).constructor.Ⲑid)) {
272
+ if (!DataUtil.isPlainObject(o) && !(o instanceof cls || cls.Ⲑid === (o as ClassInstance<T>).constructor.Ⲑid)) {
272
273
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
273
274
  throw new TypeMismatchError(cls.name, (o as ClassInstance).constructor.name);
274
275
  }