@travetto/schema 2.1.3 → 2.2.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 +58 -56
- package/package.json +3 -3
- package/src/bind-util.ts +28 -15
- package/src/decorator/common.ts +8 -4
- package/src/decorator/field.ts +39 -33
- package/src/decorator/schema.ts +4 -3
- package/src/extension/faker.ts +56 -50
- package/src/service/changes.ts +9 -9
- package/src/service/registry.ts +49 -43
- package/src/service/types.ts +2 -2
- package/src/typings.d.ts +8 -1
- package/src/validate/error.ts +4 -0
- package/src/validate/regexp.ts +1 -0
- package/src/validate/types.ts +2 -1
- package/src/validate/validator.ts +62 -45
- package/support/phase.init.ts +1 -1
- package/support/transform-util.ts +13 -4
- package/support/transformer.schema.ts +5 -5
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ The module utilizes AST transformations to collect schema information, and facil
|
|
|
25
25
|
* `description` - detailed description of the schema
|
|
26
26
|
* `examples` - A set of examples as [JSON](https://www.json.org) or [YAML](https://en.wikipedia.org/wiki/YAML)
|
|
27
27
|
|
|
28
|
-
The `title` will be picked up from the [JSDoc](http://usejsdoc.org/about-getting-started.html) comments, and additionally all fields can be set using the [@Describe](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/common.ts#
|
|
28
|
+
The `title` will be picked up from the [JSDoc](http://usejsdoc.org/about-getting-started.html) comments, and additionally all fields can be set using the [@Describe](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/common.ts#L15) decorator.
|
|
29
29
|
|
|
30
30
|
**Code: Sample User Schema**
|
|
31
31
|
```typescript
|
|
@@ -69,16 +69,16 @@ This schema provides a powerful base for data binding and validation at runtime.
|
|
|
69
69
|
* [@Enum](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L79) defines the allowable values that a field can have
|
|
70
70
|
* [@Match](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L100) defines a regular expression that the field value should match
|
|
71
71
|
* [@MinLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L108) enforces min length of a string
|
|
72
|
-
* [@MaxLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
72
|
+
* [@MaxLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L118) enforces max length of a string
|
|
73
73
|
* [@Min](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L108) enforces min value for a date or a number
|
|
74
|
-
* [@Max](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
75
|
-
* [@Email](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
76
|
-
* [@Telephone](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
77
|
-
* [@Url](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
78
|
-
* [@Ignore](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
79
|
-
* [@Integer](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
80
|
-
* [@Float](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
81
|
-
* [@Currency](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
74
|
+
* [@Max](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L118) enforces max value for a date or a number
|
|
75
|
+
* [@Email](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L145) ensures string field matches basic email regex
|
|
76
|
+
* [@Telephone](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L152) ensures string field matches basic telephone regex
|
|
77
|
+
* [@Url](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L159) ensures string field matches basic url regex
|
|
78
|
+
* [@Ignore](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L198) exclude from auto schema registration
|
|
79
|
+
* [@Integer](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L173) ensures number passed in is only a whole number
|
|
80
|
+
* [@Float](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L179) ensures number passed in allows fractional values
|
|
81
|
+
* [@Currency](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L191) provides support for standard currency
|
|
82
82
|
* [@Text](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L87) indicates that a field is expecting natural language input, not just discrete values
|
|
83
83
|
* [@LongText](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L92) same as text, but expects longer form content
|
|
84
84
|
* [@Readonly](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L65) defines a that field should not be bindable external to the class
|
|
@@ -92,7 +92,7 @@ Just like the class, all fields can be defined with
|
|
|
92
92
|
* `description` - detailed description of the schema
|
|
93
93
|
* `examples` - A set of examples as [JSON](https://www.json.org) or [YAML](https://en.wikipedia.org/wiki/YAML)
|
|
94
94
|
|
|
95
|
-
And similarly, the `description` will be picked up from the [JSDoc](http://usejsdoc.org/about-getting-started.html) comments, and additionally all fields can be set using the [@Describe](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/common.ts#
|
|
95
|
+
And similarly, the `description` will be picked up from the [JSDoc](http://usejsdoc.org/about-getting-started.html) comments, and additionally all fields can be set using the [@Describe](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/common.ts#L15) decorator.
|
|
96
96
|
|
|
97
97
|
## Binding/Validation
|
|
98
98
|
At runtime, once a schema is registered, a programmer can utilize this structure to perform specific operations. Specifically binding and validation.
|
|
@@ -124,7 +124,7 @@ A binding operation could look like:
|
|
|
124
124
|
```typescript
|
|
125
125
|
import { Person } from './person';
|
|
126
126
|
|
|
127
|
-
export function Test() {
|
|
127
|
+
export function Test(): Person {
|
|
128
128
|
return Person.from({
|
|
129
129
|
name: 'Test',
|
|
130
130
|
age: 19.999978,
|
|
@@ -181,7 +181,7 @@ import { SchemaValidator } from '@travetto/schema';
|
|
|
181
181
|
|
|
182
182
|
import { Person } from './person';
|
|
183
183
|
|
|
184
|
-
export async function validate() {
|
|
184
|
+
export async function validate(): Promise<void> {
|
|
185
185
|
|
|
186
186
|
const person = Person.from({
|
|
187
187
|
name: 'Test',
|
|
@@ -210,15 +210,14 @@ Validation Failed {
|
|
|
210
210
|
"errors": [
|
|
211
211
|
{
|
|
212
212
|
"kind": "type",
|
|
213
|
-
"
|
|
213
|
+
"message": "age is not a valid number",
|
|
214
214
|
"path": "age",
|
|
215
|
-
"
|
|
215
|
+
"type": "number"
|
|
216
216
|
},
|
|
217
217
|
{
|
|
218
218
|
"kind": "required",
|
|
219
|
-
"
|
|
220
|
-
"path": "address.street2"
|
|
221
|
-
"message": "address.street2 is required"
|
|
219
|
+
"message": "address.street2 is required",
|
|
220
|
+
"path": "address.street2"
|
|
222
221
|
}
|
|
223
222
|
]
|
|
224
223
|
}
|
|
@@ -230,9 +229,9 @@ Within the schema framework, it is possible to add custom validators class level
|
|
|
230
229
|
|
|
231
230
|
**Code: Password Validator**
|
|
232
231
|
```typescript
|
|
233
|
-
import { Schema, Validator } from '@travetto/schema';
|
|
232
|
+
import { Schema, Validator, ValidationError } from '@travetto/schema';
|
|
234
233
|
|
|
235
|
-
const passwordValidator = (user: User) => {
|
|
234
|
+
const passwordValidator = (user: User): ValidationError | undefined => {
|
|
236
235
|
const p = user.password;
|
|
237
236
|
const hasNum = /\d/.test(p);
|
|
238
237
|
const hasSpecial = /[!@#$%%^&*()<>?/,.;':"']/.test(p);
|
|
@@ -307,38 +306,41 @@ In addition to the general types, the code relies upon name matching to provide
|
|
|
307
306
|
|
|
308
307
|
**Code: Supported Mappings**
|
|
309
308
|
```typescript
|
|
310
|
-
static #namesToType
|
|
311
|
-
string: [
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
[
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
[
|
|
340
|
-
|
|
341
|
-
|
|
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
|
+
};
|
|
342
344
|
```
|
|
343
345
|
|
|
344
346
|
An example of this would be:
|
|
@@ -366,7 +368,7 @@ class User {
|
|
|
366
368
|
address: Address;
|
|
367
369
|
}
|
|
368
370
|
|
|
369
|
-
export function generate() {
|
|
371
|
+
export function generate(): User {
|
|
370
372
|
const user = SchemaFakerUtil.generate(User);
|
|
371
373
|
return user;
|
|
372
374
|
}
|
|
@@ -392,7 +394,7 @@ export type Point = [number, number];
|
|
|
392
394
|
const INVALID = Symbol.for('invalid-point');
|
|
393
395
|
|
|
394
396
|
export class PointImpl {
|
|
395
|
-
static validateSchema(input: unknown) {
|
|
397
|
+
static validateSchema(input: unknown): 'type' | undefined {
|
|
396
398
|
const ret = this.bindSchema(input);
|
|
397
399
|
return ret !== INVALID && ret && !isNaN(ret[0]) && !isNaN(ret[1]) ? undefined : 'type';
|
|
398
400
|
}
|
|
@@ -439,9 +441,9 @@ Validation Failed {
|
|
|
439
441
|
"errors": [
|
|
440
442
|
{
|
|
441
443
|
"kind": "type",
|
|
442
|
-
"
|
|
444
|
+
"message": "point is not a valid PointImpl",
|
|
443
445
|
"path": "point",
|
|
444
|
-
"
|
|
446
|
+
"type": "PointImpl"
|
|
445
447
|
}
|
|
446
448
|
]
|
|
447
449
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/schema",
|
|
3
3
|
"displayName": "Schema",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.2.0",
|
|
5
5
|
"description": "Data type registry for runtime validation, reflection and binding. ",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"schema",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"directory": "module/schema"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@travetto/registry": "^2.
|
|
33
|
-
"@travetto/transformer": "^2.
|
|
32
|
+
"@travetto/registry": "^2.2.0",
|
|
33
|
+
"@travetto/transformer": "^2.2.0"
|
|
34
34
|
},
|
|
35
35
|
"optionalPeerDependencies": {
|
|
36
36
|
"@types/faker": "^5.5.9",
|
package/src/bind-util.ts
CHANGED
|
@@ -28,15 +28,16 @@ export class BindUtil {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
31
32
|
return val as T;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* Register `from` on the Function prototype
|
|
36
37
|
*/
|
|
37
|
-
static register() {
|
|
38
|
+
static register(): void {
|
|
38
39
|
const proto = Object.getPrototypeOf(Function);
|
|
39
|
-
proto.from = function (data: object | ClassInstance, view?: string) {
|
|
40
|
+
proto.from = function (data: object | ClassInstance, view?: string): unknown {
|
|
40
41
|
return BindUtil.bindSchema(this, data, view);
|
|
41
42
|
};
|
|
42
43
|
}
|
|
@@ -48,10 +49,11 @@ export class BindUtil {
|
|
|
48
49
|
*
|
|
49
50
|
* @param obj The object to convert
|
|
50
51
|
*/
|
|
51
|
-
static expandPaths(obj: Record<string, unknown>) {
|
|
52
|
+
static expandPaths(obj: Record<string, unknown>): Record<string, unknown> {
|
|
52
53
|
const out: Record<string, unknown> = {};
|
|
53
54
|
for (const k of Object.keys(obj)) {
|
|
54
|
-
const
|
|
55
|
+
const objK = obj[k];
|
|
56
|
+
const val = Util.isPlainObject(objK) ? this.expandPaths(objK) : objK;
|
|
55
57
|
const parts = k.split('.');
|
|
56
58
|
const last = parts.pop()!;
|
|
57
59
|
let sub = out;
|
|
@@ -65,12 +67,14 @@ export class BindUtil {
|
|
|
65
67
|
if (!(name in sub)) {
|
|
66
68
|
sub[name] = typeof key === 'number' ? [] : {};
|
|
67
69
|
}
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
68
71
|
sub = sub[name] as Record<string, unknown>;
|
|
69
72
|
|
|
70
73
|
if (idx && key !== undefined) {
|
|
71
74
|
if (sub[key] === undefined) {
|
|
72
75
|
sub[key] = {};
|
|
73
76
|
}
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
74
78
|
sub = sub[key] as Record<string, unknown>;
|
|
75
79
|
}
|
|
76
80
|
}
|
|
@@ -88,8 +92,10 @@ export class BindUtil {
|
|
|
88
92
|
let key = arr ? (/^\d+$/.test(idx) ? parseInt(idx, 10) : (idx.trim() || undefined)) : undefined;
|
|
89
93
|
if (sub[name] === undefined) {
|
|
90
94
|
sub[name] = (typeof key === 'string') ? {} : [];
|
|
95
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
91
96
|
sub = sub[name] as Record<string, unknown>;
|
|
92
97
|
if (key === undefined) {
|
|
98
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
93
99
|
key = sub.length as number;
|
|
94
100
|
}
|
|
95
101
|
if (sub[key] && Util.isPlainObject(val) && Util.isPlainObject(sub[key])) {
|
|
@@ -108,7 +114,7 @@ export class BindUtil {
|
|
|
108
114
|
* @param conf The object to flatten the paths for
|
|
109
115
|
* @param val The starting prefix
|
|
110
116
|
*/
|
|
111
|
-
static flattenPaths(data: Record<string, unknown>, prefix: string = '') {
|
|
117
|
+
static flattenPaths(data: Record<string, unknown>, prefix: string = ''): Record<string, unknown> {
|
|
112
118
|
const out: Record<string, unknown> = {};
|
|
113
119
|
for (const [key, value] of Object.entries(data)) {
|
|
114
120
|
const pre = `${prefix}${key}`;
|
|
@@ -144,18 +150,23 @@ export class BindUtil {
|
|
|
144
150
|
if (data === null || data === undefined) {
|
|
145
151
|
return data;
|
|
146
152
|
}
|
|
147
|
-
|
|
153
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
154
|
+
const cls = SchemaRegistry.resolveSubTypeForInstance<T>(cons, data as T);
|
|
148
155
|
if (data instanceof cls) {
|
|
156
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
149
157
|
return data as T;
|
|
150
158
|
} else {
|
|
159
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
151
160
|
const tgt = new (cls as ConcreteClass<T>)();
|
|
152
161
|
SchemaRegistry.ensureInstanceTypeField(cls, tgt);
|
|
153
162
|
|
|
154
163
|
for (const [k, v] of Object.entries(tgt)) { // Do not retain undefined fields
|
|
155
164
|
if (v === undefined) {
|
|
165
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
156
166
|
delete tgt[k as keyof T];
|
|
157
167
|
}
|
|
158
168
|
}
|
|
169
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
159
170
|
return this.bindSchemaToObject(cls, tgt, data as object, view);
|
|
160
171
|
}
|
|
161
172
|
}
|
|
@@ -175,7 +186,9 @@ export class BindUtil {
|
|
|
175
186
|
|
|
176
187
|
// If no configuration
|
|
177
188
|
if (!conf) {
|
|
189
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
178
190
|
for (const k of Object.keys(data) as (keyof typeof obj)[]) {
|
|
191
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
179
192
|
obj[k] = data[k as keyof typeof data];
|
|
180
193
|
}
|
|
181
194
|
} else {
|
|
@@ -205,7 +218,8 @@ export class BindUtil {
|
|
|
205
218
|
continue;
|
|
206
219
|
}
|
|
207
220
|
|
|
208
|
-
|
|
221
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
222
|
+
let v: unknown = data[inboundField as keyof typeof data];
|
|
209
223
|
|
|
210
224
|
if (v !== undefined && v !== null) {
|
|
211
225
|
const config = viewConf.schema[schemaFieldName];
|
|
@@ -228,6 +242,7 @@ export class BindUtil {
|
|
|
228
242
|
}
|
|
229
243
|
}
|
|
230
244
|
|
|
245
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
231
246
|
obj[schemaFieldName as keyof typeof obj] = v as (typeof obj)[keyof typeof obj];
|
|
232
247
|
}
|
|
233
248
|
}
|
|
@@ -243,23 +258,21 @@ export class BindUtil {
|
|
|
243
258
|
* @param applyDefaults
|
|
244
259
|
* @returns
|
|
245
260
|
*/
|
|
246
|
-
static coerceField(field: FieldConfig, val: unknown, applyDefaults = false) {
|
|
261
|
+
static coerceField(field: FieldConfig, val: unknown, applyDefaults = false): unknown {
|
|
247
262
|
if ((val === undefined || val === null) && applyDefaults) {
|
|
248
263
|
val = field.default;
|
|
249
264
|
}
|
|
250
265
|
const complex = SchemaRegistry.has(field.type);
|
|
251
266
|
if (field.array) {
|
|
252
|
-
|
|
253
|
-
val = [val];
|
|
254
|
-
}
|
|
267
|
+
const valArr = !Array.isArray(val) ? [val] : val;
|
|
255
268
|
if (complex) {
|
|
256
|
-
val =
|
|
269
|
+
val = valArr.map(x => this.bindSchema(field.type, x, field.view));
|
|
257
270
|
} else {
|
|
258
|
-
val =
|
|
271
|
+
val = valArr.map(x => Util.coerceType(x, field.type, false));
|
|
259
272
|
}
|
|
260
273
|
} else {
|
|
261
274
|
if (complex) {
|
|
262
|
-
val = this.bindSchema(field.type, val
|
|
275
|
+
val = this.bindSchema(field.type, val, field.view);
|
|
263
276
|
} else {
|
|
264
277
|
val = Util.coerceType(val, field.type, false);
|
|
265
278
|
}
|
|
@@ -273,7 +286,7 @@ export class BindUtil {
|
|
|
273
286
|
* @param params
|
|
274
287
|
* @returns
|
|
275
288
|
*/
|
|
276
|
-
static
|
|
289
|
+
static coerceFields(fields: FieldConfig[], params: unknown[], applyDefaults = false): unknown[] {
|
|
277
290
|
params = [...params];
|
|
278
291
|
// Coerce types
|
|
279
292
|
for (const el of fields) {
|
package/src/decorator/common.ts
CHANGED
|
@@ -3,20 +3,24 @@ import { Class, ClassInstance } from '@travetto/base';
|
|
|
3
3
|
import { DescribableConfig } from '../service/types';
|
|
4
4
|
import { SchemaRegistry } from '../service/registry';
|
|
5
5
|
|
|
6
|
+
function isClassInstance(o: Class | ClassInstance, property?: string): o is ClassInstance {
|
|
7
|
+
return !!property;
|
|
8
|
+
}
|
|
9
|
+
|
|
6
10
|
/**
|
|
7
11
|
* Describe a model or a field
|
|
8
12
|
* @param config The describe configuration
|
|
9
13
|
* @augments `@trv:schema/Describe`
|
|
10
14
|
*/
|
|
11
15
|
export function Describe(config: Partial<DescribableConfig>) {
|
|
12
|
-
return (target: Class | ClassInstance, property?: string, descOrIdx?: PropertyDescriptor | number) => {
|
|
13
|
-
if (property) {
|
|
16
|
+
return (target: Class | ClassInstance, property?: string, descOrIdx?: PropertyDescriptor | number): void => {
|
|
17
|
+
if (isClassInstance(target, property)) {
|
|
14
18
|
if (descOrIdx !== undefined && typeof descOrIdx === 'number') {
|
|
15
19
|
property = `${property}.${descOrIdx}`;
|
|
16
20
|
}
|
|
17
|
-
SchemaRegistry.registerPendingFieldFacet(
|
|
21
|
+
SchemaRegistry.registerPendingFieldFacet(target.constructor, property!, config);
|
|
18
22
|
} else {
|
|
19
|
-
SchemaRegistry.register(target
|
|
23
|
+
SchemaRegistry.register(target, config);
|
|
20
24
|
}
|
|
21
25
|
};
|
|
22
26
|
}
|
package/src/decorator/field.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { CommonRegExp } from '../validate/regexp';
|
|
|
5
5
|
import { ClassList, FieldConfig } from '../service/types';
|
|
6
6
|
|
|
7
7
|
function prop(obj: Partial<FieldConfig>) {
|
|
8
|
-
return (t: ClassInstance, k: string, idx?: number | PropertyDescriptor) => {
|
|
8
|
+
return (t: ClassInstance, k: string, idx?: number | PropertyDescriptor): void => {
|
|
9
9
|
if (idx !== undefined && typeof idx === 'number') {
|
|
10
10
|
SchemaRegistry.registerPendingParamFacet(t.constructor, k, idx, obj);
|
|
11
11
|
} else {
|
|
@@ -14,20 +14,20 @@ function prop(obj: Partial<FieldConfig>) {
|
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
// eslint-disable-next-line max-len
|
|
18
|
+
const stringArrProp: (obj: Partial<FieldConfig>) => <T extends Partial<Record<K, string | unknown[]>>, K extends string>(t: T, k: K, idx?: number | PropertyDescriptor) => void = prop;
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// eslint-disable-next-line max-len
|
|
21
|
+
const stringArrStringProp: (obj: Partial<FieldConfig>) => <T extends Partial<Record<K, string | string[]>>, K extends string>(t: T, k: K, idx?: number | PropertyDescriptor) => void = prop;
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
// eslint-disable-next-line max-len
|
|
24
|
+
const numberProp: (obj: Partial<FieldConfig>) => <T extends Partial<Record<K, number>>, K extends string>(t: T, k: K, idx?: number | PropertyDescriptor) => void = prop;
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
// eslint-disable-next-line max-len
|
|
27
|
+
const stringNumberProp: (obj: Partial<FieldConfig>) => <T extends Partial<Record<K, string | number>>, K extends string>(t: T, k: K, idx?: number | PropertyDescriptor) => void = prop;
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
// eslint-disable-next-line max-len
|
|
30
|
+
const dateNumberProp: (obj: Partial<FieldConfig>) => <T extends Partial<Record<K, Date | number>>, K extends string>(t: T, k: K, idx?: number | PropertyDescriptor) => void = prop;
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Registering a field
|
|
@@ -36,7 +36,7 @@ const dateNumberProp = prop as
|
|
|
36
36
|
* @augments `@trv:schema/Field`
|
|
37
37
|
*/
|
|
38
38
|
export function Field(type: ClassList, config?: Partial<FieldConfig>) {
|
|
39
|
-
return (f: ClassInstance, k: string, idx?: number | PropertyDescriptor) => {
|
|
39
|
+
return (f: ClassInstance, k: string, idx?: number | PropertyDescriptor): void => {
|
|
40
40
|
if (idx !== undefined && typeof idx === 'number') {
|
|
41
41
|
SchemaRegistry.registerPendingParamConfig(f.constructor, k, idx, type, config);
|
|
42
42
|
} else {
|
|
@@ -50,33 +50,33 @@ export function Field(type: ClassList, config?: Partial<FieldConfig>) {
|
|
|
50
50
|
* @param aliases List of all aliases for a field
|
|
51
51
|
* @augments `@trv:schema/Field`
|
|
52
52
|
*/
|
|
53
|
-
export function Alias(...aliases: string[]) { return prop({ aliases }); }
|
|
53
|
+
export function Alias(...aliases: string[]): ReturnType<typeof prop> { return prop({ aliases }); }
|
|
54
54
|
/**
|
|
55
55
|
* Mark a field as writeonly
|
|
56
56
|
* @param active This determines if this field is readonly or not.
|
|
57
57
|
* @augments `@trv:schema/Field`
|
|
58
58
|
*/
|
|
59
|
-
export function Writeonly(active = true) { return prop({ access: 'writeonly' }); }
|
|
59
|
+
export function Writeonly(active = true): ReturnType<typeof prop> { return prop({ access: 'writeonly' }); }
|
|
60
60
|
/**
|
|
61
61
|
* Mark a field as readonly
|
|
62
62
|
* @param active This determines if this field is readonly or not.
|
|
63
63
|
* @augments `@trv:schema/Field`
|
|
64
64
|
*/
|
|
65
|
-
export function Readonly(active = true) { return prop({ access: 'readonly' }); }
|
|
65
|
+
export function Readonly(active = true): ReturnType<typeof prop> { return prop({ access: 'readonly' }); }
|
|
66
66
|
/**
|
|
67
67
|
* Mark a field as required
|
|
68
68
|
* @param active This determines if this field is required or not.
|
|
69
69
|
* @param message The error message when a the constraint fails.
|
|
70
70
|
* @augments `@trv:schema/Field`
|
|
71
71
|
*/
|
|
72
|
-
export function Required(active = true, message?: string) { return prop({ required: { active, message } }); }
|
|
72
|
+
export function Required(active = true, message?: string): ReturnType<typeof prop> { return prop({ required: { active, message } }); }
|
|
73
73
|
/**
|
|
74
74
|
* Define a field as a set of enumerated values
|
|
75
|
-
* @param
|
|
75
|
+
* @param values The list of values allowed for the enumeration
|
|
76
76
|
* @param message The error message to show when the constraint fails
|
|
77
77
|
* @augments `@trv:schema/Field`
|
|
78
78
|
*/
|
|
79
|
-
export function Enum(values: string[], message?: string) {
|
|
79
|
+
export function Enum(values: string[], message?: string): ReturnType<typeof stringNumberProp> {
|
|
80
80
|
message = message || `{path} is only allowed to be "${values.join('" or "')}"`;
|
|
81
81
|
return stringNumberProp({ enum: { values, message } });
|
|
82
82
|
}
|
|
@@ -84,12 +84,12 @@ export function Enum(values: string[], message?: string) {
|
|
|
84
84
|
* Mark the field as indicating it's storing textual data
|
|
85
85
|
* @augments `@trv:schema/Field`
|
|
86
86
|
*/
|
|
87
|
-
export function Text() { return stringArrStringProp({ specifier: 'text' }); }
|
|
87
|
+
export function Text(): ReturnType<typeof stringArrStringProp> { return stringArrStringProp({ specifier: 'text' }); }
|
|
88
88
|
/**
|
|
89
89
|
* Mark the field to indicate it's for long form text
|
|
90
90
|
* @augments `@trv:schema/Field`
|
|
91
91
|
*/
|
|
92
|
-
export function LongText() { return stringArrStringProp({ specifier: 'text-long' }); }
|
|
92
|
+
export function LongText(): ReturnType<typeof stringArrStringProp> { return stringArrStringProp({ specifier: 'text-long' }); }
|
|
93
93
|
|
|
94
94
|
/**
|
|
95
95
|
* Require the field to match a specific RegExp
|
|
@@ -97,7 +97,7 @@ export function LongText() { return stringArrStringProp({ specifier: 'text-long'
|
|
|
97
97
|
* @param message The message to show when the constraint fails
|
|
98
98
|
* @augments `@trv:schema/Field`
|
|
99
99
|
*/
|
|
100
|
-
export function Match(re: RegExp, message?: string) { return stringArrStringProp({ match: { re, message } }); }
|
|
100
|
+
export function Match(re: RegExp, message?: string): ReturnType<typeof stringArrStringProp> { return stringArrStringProp({ match: { re, message } }); }
|
|
101
101
|
|
|
102
102
|
/**
|
|
103
103
|
* The minimum length for the string or array
|
|
@@ -105,7 +105,9 @@ export function Match(re: RegExp, message?: string) { return stringArrStringProp
|
|
|
105
105
|
* @param message The message to show when the constraint fails
|
|
106
106
|
* @augments `@trv:schema/Field`
|
|
107
107
|
*/
|
|
108
|
-
export function MinLength(n: number, message?: string)
|
|
108
|
+
export function MinLength(n: number, message?: string): ReturnType<typeof stringArrProp> {
|
|
109
|
+
return stringArrProp({ minlength: { n, message }, ...(n === 0 ? { required: { active: false } } : {}) });
|
|
110
|
+
}
|
|
109
111
|
|
|
110
112
|
/**
|
|
111
113
|
* The maximum length for the string or array
|
|
@@ -113,7 +115,7 @@ export function MinLength(n: number, message?: string) { return stringArrProp({
|
|
|
113
115
|
* @param message The message to show when the constraint fails
|
|
114
116
|
* @augments `@trv:schema/Field`
|
|
115
117
|
*/
|
|
116
|
-
export function MaxLength(n: number, message?: string) { return stringArrProp({ maxlength: { n, message } }); }
|
|
118
|
+
export function MaxLength(n: number, message?: string): ReturnType<typeof stringArrProp> { return stringArrProp({ maxlength: { n, message } }); }
|
|
117
119
|
|
|
118
120
|
/**
|
|
119
121
|
* The minimum value
|
|
@@ -121,7 +123,9 @@ export function MaxLength(n: number, message?: string) { return stringArrProp({
|
|
|
121
123
|
* @param message The message to show when the constraint fails
|
|
122
124
|
* @augments `@trv:schema/Field`
|
|
123
125
|
*/
|
|
124
|
-
export function Min<T extends number | Date>(n: T, message?: string)
|
|
126
|
+
export function Min<T extends number | Date>(n: T, message?: string): ReturnType<typeof dateNumberProp> {
|
|
127
|
+
return dateNumberProp({ min: { n, message } });
|
|
128
|
+
}
|
|
125
129
|
|
|
126
130
|
/**
|
|
127
131
|
* The maximum value
|
|
@@ -129,28 +133,30 @@ export function Min<T extends number | Date>(n: T, message?: string) { return da
|
|
|
129
133
|
* @param message The message to show when the constraint fails
|
|
130
134
|
* @augments `@trv:schema/Field`
|
|
131
135
|
*/
|
|
132
|
-
export function Max<T extends number | Date>(n: T, message?: string)
|
|
136
|
+
export function Max<T extends number | Date>(n: T, message?: string): ReturnType<typeof dateNumberProp> {
|
|
137
|
+
return dateNumberProp({ max: { n, message } });
|
|
138
|
+
}
|
|
133
139
|
|
|
134
140
|
/**
|
|
135
141
|
* Mark a field as an email
|
|
136
142
|
* @param message The message to show when the constraint fails
|
|
137
143
|
* @augments `@trv:schema/Field`
|
|
138
144
|
*/
|
|
139
|
-
export function Email(message?: string) { return Match(CommonRegExp.email, message); }
|
|
145
|
+
export function Email(message?: string): ReturnType<typeof Match> { return Match(CommonRegExp.email, message); }
|
|
140
146
|
|
|
141
147
|
/**
|
|
142
148
|
* Mark a field as an telephone number
|
|
143
149
|
* @param message The message to show when the constraint fails
|
|
144
150
|
* @augments `@trv:schema/Field`
|
|
145
151
|
*/
|
|
146
|
-
export function Telephone(message?: string) { return Match(CommonRegExp.telephone, message); }
|
|
152
|
+
export function Telephone(message?: string): ReturnType<typeof Match> { return Match(CommonRegExp.telephone, message); }
|
|
147
153
|
|
|
148
154
|
/**
|
|
149
155
|
* Mark a field as a url
|
|
150
156
|
* @param message The message to show when the constraint fails
|
|
151
157
|
* @augments `@trv:schema/Field`
|
|
152
158
|
*/
|
|
153
|
-
export function Url(message?: string) { return Match(CommonRegExp.url, message); }
|
|
159
|
+
export function Url(message?: string): ReturnType<typeof Match> { return Match(CommonRegExp.url, message); }
|
|
154
160
|
|
|
155
161
|
/**
|
|
156
162
|
* Determine the numeric precision of the value
|
|
@@ -158,31 +164,31 @@ export function Url(message?: string) { return Match(CommonRegExp.url, message);
|
|
|
158
164
|
* @param decimals The number of decimal digits to support
|
|
159
165
|
* @augments `@trv:schema/Field`
|
|
160
166
|
*/
|
|
161
|
-
export function Precision(digits: number, decimals?: number) { return numberProp({ precision: [digits, decimals] }); }
|
|
167
|
+
export function Precision(digits: number, decimals?: number): ReturnType<typeof numberProp> { return numberProp({ precision: [digits, decimals] }); }
|
|
162
168
|
|
|
163
169
|
/**
|
|
164
170
|
* Mark a number as an integer
|
|
165
171
|
* @augments `@trv:schema/Field`
|
|
166
172
|
*/
|
|
167
|
-
export function Integer() { return Precision(0); }
|
|
173
|
+
export function Integer(): ReturnType<typeof numberProp> { return Precision(0); }
|
|
168
174
|
|
|
169
175
|
/**
|
|
170
176
|
* Mark a number as a float
|
|
171
177
|
* @augments `@trv:schema/Field`
|
|
172
178
|
*/
|
|
173
|
-
export function Float() { return Precision(10, 7); }
|
|
179
|
+
export function Float(): ReturnType<typeof numberProp> { return Precision(10, 7); }
|
|
174
180
|
|
|
175
181
|
/**
|
|
176
182
|
* Mark a number as a long value
|
|
177
183
|
* @augments `@trv:schema/Field`
|
|
178
184
|
*/
|
|
179
|
-
export function Long() { return Precision(19, 0); }
|
|
185
|
+
export function Long(): ReturnType<typeof numberProp> { return Precision(19, 0); }
|
|
180
186
|
|
|
181
187
|
/**
|
|
182
188
|
* Mark a number as a currency
|
|
183
189
|
* @augments `@trv:schema/Field`
|
|
184
190
|
*/
|
|
185
|
-
export function Currency() { return Precision(13, 2); }
|
|
191
|
+
export function Currency(): ReturnType<typeof numberProp> { return Precision(13, 2); }
|
|
186
192
|
|
|
187
193
|
/**
|
|
188
194
|
* Mark a field as ignored
|
package/src/decorator/schema.ts
CHANGED
|
@@ -22,7 +22,8 @@ export function Schema() { // Auto is used during compilation
|
|
|
22
22
|
* @param fn The validator function
|
|
23
23
|
*/
|
|
24
24
|
export function Validator<T>(fn: ValidatorFn<T, string>) {
|
|
25
|
-
return (target: Class<T>) => {
|
|
25
|
+
return (target: Class<T>): void => {
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
26
27
|
SchemaRegistry.getOrCreatePending(target).validators!.push(fn as ValidatorFn<unknown, unknown>);
|
|
27
28
|
};
|
|
28
29
|
}
|
|
@@ -33,7 +34,7 @@ export function Validator<T>(fn: ValidatorFn<T, string>) {
|
|
|
33
34
|
* @param fields The specific fields to add as part of a view
|
|
34
35
|
*/
|
|
35
36
|
export function View<T>(name: string, fields: ViewFieldsConfig<Partial<T>>) {
|
|
36
|
-
return (target: Class<Partial<T>>) => {
|
|
37
|
+
return (target: Class<Partial<T>>): void => {
|
|
37
38
|
SchemaRegistry.registerPendingView(target, name, fields);
|
|
38
39
|
};
|
|
39
40
|
}
|
|
@@ -44,7 +45,7 @@ export function View<T>(name: string, fields: ViewFieldsConfig<Partial<T>>) {
|
|
|
44
45
|
* @returns
|
|
45
46
|
*/
|
|
46
47
|
export function SubType<T>(name: string) {
|
|
47
|
-
return (target: Class<Partial<T>>) => {
|
|
48
|
+
return (target: Class<Partial<T>>): void => {
|
|
48
49
|
SchemaRegistry.registerSubTypes(target, name);
|
|
49
50
|
};
|
|
50
51
|
}
|