@travetto/schema 6.0.0-rc.0 → 6.0.0-rc.2
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 +56 -40
- package/__index__.ts +16 -15
- package/package.json +3 -3
- package/src/bind-util.ts +11 -10
- package/src/data.ts +24 -22
- package/src/decorator/common.ts +2 -2
- package/src/decorator/field.ts +6 -8
- package/src/decorator/schema.ts +6 -7
- package/src/internal/types.ts +31 -2
- package/src/name.ts +1 -1
- package/src/service/changes.ts +3 -4
- package/src/service/registry.ts +44 -41
- package/src/service/types.ts +6 -3
- package/src/types.ts +8 -0
- package/src/validate/error.ts +1 -1
- package/src/validate/regexp.ts +1 -1
- package/src/validate/types.ts +1 -0
- package/src/validate/validator.ts +14 -14
- package/support/transformer/util.ts +17 -17
- package/support/transformer.schema.ts +19 -22
package/README.md
CHANGED
|
@@ -13,12 +13,12 @@ npm install @travetto/schema
|
|
|
13
13
|
yarn add @travetto/schema
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
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 leveraged in the [Configuration](https://github.com/travetto/travetto/tree/main/module/config#readme "Configuration support"), [Command Line Interface](https://github.com/travetto/travetto/tree/main/module/cli#readme "CLI infrastructure for Travetto framework"), [
|
|
16
|
+
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 leveraged in the [Configuration](https://github.com/travetto/travetto/tree/main/module/config#readme "Configuration support"), [Command Line Interface](https://github.com/travetto/travetto/tree/main/module/cli#readme "CLI infrastructure for Travetto framework"), [Web API](https://github.com/travetto/travetto/tree/main/module/web#readme "Declarative api for Web Applications with support for the dependency injection."), [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 provide validation on correctness of input, whether it is a web request, command line inputs, or a configuration file.
|
|
17
17
|
|
|
18
18
|
This module provides a mechanism for registering classes and field level information as well the ability to apply that information at runtime.
|
|
19
19
|
|
|
20
20
|
## Registration
|
|
21
|
-
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#
|
|
21
|
+
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#L13) decoration.
|
|
22
22
|
|
|
23
23
|
### Classes
|
|
24
24
|
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:
|
|
@@ -64,27 +64,27 @@ User:
|
|
|
64
64
|
### Fields
|
|
65
65
|
This schema provides a powerful base for data binding and validation at runtime. Additionally there may be types that cannot be detected, or some information that the programmer would like to override. Below are the supported field decorators:
|
|
66
66
|
* [@Field](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L25) defines a field that will be serialized.
|
|
67
|
-
* [@Required](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
68
|
-
* [@Enum](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
69
|
-
* [@Match](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
70
|
-
* [@MinLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
71
|
-
* [@MaxLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
72
|
-
* [@Min](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
73
|
-
* [@Max](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
74
|
-
* [@Email](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
75
|
-
* [@Telephone](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
76
|
-
* [@Url](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
77
|
-
* [@Ignore](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
78
|
-
* [@Integer](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
79
|
-
* [@Float](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
80
|
-
* [@Currency](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
81
|
-
* [@Text](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
82
|
-
* [@LongText](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
83
|
-
* [@Readonly](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
84
|
-
* [@Writeonly](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
85
|
-
* [@Secret](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
86
|
-
* [@Specifier](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
87
|
-
* [@SubTypeField](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
67
|
+
* [@Required](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L63) defines a that field should be required
|
|
68
|
+
* [@Enum](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L70) defines the allowable values that a field can have
|
|
69
|
+
* [@Match](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L91) defines a regular expression that the field value should match
|
|
70
|
+
* [@MinLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L99) enforces min length of a string
|
|
71
|
+
* [@MaxLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L109) enforces max length of a string
|
|
72
|
+
* [@Min](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L99) enforces min value for a date or a number
|
|
73
|
+
* [@Max](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L109) enforces max value for a date or a number
|
|
74
|
+
* [@Email](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L136) ensures string field matches basic email regex
|
|
75
|
+
* [@Telephone](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L143) ensures string field matches basic telephone regex
|
|
76
|
+
* [@Url](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L150) ensures string field matches basic url regex
|
|
77
|
+
* [@Ignore](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L189) exclude from auto schema registration
|
|
78
|
+
* [@Integer](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L164) ensures number passed in is only a whole number
|
|
79
|
+
* [@Float](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L170) ensures number passed in allows fractional values
|
|
80
|
+
* [@Currency](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L182) provides support for standard currency
|
|
81
|
+
* [@Text](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L78) indicates that a field is expecting natural language input, not just discrete values
|
|
82
|
+
* [@LongText](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L83) same as text, but expects longer form content
|
|
83
|
+
* [@Readonly](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L50) defines a that field should not be bindable external to the class
|
|
84
|
+
* [@Writeonly](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L45) defines a that field should not be exported in serialization, but that it can be bound to
|
|
85
|
+
* [@Secret](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L56) marks a field as being sensitive. This is used by certain logging activities to ensure sensitive information is not logged out.
|
|
86
|
+
* [@Specifier](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L198) attributes additional specifiers to a field, allowing for more specification beyond just the field's type.
|
|
87
|
+
* [@SubTypeField](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L204) allows for promoting a given field as the owner of the sub type discriminator (defaults to `type`).
|
|
88
88
|
Additionally, schemas can be nested to form more complex data structures that are able to bound and validated.
|
|
89
89
|
|
|
90
90
|
Just like the class, all fields can be defined with
|
|
@@ -93,7 +93,7 @@ Just like the class, all fields can be defined with
|
|
|
93
93
|
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.
|
|
94
94
|
|
|
95
95
|
### Parameters
|
|
96
|
-
Parameters are available in certain scenarios (e.g. [
|
|
96
|
+
Parameters are available in certain scenarios (e.g. [Web API](https://github.com/travetto/travetto/tree/main/module/web#readme "Declarative api for Web Applications with support for the dependency injection.") endpoints and [Command Line Interface](https://github.com/travetto/travetto/tree/main/module/cli#readme "CLI infrastructure for Travetto framework") main methods). In these scenarios, all of the field decorators are valid, but need to be called slightly differently to pass the typechecker. The simple solution is to use the `Arg` field of the decorator to convince Typescript its the correct type.
|
|
97
97
|
|
|
98
98
|
**Code: Sample Parameter Usage**
|
|
99
99
|
```typescript
|
|
@@ -112,7 +112,7 @@ export class ParamUsage {
|
|
|
112
112
|
At runtime, once a schema is registered, a programmer can utilize this structure to perform specific operations. Specifically binding and validation.
|
|
113
113
|
|
|
114
114
|
### Binding
|
|
115
|
-
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#
|
|
115
|
+
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#L13) and a JS object that will be the source of the binding. Given the schema:
|
|
116
116
|
|
|
117
117
|
**Code: Sub Schemas via Address**
|
|
118
118
|
```typescript
|
|
@@ -136,7 +136,7 @@ A binding operation could look like:
|
|
|
136
136
|
|
|
137
137
|
**Code: Binding from JSON to Schema**
|
|
138
138
|
```typescript
|
|
139
|
-
import { Person } from './person';
|
|
139
|
+
import { Person } from './person.ts';
|
|
140
140
|
|
|
141
141
|
export function Test(): Person {
|
|
142
142
|
return Person.from({
|
|
@@ -192,7 +192,7 @@ But now with an invalid json object
|
|
|
192
192
|
```typescript
|
|
193
193
|
import { SchemaValidator } from '@travetto/schema';
|
|
194
194
|
|
|
195
|
-
import { Person } from './person';
|
|
195
|
+
import { Person } from './person.ts';
|
|
196
196
|
|
|
197
197
|
export async function validate(): Promise<void> {
|
|
198
198
|
|
|
@@ -310,32 +310,49 @@ To that end, the module supports two concepts:
|
|
|
310
310
|
### Type Adapters
|
|
311
311
|
This feature is meant to allow for simple Typescript types to be able to be backed by a proper class. This is because all of the typescript type information disappears at runtime, and so only concrete types (like classes) remain. An example of this, can be found with how the [Data Model Querying](https://github.com/travetto/travetto/tree/main/module/model-query#readme "Datastore abstraction for advanced query support.") module handles geo data.
|
|
312
312
|
|
|
313
|
-
**Code:
|
|
313
|
+
**Code: Point Contract**
|
|
314
314
|
```typescript
|
|
315
|
-
import { DataUtil } from '@travetto/schema';
|
|
316
|
-
|
|
317
315
|
/**
|
|
318
|
-
*
|
|
316
|
+
* Geometric Point as [number,number] with validation and binding support
|
|
317
|
+
*
|
|
318
|
+
* @concrete ./internal/types.ts#PointImpl
|
|
319
319
|
*/
|
|
320
320
|
export type Point = [number, number];
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**Code: Point Implementation**
|
|
324
|
+
```typescript
|
|
325
|
+
import { DataUtil } from '../data.ts';
|
|
321
326
|
|
|
322
|
-
const
|
|
327
|
+
const InvalidSymbol = Symbol();
|
|
323
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Point Implementation
|
|
331
|
+
*/
|
|
324
332
|
export class PointImpl {
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Validate we have an actual point
|
|
336
|
+
*/
|
|
325
337
|
static validateSchema(input: unknown): 'type' | undefined {
|
|
326
|
-
const
|
|
327
|
-
return
|
|
338
|
+
const bound = this.bindSchema(input);
|
|
339
|
+
return bound !== InvalidSymbol && bound && !isNaN(bound[0]) && !isNaN(bound[1]) ? undefined : 'type';
|
|
328
340
|
}
|
|
329
341
|
|
|
330
|
-
|
|
342
|
+
/**
|
|
343
|
+
* Convert to tuple of two numbers
|
|
344
|
+
*/
|
|
345
|
+
static bindSchema(input: unknown): [number, number] | typeof InvalidSymbol | undefined {
|
|
331
346
|
if (Array.isArray(input) && input.length === 2) {
|
|
332
347
|
const [a, b] = input.map(x => DataUtil.coerceType(x, Number, false));
|
|
333
348
|
return [a, b];
|
|
334
349
|
} else {
|
|
335
|
-
return
|
|
350
|
+
return InvalidSymbol;
|
|
336
351
|
}
|
|
337
352
|
}
|
|
338
353
|
}
|
|
354
|
+
|
|
355
|
+
Object.defineProperty(PointImpl, 'name', { value: 'Point' });
|
|
339
356
|
```
|
|
340
357
|
|
|
341
358
|
What you can see here is that the `Point` type is now backed by a class that supports:
|
|
@@ -344,8 +361,7 @@ What you can see here is that the `Point` type is now backed by a class that sup
|
|
|
344
361
|
|
|
345
362
|
**Code: Simple Custom Type Usage**
|
|
346
363
|
```typescript
|
|
347
|
-
import { Schema } from '@travetto/schema';
|
|
348
|
-
import { Point } from './custom-type';
|
|
364
|
+
import { Schema, Point } from '@travetto/schema';
|
|
349
365
|
|
|
350
366
|
@Schema()
|
|
351
367
|
export class LocationAware {
|
|
@@ -369,8 +385,8 @@ Validation Failed {
|
|
|
369
385
|
"errors": [
|
|
370
386
|
{
|
|
371
387
|
"kind": "type",
|
|
372
|
-
"type": "
|
|
373
|
-
"message": "point is not a valid
|
|
388
|
+
"type": "Point",
|
|
389
|
+
"message": "point is not a valid Point",
|
|
374
390
|
"path": "point"
|
|
375
391
|
}
|
|
376
392
|
]
|
package/__index__.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import type { } from './src/global';
|
|
2
|
-
export * from './src/decorator/field';
|
|
3
|
-
export * from './src/decorator/schema';
|
|
4
|
-
export * from './src/decorator/common';
|
|
5
|
-
export * from './src/service/changes';
|
|
6
|
-
export * from './src/service/registry';
|
|
7
|
-
export * from './src/service/types';
|
|
8
|
-
export * from './src/validate/messages';
|
|
9
|
-
export * from './src/validate/regexp';
|
|
10
|
-
export * from './src/validate/validator';
|
|
11
|
-
export * from './src/validate/error';
|
|
12
|
-
export * from './src/validate/types';
|
|
13
|
-
export * from './src/bind-util';
|
|
14
|
-
export * from './src/data';
|
|
15
|
-
export * from './src/name';
|
|
1
|
+
import type { } from './src/global.d.ts';
|
|
2
|
+
export * from './src/decorator/field.ts';
|
|
3
|
+
export * from './src/decorator/schema.ts';
|
|
4
|
+
export * from './src/decorator/common.ts';
|
|
5
|
+
export * from './src/service/changes.ts';
|
|
6
|
+
export * from './src/service/registry.ts';
|
|
7
|
+
export * from './src/service/types.ts';
|
|
8
|
+
export * from './src/validate/messages.ts';
|
|
9
|
+
export * from './src/validate/regexp.ts';
|
|
10
|
+
export * from './src/validate/validator.ts';
|
|
11
|
+
export * from './src/validate/error.ts';
|
|
12
|
+
export * from './src/validate/types.ts';
|
|
13
|
+
export * from './src/bind-util.ts';
|
|
14
|
+
export * from './src/data.ts';
|
|
15
|
+
export * from './src/name.ts';
|
|
16
|
+
export * from './src/types.ts';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/schema",
|
|
3
|
-
"version": "6.0.0-rc.
|
|
3
|
+
"version": "6.0.0-rc.2",
|
|
4
4
|
"description": "Data type registry for runtime validation, reflection and binding.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"schema",
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
"directory": "module/schema"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@travetto/registry": "^6.0.0-rc.
|
|
30
|
+
"@travetto/registry": "^6.0.0-rc.2"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@travetto/transformer": "^6.0.0-rc.
|
|
33
|
+
"@travetto/transformer": "^6.0.0-rc.3"
|
|
34
34
|
},
|
|
35
35
|
"peerDependenciesMeta": {
|
|
36
36
|
"@travetto/transformer": {
|
package/src/bind-util.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { castTo, Class, classConstruct, asFull, TypedObject, castKey } from '@travetto/runtime';
|
|
2
2
|
|
|
3
|
-
import { DataUtil } from './data';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { FieldConfig } from './service/types';
|
|
3
|
+
import { DataUtil } from './data.ts';
|
|
4
|
+
import { SchemaRegistry } from './service/registry.ts';
|
|
5
|
+
import { FieldConfig } from './service/types.ts';
|
|
7
6
|
|
|
8
7
|
type BindConfig = {
|
|
9
|
-
view?: string
|
|
8
|
+
view?: string;
|
|
10
9
|
filterField?: (field: FieldConfig) => boolean;
|
|
11
10
|
filterValue?: (value: unknown, field: FieldConfig) => boolean;
|
|
12
11
|
};
|
|
@@ -169,7 +168,7 @@ export class BindUtil {
|
|
|
169
168
|
* @param cfg The bind configuration
|
|
170
169
|
*/
|
|
171
170
|
static bindSchemaToObject<T>(cons: Class<T>, obj: T, data?: object, cfg: BindConfig = {}): T {
|
|
172
|
-
const view = cfg.view
|
|
171
|
+
const view = cfg.view; // Does not convey
|
|
173
172
|
delete cfg.view;
|
|
174
173
|
|
|
175
174
|
if (!!data && isInstance<T>(data)) {
|
|
@@ -181,10 +180,12 @@ export class BindUtil {
|
|
|
181
180
|
obj[k] = data[k];
|
|
182
181
|
}
|
|
183
182
|
} else {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
183
|
+
let viewConf = conf.totalView;
|
|
184
|
+
if (view) {
|
|
185
|
+
viewConf = conf.views[view];
|
|
186
|
+
if (!viewConf) {
|
|
187
|
+
throw new Error(`View not found: ${view.toString()}`);
|
|
188
|
+
}
|
|
188
189
|
}
|
|
189
190
|
|
|
190
191
|
for (const schemaFieldName of viewConf.fields) {
|
package/src/data.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isNumberObject as isNum, isBooleanObject as isBool, isStringObject as isStr } from 'node:util/types';
|
|
2
2
|
|
|
3
3
|
import { asConstructable, castTo, Class, asFull, TypedObject } from '@travetto/runtime';
|
|
4
|
-
import { UnknownType } from './
|
|
4
|
+
import { UnknownType } from './types.ts';
|
|
5
5
|
|
|
6
6
|
const REGEX_PAT = /[\/](.*)[\/](i|g|m|s)?/;
|
|
7
7
|
|
|
@@ -49,26 +49,26 @@ export class DataUtil {
|
|
|
49
49
|
const isSimpA = !isEmptyA && this.isSimpleValue(a);
|
|
50
50
|
const isSimpB = !isEmptyB && this.isSimpleValue(b);
|
|
51
51
|
|
|
52
|
-
let
|
|
52
|
+
let value: unknown;
|
|
53
53
|
|
|
54
54
|
if (isEmptyA || isEmptyB) { // If no `a`, `b` always wins
|
|
55
55
|
if (mode === 'replace' || b === null || !isEmptyB) {
|
|
56
|
-
|
|
56
|
+
value = isEmptyB ? b : this.shallowClone(b);
|
|
57
57
|
} else if (!isEmptyA) {
|
|
58
|
-
|
|
58
|
+
value = this.shallowClone(a);
|
|
59
59
|
} else {
|
|
60
|
-
|
|
60
|
+
value = undefined;
|
|
61
61
|
}
|
|
62
62
|
} else {
|
|
63
63
|
if (isArrA !== isArrB || isSimpA !== isSimpB) {
|
|
64
64
|
throw new Error(`Cannot merge differing types ${a} and ${b}`);
|
|
65
65
|
}
|
|
66
66
|
if (Array.isArray(b)) { // Arrays
|
|
67
|
-
|
|
67
|
+
value = a; // Write onto A
|
|
68
68
|
if (mode === 'replace') {
|
|
69
|
-
|
|
69
|
+
value = b;
|
|
70
70
|
} else {
|
|
71
|
-
const retArr: unknown[] = castTo(
|
|
71
|
+
const retArr: unknown[] = castTo(value);
|
|
72
72
|
const bArr = b;
|
|
73
73
|
for (let i = 0; i < bArr.length; i++) {
|
|
74
74
|
retArr[i] = this.#deepAssignRaw(retArr[i], bArr[i], mode);
|
|
@@ -76,19 +76,19 @@ export class DataUtil {
|
|
|
76
76
|
}
|
|
77
77
|
} else if (isSimpB) { // Scalars
|
|
78
78
|
const match = typeof a === typeof b;
|
|
79
|
-
|
|
79
|
+
value = b;
|
|
80
80
|
|
|
81
81
|
if (!match) { // If types do not match
|
|
82
82
|
if (mode === 'strict') { // Bail on strict
|
|
83
83
|
throw new Error(`Cannot merge ${a} [${typeof a}] with ${b} [${typeof b}]`);
|
|
84
84
|
} else if (mode === 'coerce') { // Force on coerce
|
|
85
|
-
|
|
85
|
+
value = this.coerceType(b, asConstructable(a).constructor, false);
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
} else { // Object merge
|
|
89
|
-
|
|
89
|
+
value = a;
|
|
90
90
|
const bObj: Record<string, unknown> = castTo(b);
|
|
91
|
-
const retObj: Record<string, unknown> = castTo(
|
|
91
|
+
const retObj: Record<string, unknown> = castTo(value);
|
|
92
92
|
|
|
93
93
|
for (const key of Object.keys(bObj)) {
|
|
94
94
|
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
|
@@ -98,7 +98,7 @@ export class DataUtil {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
-
return
|
|
101
|
+
return value;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
/**
|
|
@@ -142,11 +142,11 @@ export class DataUtil {
|
|
|
142
142
|
|
|
143
143
|
switch (type) {
|
|
144
144
|
case Date: {
|
|
145
|
-
let
|
|
145
|
+
let value: Date | undefined;
|
|
146
146
|
if (typeof input === 'object' && 'toDate' in input && typeof input.toDate === 'function') {
|
|
147
|
-
|
|
147
|
+
value = castTo(input.toDate());
|
|
148
148
|
} else {
|
|
149
|
-
|
|
149
|
+
value = input instanceof Date ?
|
|
150
150
|
input :
|
|
151
151
|
typeof input === 'number' ?
|
|
152
152
|
new Date(input) :
|
|
@@ -154,17 +154,17 @@ export class DataUtil {
|
|
|
154
154
|
new Date(parseInt(input, 10)) :
|
|
155
155
|
new Date(input.toString());
|
|
156
156
|
}
|
|
157
|
-
if (strict &&
|
|
157
|
+
if (strict && value && Number.isNaN(value.getTime())) {
|
|
158
158
|
throw new Error(`Invalid date value: ${input}`);
|
|
159
159
|
}
|
|
160
|
-
return
|
|
160
|
+
return value;
|
|
161
161
|
}
|
|
162
162
|
case Number: {
|
|
163
|
-
const
|
|
164
|
-
if (strict && Number.isNaN(
|
|
163
|
+
const value = `${input}`.includes('.') ? parseFloat(`${input}`) : parseInt(`${input}`, 10);
|
|
164
|
+
if (strict && Number.isNaN(value)) {
|
|
165
165
|
throw new Error(`Invalid numeric value: ${input}`);
|
|
166
166
|
}
|
|
167
|
-
return
|
|
167
|
+
return value;
|
|
168
168
|
}
|
|
169
169
|
case BigInt: {
|
|
170
170
|
if (typeof input === 'bigint') {
|
|
@@ -252,7 +252,9 @@ export class DataUtil {
|
|
|
252
252
|
* @returns
|
|
253
253
|
*/
|
|
254
254
|
static filterByKeys<T>(obj: T, exclude: (string | RegExp)[]): T {
|
|
255
|
-
if (obj
|
|
255
|
+
if (Array.isArray(obj)) {
|
|
256
|
+
return castTo(obj.map(x => this.filterByKeys(x, exclude)));
|
|
257
|
+
} else if (obj !== null && obj !== undefined && typeof obj === 'object') {
|
|
256
258
|
const out: Partial<T> = {};
|
|
257
259
|
for (const key of TypedObject.keys(obj)) {
|
|
258
260
|
if (!exclude.some(r => typeof key === 'string' && (typeof r === 'string' ? r === key : r.test(key)))) {
|
package/src/decorator/common.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Class, ClassInstance } from '@travetto/runtime';
|
|
2
2
|
|
|
3
|
-
import { DescribableConfig } from '../service/types';
|
|
4
|
-
import { SchemaRegistry } from '../service/registry';
|
|
3
|
+
import { DescribableConfig } from '../service/types.ts';
|
|
4
|
+
import { SchemaRegistry } from '../service/registry.ts';
|
|
5
5
|
|
|
6
6
|
function isClassInstance(o: Class | ClassInstance, property?: string): o is ClassInstance {
|
|
7
7
|
return !!property;
|
package/src/decorator/field.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Any, ClassInstance } from '@travetto/runtime';
|
|
2
2
|
|
|
3
|
-
import { SchemaRegistry } from '../service/registry';
|
|
4
|
-
import { CommonRegExp } from '../validate/regexp';
|
|
5
|
-
import { ClassList, FieldConfig } from '../service/types';
|
|
3
|
+
import { SchemaRegistry } from '../service/registry.ts';
|
|
4
|
+
import { CommonRegExp } from '../validate/regexp.ts';
|
|
5
|
+
import { ClassList, FieldConfig } from '../service/types.ts';
|
|
6
6
|
|
|
7
7
|
type PropType<V> = (<T extends Partial<Record<K, V | Function>>, K extends string>(t: T, k: K, idx?: TypedPropertyDescriptor<Any> | number) => void);
|
|
8
8
|
|
|
@@ -40,16 +40,14 @@ export function Field(type: ClassList, ...config: Partial<FieldConfig>[]) {
|
|
|
40
40
|
export function Alias(...aliases: string[]): PropType<unknown> { return prop({ aliases }); }
|
|
41
41
|
/**
|
|
42
42
|
* Mark a field as writeonly
|
|
43
|
-
* @param active This determines if this field is readonly or not.
|
|
44
43
|
* @augments `@travetto/schema:Field`
|
|
45
44
|
*/
|
|
46
|
-
export function Writeonly(
|
|
45
|
+
export function Writeonly(): PropType<unknown> { return prop({ access: 'writeonly' }); }
|
|
47
46
|
/**
|
|
48
47
|
* Mark a field as readonly
|
|
49
|
-
* @param active This determines if this field is readonly or not.
|
|
50
48
|
* @augments `@travetto/schema:Field`
|
|
51
49
|
*/
|
|
52
|
-
export function Readonly(
|
|
50
|
+
export function Readonly(): PropType<unknown> { return prop({ access: 'readonly' }); }
|
|
53
51
|
/**
|
|
54
52
|
* Mark a field as sensitive
|
|
55
53
|
* @param active This determines if this field is sensitive or not.
|
|
@@ -189,7 +187,7 @@ export function Currency(): PropType<number> { return Precision(13, 2); }
|
|
|
189
187
|
* @augments `@travetto/schema:Ignore`
|
|
190
188
|
*/
|
|
191
189
|
export function Ignore(): PropertyDecorator {
|
|
192
|
-
return (
|
|
190
|
+
return () => { };
|
|
193
191
|
}
|
|
194
192
|
|
|
195
193
|
/**
|
package/src/decorator/schema.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { Any, castTo, Class, ClassInstance, DeepPartial } from '@travetto/runtime';
|
|
2
2
|
|
|
3
|
-
import { BindUtil } from '../bind-util';
|
|
4
|
-
import { SchemaRegistry } from '../service/registry';
|
|
5
|
-
import { ClassConfig, ViewFieldsConfig } from '../service/types';
|
|
6
|
-
import { MethodValidatorFn, ValidatorFn } from '../validate/types';
|
|
3
|
+
import { BindUtil } from '../bind-util.ts';
|
|
4
|
+
import { SchemaRegistry } from '../service/registry.ts';
|
|
5
|
+
import { ClassConfig, ViewFieldsConfig } from '../service/types.ts';
|
|
6
|
+
import { MethodValidatorFn, ValidatorFn } from '../validate/types.ts';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Register a class as a Schema
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
11
|
* @augments `@travetto/schema:Schema`
|
|
13
12
|
*/
|
|
14
13
|
export function Schema(cfg?: Partial<Pick<ClassConfig, 'subTypeName' | 'subTypeField' | 'baseType'>>) { // Auto is used during compilation
|
|
@@ -27,7 +26,7 @@ export function Schema(cfg?: Partial<Pick<ClassConfig, 'subTypeName' | 'subTypeF
|
|
|
27
26
|
* @param fn The validator function
|
|
28
27
|
*/
|
|
29
28
|
export const Validator = <T>(fn: ValidatorFn<T, string>) =>
|
|
30
|
-
(target: Class<T>,
|
|
29
|
+
(target: Class<T>, _k?: string): void => {
|
|
31
30
|
SchemaRegistry.getOrCreatePending(target).validators!.push(castTo(fn));
|
|
32
31
|
};
|
|
33
32
|
|
|
@@ -37,7 +36,7 @@ export const Validator = <T>(fn: ValidatorFn<T, string>) =>
|
|
|
37
36
|
* @param fn The validator function
|
|
38
37
|
*/
|
|
39
38
|
export function MethodValidator<T extends (...args: Any[]) => Any>(fn: MethodValidatorFn<Parameters<T>>) {
|
|
40
|
-
return (target: ClassInstance, k: string,
|
|
39
|
+
return (target: ClassInstance, k: string, _prop: TypedPropertyDescriptor<T>): void => {
|
|
41
40
|
SchemaRegistry.registerPendingMethod(target.constructor, k).validators!.push(castTo(fn));
|
|
42
41
|
};
|
|
43
42
|
}
|
package/src/internal/types.ts
CHANGED
|
@@ -1,2 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { DataUtil } from '../data.ts';
|
|
2
|
+
|
|
3
|
+
const InvalidSymbol = Symbol();
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Point Implementation
|
|
7
|
+
*/
|
|
8
|
+
export class PointImpl {
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Validate we have an actual point
|
|
12
|
+
*/
|
|
13
|
+
static validateSchema(input: unknown): 'type' | undefined {
|
|
14
|
+
const bound = this.bindSchema(input);
|
|
15
|
+
return bound !== InvalidSymbol && bound && !isNaN(bound[0]) && !isNaN(bound[1]) ? undefined : 'type';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Convert to tuple of two numbers
|
|
20
|
+
*/
|
|
21
|
+
static bindSchema(input: unknown): [number, number] | typeof InvalidSymbol | undefined {
|
|
22
|
+
if (Array.isArray(input) && input.length === 2) {
|
|
23
|
+
const [a, b] = input.map(x => DataUtil.coerceType(x, Number, false));
|
|
24
|
+
return [a, b];
|
|
25
|
+
} else {
|
|
26
|
+
return InvalidSymbol;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
Object.defineProperty(PointImpl, 'name', { value: 'Point' });
|
package/src/name.ts
CHANGED
package/src/service/changes.ts
CHANGED
|
@@ -3,8 +3,7 @@ import { EventEmitter } from 'node:events';
|
|
|
3
3
|
import { Class } from '@travetto/runtime';
|
|
4
4
|
import { ChangeEvent } from '@travetto/registry';
|
|
5
5
|
|
|
6
|
-
import { FieldConfig, ClassConfig } from './types';
|
|
7
|
-
import { AllViewSymbol } from '../internal/types';
|
|
6
|
+
import { FieldConfig, ClassConfig } from './types.ts';
|
|
8
7
|
|
|
9
8
|
const id = (c: Class | string): string => typeof c === 'string' ? c : c.Ⲑid;
|
|
10
9
|
|
|
@@ -111,8 +110,8 @@ class $SchemaChangeListener {
|
|
|
111
110
|
*/
|
|
112
111
|
emitFieldChanges({ prev, curr }: ChangeEvent<ClassConfig>): void {
|
|
113
112
|
|
|
114
|
-
const prevView = prev?.
|
|
115
|
-
const currView = curr!.
|
|
113
|
+
const prevView = prev?.totalView || { fields: [], schema: {} };
|
|
114
|
+
const currView = curr!.totalView;
|
|
116
115
|
|
|
117
116
|
const prevFields = new Set(prevView.fields);
|
|
118
117
|
const currFields = new Set(currView.fields);
|
package/src/service/registry.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { Class, AppError, describeFunction, castTo, classConstruct, asFull, castKey } from '@travetto/runtime';
|
|
2
2
|
import { MetadataRegistry, RootRegistry, ChangeEvent } from '@travetto/registry';
|
|
3
3
|
|
|
4
|
-
import { ClassList, FieldConfig, ClassConfig, SchemaConfig, ViewFieldsConfig, ViewConfig, SchemaMethodConfig } from './types';
|
|
5
|
-
import { SchemaChangeListener } from './changes';
|
|
6
|
-
import {
|
|
7
|
-
import { MethodValidatorFn } from '../validate/types';
|
|
4
|
+
import { ClassList, FieldConfig, ClassConfig, SchemaConfig, ViewFieldsConfig, ViewConfig, SchemaMethodConfig } from './types.ts';
|
|
5
|
+
import { SchemaChangeListener } from './changes.ts';
|
|
6
|
+
import { MethodValidatorFn } from '../validate/types.ts';
|
|
8
7
|
|
|
9
8
|
const classToSubTypeName = (cls: Class): string => cls.name
|
|
10
9
|
.replace(/([A-Z])([A-Z][a-z])/g, (all, l, r) => `${l}_${r.toLowerCase()}`)
|
|
@@ -73,7 +72,7 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
73
72
|
ensureInstanceTypeField<T>(cls: Class, o: T): void {
|
|
74
73
|
const schema = this.get(cls);
|
|
75
74
|
const typeField = castKey<T>(schema.subTypeField);
|
|
76
|
-
if (schema.subTypeName && typeField in schema.
|
|
75
|
+
if (schema.subTypeName && typeField in schema.totalView.schema && !o[typeField]) { // Do we have a type field defined
|
|
77
76
|
o[typeField] = castTo(schema.subTypeName); // Assign if missing
|
|
78
77
|
}
|
|
79
78
|
}
|
|
@@ -110,11 +109,11 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
110
109
|
|
|
111
110
|
if (clsSchema.subTypeName || baseSchema.baseType) { // We have a sub type
|
|
112
111
|
const type = castTo<string>(o[castKey<T>(baseSchema.subTypeField)]) ?? clsSchema.subTypeName ?? baseSchema.subTypeName;
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
115
|
-
throw new AppError(`Resolved class ${
|
|
112
|
+
const subType = this.#subTypes.get(base)!.get(type)!;
|
|
113
|
+
if (subType && !(classConstruct(subType) instanceof cls)) {
|
|
114
|
+
throw new AppError(`Resolved class ${subType.name} is not assignable to ${cls.name}`);
|
|
116
115
|
}
|
|
117
|
-
return
|
|
116
|
+
return subType;
|
|
118
117
|
} else {
|
|
119
118
|
return cls;
|
|
120
119
|
}
|
|
@@ -169,7 +168,7 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
169
168
|
SchemaChangeListener.trackSchemaDependency(curr, cls, path, this.get(cls));
|
|
170
169
|
|
|
171
170
|
// Read children
|
|
172
|
-
const view = config.
|
|
171
|
+
const view = config.totalView;
|
|
173
172
|
for (const k of view.fields) {
|
|
174
173
|
if (this.has(view.schema[k].type) && view.schema[k].type !== cls) {
|
|
175
174
|
this.trackSchemaDependencies(cls, view.schema[k].type, [...path, view.schema[k]]);
|
|
@@ -185,12 +184,11 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
185
184
|
baseType: describeFunction(cls)?.abstract,
|
|
186
185
|
metadata: {},
|
|
187
186
|
methods: {},
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
187
|
+
totalView: {
|
|
188
|
+
schema: {},
|
|
189
|
+
fields: [],
|
|
190
|
+
},
|
|
191
|
+
views: {}
|
|
194
192
|
};
|
|
195
193
|
}
|
|
196
194
|
|
|
@@ -199,16 +197,17 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
199
197
|
* @param cls The class to retrieve the schema for
|
|
200
198
|
* @param view The view name
|
|
201
199
|
*/
|
|
202
|
-
getViewSchema<T>(cls: Class<T>, view?: string
|
|
203
|
-
view = view ?? AllViewSymbol;
|
|
204
|
-
|
|
200
|
+
getViewSchema<T>(cls: Class<T>, view?: string): ViewConfig {
|
|
205
201
|
const schema = this.get(cls)!;
|
|
206
202
|
if (!schema) {
|
|
207
203
|
throw new Error(`Unknown schema class ${cls.name}`);
|
|
208
204
|
}
|
|
209
|
-
|
|
210
|
-
if (
|
|
211
|
-
|
|
205
|
+
let res = schema.totalView;
|
|
206
|
+
if (view) {
|
|
207
|
+
res = schema.views[view];
|
|
208
|
+
if (!res) {
|
|
209
|
+
throw new Error(`Unknown view ${view.toString()} for ${cls.name}`);
|
|
210
|
+
}
|
|
212
211
|
}
|
|
213
212
|
return res;
|
|
214
213
|
}
|
|
@@ -219,7 +218,7 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
219
218
|
* @param method
|
|
220
219
|
*/
|
|
221
220
|
getMethodSchema<T>(cls: Class<T>, method: string): FieldConfig[] {
|
|
222
|
-
return (this.get(cls)?.methods?.[method] ?? {}).fields?.filter(x => !!x).
|
|
221
|
+
return (this.get(cls)?.methods?.[method] ?? {}).fields?.filter(x => !!x).toSorted((a, b) => a.index! - b.index!) ?? [];
|
|
223
222
|
}
|
|
224
223
|
|
|
225
224
|
/**
|
|
@@ -275,7 +274,7 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
275
274
|
config.specifiers = [...params[idx]?.specifiers ?? [], ...config.specifiers];
|
|
276
275
|
}
|
|
277
276
|
if (config.enum?.values) {
|
|
278
|
-
config.enum.values = config.enum.values.
|
|
277
|
+
config.enum.values = config.enum.values.toSorted();
|
|
279
278
|
}
|
|
280
279
|
|
|
281
280
|
params[idx] = {
|
|
@@ -296,24 +295,28 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
296
295
|
* @param config The config to register
|
|
297
296
|
*/
|
|
298
297
|
registerPendingFieldFacet(target: Class, prop: string, config: Partial<FieldConfig>): Class {
|
|
299
|
-
|
|
298
|
+
if (prop === '__proto__' || prop === 'constructor' || prop === 'prototype') {
|
|
299
|
+
throw new AppError('Invalid property name');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const totalViewConf = this.getOrCreatePending(target).totalView!;
|
|
300
303
|
|
|
301
|
-
if (!
|
|
302
|
-
|
|
304
|
+
if (!totalViewConf.schema[prop]) {
|
|
305
|
+
totalViewConf.fields.push(prop);
|
|
303
306
|
// Partial config while building
|
|
304
|
-
|
|
307
|
+
totalViewConf.schema[prop] = asFull<FieldConfig>({});
|
|
305
308
|
}
|
|
306
309
|
if (config.aliases) {
|
|
307
|
-
config.aliases = [...
|
|
310
|
+
config.aliases = [...totalViewConf.schema[prop].aliases ?? [], ...config.aliases];
|
|
308
311
|
}
|
|
309
312
|
if (config.specifiers) {
|
|
310
|
-
config.specifiers = [...
|
|
313
|
+
config.specifiers = [...totalViewConf.schema[prop].specifiers ?? [], ...config.specifiers];
|
|
311
314
|
}
|
|
312
315
|
if (config.enum?.values) {
|
|
313
|
-
config.enum.values = config.enum.values.
|
|
316
|
+
config.enum.values = config.enum.values.toSorted();
|
|
314
317
|
}
|
|
315
318
|
|
|
316
|
-
Object.assign(
|
|
319
|
+
Object.assign(totalViewConf.schema[prop], config);
|
|
317
320
|
|
|
318
321
|
return target;
|
|
319
322
|
}
|
|
@@ -359,9 +362,9 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
359
362
|
* @param src Source config
|
|
360
363
|
*/
|
|
361
364
|
mergeConfigs(dest: ClassConfig, src: Partial<ClassConfig>, inherited = false): ClassConfig {
|
|
362
|
-
dest.
|
|
363
|
-
schema: { ...dest.
|
|
364
|
-
fields: [...dest.
|
|
365
|
+
dest.totalView = {
|
|
366
|
+
schema: { ...dest.totalView.schema, ...src.totalView?.schema },
|
|
367
|
+
fields: [...dest.totalView.fields, ...src.totalView?.fields ?? []]
|
|
365
368
|
};
|
|
366
369
|
if (!inherited) {
|
|
367
370
|
dest.baseType = src.baseType ?? dest.baseType;
|
|
@@ -381,20 +384,20 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
381
384
|
* @param conf The class config
|
|
382
385
|
*/
|
|
383
386
|
finalizeViews<T>(target: Class<T>, conf: ClassConfig): ClassConfig {
|
|
384
|
-
const
|
|
387
|
+
const totalViewConf = conf.totalView;
|
|
385
388
|
const pending = this.#pendingViews.get(target) ?? new Map<string, ViewFieldsConfig<string>>();
|
|
386
389
|
this.#pendingViews.delete(target);
|
|
387
390
|
|
|
388
391
|
for (const [view, fields] of pending.entries()) {
|
|
389
392
|
const withoutSet = 'without' in fields ? new Set<string>(fields.without) : undefined;
|
|
390
393
|
const fieldList = withoutSet ?
|
|
391
|
-
|
|
394
|
+
totalViewConf.fields.filter(x => !withoutSet.has(x)) :
|
|
392
395
|
('with' in fields ? fields.with : []);
|
|
393
396
|
|
|
394
397
|
conf.views![view] = {
|
|
395
398
|
fields: fieldList,
|
|
396
399
|
schema: fieldList.reduce<SchemaConfig>((acc, v) => {
|
|
397
|
-
acc[v] =
|
|
400
|
+
acc[v] = totalViewConf.schema[v];
|
|
398
401
|
return acc;
|
|
399
402
|
}, {})
|
|
400
403
|
};
|
|
@@ -427,9 +430,9 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
427
430
|
// Write views out
|
|
428
431
|
config = this.finalizeViews(cls, config);
|
|
429
432
|
|
|
430
|
-
if (config.subTypeName && config.subTypeField in config.
|
|
431
|
-
const field = config.
|
|
432
|
-
config.
|
|
433
|
+
if (config.subTypeName && config.subTypeField in config.totalView.schema) {
|
|
434
|
+
const field = config.totalView.schema[config.subTypeField];
|
|
435
|
+
config.totalView.schema[config.subTypeField] = {
|
|
433
436
|
...field,
|
|
434
437
|
enum: {
|
|
435
438
|
values: [config.subTypeName],
|
package/src/service/types.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Any, Class, Primitive } from '@travetto/runtime';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { MethodValidatorFn, ValidatorFn } from '../validate/types';
|
|
3
|
+
import { MethodValidatorFn, ValidatorFn } from '../validate/types.ts';
|
|
5
4
|
|
|
6
5
|
export type ClassList = Class | [Class];
|
|
7
6
|
|
|
@@ -69,7 +68,11 @@ export interface ClassConfig extends DescribableConfig {
|
|
|
69
68
|
/**
|
|
70
69
|
* List of all views
|
|
71
70
|
*/
|
|
72
|
-
views: Record<string, ViewConfig
|
|
71
|
+
views: Record<string, ViewConfig>;
|
|
72
|
+
/**
|
|
73
|
+
* Composite of all views
|
|
74
|
+
*/
|
|
75
|
+
totalView: ViewConfig;
|
|
73
76
|
/**
|
|
74
77
|
* Global validators
|
|
75
78
|
*/
|
package/src/types.ts
ADDED
package/src/validate/error.ts
CHANGED
package/src/validate/regexp.ts
CHANGED
package/src/validate/types.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { castKey, castTo, Class, ClassInstance, TypedObject } from '@travetto/runtime';
|
|
2
2
|
|
|
3
|
-
import { FieldConfig, SchemaConfig } from '../service/types';
|
|
4
|
-
import { SchemaRegistry } from '../service/registry';
|
|
5
|
-
import { ValidationError, ValidationKindCore, ValidationResult } from './types';
|
|
6
|
-
import { Messages } from './messages';
|
|
7
|
-
import { isValidationError, TypeMismatchError, ValidationResultError } from './error';
|
|
8
|
-
import { DataUtil } from '../data';
|
|
9
|
-
import { CommonRegExpToName } from './regexp';
|
|
3
|
+
import { FieldConfig, SchemaConfig } from '../service/types.ts';
|
|
4
|
+
import { SchemaRegistry } from '../service/registry.ts';
|
|
5
|
+
import { ValidationError, ValidationKindCore, ValidationResult } from './types.ts';
|
|
6
|
+
import { Messages } from './messages.ts';
|
|
7
|
+
import { isValidationError, TypeMismatchError, ValidationResultError } from './error.ts';
|
|
8
|
+
import { DataUtil } from '../data.ts';
|
|
9
|
+
import { CommonRegExpToName } from './regexp.ts';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Get the schema config for Class/Schema config, including support for polymorphism
|
|
@@ -184,22 +184,22 @@ export class SchemaValidator {
|
|
|
184
184
|
*/
|
|
185
185
|
static #prepareErrors(path: string, results: ValidationResult[]): ValidationError[] {
|
|
186
186
|
const out: ValidationError[] = [];
|
|
187
|
-
for (const
|
|
187
|
+
for (const result of results) {
|
|
188
188
|
const err: ValidationError = {
|
|
189
|
-
...
|
|
190
|
-
kind:
|
|
191
|
-
value:
|
|
189
|
+
...result,
|
|
190
|
+
kind: result.kind,
|
|
191
|
+
value: result.value,
|
|
192
192
|
message: '',
|
|
193
|
-
re: CommonRegExpToName.get(
|
|
193
|
+
re: CommonRegExpToName.get(result.re!) ?? result.re?.source ?? '',
|
|
194
194
|
path,
|
|
195
|
-
type: (typeof
|
|
195
|
+
type: (typeof result.type === 'function' ? result.type.name : result.type)
|
|
196
196
|
};
|
|
197
197
|
|
|
198
198
|
if (!err.re) {
|
|
199
199
|
delete err.re;
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
const msg =
|
|
202
|
+
const msg = result.message ?? (
|
|
203
203
|
Messages.get(err.re ?? '') ??
|
|
204
204
|
Messages.get(err.kind) ??
|
|
205
205
|
Messages.get('default')!
|
|
@@ -4,13 +4,13 @@ import {
|
|
|
4
4
|
DecoratorUtil, DocUtil, ParamDocumentation, TransformerState, transformCast,
|
|
5
5
|
} from '@travetto/transformer';
|
|
6
6
|
|
|
7
|
-
const SCHEMA_MOD = '@travetto/schema/src/decorator/schema';
|
|
8
|
-
const FIELD_MOD = '@travetto/schema/src/decorator/field';
|
|
9
|
-
const COMMON_MOD = '@travetto/schema/src/decorator/common';
|
|
10
|
-
const TYPES_FILE = '@travetto/schema/src/internal/types';
|
|
11
|
-
|
|
12
7
|
export class SchemaTransformUtil {
|
|
13
8
|
|
|
9
|
+
static SCHEMA_IMPORT = '@travetto/schema/src/decorator/schema.ts';
|
|
10
|
+
static FIELD_IMPORT = '@travetto/schema/src/decorator/field.ts';
|
|
11
|
+
static COMMON_IMPORT = '@travetto/schema/src/decorator/common.ts';
|
|
12
|
+
static TYPES_IMPORT = '@travetto/schema/src/types.ts';
|
|
13
|
+
|
|
14
14
|
/**
|
|
15
15
|
* Produce concrete type given transformer type
|
|
16
16
|
*/
|
|
@@ -29,7 +29,7 @@ export class SchemaTransformUtil {
|
|
|
29
29
|
break;
|
|
30
30
|
}
|
|
31
31
|
case 'unknown': {
|
|
32
|
-
const imp = state.importFile(
|
|
32
|
+
const imp = state.importFile(this.TYPES_IMPORT);
|
|
33
33
|
return state.createAccess(imp.ident, 'UnknownType');
|
|
34
34
|
}
|
|
35
35
|
case 'shape': {
|
|
@@ -40,8 +40,8 @@ export class SchemaTransformUtil {
|
|
|
40
40
|
if (!existing) {
|
|
41
41
|
const cls = state.factory.createClassDeclaration(
|
|
42
42
|
[
|
|
43
|
-
state.createDecorator(
|
|
44
|
-
state.createDecorator(
|
|
43
|
+
state.createDecorator(this.SCHEMA_IMPORT, 'Schema'),
|
|
44
|
+
state.createDecorator(this.COMMON_IMPORT, 'Describe',
|
|
45
45
|
state.fromLiteral({
|
|
46
46
|
title: type.name,
|
|
47
47
|
description: type.comment
|
|
@@ -163,7 +163,7 @@ export class SchemaTransformUtil {
|
|
|
163
163
|
|
|
164
164
|
const params: ts.Expression[] = [];
|
|
165
165
|
|
|
166
|
-
const existing = state.findDecorator('@travetto/schema', node, 'Field',
|
|
166
|
+
const existing = state.findDecorator('@travetto/schema', node, 'Field', this.FIELD_IMPORT);
|
|
167
167
|
if (!existing) {
|
|
168
168
|
const resolved = this.toConcreteType(state, typeExpr, node, config.root);
|
|
169
169
|
params.push(resolved);
|
|
@@ -183,31 +183,31 @@ export class SchemaTransformUtil {
|
|
|
183
183
|
|
|
184
184
|
const newModifiers = [
|
|
185
185
|
...(node.modifiers ?? []).filter(x => x !== existing),
|
|
186
|
-
state.createDecorator(
|
|
186
|
+
state.createDecorator(this.FIELD_IMPORT, 'Field', ...params)
|
|
187
187
|
];
|
|
188
188
|
|
|
189
|
-
let
|
|
189
|
+
let result: unknown;
|
|
190
190
|
if (ts.isPropertyDeclaration(node)) {
|
|
191
191
|
const comments = DocUtil.describeDocs(node);
|
|
192
192
|
if (comments.description) {
|
|
193
|
-
newModifiers.push(state.createDecorator(
|
|
193
|
+
newModifiers.push(state.createDecorator(this.COMMON_IMPORT, 'Describe', state.fromLiteral({
|
|
194
194
|
description: comments.description
|
|
195
195
|
})));
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
|
|
198
|
+
result = state.factory.updatePropertyDeclaration(node,
|
|
199
199
|
newModifiers, node.name, node.questionToken, node.type, node.initializer);
|
|
200
200
|
} else if (ts.isParameter(node)) {
|
|
201
|
-
|
|
201
|
+
result = state.factory.updateParameterDeclaration(node,
|
|
202
202
|
newModifiers, node.dotDotDotToken, node.name, node.questionToken, node.type, node.initializer);
|
|
203
203
|
} else if (ts.isGetAccessorDeclaration(node)) {
|
|
204
|
-
|
|
204
|
+
result = state.factory.updateGetAccessorDeclaration(node,
|
|
205
205
|
newModifiers, node.name, node.parameters, node.type, node.body);
|
|
206
206
|
} else {
|
|
207
|
-
|
|
207
|
+
result = state.factory.updateSetAccessorDeclaration(node,
|
|
208
208
|
newModifiers, node.name, node.parameters, node.body);
|
|
209
209
|
}
|
|
210
|
-
return transformCast(
|
|
210
|
+
return transformCast(result);
|
|
211
211
|
}
|
|
212
212
|
|
|
213
213
|
/**
|
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
TransformerState, OnProperty, OnClass, AfterClass,
|
|
4
|
+
TransformerState, OnProperty, OnClass, AfterClass, DocUtil, DeclarationUtil, OnGetter, OnSetter
|
|
5
5
|
} from '@travetto/transformer';
|
|
6
6
|
|
|
7
|
-
import { SchemaTransformUtil } from './transformer/util';
|
|
7
|
+
import { SchemaTransformUtil } from './transformer/util.ts';
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
const
|
|
9
|
+
const InSchemaSymbol = Symbol();
|
|
10
|
+
const AccessorsSymbol = Symbol();
|
|
11
11
|
|
|
12
12
|
interface AutoState {
|
|
13
|
-
[
|
|
14
|
-
[
|
|
13
|
+
[InSchemaSymbol]?: boolean;
|
|
14
|
+
[AccessorsSymbol]?: Set<string>;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const SCHEMA_IMPORT = '@travetto/schema/src/decorator/schema';
|
|
18
|
-
const COMMON_IMPORT = '@travetto/schema/src/decorator/common';
|
|
19
|
-
|
|
20
17
|
/**
|
|
21
18
|
* Processes `@Schema` to register class as a valid Schema
|
|
22
19
|
*/
|
|
@@ -26,9 +23,9 @@ export class SchemaTransformer {
|
|
|
26
23
|
* Track schema on start
|
|
27
24
|
*/
|
|
28
25
|
@OnClass('Schema')
|
|
29
|
-
static startSchema(state: AutoState & TransformerState, node: ts.ClassDeclaration
|
|
30
|
-
state[
|
|
31
|
-
state[
|
|
26
|
+
static startSchema(state: AutoState & TransformerState, node: ts.ClassDeclaration): ts.ClassDeclaration {
|
|
27
|
+
state[InSchemaSymbol] = true;
|
|
28
|
+
state[AccessorsSymbol] = new Set();
|
|
32
29
|
return node;
|
|
33
30
|
}
|
|
34
31
|
|
|
@@ -41,18 +38,18 @@ export class SchemaTransformer {
|
|
|
41
38
|
|
|
42
39
|
const comments = DocUtil.describeDocs(node);
|
|
43
40
|
|
|
44
|
-
if (!state.findDecorator(this, node, 'Schema', SCHEMA_IMPORT)) {
|
|
45
|
-
modifiers.unshift(state.createDecorator(SCHEMA_IMPORT, 'Schema'));
|
|
41
|
+
if (!state.findDecorator(this, node, 'Schema', SchemaTransformUtil.SCHEMA_IMPORT)) {
|
|
42
|
+
modifiers.unshift(state.createDecorator(SchemaTransformUtil.SCHEMA_IMPORT, 'Schema'));
|
|
46
43
|
}
|
|
47
44
|
|
|
48
45
|
if (comments.description) {
|
|
49
|
-
modifiers.push(state.createDecorator(COMMON_IMPORT, 'Describe', state.fromLiteral({
|
|
46
|
+
modifiers.push(state.createDecorator(SchemaTransformUtil.COMMON_IMPORT, 'Describe', state.fromLiteral({
|
|
50
47
|
title: comments.description
|
|
51
48
|
})));
|
|
52
49
|
}
|
|
53
50
|
|
|
54
|
-
delete state[
|
|
55
|
-
delete state[
|
|
51
|
+
delete state[InSchemaSymbol];
|
|
52
|
+
delete state[AccessorsSymbol];
|
|
56
53
|
|
|
57
54
|
return state.factory.updateClassDeclaration(
|
|
58
55
|
node,
|
|
@@ -70,7 +67,7 @@ export class SchemaTransformer {
|
|
|
70
67
|
@OnProperty()
|
|
71
68
|
static processSchemaField(state: TransformerState & AutoState, node: ts.PropertyDeclaration): ts.PropertyDeclaration {
|
|
72
69
|
const ignore = state.findDecorator(this, node, 'Ignore');
|
|
73
|
-
return state[
|
|
70
|
+
return state[InSchemaSymbol] && !ignore && DeclarationUtil.isPublic(node) ?
|
|
74
71
|
SchemaTransformUtil.computeField(state, node) : node;
|
|
75
72
|
}
|
|
76
73
|
|
|
@@ -80,8 +77,8 @@ export class SchemaTransformer {
|
|
|
80
77
|
@OnGetter()
|
|
81
78
|
static processSchemaGetter(state: TransformerState & AutoState, node: ts.GetAccessorDeclaration): ts.GetAccessorDeclaration {
|
|
82
79
|
const ignore = state.findDecorator(this, node, 'Ignore');
|
|
83
|
-
if (state[
|
|
84
|
-
state[
|
|
80
|
+
if (state[InSchemaSymbol] && !ignore && DeclarationUtil.isPublic(node) && !state[AccessorsSymbol]?.has(node.name.getText())) {
|
|
81
|
+
state[AccessorsSymbol]?.add(node.name.getText());
|
|
85
82
|
return SchemaTransformUtil.computeField(state, node);
|
|
86
83
|
}
|
|
87
84
|
return node;
|
|
@@ -93,8 +90,8 @@ export class SchemaTransformer {
|
|
|
93
90
|
@OnSetter()
|
|
94
91
|
static processSchemaSetter(state: TransformerState & AutoState, node: ts.SetAccessorDeclaration): ts.SetAccessorDeclaration {
|
|
95
92
|
const ignore = state.findDecorator(this, node, 'Ignore');
|
|
96
|
-
if (state[
|
|
97
|
-
state[
|
|
93
|
+
if (state[InSchemaSymbol] && !ignore && DeclarationUtil.isPublic(node) && !state[AccessorsSymbol]?.has(node.name.getText())) {
|
|
94
|
+
state[AccessorsSymbol]?.add(node.name.getText());
|
|
98
95
|
return SchemaTransformUtil.computeField(state, node);
|
|
99
96
|
}
|
|
100
97
|
return node;
|