@travetto/schema 2.2.2 → 3.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
@@ -185,7 +185,7 @@ export async function validate(): Promise<void> {
185
185
 
186
186
  const person = Person.from({
187
187
  name: 'Test',
188
- // @ts-ignore
188
+ // @ts-expect-error
189
189
  age: 'abc',
190
190
  address: {
191
191
  street1: '1234 Fun'
@@ -284,96 +284,6 @@ export interface ValidationError {
284
284
  }
285
285
  ```
286
286
 
287
- ## Extension - Generation
288
- In the course of application development, there is often a need to generate fake data on demand. Given all the information that we have about the schemas provided, translating that into data generation is fairly straightforward. The generation utility is built upon [faker](https://github.com/marak/Faker.js/), mapping data types, and various field names into specific [faker](https://github.com/marak/Faker.js/) generation routines.
289
-
290
- By default all types are mapped as-is:
291
-
292
-
293
- * `string`
294
- * `number`
295
- * `Date`
296
- * `boolean`
297
- * Enumerations as `string` or `number` types.
298
- * Provided regular expressions:
299
- * email
300
- * url
301
- * telephone
302
- * postalCode
303
- * Sub-schemas as registered via [@Schema](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/schema.ts#L12) decorators.
304
-
305
- In addition to the general types, the code relies upon name matching to provide additional refinement:
306
-
307
- **Code: Supported Mappings**
308
- ```typescript
309
- static #namesToType: {
310
- string: [RegExp, () => string][];
311
- date: [RegExp, () => Date][];
312
- } = {
313
- string: [
314
- [/^(image|img).*url$/, (): string => faker.image.imageUrl()],
315
- [/^url$/, (): string => faker.internet.url()],
316
- [/^email(addr(ress)?)?$/, (): string => faker.internet.email()],
317
- [/^(tele)?phone(num|number)?$/, (): string => faker.phone.phoneNumber()],
318
- [/^((postal|zip)code)|zip$/, (): string => faker.address.zipCode()],
319
- [/f(irst)?name/, (): string => faker.name.firstName()],
320
- [/l(ast)?name/, (): string => faker.name.lastName()],
321
- [/^ip(add(ress)?)?$/, (): string => faker.internet.ip()],
322
- [/^ip(add(ress)?)?(v?)6$/, (): string => faker.internet.ipv6()],
323
- [/^username$/, (): string => faker.internet.userName()],
324
- [/^domain(name)?$/, (): string => faker.internet.domainName()],
325
- [/^file(path|name)?$/, (): string => faker.system.filePath()],
326
- [/^street(1)?$/, (): string => faker.address.streetAddress()],
327
- [/^street2$/, (): string => faker.address.secondaryAddress()],
328
- [/^county$/, (): string => faker.address.county()],
329
- [/^country$/, (): string => faker.address.country()],
330
- [/^state$/, (): string => faker.address.state()],
331
- [/^lon(gitude)?$/, (): string => faker.address.longitude()],
332
- [/^lat(itude)?$/, (): string => faker.address.latitude()],
333
- [/(profile).*(image|img)/, (): string => faker.image.avatar()],
334
- [/(image|img)/, (): string => faker.image.image()],
335
- [/^company(name)?$/, (): string => faker.company.companyName()],
336
- [/(desc|description)$/, (): string => faker.lorem.sentences(10)]
337
- ],
338
- date: [
339
- [/dob|birth/, (): Date => faker.date.past(60)],
340
- [/creat(e|ion)/, (): Date => between(-200, -100)],
341
- [/(update|modif(y|ied))/, (): Date => between(-100, -50)]
342
- ],
343
- };
344
- ```
345
-
346
- An example of this would be:
347
-
348
- **Code: More complex Schema, used with Faker**
349
- ```typescript
350
- import { Schema, SchemaFakerUtil } from '@travetto/schema';
351
-
352
- @Schema()
353
- class Address {
354
- street1: string;
355
- street2?: string;
356
- city: string;
357
- state: string;
358
- country: string;
359
- }
360
-
361
- @Schema()
362
- class User {
363
- fName: string;
364
- lName: string;
365
- email: string;
366
- phone: string;
367
- dob?: Date;
368
- address: Address;
369
- }
370
-
371
- export function generate(): User {
372
- const user = SchemaFakerUtil.generate(User);
373
- return user;
374
- }
375
- ```
376
-
377
287
  ## Custom Types
378
288
  When working with the schema, the basic types are easily understood, but some of [Typescript](https://typescriptlang.org)'s more complex constructs are too complex to automate cleanly.
379
289
 
package/index.ts CHANGED
@@ -12,6 +12,4 @@ export * from './src/validate/validator';
12
12
  export * from './src/validate/error';
13
13
  export * from './src/validate/types';
14
14
  export * from './src/bind-util';
15
- export * from './src/types';
16
- // Named export needed for proxying
17
- export { SchemaFakerUtil } from './src/extension/faker';
15
+ export * from './src/types';
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "@travetto/schema",
3
3
  "displayName": "Schema",
4
- "version": "2.2.2",
4
+ "version": "3.0.0-rc.0",
5
5
  "description": "Data type registry for runtime validation, reflection and binding. ",
6
6
  "keywords": [
7
7
  "schema",
8
8
  "ast-transformations",
9
9
  "validation",
10
- "faker",
11
10
  "typescript",
12
11
  "travetto",
13
12
  "decorators"
@@ -29,12 +28,8 @@
29
28
  "directory": "module/schema"
30
29
  },
31
30
  "dependencies": {
32
- "@travetto/registry": "^2.2.2",
33
- "@travetto/transformer": "^2.2.2"
34
- },
35
- "optionalPeerDependencies": {
36
- "@types/faker": "^5.5.9",
37
- "faker": "^5.5.3"
31
+ "@travetto/registry": "^3.0.0-rc.0",
32
+ "@travetto/transformer": "^3.0.0-rc.0"
38
33
  },
39
34
  "publishConfig": {
40
35
  "access": "public"
package/src/bind-util.ts CHANGED
@@ -157,13 +157,13 @@ export class BindUtil {
157
157
  return data as T;
158
158
  } else {
159
159
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
160
- const tgt = new (cls as ConcreteClass<T>)();
161
- SchemaRegistry.ensureInstanceTypeField(cls, tgt);
160
+ const tgt = new (cls as ConcreteClass<T>)() as T;
161
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
162
+ SchemaRegistry.ensureInstanceTypeField(cls, tgt as T & { type?: string });
162
163
 
163
- for (const [k, v] of Object.entries(tgt)) { // Do not retain undefined fields
164
- if (v === undefined) {
165
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
166
- delete tgt[k as keyof T];
164
+ for (const k of Object.keys(tgt)) { // Do not retain undefined fields
165
+ if (tgt[k] === undefined) {
166
+ delete tgt[k];
167
167
  }
168
168
  }
169
169
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
@@ -1,4 +1,4 @@
1
- import { Class, AppError, Util, ClassInstance } from '@travetto/base';
1
+ import { Class, AppError, Util, ClassInstance, ConcreteClass } from '@travetto/base';
2
2
  import { MetadataRegistry, RootRegistry, ChangeEvent } from '@travetto/registry';
3
3
 
4
4
  import { ClassList, FieldConfig, ClassConfig, SchemaConfig, ViewFieldsConfig, ViewConfig } from './types';
@@ -11,6 +11,16 @@ function hasType<T>(o: unknown): o is { type: Class<T> | string } {
11
11
  return !!o && !Util.isPrimitive(o) && 'type' in (o as object) && !!(o as Record<string, string>)['type'];
12
12
  }
13
13
 
14
+ function isWithType<T>(o: T, cfg: ClassConfig | undefined): o is T & { type?: string } {
15
+ return !!cfg && !!cfg.subType && 'type' in cfg.views[AllViewⲐ].schema;
16
+ }
17
+
18
+ function getConstructor<T>(o: T): ConcreteClass<T> {
19
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
20
+ return (o as unknown as ClassInstance<T>).constructor;
21
+ }
22
+
23
+
14
24
  /**
15
25
  * Schema registry for listening to changes
16
26
  */
@@ -49,9 +59,8 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
49
59
  * Ensure type is set properly
50
60
  */
51
61
  ensureInstanceTypeField<T>(cls: Class, o: T): void {
52
- const withType: { type?: string } = o;
53
- if (this.get(cls)?.subType && 'type' in this.get(cls).views[AllViewⲐ].schema && !withType.type) { // Do we have a type field defined
54
- withType.type = this.#computeSubTypeName(cls); // Assign if missing
62
+ if (isWithType(o, this.get(cls)) && !o.type) { // Do we have a type field defined
63
+ o.type = this.#computeSubTypeName(cls); // Assign if missing
55
64
  }
56
65
  }
57
66
 
@@ -61,8 +70,7 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
61
70
  * @param o Actual instance
62
71
  */
63
72
  resolveSubTypeForInstance<T>(cls: Class<T>, o: T): Class {
64
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
65
- return this.resolveSubType(cls, hasType<T>(o) ? o.type : (o as unknown as ClassInstance<T>).constructor);
73
+ return this.resolveSubType(cls, hasType<T>(o) ? o.type : getConstructor(o));
66
74
  }
67
75
 
68
76
  /**
@@ -38,7 +38,7 @@ export class SchemaValidator {
38
38
  static #validateSchema<T>(schema: SchemaConfig, o: T, relative: string): ValidationError[] {
39
39
  let errors: ValidationError[] = [];
40
40
 
41
- const fields: (keyof SchemaConfig)[] = Object.keys(schema);
41
+ const fields = Object.keys<SchemaConfig>(schema);
42
42
  for (const field of fields) {
43
43
  if (schema[field].access !== 'readonly') { // Do not validate readonly fields
44
44
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
@@ -104,10 +104,7 @@ export class SchemaValidator {
104
104
  * @param key The bounds to check
105
105
  * @param value The value to validate
106
106
  */
107
- static #validateRange(field: FieldConfig, key: 'min' | 'max', rawValue: string | number | Date | unknown): boolean {
108
-
109
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
110
- let value = rawValue as string | number | Date;
107
+ static #validateRange(field: FieldConfig, key: 'min' | 'max', value: string | number | Date): boolean {
111
108
 
112
109
  const f = field[key]!;
113
110
  if (typeof f.n === 'number') {
@@ -178,11 +175,13 @@ export class SchemaValidator {
178
175
  criteria.push(['enum', field.enum]);
179
176
  }
180
177
 
181
- if (field.min && this.#validateRange(field, 'min', value)) {
178
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
179
+ if (field.min && this.#validateRange(field, 'min', value as number)) {
182
180
  criteria.push(['min', field.min]);
183
181
  }
184
182
 
185
- if (field.max && this.#validateRange(field, 'max', value)) {
183
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
184
+ if (field.max && this.#validateRange(field, 'max', value as number)) {
186
185
  criteria.push(['max', field.max]);
187
186
  }
188
187
 
@@ -1,223 +0,0 @@
1
- // @file-if faker
2
- import * as fakerType from 'faker';
3
-
4
- import { Class } from '@travetto/base';
5
-
6
- import { CommonRegExp } from '../validate/regexp';
7
- import { FieldConfig } from '../service/types';
8
- import { SchemaRegistry } from '../service/registry';
9
- import { BindUtil } from '../bind-util';
10
-
11
- // Load faker on demand, as its very heavy in loading
12
- let faker: typeof fakerType = new Proxy(
13
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
14
- {} as typeof fakerType,
15
- {
16
- get: (t, prop, r) => (faker = require('faker'))[prop]
17
- });
18
-
19
- const DAY_IN_MS = 24 * 60 * 60 * 1000;
20
-
21
- const between = (fromDays: number, toDays: number): Date =>
22
- faker.date.between(
23
- new Date(Date.now() + fromDays * DAY_IN_MS),
24
- new Date(Date.now() + toDays * DAY_IN_MS)
25
- );
26
-
27
- /**
28
- * Provide a faker utility for generating content
29
- */
30
- export class SchemaFakerUtil {
31
-
32
- static #stringToReType: [RegExp, () => string][] = [
33
- [CommonRegExp.email, (): string => faker.internet.email()],
34
- [CommonRegExp.url, (): string => faker.internet.url()],
35
- [CommonRegExp.telephone, (): string => faker.phone.phoneNumber()],
36
- [CommonRegExp.postalCode, (): string => faker.address.zipCode()]
37
- ];
38
-
39
- /**
40
- * Mapping of field types to faker utils
41
- */
42
- static #namesToType: {
43
- string: [RegExp, () => string][];
44
- date: [RegExp, () => Date][];
45
- } = {
46
- string: [
47
- [/^(image|img).*url$/, (): string => faker.image.imageUrl()],
48
- [/^url$/, (): string => faker.internet.url()],
49
- [/^email(addr(ress)?)?$/, (): string => faker.internet.email()],
50
- [/^(tele)?phone(num|number)?$/, (): string => faker.phone.phoneNumber()],
51
- [/^((postal|zip)code)|zip$/, (): string => faker.address.zipCode()],
52
- [/f(irst)?name/, (): string => faker.name.firstName()],
53
- [/l(ast)?name/, (): string => faker.name.lastName()],
54
- [/^ip(add(ress)?)?$/, (): string => faker.internet.ip()],
55
- [/^ip(add(ress)?)?(v?)6$/, (): string => faker.internet.ipv6()],
56
- [/^username$/, (): string => faker.internet.userName()],
57
- [/^domain(name)?$/, (): string => faker.internet.domainName()],
58
- [/^file(path|name)?$/, (): string => faker.system.filePath()],
59
- [/^street(1)?$/, (): string => faker.address.streetAddress()],
60
- [/^street2$/, (): string => faker.address.secondaryAddress()],
61
- [/^county$/, (): string => faker.address.county()],
62
- [/^country$/, (): string => faker.address.country()],
63
- [/^state$/, (): string => faker.address.state()],
64
- [/^lon(gitude)?$/, (): string => faker.address.longitude()],
65
- [/^lat(itude)?$/, (): string => faker.address.latitude()],
66
- [/(profile).*(image|img)/, (): string => faker.image.avatar()],
67
- [/(image|img)/, (): string => faker.image.image()],
68
- [/^company(name)?$/, (): string => faker.company.companyName()],
69
- [/(desc|description)$/, (): string => faker.lorem.sentences(10)]
70
- ],
71
- date: [
72
- [/dob|birth/, (): Date => faker.date.past(60)],
73
- [/creat(e|ion)/, (): Date => between(-200, -100)],
74
- [/(update|modif(y|ied))/, (): Date => between(-100, -50)]
75
- ],
76
- };
77
-
78
- /**
79
- * Get an array of values
80
- * @param cfg Field configuration
81
- */
82
- static getArrayValue(cfg: FieldConfig): unknown[] {
83
- const min = cfg.minlength ? cfg.minlength.n : 0;
84
- const max = cfg.maxlength ? cfg.maxlength.n : 10;
85
- const size = faker.datatype.number({ min, max });
86
- const out = [];
87
- for (let i = 0; i < size; i++) {
88
- out.push(this.getValue(cfg, true));
89
- }
90
- return out;
91
- }
92
-
93
- /**
94
- * Get a new number value
95
- * @param cfg Number config
96
- */
97
- static getNumberValue(cfg: FieldConfig): number {
98
- let min = cfg.min && typeof cfg.min.n === 'number' ? cfg.min.n : undefined;
99
- let max = cfg.max && typeof cfg.max.n === 'number' ? cfg.max.n : undefined;
100
- let precision = cfg.precision;
101
-
102
- const name = cfg.name.toUpperCase();
103
-
104
- if (/(price|amt|amount)$/.test(name)) {
105
- precision = [13, 2];
106
- }
107
-
108
- let offset = 1;
109
-
110
- if (precision !== undefined) {
111
- min = min === undefined ? -((10 ** precision[0]) - 1) : min;
112
- max = max === undefined ? ((10 ** precision[0]) - 1) : max;
113
- if (precision[1] !== undefined) {
114
- offset = (10 ** (precision[1] || 0));
115
- }
116
- }
117
-
118
- max = max === undefined ? 1000 : max;
119
- min = min === undefined ? 0 : min;
120
-
121
- const range = (max - min) * offset;
122
-
123
- const val = Math.trunc(Math.random() * range);
124
-
125
- return (val / offset) + min;
126
- }
127
-
128
- /**
129
- * Get a date value
130
- * @param cfg Field config
131
- */
132
- static getDateValue(cfg: FieldConfig): Date {
133
- const name = cfg.name.toUpperCase();
134
- const min = cfg.min && typeof cfg.min.n !== 'number' ? cfg.min.n : undefined;
135
- const max = cfg.max && typeof cfg.max.n !== 'number' ? cfg.max.n : undefined;
136
-
137
- if (min !== undefined || max !== undefined) {
138
- return faker.date.between(min || new Date(Date.now() - (50 * DAY_IN_MS)), max || new Date());
139
- } else {
140
- for (const [re, fn] of this.#namesToType.date) {
141
- if (re.test(name)) {
142
- return fn();
143
- }
144
- }
145
- return faker.date.recent(50);
146
- }
147
- }
148
-
149
- /**
150
- * Get a string value
151
- * @param cfg Field config
152
- */
153
- static getStringValue(cfg: FieldConfig): string {
154
- const name = cfg.name.toLowerCase();
155
-
156
- if (cfg.match) {
157
- const mre = cfg.match && cfg.match.re;
158
- for (const [re, fn] of this.#stringToReType) {
159
- if (mre === re) {
160
- return fn();
161
- }
162
- }
163
- }
164
-
165
- for (const [re, fn] of this.#namesToType.string) {
166
- if (re.test(name)) {
167
- return fn();
168
- }
169
- }
170
-
171
- return faker.random.word();
172
- }
173
-
174
- /**
175
- * Get a value for a field config
176
- * @param cfg Field config
177
- */
178
- static getValue(cfg: FieldConfig, subArray = false): unknown {
179
- if (!subArray && cfg.array) {
180
- return this.getArrayValue(cfg);
181
- } else if (cfg.enum) {
182
- return faker.random.arrayElement(cfg.enum.values);
183
- } else {
184
-
185
- const typ = cfg.type;
186
-
187
- if (typ === Number) {
188
- return this.getNumberValue(cfg);
189
- } else if (typ === String) {
190
- return this.getStringValue(cfg);
191
- } else if (typ === Date) {
192
- return this.getDateValue(cfg);
193
- } else if (typ === Boolean) {
194
- return faker.datatype.boolean();
195
- } else if (SchemaRegistry.has(typ)) {
196
- return this.generate(typ);
197
- }
198
- }
199
- }
200
-
201
- /**
202
- * Generate a new instance of a class
203
- * @param cls The class to get an instance of
204
- * @param view The view to define specifically
205
- */
206
- static generate<T>(cls: Class<T>, view?: string): T {
207
- const cfg = SchemaRegistry.getViewSchema(cls, view);
208
- const out: Record<string, unknown> = {};
209
-
210
- for (const f of cfg.fields) {
211
- if (f === 'type' || f === 'id') { // Do not set primary fields
212
- continue;
213
- }
214
- const fieldConfig = cfg.schema[f];
215
- if (!fieldConfig.required && (Math.random() < .5)) {
216
- continue;
217
- }
218
- out[f] = this.getValue(fieldConfig);
219
- }
220
-
221
- return BindUtil.bindSchema(cls, out, view);
222
- }
223
- }