@travetto/schema 3.0.0-rc.4 → 3.0.0-rc.7
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 +31 -62
- package/{index.ts → __index__.ts} +0 -0
- package/package.json +16 -7
- package/src/bind-util.ts +62 -49
- package/src/decorator/common.ts +1 -1
- package/src/decorator/field.ts +28 -22
- package/src/decorator/schema.ts +6 -1
- package/src/internal/types.ts +1 -1
- package/src/service/changes.ts +2 -9
- package/src/service/registry.ts +52 -5
- package/src/service/types.ts +13 -2
- package/src/typings.d.ts +2 -2
- package/src/validate/regexp.ts +2 -1
- package/src/validate/validator.ts +3 -3
- package/support/transform-util.ts +9 -8
- package/support/transformer.schema.ts +9 -11
- package/support/phase.init.ts +0 -11
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!-- This file was generated by @travetto/doc and should not be modified directly -->
|
|
2
|
-
<!-- Please modify https://github.com/travetto/travetto/tree/main/module/schema/
|
|
2
|
+
<!-- Please modify https://github.com/travetto/travetto/tree/main/module/schema/DOC.ts and execute "npx trv doc" to rebuild -->
|
|
3
3
|
# Schema
|
|
4
|
-
## Data type registry for runtime validation, reflection and binding.
|
|
4
|
+
## Data type registry for runtime validation, reflection and binding.
|
|
5
5
|
|
|
6
6
|
**Install: @travetto/schema**
|
|
7
7
|
```bash
|
|
@@ -9,13 +9,13 @@ npm install @travetto/schema
|
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
This module's purpose is to allow for proper declaration and validation of data types, in the course of running a program. The framework defined here, is
|
|
12
|
-
leveraged in the [Configuration](https://github.com/travetto/travetto/tree/main/module/config#readme "
|
|
12
|
+
leveraged in the [Configuration](https://github.com/travetto/travetto/tree/main/module/config#readme "Configuration support"), [Application](https://github.com/travetto/travetto/tree/main/module/app#readme "Application registration/management and run support."), [RESTful API](https://github.com/travetto/travetto/tree/main/module/rest#readme "Declarative api for RESTful APIs with support for the dependency injection module."), [OpenAPI Specification](https://github.com/travetto/travetto/tree/main/module/openapi#readme "OpenAPI integration support for the travetto framework") and [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") modules. The schema is the backbone of all data transfer, as it helps to
|
|
13
13
|
provide validation on correctness of input, whether it is a rest request, command line inputs, or a configuration file.
|
|
14
14
|
|
|
15
15
|
This module provides a mechanism for registering classes and field level information as well the ability to apply that information at runtime.
|
|
16
16
|
|
|
17
17
|
## Registration
|
|
18
|
-
The registry's schema information is defined by [Typescript](https://typescriptlang.org) AST and only applies to classes registered with the [@Schema](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/schema.ts#
|
|
18
|
+
The registry's schema information is defined by [Typescript](https://typescriptlang.org) AST and only applies to classes registered with the [@Schema](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/schema.ts#L14) decoration.
|
|
19
19
|
|
|
20
20
|
### Classes
|
|
21
21
|
The module utilizes AST transformations to collect schema information, and facilitate the registration process without user intervention. The class can also be described using providing a:
|
|
@@ -65,22 +65,22 @@ This schema provides a powerful base for data binding and validation at runtime.
|
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
* [@Field](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L38) defines a field that will be serialized.
|
|
68
|
-
* [@Required](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
69
|
-
* [@Enum](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
70
|
-
* [@Match](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
71
|
-
* [@MinLength](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#
|
|
73
|
-
* [@Min](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#
|
|
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#
|
|
82
|
-
* [@Text](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
83
|
-
* [@LongText](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
68
|
+
* [@Required](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L78) defines a that field should be required
|
|
69
|
+
* [@Enum](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L85) defines the allowable values that a field can have
|
|
70
|
+
* [@Match](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L106) defines a regular expression that the field value should match
|
|
71
|
+
* [@MinLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L114) enforces min length of a string
|
|
72
|
+
* [@MaxLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L124) enforces max length of a string
|
|
73
|
+
* [@Min](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L114) enforces min value for a date or a number
|
|
74
|
+
* [@Max](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L124) enforces max value for a date or a number
|
|
75
|
+
* [@Email](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L151) ensures string field matches basic email regex
|
|
76
|
+
* [@Telephone](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L158) ensures string field matches basic telephone regex
|
|
77
|
+
* [@Url](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L165) ensures string field matches basic url regex
|
|
78
|
+
* [@Ignore](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L204) exclude from auto schema registration
|
|
79
|
+
* [@Integer](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L179) ensures number passed in is only a whole number
|
|
80
|
+
* [@Float](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L185) ensures number passed in allows fractional values
|
|
81
|
+
* [@Currency](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L197) provides support for standard currency
|
|
82
|
+
* [@Text](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L93) indicates that a field is expecting natural language input, not just discrete values
|
|
83
|
+
* [@LongText](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L98) 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
|
|
85
85
|
* [@Writeonly](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L59) defines a that field should not be exported in serialization, but that it can be bound to
|
|
86
86
|
|
|
@@ -98,7 +98,7 @@ And similarly, the `description` will be picked up from the [JSDoc](http://usejs
|
|
|
98
98
|
At runtime, once a schema is registered, a programmer can utilize this structure to perform specific operations. Specifically binding and validation.
|
|
99
99
|
|
|
100
100
|
### Binding
|
|
101
|
-
Binding is a very simple operation, as it takes in a class registered as as [@Schema](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/schema.ts#
|
|
101
|
+
Binding is a very simple operation, as it takes in a class registered as as [@Schema](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/schema.ts#L14) and a JS object that will be the source of the binding. Given the schema:
|
|
102
102
|
|
|
103
103
|
**Code: Sub Schemas via Address**
|
|
104
104
|
```typescript
|
|
@@ -140,13 +140,14 @@ and the output would be a `Person` instance with the following structure
|
|
|
140
140
|
|
|
141
141
|
**Terminal: Sample data output after binding**
|
|
142
142
|
```bash
|
|
143
|
-
$
|
|
143
|
+
$ trv main support/main.person-output.ts
|
|
144
144
|
|
|
145
145
|
Person {
|
|
146
146
|
name: 'Test',
|
|
147
147
|
age: 19,
|
|
148
148
|
address: Address { street1: '1234 Fun', street2: 'Unit 20' }
|
|
149
149
|
}
|
|
150
|
+
[s[r[u
|
|
150
151
|
```
|
|
151
152
|
|
|
152
153
|
**Note**: Binding will attempt to convert/coerce types as much as possible to honor the pattern of Javascript and it's dynamic nature.
|
|
@@ -200,27 +201,9 @@ would produce an exception similar to following structure
|
|
|
200
201
|
|
|
201
202
|
**Terminal: Sample error output**
|
|
202
203
|
```bash
|
|
203
|
-
$
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
"message": "Validation errors have occurred",
|
|
207
|
-
"category": "data",
|
|
208
|
-
"type": "ValidationResultError",
|
|
209
|
-
"at": "2022-03-14T04:00:00.618Z",
|
|
210
|
-
"errors": [
|
|
211
|
-
{
|
|
212
|
-
"kind": "type",
|
|
213
|
-
"message": "age is not a valid number",
|
|
214
|
-
"path": "age",
|
|
215
|
-
"type": "number"
|
|
216
|
-
},
|
|
217
|
-
{
|
|
218
|
-
"kind": "required",
|
|
219
|
-
"message": "address.street2 is required",
|
|
220
|
-
"path": "address.street2"
|
|
221
|
-
}
|
|
222
|
-
]
|
|
223
|
-
}
|
|
204
|
+
$ trv main support/main.person-invalid-output.ts
|
|
205
|
+
|
|
206
|
+
[s[r[u
|
|
224
207
|
```
|
|
225
208
|
|
|
226
209
|
### Custom Validators
|
|
@@ -294,7 +277,7 @@ This feature is meant to allow for simple Typescript types to be able to be back
|
|
|
294
277
|
|
|
295
278
|
**Code: Simple Custom Type**
|
|
296
279
|
```typescript
|
|
297
|
-
import {
|
|
280
|
+
import { DataUtil } from '@travetto/base';
|
|
298
281
|
|
|
299
282
|
/**
|
|
300
283
|
* @concrete .:PointImpl
|
|
@@ -311,7 +294,7 @@ export class PointImpl {
|
|
|
311
294
|
|
|
312
295
|
static bindSchema(input: unknown): [number, number] | typeof INVALID | undefined {
|
|
313
296
|
if (Array.isArray(input) && input.length === 2) {
|
|
314
|
-
return input.map(x =>
|
|
297
|
+
return input.map(x => DataUtil.coerceType(x, Number, false)) as [number, number];
|
|
315
298
|
} else {
|
|
316
299
|
return INVALID;
|
|
317
300
|
}
|
|
@@ -341,21 +324,7 @@ All that happens now, is the type is exported, and the class above is able to pr
|
|
|
341
324
|
|
|
342
325
|
**Terminal: Custom Type Validation**
|
|
343
326
|
```bash
|
|
344
|
-
$
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
"message": "Validation errors have occurred",
|
|
348
|
-
"category": "data",
|
|
349
|
-
"type": "ValidationResultError",
|
|
350
|
-
"at": "2022-03-14T04:00:00.837Z",
|
|
351
|
-
"errors": [
|
|
352
|
-
{
|
|
353
|
-
"kind": "type",
|
|
354
|
-
"message": "point is not a valid PointImpl",
|
|
355
|
-
"path": "point",
|
|
356
|
-
"type": "PointImpl"
|
|
357
|
-
}
|
|
358
|
-
]
|
|
359
|
-
}
|
|
327
|
+
$ trv main support/main.custom-type-output.ts
|
|
328
|
+
|
|
329
|
+
[s[r[u
|
|
360
330
|
```
|
|
361
|
-
;
|
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/schema",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"description": "Data type registry for runtime validation, reflection and binding. ",
|
|
3
|
+
"version": "3.0.0-rc.7",
|
|
4
|
+
"description": "Data type registry for runtime validation, reflection and binding.",
|
|
6
5
|
"keywords": [
|
|
7
6
|
"schema",
|
|
8
7
|
"ast-transformations",
|
|
@@ -18,18 +17,28 @@
|
|
|
18
17
|
"name": "Travetto Framework"
|
|
19
18
|
},
|
|
20
19
|
"files": [
|
|
21
|
-
"
|
|
20
|
+
"__index__.ts",
|
|
22
21
|
"src",
|
|
23
22
|
"support"
|
|
24
23
|
],
|
|
25
|
-
"main": "
|
|
24
|
+
"main": "__index__.ts",
|
|
26
25
|
"repository": {
|
|
27
26
|
"url": "https://github.com/travetto/travetto.git",
|
|
28
27
|
"directory": "module/schema"
|
|
29
28
|
},
|
|
30
29
|
"dependencies": {
|
|
31
|
-
"@travetto/registry": "^3.0.0-rc.
|
|
32
|
-
|
|
30
|
+
"@travetto/registry": "^3.0.0-rc.7"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@travetto/transformer": "^3.0.0-rc.7"
|
|
34
|
+
},
|
|
35
|
+
"peerDependenciesMeta": {
|
|
36
|
+
"@travetto/transformer": {
|
|
37
|
+
"optional": true
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"travetto": {
|
|
41
|
+
"displayName": "Schema"
|
|
33
42
|
},
|
|
34
43
|
"publishConfig": {
|
|
35
44
|
"access": "public"
|
package/src/bind-util.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import { Class,
|
|
1
|
+
import { Class, ConcreteClass, TypedObject, ObjectUtil, DataUtil } from '@travetto/base';
|
|
2
2
|
|
|
3
3
|
import { AllViewⲐ } from './internal/types';
|
|
4
4
|
import { SchemaRegistry } from './service/registry';
|
|
5
5
|
import { FieldConfig } from './service/types';
|
|
6
6
|
|
|
7
|
+
type BindConfig = {
|
|
8
|
+
view?: string | typeof AllViewⲐ;
|
|
9
|
+
filterField?: (field: FieldConfig) => boolean;
|
|
10
|
+
filterValue?: (value: unknown, field: FieldConfig) => boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
7
13
|
/**
|
|
8
14
|
* Utilities for binding objects to schemas
|
|
9
15
|
*/
|
|
@@ -18,7 +24,7 @@ export class BindUtil {
|
|
|
18
24
|
if (conf.type?.bindSchema) {
|
|
19
25
|
val = conf.type.bindSchema(val);
|
|
20
26
|
} else {
|
|
21
|
-
val =
|
|
27
|
+
val = DataUtil.coerceType(val, conf.type, false);
|
|
22
28
|
|
|
23
29
|
if (conf.type === Number && conf.precision && typeof val === 'number') {
|
|
24
30
|
if (conf.precision[1]) { // Supports decimal
|
|
@@ -32,16 +38,6 @@ export class BindUtil {
|
|
|
32
38
|
return val as T;
|
|
33
39
|
}
|
|
34
40
|
|
|
35
|
-
/**
|
|
36
|
-
* Register `from` on the Function prototype
|
|
37
|
-
*/
|
|
38
|
-
static register(): void {
|
|
39
|
-
const proto = Object.getPrototypeOf(Function);
|
|
40
|
-
proto.from = function (data: object | ClassInstance, view?: string): unknown {
|
|
41
|
-
return BindUtil.bindSchema(this, data, view);
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
41
|
/**
|
|
46
42
|
* Convert dotted paths into a full object
|
|
47
43
|
*
|
|
@@ -53,7 +49,7 @@ export class BindUtil {
|
|
|
53
49
|
const out: Record<string, unknown> = {};
|
|
54
50
|
for (const k of Object.keys(obj)) {
|
|
55
51
|
const objK = obj[k];
|
|
56
|
-
const val =
|
|
52
|
+
const val = ObjectUtil.isPlainObject(objK) ? this.expandPaths(objK) : objK;
|
|
57
53
|
const parts = k.split('.');
|
|
58
54
|
const last = parts.pop()!;
|
|
59
55
|
let sub = out;
|
|
@@ -80,8 +76,8 @@ export class BindUtil {
|
|
|
80
76
|
}
|
|
81
77
|
|
|
82
78
|
if (last.indexOf('[') < 0) {
|
|
83
|
-
if (sub[last] &&
|
|
84
|
-
sub[last] =
|
|
79
|
+
if (sub[last] && ObjectUtil.isPlainObject(val)) {
|
|
80
|
+
sub[last] = DataUtil.deepAssign(sub[last], val, 'coerce');
|
|
85
81
|
} else {
|
|
86
82
|
sub[last] = val;
|
|
87
83
|
}
|
|
@@ -98,8 +94,8 @@ export class BindUtil {
|
|
|
98
94
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
99
95
|
key = sub.length as number;
|
|
100
96
|
}
|
|
101
|
-
if (sub[key] &&
|
|
102
|
-
sub[key] =
|
|
97
|
+
if (sub[key] && ObjectUtil.isPlainObject(val) && ObjectUtil.isPlainObject(sub[key])) {
|
|
98
|
+
sub[key] = DataUtil.deepAssign(sub[key], val, 'coerce');
|
|
103
99
|
} else {
|
|
104
100
|
sub[key] = val;
|
|
105
101
|
}
|
|
@@ -118,13 +114,13 @@ export class BindUtil {
|
|
|
118
114
|
const out: Record<string, unknown> = {};
|
|
119
115
|
for (const [key, value] of Object.entries(data)) {
|
|
120
116
|
const pre = `${prefix}${key}`;
|
|
121
|
-
if (
|
|
117
|
+
if (ObjectUtil.isPlainObject(value)) {
|
|
122
118
|
Object.assign(out, this.flattenPaths(value, `${pre}.`)
|
|
123
119
|
);
|
|
124
120
|
} else if (Array.isArray(value)) {
|
|
125
121
|
for (let i = 0; i < value.length; i++) {
|
|
126
122
|
const v = value[i];
|
|
127
|
-
if (
|
|
123
|
+
if (ObjectUtil.isPlainObject(v)) {
|
|
128
124
|
Object.assign(out, this.flattenPaths(v, `${pre}[${i}].`));
|
|
129
125
|
} else {
|
|
130
126
|
out[`${pre}[${i}]`] = v;
|
|
@@ -141,12 +137,12 @@ export class BindUtil {
|
|
|
141
137
|
* Bind data to the schema for a class, with an optional view
|
|
142
138
|
* @param cons The schema class to bind against
|
|
143
139
|
* @param data The provided data to bind
|
|
144
|
-
* @param
|
|
140
|
+
* @param cfg The bind configuration
|
|
145
141
|
*/
|
|
146
|
-
static bindSchema<T>(cons: Class<T>, data?: undefined,
|
|
147
|
-
static bindSchema<T>(cons: Class<T>, data?: null,
|
|
148
|
-
static bindSchema<T>(cons: Class<T>, data?: object | T,
|
|
149
|
-
static bindSchema<T>(cons: Class<T>, data?: object | T,
|
|
142
|
+
static bindSchema<T>(cons: Class<T>, data?: undefined, cfg?: BindConfig): undefined;
|
|
143
|
+
static bindSchema<T>(cons: Class<T>, data?: null, cfg?: BindConfig): null;
|
|
144
|
+
static bindSchema<T>(cons: Class<T>, data?: object | T, cfg?: BindConfig): T;
|
|
145
|
+
static bindSchema<T>(cons: Class<T>, data?: object | T, cfg?: BindConfig): T | null | undefined {
|
|
150
146
|
if (data === null || data === undefined) {
|
|
151
147
|
return data;
|
|
152
148
|
}
|
|
@@ -161,13 +157,13 @@ export class BindUtil {
|
|
|
161
157
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
162
158
|
SchemaRegistry.ensureInstanceTypeField(cls, tgt as T & { type?: string });
|
|
163
159
|
|
|
164
|
-
for (const k of
|
|
160
|
+
for (const k of TypedObject.keys(tgt)) { // Do not retain undefined fields
|
|
165
161
|
if (tgt[k] === undefined) {
|
|
166
162
|
delete tgt[k];
|
|
167
163
|
}
|
|
168
164
|
}
|
|
169
165
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
170
|
-
return this.bindSchemaToObject(cls, tgt, data as object,
|
|
166
|
+
return this.bindSchemaToObject(cls, tgt, data as object, cfg);
|
|
171
167
|
}
|
|
172
168
|
}
|
|
173
169
|
|
|
@@ -176,17 +172,18 @@ export class BindUtil {
|
|
|
176
172
|
* @param cons The schema class
|
|
177
173
|
* @param obj The target object (instance of cons)
|
|
178
174
|
* @param data The data to bind
|
|
179
|
-
* @param
|
|
175
|
+
* @param cfg The bind configuration
|
|
180
176
|
*/
|
|
181
|
-
static bindSchemaToObject<T>(cons: Class<T>, obj: T, data?: object,
|
|
182
|
-
view
|
|
177
|
+
static bindSchemaToObject<T>(cons: Class<T>, obj: T, data?: object, cfg: BindConfig = {}): T {
|
|
178
|
+
const view = cfg.view ?? AllViewⲐ; // Does not convey
|
|
179
|
+
delete cfg.view;
|
|
183
180
|
|
|
184
|
-
if (!!data && !
|
|
181
|
+
if (!!data && !ObjectUtil.isPrimitive(data)) {
|
|
185
182
|
const conf = SchemaRegistry.get(cons);
|
|
186
183
|
|
|
187
184
|
// If no configuration
|
|
188
185
|
if (!conf) {
|
|
189
|
-
for (const k of
|
|
186
|
+
for (const k of TypedObject.keys(data)) {
|
|
190
187
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
191
188
|
obj[k] = data[k as keyof typeof data];
|
|
192
189
|
}
|
|
@@ -198,14 +195,16 @@ export class BindUtil {
|
|
|
198
195
|
}
|
|
199
196
|
|
|
200
197
|
for (const schemaFieldName of viewConf.fields) {
|
|
198
|
+
const field = viewConf.schema[schemaFieldName];
|
|
199
|
+
|
|
201
200
|
let inboundField: string | undefined = undefined;
|
|
202
|
-
if (
|
|
201
|
+
if (field.access === 'readonly' || cfg.filterField?.(field) === false) {
|
|
203
202
|
continue; // Skip trying to write readonly fields
|
|
204
203
|
}
|
|
205
204
|
if (schemaFieldName in data) {
|
|
206
205
|
inboundField = schemaFieldName;
|
|
207
|
-
} else if (
|
|
208
|
-
for (const aliasedField of (
|
|
206
|
+
} else if (field.aliases) {
|
|
207
|
+
for (const aliasedField of (field.aliases ?? [])) {
|
|
209
208
|
if (aliasedField in data) {
|
|
210
209
|
inboundField = aliasedField;
|
|
211
210
|
break;
|
|
@@ -220,29 +219,43 @@ export class BindUtil {
|
|
|
220
219
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
221
220
|
let v: unknown = data[inboundField as keyof typeof data];
|
|
222
221
|
|
|
223
|
-
|
|
224
|
-
|
|
222
|
+
// Filtering values
|
|
223
|
+
if (cfg.filterValue && !cfg.filterValue(v, field)) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
225
226
|
|
|
227
|
+
if (v !== undefined && v !== null) {
|
|
226
228
|
// Ensure its an array
|
|
227
|
-
if (!Array.isArray(v) &&
|
|
228
|
-
v
|
|
229
|
+
if (!Array.isArray(v) && field.array) {
|
|
230
|
+
if (typeof v === 'string' && v.includes(',')) {
|
|
231
|
+
v = v.split(/\s*,\s*/);
|
|
232
|
+
} else {
|
|
233
|
+
v = [v];
|
|
234
|
+
}
|
|
229
235
|
}
|
|
230
236
|
|
|
231
|
-
if (SchemaRegistry.has(
|
|
232
|
-
if (
|
|
233
|
-
v = v.map(el => this.bindSchema(
|
|
237
|
+
if (SchemaRegistry.has(field.type)) {
|
|
238
|
+
if (field.array && Array.isArray(v)) {
|
|
239
|
+
v = v.map(el => this.bindSchema(field.type, el, cfg));
|
|
234
240
|
} else {
|
|
235
|
-
v = this.bindSchema(
|
|
241
|
+
v = this.bindSchema(field.type, v, cfg);
|
|
236
242
|
}
|
|
237
|
-
} else if (
|
|
238
|
-
v = v.map(el => this.#coerceType(
|
|
243
|
+
} else if (field.array && Array.isArray(v)) {
|
|
244
|
+
v = v.map(el => this.#coerceType(field, el));
|
|
239
245
|
} else {
|
|
240
|
-
v = this.#coerceType(
|
|
246
|
+
v = this.#coerceType(field, v);
|
|
241
247
|
}
|
|
242
248
|
}
|
|
243
249
|
|
|
244
250
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
245
251
|
obj[schemaFieldName as keyof typeof obj] = v as (typeof obj)[keyof typeof obj];
|
|
252
|
+
|
|
253
|
+
if (field.accessor) {
|
|
254
|
+
Object.defineProperty(obj, schemaFieldName, {
|
|
255
|
+
...SchemaRegistry.getAccessorDescriptor(cons, schemaFieldName),
|
|
256
|
+
enumerable: true
|
|
257
|
+
});
|
|
258
|
+
}
|
|
246
259
|
}
|
|
247
260
|
}
|
|
248
261
|
}
|
|
@@ -265,15 +278,15 @@ export class BindUtil {
|
|
|
265
278
|
if (field.array) {
|
|
266
279
|
const valArr = !Array.isArray(val) ? [val] : val;
|
|
267
280
|
if (complex) {
|
|
268
|
-
val = valArr.map(x => this.bindSchema(field.type, x, field.view));
|
|
281
|
+
val = valArr.map(x => this.bindSchema(field.type, x, { view: field.view }));
|
|
269
282
|
} else {
|
|
270
|
-
val = valArr.map(x =>
|
|
283
|
+
val = valArr.map(x => DataUtil.coerceType(x, field.type, false));
|
|
271
284
|
}
|
|
272
285
|
} else {
|
|
273
286
|
if (complex) {
|
|
274
|
-
val = this.bindSchema(field.type, val, field.view);
|
|
287
|
+
val = this.bindSchema(field.type, val, { view: field.view });
|
|
275
288
|
} else {
|
|
276
|
-
val =
|
|
289
|
+
val = DataUtil.coerceType(val, field.type, false);
|
|
277
290
|
}
|
|
278
291
|
}
|
|
279
292
|
return val;
|
package/src/decorator/common.ts
CHANGED
|
@@ -10,7 +10,7 @@ function isClassInstance(o: Class | ClassInstance, property?: string): o is Clas
|
|
|
10
10
|
/**
|
|
11
11
|
* Describe a model or a field
|
|
12
12
|
* @param config The describe configuration
|
|
13
|
-
* @augments `@
|
|
13
|
+
* @augments `@travetto/schema:Describe`
|
|
14
14
|
*/
|
|
15
15
|
export function Describe(config: Partial<DescribableConfig>) {
|
|
16
16
|
return (target: Class | ClassInstance, property?: string, descOrIdx?: PropertyDescriptor | number): void => {
|
package/src/decorator/field.ts
CHANGED
|
@@ -33,7 +33,7 @@ const dateNumberProp: (obj: Partial<FieldConfig>) => <T extends Partial<Record<K
|
|
|
33
33
|
* Registering a field
|
|
34
34
|
* @param type The type for the field
|
|
35
35
|
* @param config The field configuration
|
|
36
|
-
* @augments `@
|
|
36
|
+
* @augments `@travetto/schema:Field`
|
|
37
37
|
*/
|
|
38
38
|
export function Field(type: ClassList, config?: Partial<FieldConfig>) {
|
|
39
39
|
return (f: ClassInstance, k: string, idx?: number | PropertyDescriptor): void => {
|
|
@@ -48,33 +48,39 @@ export function Field(type: ClassList, config?: Partial<FieldConfig>) {
|
|
|
48
48
|
/**
|
|
49
49
|
* Alias for the field
|
|
50
50
|
* @param aliases List of all aliases for a field
|
|
51
|
-
* @augments `@
|
|
51
|
+
* @augments `@travetto/schema:Field`
|
|
52
52
|
*/
|
|
53
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
|
-
* @augments `@
|
|
57
|
+
* @augments `@travetto/schema:Field`
|
|
58
58
|
*/
|
|
59
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
|
-
* @augments `@
|
|
63
|
+
* @augments `@travetto/schema:Field`
|
|
64
64
|
*/
|
|
65
65
|
export function Readonly(active = true): ReturnType<typeof prop> { return prop({ access: 'readonly' }); }
|
|
66
|
+
/**
|
|
67
|
+
* Mark a field as sensitive
|
|
68
|
+
* @param active This determines if this field is sensitive or not.
|
|
69
|
+
* @augments `@travetto/schema:Field`
|
|
70
|
+
*/
|
|
71
|
+
export function Secret(active = true): ReturnType<typeof prop> { return prop({ secret: active }); }
|
|
66
72
|
/**
|
|
67
73
|
* Mark a field as required
|
|
68
74
|
* @param active This determines if this field is required or not.
|
|
69
75
|
* @param message The error message when a the constraint fails.
|
|
70
|
-
* @augments `@
|
|
76
|
+
* @augments `@travetto/schema:Field`
|
|
71
77
|
*/
|
|
72
78
|
export function Required(active = true, message?: string): ReturnType<typeof prop> { return prop({ required: { active, message } }); }
|
|
73
79
|
/**
|
|
74
80
|
* Define a field as a set of enumerated values
|
|
75
81
|
* @param values The list of values allowed for the enumeration
|
|
76
82
|
* @param message The error message to show when the constraint fails
|
|
77
|
-
* @augments `@
|
|
83
|
+
* @augments `@travetto/schema:Field`
|
|
78
84
|
*/
|
|
79
85
|
export function Enum(values: string[], message?: string): ReturnType<typeof stringNumberProp> {
|
|
80
86
|
message = message || `{path} is only allowed to be "${values.join('" or "')}"`;
|
|
@@ -82,12 +88,12 @@ export function Enum(values: string[], message?: string): ReturnType<typeof stri
|
|
|
82
88
|
}
|
|
83
89
|
/**
|
|
84
90
|
* Mark the field as indicating it's storing textual data
|
|
85
|
-
* @augments `@
|
|
91
|
+
* @augments `@travetto/schema:Field`
|
|
86
92
|
*/
|
|
87
93
|
export function Text(): ReturnType<typeof stringArrStringProp> { return stringArrStringProp({ specifier: 'text' }); }
|
|
88
94
|
/**
|
|
89
95
|
* Mark the field to indicate it's for long form text
|
|
90
|
-
* @augments `@
|
|
96
|
+
* @augments `@travetto/schema:Field`
|
|
91
97
|
*/
|
|
92
98
|
export function LongText(): ReturnType<typeof stringArrStringProp> { return stringArrStringProp({ specifier: 'text-long' }); }
|
|
93
99
|
|
|
@@ -95,7 +101,7 @@ export function LongText(): ReturnType<typeof stringArrStringProp> { return stri
|
|
|
95
101
|
* Require the field to match a specific RegExp
|
|
96
102
|
* @param re The regular expression to match against
|
|
97
103
|
* @param message The message to show when the constraint fails
|
|
98
|
-
* @augments `@
|
|
104
|
+
* @augments `@travetto/schema:Field`
|
|
99
105
|
*/
|
|
100
106
|
export function Match(re: RegExp, message?: string): ReturnType<typeof stringArrStringProp> { return stringArrStringProp({ match: { re, message } }); }
|
|
101
107
|
|
|
@@ -103,7 +109,7 @@ export function Match(re: RegExp, message?: string): ReturnType<typeof stringArr
|
|
|
103
109
|
* The minimum length for the string or array
|
|
104
110
|
* @param n The minimum length
|
|
105
111
|
* @param message The message to show when the constraint fails
|
|
106
|
-
* @augments `@
|
|
112
|
+
* @augments `@travetto/schema:Field`
|
|
107
113
|
*/
|
|
108
114
|
export function MinLength(n: number, message?: string): ReturnType<typeof stringArrProp> {
|
|
109
115
|
return stringArrProp({ minlength: { n, message }, ...(n === 0 ? { required: { active: false } } : {}) });
|
|
@@ -113,7 +119,7 @@ export function MinLength(n: number, message?: string): ReturnType<typeof string
|
|
|
113
119
|
* The maximum length for the string or array
|
|
114
120
|
* @param n The maximum length
|
|
115
121
|
* @param message The message to show when the constraint fails
|
|
116
|
-
* @augments `@
|
|
122
|
+
* @augments `@travetto/schema:Field`
|
|
117
123
|
*/
|
|
118
124
|
export function MaxLength(n: number, message?: string): ReturnType<typeof stringArrProp> { return stringArrProp({ maxlength: { n, message } }); }
|
|
119
125
|
|
|
@@ -121,7 +127,7 @@ export function MaxLength(n: number, message?: string): ReturnType<typeof string
|
|
|
121
127
|
* The minimum value
|
|
122
128
|
* @param n The minimum value
|
|
123
129
|
* @param message The message to show when the constraint fails
|
|
124
|
-
* @augments `@
|
|
130
|
+
* @augments `@travetto/schema:Field`
|
|
125
131
|
*/
|
|
126
132
|
export function Min<T extends number | Date>(n: T, message?: string): ReturnType<typeof dateNumberProp> {
|
|
127
133
|
return dateNumberProp({ min: { n, message } });
|
|
@@ -131,7 +137,7 @@ export function Min<T extends number | Date>(n: T, message?: string): ReturnType
|
|
|
131
137
|
* The maximum value
|
|
132
138
|
* @param n The maximum value
|
|
133
139
|
* @param message The message to show when the constraint fails
|
|
134
|
-
* @augments `@
|
|
140
|
+
* @augments `@travetto/schema:Field`
|
|
135
141
|
*/
|
|
136
142
|
export function Max<T extends number | Date>(n: T, message?: string): ReturnType<typeof dateNumberProp> {
|
|
137
143
|
return dateNumberProp({ max: { n, message } });
|
|
@@ -140,21 +146,21 @@ export function Max<T extends number | Date>(n: T, message?: string): ReturnType
|
|
|
140
146
|
/**
|
|
141
147
|
* Mark a field as an email
|
|
142
148
|
* @param message The message to show when the constraint fails
|
|
143
|
-
* @augments `@
|
|
149
|
+
* @augments `@travetto/schema:Field`
|
|
144
150
|
*/
|
|
145
151
|
export function Email(message?: string): ReturnType<typeof Match> { return Match(CommonRegExp.email, message); }
|
|
146
152
|
|
|
147
153
|
/**
|
|
148
154
|
* Mark a field as an telephone number
|
|
149
155
|
* @param message The message to show when the constraint fails
|
|
150
|
-
* @augments `@
|
|
156
|
+
* @augments `@travetto/schema:Field`
|
|
151
157
|
*/
|
|
152
158
|
export function Telephone(message?: string): ReturnType<typeof Match> { return Match(CommonRegExp.telephone, message); }
|
|
153
159
|
|
|
154
160
|
/**
|
|
155
161
|
* Mark a field as a url
|
|
156
162
|
* @param message The message to show when the constraint fails
|
|
157
|
-
* @augments `@
|
|
163
|
+
* @augments `@travetto/schema:Field`
|
|
158
164
|
*/
|
|
159
165
|
export function Url(message?: string): ReturnType<typeof Match> { return Match(CommonRegExp.url, message); }
|
|
160
166
|
|
|
@@ -162,38 +168,38 @@ export function Url(message?: string): ReturnType<typeof Match> { return Match(C
|
|
|
162
168
|
* Determine the numeric precision of the value
|
|
163
169
|
* @param digits The number of digits a number should have
|
|
164
170
|
* @param decimals The number of decimal digits to support
|
|
165
|
-
* @augments `@
|
|
171
|
+
* @augments `@travetto/schema:Field`
|
|
166
172
|
*/
|
|
167
173
|
export function Precision(digits: number, decimals?: number): ReturnType<typeof numberProp> { return numberProp({ precision: [digits, decimals] }); }
|
|
168
174
|
|
|
169
175
|
/**
|
|
170
176
|
* Mark a number as an integer
|
|
171
|
-
* @augments `@
|
|
177
|
+
* @augments `@travetto/schema:Field`
|
|
172
178
|
*/
|
|
173
179
|
export function Integer(): ReturnType<typeof numberProp> { return Precision(0); }
|
|
174
180
|
|
|
175
181
|
/**
|
|
176
182
|
* Mark a number as a float
|
|
177
|
-
* @augments `@
|
|
183
|
+
* @augments `@travetto/schema:Field`
|
|
178
184
|
*/
|
|
179
185
|
export function Float(): ReturnType<typeof numberProp> { return Precision(10, 7); }
|
|
180
186
|
|
|
181
187
|
/**
|
|
182
188
|
* Mark a number as a long value
|
|
183
|
-
* @augments `@
|
|
189
|
+
* @augments `@travetto/schema:Field`
|
|
184
190
|
*/
|
|
185
191
|
export function Long(): ReturnType<typeof numberProp> { return Precision(19, 0); }
|
|
186
192
|
|
|
187
193
|
/**
|
|
188
194
|
* Mark a number as a currency
|
|
189
|
-
* @augments `@
|
|
195
|
+
* @augments `@travetto/schema:Field`
|
|
190
196
|
*/
|
|
191
197
|
export function Currency(): ReturnType<typeof numberProp> { return Precision(13, 2); }
|
|
192
198
|
|
|
193
199
|
/**
|
|
194
200
|
* Mark a field as ignored
|
|
195
201
|
*
|
|
196
|
-
* @augments `@
|
|
202
|
+
* @augments `@travetto/schema:Ignore`
|
|
197
203
|
*/
|
|
198
204
|
export function Ignore(): PropertyDecorator {
|
|
199
205
|
return (target: Object, property: string | symbol) => { };
|
package/src/decorator/schema.ts
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { Class } from '@travetto/base';
|
|
2
2
|
|
|
3
|
+
import { BindUtil } from '../bind-util';
|
|
3
4
|
import { SchemaRegistry } from '../service/registry';
|
|
4
5
|
import { ViewFieldsConfig } from '../service/types';
|
|
6
|
+
import { DeepPartial } from '../types';
|
|
5
7
|
import { ValidatorFn } from '../validate/types';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Register a class as a Schema
|
|
9
11
|
*
|
|
10
|
-
* @augments `@
|
|
12
|
+
* @augments `@travetto/schema:Schema`
|
|
11
13
|
*/
|
|
12
14
|
export function Schema() { // Auto is used during compilation
|
|
13
15
|
return <T, U extends Class<T>>(target: U): U => {
|
|
16
|
+
target.from ??= function <V>(this: Class<V>, data: DeepPartial<V>, view?: string): V {
|
|
17
|
+
return BindUtil.bindSchema(this, data, { view });
|
|
18
|
+
};
|
|
14
19
|
SchemaRegistry.getOrCreatePending(target);
|
|
15
20
|
return target;
|
|
16
21
|
};
|
package/src/internal/types.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const AllViewⲐ: unique symbol = Symbol.for('@
|
|
1
|
+
export const AllViewⲐ: unique symbol = Symbol.for('@travetto/schema:all');
|
package/src/service/changes.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { ChangeEvent } from '@travetto/registry';
|
|
|
6
6
|
import { FieldConfig, ClassConfig } from './types';
|
|
7
7
|
import { AllViewⲐ } from '../internal/types';
|
|
8
8
|
|
|
9
|
-
const id = (c: Class | string): string => typeof c === 'string' ? c : c
|
|
9
|
+
const id = (c: Class | string): string => typeof c === 'string' ? c : c.Ⲑid;
|
|
10
10
|
|
|
11
11
|
interface FieldMapping {
|
|
12
12
|
path: FieldConfig[];
|
|
@@ -57,13 +57,6 @@ class $SchemaChangeListener {
|
|
|
57
57
|
this.#emitter.on('field', handler);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
/**
|
|
61
|
-
* Reset the listener
|
|
62
|
-
*/
|
|
63
|
-
reset(): void {
|
|
64
|
-
this.#mapping.clear();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
60
|
/**
|
|
68
61
|
* Clear dependency mappings for a given class
|
|
69
62
|
*/
|
|
@@ -139,7 +132,7 @@ class $SchemaChangeListener {
|
|
|
139
132
|
}
|
|
140
133
|
|
|
141
134
|
// Handle class references changing, but keeping same id
|
|
142
|
-
const compareTypes = (a: Class, b: Class): boolean => '
|
|
135
|
+
const compareTypes = (a: Class, b: Class): boolean => 'Ⲑid' in a ? a.Ⲑid === b.Ⲑid : a === b;
|
|
143
136
|
|
|
144
137
|
for (const c of currFields) {
|
|
145
138
|
if (prevFields.has(c)) {
|
package/src/service/registry.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Class, AppError,
|
|
1
|
+
import { Class, AppError, ObjectUtil, 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';
|
|
@@ -8,7 +8,7 @@ import { AllViewⲐ } from '../internal/types';
|
|
|
8
8
|
|
|
9
9
|
function hasType<T>(o: unknown): o is { type: Class<T> | string } {
|
|
10
10
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
11
|
-
return !!o && !
|
|
11
|
+
return !!o && !ObjectUtil.isPrimitive(o) && 'type' in (o as object) && !!(o as Record<string, string>)['type'];
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
function isWithType<T>(o: T, cfg: ClassConfig | undefined): o is T & { type?: string } {
|
|
@@ -20,12 +20,12 @@ function getConstructor<T>(o: T): ConcreteClass<T> {
|
|
|
20
20
|
return (o as unknown as ClassInstance<T>).constructor;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
24
23
|
/**
|
|
25
24
|
* Schema registry for listening to changes
|
|
26
25
|
*/
|
|
27
26
|
class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
28
27
|
|
|
28
|
+
#accessorDescriptors = new Map<Class, Map<string, PropertyDescriptor>>();
|
|
29
29
|
#subTypes = new Map<Class, Map<string, Class>>();
|
|
30
30
|
#typeKeys = new Map<Class, string>();
|
|
31
31
|
#pendingViews = new Map<Class, Map<string, ViewFieldsConfig<unknown>>>();
|
|
@@ -55,6 +55,32 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Retrieve class level metadata
|
|
60
|
+
* @param cls
|
|
61
|
+
* @param prop
|
|
62
|
+
* @param key
|
|
63
|
+
* @returns
|
|
64
|
+
*/
|
|
65
|
+
getMetadata<K>(cls: Class, key: symbol): K | undefined {
|
|
66
|
+
const cfg = this.get(cls);
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
68
|
+
return cfg.metadata?.[key] as K;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Retrieve pending class level metadata, or create if needed
|
|
73
|
+
* @param cls
|
|
74
|
+
* @param prop
|
|
75
|
+
* @param key
|
|
76
|
+
* @returns
|
|
77
|
+
*/
|
|
78
|
+
getOrCreatePendingMetadata<K>(cls: Class, key: symbol, value: K): K {
|
|
79
|
+
const cfg = this.getOrCreatePending(cls);
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
81
|
+
return ((cfg.metadata ??= {})[key] ??= value) as K;
|
|
82
|
+
}
|
|
83
|
+
|
|
58
84
|
/**
|
|
59
85
|
* Ensure type is set properly
|
|
60
86
|
*/
|
|
@@ -64,6 +90,24 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
64
90
|
}
|
|
65
91
|
}
|
|
66
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Provides the prototype-derived descriptor for a property
|
|
95
|
+
*/
|
|
96
|
+
getAccessorDescriptor(cls: Class, field: string): PropertyDescriptor {
|
|
97
|
+
if (!this.#accessorDescriptors.has(cls)) {
|
|
98
|
+
this.#accessorDescriptors.set(cls, new Map());
|
|
99
|
+
}
|
|
100
|
+
const map = this.#accessorDescriptors.get(cls)!;
|
|
101
|
+
if (!map.has(field)) {
|
|
102
|
+
let proto = cls.prototype;
|
|
103
|
+
while (proto && !Object.hasOwn(proto, field)) {
|
|
104
|
+
proto = proto.prototype;
|
|
105
|
+
}
|
|
106
|
+
map.set(field, Object.getOwnPropertyDescriptor(proto, field)!);
|
|
107
|
+
}
|
|
108
|
+
return map.get(field)!;
|
|
109
|
+
}
|
|
110
|
+
|
|
67
111
|
/**
|
|
68
112
|
* Find the subtype for a given instance
|
|
69
113
|
* @param cls Class for instance
|
|
@@ -80,7 +124,7 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
80
124
|
*/
|
|
81
125
|
resolveSubType(cls: Class, type: Class | string): Class {
|
|
82
126
|
if (this.#subTypes.has(cls)) {
|
|
83
|
-
const typeId = type && (typeof type === 'string' ? type : type
|
|
127
|
+
const typeId = type && (typeof type === 'string' ? type : type.Ⲑid);
|
|
84
128
|
if (type) {
|
|
85
129
|
return this.#subTypes.get(cls)!.get(typeId) ?? cls;
|
|
86
130
|
}
|
|
@@ -114,7 +158,7 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
114
158
|
this.#subTypes.set(parent, new Map());
|
|
115
159
|
}
|
|
116
160
|
this.#subTypes.get(parent)!.set(type, cls);
|
|
117
|
-
this.#subTypes.get(parent)!.set(cls
|
|
161
|
+
this.#subTypes.get(parent)!.set(cls.Ⲑid, cls);
|
|
118
162
|
parent = this.getParentClass(parent!)!;
|
|
119
163
|
parentConfig = this.get(parent);
|
|
120
164
|
}
|
|
@@ -147,6 +191,7 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
147
191
|
class: cls,
|
|
148
192
|
validators: [],
|
|
149
193
|
subType: false,
|
|
194
|
+
metadata: {},
|
|
150
195
|
views: {
|
|
151
196
|
[AllViewⲐ]: {
|
|
152
197
|
schema: {},
|
|
@@ -301,6 +346,7 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
301
346
|
schema: { ...dest.views[AllViewⲐ].schema, ...src.views?.[AllViewⲐ].schema },
|
|
302
347
|
fields: [...dest.views[AllViewⲐ].fields, ...src.views?.[AllViewⲐ].fields ?? []]
|
|
303
348
|
};
|
|
349
|
+
dest.metadata = { ...src.metadata ?? {}, ...dest.metadata ?? {} };
|
|
304
350
|
dest.subType = src.subType || dest.subType;
|
|
305
351
|
dest.title = src.title || dest.title;
|
|
306
352
|
dest.validators = [...src.validators ?? [], ...dest.validators];
|
|
@@ -377,6 +423,7 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
377
423
|
this.#subTypes.clear();
|
|
378
424
|
this.#typeKeys.delete(cls);
|
|
379
425
|
this.#methodSchemas.delete(cls);
|
|
426
|
+
this.#accessorDescriptors.delete(cls);
|
|
380
427
|
|
|
381
428
|
// Recompute subtype mappings
|
|
382
429
|
for (const el of this.entries.keys()) {
|
package/src/service/types.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { Class } from '@travetto/base';
|
|
2
|
-
import { Primitive } from '@travetto/base/src/internal/global-types';
|
|
1
|
+
import { Primitive, Class } from '@travetto/base';
|
|
3
2
|
|
|
4
3
|
import { AllViewⲐ } from '../internal/types';
|
|
5
4
|
import { ValidatorFn } from '../validate/types';
|
|
@@ -68,6 +67,10 @@ export interface ClassConfig extends DescribableConfig {
|
|
|
68
67
|
* Is the class a sub type
|
|
69
68
|
*/
|
|
70
69
|
subType?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Metadata that is related to the schema structure
|
|
72
|
+
*/
|
|
73
|
+
metadata?: Record<symbol, unknown>;
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
/**
|
|
@@ -149,6 +152,14 @@ export interface FieldConfig extends DescribableConfig {
|
|
|
149
152
|
* Is the field readonly, or write only?, defaults to no restrictions
|
|
150
153
|
*/
|
|
151
154
|
access?: 'readonly' | 'writeonly';
|
|
155
|
+
/**
|
|
156
|
+
* Is this field secret, defaults to no, can be used to hide field when exporting values
|
|
157
|
+
*/
|
|
158
|
+
secret?: boolean;
|
|
159
|
+
/**
|
|
160
|
+
* Is this field a getter or setter
|
|
161
|
+
*/
|
|
162
|
+
accessor?: string;
|
|
152
163
|
}
|
|
153
164
|
|
|
154
165
|
export type ViewFieldsConfig<T> = { with: Extract<(keyof T), string>[] } | { without: Extract<(keyof T), string>[] };
|
package/src/typings.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ declare global {
|
|
|
13
13
|
|
|
14
14
|
namespace NodeJS {
|
|
15
15
|
/**
|
|
16
|
-
* @concrete stream:Readable
|
|
16
|
+
* @concrete stream:Readable
|
|
17
17
|
*/
|
|
18
18
|
interface ReadableStream { }
|
|
19
19
|
}
|
|
@@ -21,7 +21,7 @@ declare global {
|
|
|
21
21
|
|
|
22
22
|
declare module 'stream' {
|
|
23
23
|
/**
|
|
24
|
-
* @concrete stream:Readable
|
|
24
|
+
* @concrete stream:Readable
|
|
25
25
|
*/
|
|
26
26
|
interface Readable { }
|
|
27
27
|
}
|
package/src/validate/regexp.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TypedObject } from '@travetto/base';
|
|
1
2
|
import { Messages } from './messages';
|
|
2
3
|
|
|
3
4
|
declare global {
|
|
@@ -18,7 +19,7 @@ export const CommonRegExp = {
|
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
// Rebind regexes
|
|
21
|
-
for (const k of
|
|
22
|
+
for (const k of TypedObject.keys(CommonRegExp)) {
|
|
22
23
|
CommonRegExp[k].name = `[[:${k}:]]`;
|
|
23
24
|
Messages.set(CommonRegExp[k].name!, Messages.get(k)!);
|
|
24
25
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Class, ClassInstance,
|
|
1
|
+
import { Class, ClassInstance, TypedObject, ObjectUtil } from '@travetto/base';
|
|
2
2
|
|
|
3
3
|
import { FieldConfig, SchemaConfig } from '../service/types';
|
|
4
4
|
import { SchemaRegistry } from '../service/registry';
|
|
@@ -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 =
|
|
41
|
+
const fields = TypedObject.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
|
|
@@ -236,7 +236,7 @@ export class SchemaValidator {
|
|
|
236
236
|
*/
|
|
237
237
|
static async validate<T>(cls: Class<T>, o: T, view?: string): Promise<T> {
|
|
238
238
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
239
|
-
if (!
|
|
239
|
+
if (!ObjectUtil.isPlainObject(o) && !(o instanceof cls || cls.Ⲑid === (o as ClassInstance<T>).constructor.Ⲑid)) {
|
|
240
240
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
241
241
|
throw new TypeMismatchError(cls.name, (o as ClassInstance).constructor.name);
|
|
242
242
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { AnyType, DeclarationUtil, DecoratorUtil, DocUtil, ParamDocumentation,
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { type AnyType, DeclarationUtil, DecoratorUtil, DocUtil, ParamDocumentation, TransformerState } from '@travetto/transformer';
|
|
3
3
|
|
|
4
4
|
const SCHEMA_MOD = '@travetto/schema/src/decorator/schema';
|
|
5
5
|
const FIELD_MOD = '@travetto/schema/src/decorator/field';
|
|
@@ -16,7 +16,7 @@ export class SchemaTransformUtil {
|
|
|
16
16
|
case 'external': return state.getOrImport(type);
|
|
17
17
|
case 'tuple': return state.fromLiteral(type.subTypes.map(x => this.toConcreteType(state, x, node, root)!));
|
|
18
18
|
case 'literal': {
|
|
19
|
-
if ((type.ctor === Array
|
|
19
|
+
if ((type.ctor === Array) && type.typeArguments?.length) {
|
|
20
20
|
return state.fromLiteral([this.toConcreteType(state, type.typeArguments[0], node, root)]);
|
|
21
21
|
} else if (type.ctor) {
|
|
22
22
|
return state.createIdentifier(type.ctor.name!);
|
|
@@ -50,7 +50,7 @@ export class SchemaTransformUtil {
|
|
|
50
50
|
)
|
|
51
51
|
);
|
|
52
52
|
cls.getText = (): string => '';
|
|
53
|
-
state.
|
|
53
|
+
state.addStatements([cls], root || node);
|
|
54
54
|
}
|
|
55
55
|
return id;
|
|
56
56
|
}
|
|
@@ -81,7 +81,7 @@ export class SchemaTransformUtil {
|
|
|
81
81
|
if (!ts.isGetAccessorDeclaration(node) && !ts.isSetAccessorDeclaration(node)) {
|
|
82
82
|
// eslint-disable-next-line no-bitwise
|
|
83
83
|
if ((ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Readonly) > 0) {
|
|
84
|
-
attrs.push(state.factory.createPropertyAssignment('
|
|
84
|
+
attrs.push(state.factory.createPropertyAssignment('access', state.fromLiteral('readonly')));
|
|
85
85
|
} else if (!node.questionToken && !typeExpr.undefinable && !node.initializer) {
|
|
86
86
|
attrs.push(state.factory.createPropertyAssignment('required', state.fromLiteral({ active: true })));
|
|
87
87
|
}
|
|
@@ -90,11 +90,12 @@ export class SchemaTransformUtil {
|
|
|
90
90
|
}
|
|
91
91
|
} else {
|
|
92
92
|
const acc = DeclarationUtil.getAccessorPair(node);
|
|
93
|
+
attrs.push(state.factory.createPropertyAssignment('accessor', state.fromLiteral(true)));
|
|
93
94
|
if (!acc.setter) {
|
|
94
95
|
attrs.push(state.factory.createPropertyAssignment('access', state.fromLiteral('readonly')));
|
|
95
96
|
}
|
|
96
97
|
if (!acc.getter) {
|
|
97
|
-
attrs.push(state.factory.createPropertyAssignment('
|
|
98
|
+
attrs.push(state.factory.createPropertyAssignment('access', state.fromLiteral('writeonly')));
|
|
98
99
|
} else if (!typeExpr.undefinable) {
|
|
99
100
|
attrs.push(state.factory.createPropertyAssignment('required', state.fromLiteral({ active: true })));
|
|
100
101
|
}
|
|
@@ -129,7 +130,7 @@ export class SchemaTransformUtil {
|
|
|
129
130
|
|
|
130
131
|
const params: ts.Expression[] = [];
|
|
131
132
|
|
|
132
|
-
const existing = state.findDecorator(
|
|
133
|
+
const existing = state.findDecorator('@travetto/schema', node, 'Field', FIELD_MOD);
|
|
133
134
|
if (!existing) {
|
|
134
135
|
const resolved = this.toConcreteType(state, typeExpr, node, config.root);
|
|
135
136
|
params.push(resolved);
|
|
@@ -179,7 +180,7 @@ export class SchemaTransformUtil {
|
|
|
179
180
|
const out: Record<string, unknown> = {};
|
|
180
181
|
|
|
181
182
|
while (type?.key === 'literal' && type.typeArguments?.length) {
|
|
182
|
-
if (type.ctor === Array
|
|
183
|
+
if (type.ctor === Array) {
|
|
183
184
|
out.array = true;
|
|
184
185
|
}
|
|
185
186
|
type = type.typeArguments?.[0] ?? { key: 'literal', ctor: Object }; // We have a promise nested
|
|
@@ -1,29 +1,27 @@
|
|
|
1
|
-
import
|
|
1
|
+
import ts from 'typescript';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
TransformerState, OnProperty, OnClass, AfterClass, DecoratorMeta, DocUtil, DeclarationUtil,
|
|
4
|
+
TransformerState, OnProperty, OnClass, AfterClass, DecoratorMeta, DocUtil, DeclarationUtil, OnGetter, OnSetter
|
|
5
5
|
} from '@travetto/transformer';
|
|
6
6
|
|
|
7
7
|
import { SchemaTransformUtil } from './transform-util';
|
|
8
8
|
|
|
9
|
-
const inSchema = Symbol.for('@
|
|
10
|
-
const accessors = Symbol.for('@
|
|
9
|
+
const inSchema = Symbol.for('@travetto/schema:schema');
|
|
10
|
+
const accessors = Symbol.for('@travetto/schema:accessors');
|
|
11
11
|
|
|
12
12
|
interface AutoState {
|
|
13
13
|
[inSchema]?: boolean;
|
|
14
14
|
[accessors]?: Set<string>;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const
|
|
18
|
-
const
|
|
17
|
+
const SCHEMA_IMPORT = '@travetto/schema/src/decorator/schema';
|
|
18
|
+
const COMMON_IMPORT = '@travetto/schema/src/decorator/common';
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Processes `@Schema` to register class as a valid Schema
|
|
22
22
|
*/
|
|
23
23
|
export class SchemaTransformer {
|
|
24
24
|
|
|
25
|
-
static [TransformerId] = '@trv:schema';
|
|
26
|
-
|
|
27
25
|
/**
|
|
28
26
|
* Track schema on start
|
|
29
27
|
*/
|
|
@@ -43,12 +41,12 @@ export class SchemaTransformer {
|
|
|
43
41
|
|
|
44
42
|
const comments = DocUtil.describeDocs(node);
|
|
45
43
|
|
|
46
|
-
if (!state.findDecorator(this, node, 'Schema',
|
|
47
|
-
modifiers.unshift(state.createDecorator(
|
|
44
|
+
if (!state.findDecorator(this, node, 'Schema', SCHEMA_IMPORT)) {
|
|
45
|
+
modifiers.unshift(state.createDecorator(SCHEMA_IMPORT, 'Schema'));
|
|
48
46
|
}
|
|
49
47
|
|
|
50
48
|
if (comments.description) {
|
|
51
|
-
modifiers.push(state.createDecorator(
|
|
49
|
+
modifiers.push(state.createDecorator(COMMON_IMPORT, 'Describe', state.fromLiteral({
|
|
52
50
|
title: comments.description
|
|
53
51
|
})));
|
|
54
52
|
}
|
package/support/phase.init.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Registers the bind utilities on class
|
|
3
|
-
*/
|
|
4
|
-
export const init = {
|
|
5
|
-
key: '@trv:schema/init',
|
|
6
|
-
after: ['@trv:registry/init'], // Should be global
|
|
7
|
-
action: async (): Promise<void> => {
|
|
8
|
-
const { BindUtil } = await import('../src/bind-util');
|
|
9
|
-
BindUtil.register();
|
|
10
|
-
}
|
|
11
|
-
};
|