@travetto/schema 2.0.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -19
- package/package.json +4 -4
- package/src/bind-util.ts +5 -1
- package/src/decorator/field.ts +20 -14
- package/src/decorator/schema.ts +11 -0
- package/src/service/registry.ts +48 -16
- package/src/service/types.ts +8 -0
- package/src/validate/validator.ts +4 -2
- package/support/transform-util.ts +31 -24
- package/support/transformer.schema.ts +32 -1
package/README.md
CHANGED
|
@@ -65,23 +65,24 @@ 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
|
-
* [@
|
|
71
|
-
* [@
|
|
72
|
-
* [@
|
|
73
|
-
* [@
|
|
74
|
-
* [@
|
|
75
|
-
* [@
|
|
76
|
-
* [@
|
|
77
|
-
* [@
|
|
78
|
-
* [@
|
|
79
|
-
* [@
|
|
80
|
-
* [@
|
|
81
|
-
* [@
|
|
82
|
-
* [@
|
|
83
|
-
* [@
|
|
84
|
-
* [@
|
|
68
|
+
* [@Required](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L72) defines a that field should be required
|
|
69
|
+
* [@Enum](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L79) defines the allowable values that a field can have
|
|
70
|
+
* [@Match](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L100) defines a regular expression that the field value should match
|
|
71
|
+
* [@MinLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L108) enforces min length of a string
|
|
72
|
+
* [@MaxLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L116) enforces max length of a string
|
|
73
|
+
* [@Min](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L108) enforces min value for a date or a number
|
|
74
|
+
* [@Max](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L116) enforces max value for a date or a number
|
|
75
|
+
* [@Email](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L139) ensures string field matches basic email regex
|
|
76
|
+
* [@Telephone](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L146) ensures string field matches basic telephone regex
|
|
77
|
+
* [@Url](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L153) ensures string field matches basic url regex
|
|
78
|
+
* [@Ignore](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L192) exclude from auto schema registration
|
|
79
|
+
* [@Integer](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L167) ensures number passed in is only a whole number
|
|
80
|
+
* [@Float](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L173) ensures number passed in allows fractional values
|
|
81
|
+
* [@Currency](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L185) provides support for standard currency
|
|
82
|
+
* [@Text](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L87) indicates that a field is expecting natural language input, not just discrete values
|
|
83
|
+
* [@LongText](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L92) same as text, but expects longer form content
|
|
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
|
+
* [@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
|
|
85
86
|
|
|
86
87
|
Additionally, schemas can be nested to form more complex data structures that are able to bound and validated.
|
|
87
88
|
|
|
@@ -205,7 +206,7 @@ Validation Failed {
|
|
|
205
206
|
"message": "Validation errors have occurred",
|
|
206
207
|
"category": "data",
|
|
207
208
|
"type": "ValidationResultError",
|
|
208
|
-
"at": "
|
|
209
|
+
"at": "2022-03-14T04:00:00.618Z",
|
|
209
210
|
"errors": [
|
|
210
211
|
{
|
|
211
212
|
"kind": "type",
|
|
@@ -434,7 +435,7 @@ Validation Failed {
|
|
|
434
435
|
"message": "Validation errors have occurred",
|
|
435
436
|
"category": "data",
|
|
436
437
|
"type": "ValidationResultError",
|
|
437
|
-
"at": "
|
|
438
|
+
"at": "2022-03-14T04:00:00.837Z",
|
|
438
439
|
"errors": [
|
|
439
440
|
{
|
|
440
441
|
"kind": "type",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/schema",
|
|
3
3
|
"displayName": "Schema",
|
|
4
|
-
"version": "2.0
|
|
4
|
+
"version": "2.1.0",
|
|
5
5
|
"description": "Data type registry for runtime validation, reflection and binding. ",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"schema",
|
|
@@ -29,11 +29,11 @@
|
|
|
29
29
|
"directory": "module/schema"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@travetto/registry": "^2.0
|
|
33
|
-
"@travetto/transformer": "^2.0
|
|
32
|
+
"@travetto/registry": "^2.1.0",
|
|
33
|
+
"@travetto/transformer": "^2.1.0"
|
|
34
34
|
},
|
|
35
35
|
"optionalPeerDependencies": {
|
|
36
|
-
"@types/faker": "^5.5.
|
|
36
|
+
"@types/faker": "^5.5.9",
|
|
37
37
|
"faker": "^5.5.3"
|
|
38
38
|
},
|
|
39
39
|
"publishConfig": {
|
package/src/bind-util.ts
CHANGED
|
@@ -149,6 +149,8 @@ export class BindUtil {
|
|
|
149
149
|
return data as T;
|
|
150
150
|
} else {
|
|
151
151
|
const tgt = new (cls as ConcreteClass<T>)();
|
|
152
|
+
SchemaRegistry.ensureInstanceTypeField(cls, tgt);
|
|
153
|
+
|
|
152
154
|
for (const [k, v] of Object.entries(tgt)) { // Do not retain undefined fields
|
|
153
155
|
if (v === undefined) {
|
|
154
156
|
delete tgt[k as keyof T];
|
|
@@ -185,7 +187,9 @@ export class BindUtil {
|
|
|
185
187
|
|
|
186
188
|
for (const schemaFieldName of viewConf.fields) {
|
|
187
189
|
let inboundField: string | undefined = undefined;
|
|
188
|
-
|
|
190
|
+
if (viewConf.schema[schemaFieldName].access === 'readonly') {
|
|
191
|
+
continue; // Skip trying to write readonly fields
|
|
192
|
+
}
|
|
189
193
|
if (schemaFieldName in data) {
|
|
190
194
|
inboundField = schemaFieldName;
|
|
191
195
|
} else if (viewConf.schema[schemaFieldName].aliases) {
|
package/src/decorator/field.ts
CHANGED
|
@@ -4,8 +4,8 @@ import { SchemaRegistry } from '../service/registry';
|
|
|
4
4
|
import { CommonRegExp } from '../validate/regexp';
|
|
5
5
|
import { ClassList, FieldConfig } from '../service/types';
|
|
6
6
|
|
|
7
|
-
function prop(obj:
|
|
8
|
-
return (t: ClassInstance, k: string, idx?: number) => {
|
|
7
|
+
function prop(obj: Partial<FieldConfig>) {
|
|
8
|
+
return (t: ClassInstance, k: string, idx?: number | PropertyDescriptor) => {
|
|
9
9
|
if (idx !== undefined && typeof idx === 'number') {
|
|
10
10
|
SchemaRegistry.registerPendingParamFacet(t.constructor, k, idx, obj);
|
|
11
11
|
} else {
|
|
@@ -15,19 +15,19 @@ function prop(obj: Record<string, unknown>) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
const stringArrProp = prop as
|
|
18
|
-
(obj:
|
|
18
|
+
(obj: Partial<FieldConfig>) => <T extends Partial<Record<K, string | unknown[]>>, K extends string>(t: T, k: K, idx?: number | PropertyDescriptor) => void;
|
|
19
19
|
|
|
20
20
|
const stringArrStringProp = prop as
|
|
21
|
-
(obj:
|
|
21
|
+
(obj: Partial<FieldConfig>) => <T extends Partial<Record<K, string | string[]>>, K extends string>(t: T, k: K, idx?: number | PropertyDescriptor) => void;
|
|
22
22
|
|
|
23
23
|
const numberProp = prop as
|
|
24
|
-
(obj:
|
|
24
|
+
(obj: Partial<FieldConfig>) => <T extends Partial<Record<K, number>>, K extends string>(t: T, k: K, idx?: number | PropertyDescriptor) => void;
|
|
25
25
|
|
|
26
26
|
const stringNumberProp = prop as
|
|
27
|
-
(obj:
|
|
27
|
+
(obj: Partial<FieldConfig>) => <T extends Partial<Record<K, string | number>>, K extends string>(t: T, k: K, idx?: number | PropertyDescriptor) => void;
|
|
28
28
|
|
|
29
29
|
const dateNumberProp = prop as
|
|
30
|
-
(obj:
|
|
30
|
+
(obj: Partial<FieldConfig>) => <T extends Partial<Record<K, Date | number>>, K extends string>(t: T, k: K, idx?: number | PropertyDescriptor) => void;
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Registering a field
|
|
@@ -36,7 +36,7 @@ const dateNumberProp = prop as
|
|
|
36
36
|
* @augments `@trv:schema/Field`
|
|
37
37
|
*/
|
|
38
38
|
export function Field(type: ClassList, config?: Partial<FieldConfig>) {
|
|
39
|
-
return (f: ClassInstance, k: string, idx?: number) => {
|
|
39
|
+
return (f: ClassInstance, k: string, idx?: number | PropertyDescriptor) => {
|
|
40
40
|
if (idx !== undefined && typeof idx === 'number') {
|
|
41
41
|
SchemaRegistry.registerPendingParamConfig(f.constructor, k, idx, type, config);
|
|
42
42
|
} else {
|
|
@@ -51,6 +51,18 @@ export function Field(type: ClassList, config?: Partial<FieldConfig>) {
|
|
|
51
51
|
* @augments `@trv:schema/Field`
|
|
52
52
|
*/
|
|
53
53
|
export function Alias(...aliases: string[]) { return prop({ aliases }); }
|
|
54
|
+
/**
|
|
55
|
+
* Mark a field as writeonly
|
|
56
|
+
* @param active This determines if this field is readonly or not.
|
|
57
|
+
* @augments `@trv:schema/Field`
|
|
58
|
+
*/
|
|
59
|
+
export function Writeonly(active = true) { return prop({ access: 'writeonly' }); }
|
|
60
|
+
/**
|
|
61
|
+
* Mark a field as readonly
|
|
62
|
+
* @param active This determines if this field is readonly or not.
|
|
63
|
+
* @augments `@trv:schema/Field`
|
|
64
|
+
*/
|
|
65
|
+
export function Readonly(active = true) { return prop({ access: 'readonly' }); }
|
|
54
66
|
/**
|
|
55
67
|
* Mark a field as required
|
|
56
68
|
* @param active This determines if this field is required or not.
|
|
@@ -68,12 +80,6 @@ export function Enum(values: string[], message?: string) {
|
|
|
68
80
|
message = message || `{path} is only allowed to be "${values.join('" or "')}"`;
|
|
69
81
|
return stringNumberProp({ enum: { values, message } });
|
|
70
82
|
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Should the field be trimmed on storage
|
|
74
|
-
* @augments `@trv:schema/Field`
|
|
75
|
-
*/
|
|
76
|
-
export function Trimmed() { return stringArrStringProp({ trim: true }); }
|
|
77
83
|
/**
|
|
78
84
|
* Mark the field as indicating it's storing textual data
|
|
79
85
|
* @augments `@trv:schema/Field`
|
package/src/decorator/schema.ts
CHANGED
|
@@ -36,4 +36,15 @@ export function View<T>(name: string, fields: ViewFieldsConfig<Partial<T>>) {
|
|
|
36
36
|
return (target: Class<Partial<T>>) => {
|
|
37
37
|
SchemaRegistry.registerPendingView(target, name, fields);
|
|
38
38
|
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Register a class as a subtype, with a specific discriminator
|
|
43
|
+
* @param name
|
|
44
|
+
* @returns
|
|
45
|
+
*/
|
|
46
|
+
export function SubType<T>(name: string) {
|
|
47
|
+
return (target: Class<Partial<T>>) => {
|
|
48
|
+
SchemaRegistry.registerSubTypes(target, name);
|
|
49
|
+
};
|
|
39
50
|
}
|
package/src/service/registry.ts
CHANGED
|
@@ -24,6 +24,36 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
24
24
|
super(RootRegistry);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
#computeSubTypeName(cls: Class) {
|
|
28
|
+
if (!this.#typeKeys.has(cls)) {
|
|
29
|
+
this.#typeKeys.set(cls, cls.name
|
|
30
|
+
.replace(/([A-Z])([A-Z][a-z])/g, (all, l, r) => `${l}_${r.toLowerCase()}`)
|
|
31
|
+
.replace(/([a-z]|\b)([A-Z])/g, (all, l, r) => l ? `${l}_${r.toLowerCase()}` : r.toLowerCase())
|
|
32
|
+
.toLowerCase());
|
|
33
|
+
}
|
|
34
|
+
return this.#typeKeys.get(cls);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get subtype name for a class
|
|
39
|
+
* @param cls Base class
|
|
40
|
+
*/
|
|
41
|
+
getSubTypeName(cls: Class) {
|
|
42
|
+
if (this.get(cls).subType) {
|
|
43
|
+
return this.#computeSubTypeName(cls);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Ensure type is set properl
|
|
49
|
+
*/
|
|
50
|
+
ensureInstanceTypeField<T>(cls: Class, o: T) {
|
|
51
|
+
const withType = (o as { type?: string });
|
|
52
|
+
if (this.get(cls)?.subType && 'type' in this.get(cls).views[AllViewⲐ].schema && !withType.type) { // Do we have a type field defined
|
|
53
|
+
withType.type = this.#computeSubTypeName(cls); // Assign if missing
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
27
57
|
/**
|
|
28
58
|
* Find the subtype for a given instance
|
|
29
59
|
* @param cls Class for instance
|
|
@@ -44,7 +74,7 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
44
74
|
if (type) {
|
|
45
75
|
return this.#subTypes.get(cls)!.get(typeId) ?? cls;
|
|
46
76
|
}
|
|
47
|
-
} else {
|
|
77
|
+
} else if (this.get(cls)?.subType) {
|
|
48
78
|
const expectedType = this.#typeKeys.get(cls);
|
|
49
79
|
if (expectedType && typeof type === 'string' && expectedType !== type) {
|
|
50
80
|
throw new AppError(`Data of type ${type} does not match expected class type ${expectedType}`, 'data');
|
|
@@ -53,23 +83,19 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
53
83
|
return cls;
|
|
54
84
|
}
|
|
55
85
|
|
|
56
|
-
/**
|
|
57
|
-
* Get subtype name for a class
|
|
58
|
-
* @param cls Base class
|
|
59
|
-
*/
|
|
60
|
-
getSubTypeName(cls: Class) {
|
|
61
|
-
return cls.name
|
|
62
|
-
.replace(/([A-Z])([A-Z][a-z])/g, (all, l, r) => `${l}_${r.toLowerCase()}`)
|
|
63
|
-
.replace(/([a-z]|\b)([A-Z])/g, (all, l, r) => l ? `${l}_${r.toLowerCase()}` : r.toLowerCase())
|
|
64
|
-
.toLowerCase();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
86
|
/**
|
|
68
87
|
* Register sub types for a class
|
|
69
88
|
* @param cls The class to register against
|
|
70
89
|
* @param type The subtype name
|
|
71
90
|
*/
|
|
72
|
-
registerSubTypes(cls: Class, type
|
|
91
|
+
registerSubTypes(cls: Class, type?: string) {
|
|
92
|
+
// Mark as subtype
|
|
93
|
+
(this.get(cls) ?? this.getOrCreatePending(cls)).subType = true;
|
|
94
|
+
|
|
95
|
+
type ??= this.#computeSubTypeName(cls)!;
|
|
96
|
+
|
|
97
|
+
this.#typeKeys.set(cls, type);
|
|
98
|
+
|
|
73
99
|
let parent = this.getParentClass(cls)!;
|
|
74
100
|
let parentConfig = this.get(parent);
|
|
75
101
|
|
|
@@ -77,12 +103,13 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
77
103
|
if (!this.#subTypes.has(parent)) {
|
|
78
104
|
this.#subTypes.set(parent, new Map());
|
|
79
105
|
}
|
|
80
|
-
this.#typeKeys.set(cls, type);
|
|
81
106
|
this.#subTypes.get(parent)!.set(type, cls);
|
|
82
107
|
this.#subTypes.get(parent)!.set(cls.ᚕid, cls);
|
|
83
108
|
parent = this.getParentClass(parent!)!;
|
|
84
109
|
parentConfig = this.get(parent);
|
|
85
110
|
}
|
|
111
|
+
|
|
112
|
+
return type;
|
|
86
113
|
}
|
|
87
114
|
|
|
88
115
|
/**
|
|
@@ -109,6 +136,7 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
109
136
|
return {
|
|
110
137
|
class: cls,
|
|
111
138
|
validators: [],
|
|
139
|
+
subType: false,
|
|
112
140
|
views: {
|
|
113
141
|
[AllViewⲐ]: {
|
|
114
142
|
schema: {},
|
|
@@ -259,6 +287,7 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
259
287
|
schema: { ...dest.views[AllViewⲐ].schema, ...src.views[AllViewⲐ].schema },
|
|
260
288
|
fields: [...dest.views[AllViewⲐ].fields, ...src.views[AllViewⲐ].fields]
|
|
261
289
|
};
|
|
290
|
+
dest.subType = src.subType || dest.subType;
|
|
262
291
|
dest.title = src.title || dest.title;
|
|
263
292
|
dest.validators = [...src.validators, ...dest.validators];
|
|
264
293
|
return dest;
|
|
@@ -305,7 +334,7 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
305
334
|
}
|
|
306
335
|
}
|
|
307
336
|
|
|
308
|
-
this.registerSubTypes(cls
|
|
337
|
+
this.registerSubTypes(cls);
|
|
309
338
|
|
|
310
339
|
// Merge pending, back on top, to allow child to have higher precedence
|
|
311
340
|
const pending = this.getOrCreatePending(cls);
|
|
@@ -332,10 +361,13 @@ class $SchemaRegistry extends MetadataRegistry<ClassConfig, FieldConfig> {
|
|
|
332
361
|
if (e.type === 'removing' && this.hasExpired(cls)) {
|
|
333
362
|
// Recompute subtypes
|
|
334
363
|
this.#subTypes.clear();
|
|
364
|
+
this.#typeKeys.delete(cls);
|
|
335
365
|
this.#methodSchemas.delete(cls);
|
|
366
|
+
|
|
367
|
+
// Recompute subtype mappings
|
|
336
368
|
for (const el of this.entries.keys()) {
|
|
337
369
|
const clz = this.entries.get(el)!.class;
|
|
338
|
-
this.registerSubTypes(clz
|
|
370
|
+
this.registerSubTypes(clz);
|
|
339
371
|
}
|
|
340
372
|
|
|
341
373
|
SchemaChangeListener.clearSchemaDependency(cls);
|
package/src/service/types.ts
CHANGED
|
@@ -64,6 +64,10 @@ export interface ClassConfig extends DescribableConfig {
|
|
|
64
64
|
* Global validators
|
|
65
65
|
*/
|
|
66
66
|
validators: ValidatorFn<unknown, unknown>[];
|
|
67
|
+
/**
|
|
68
|
+
* Is the class a sub type
|
|
69
|
+
*/
|
|
70
|
+
subType?: boolean;
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
/**
|
|
@@ -141,6 +145,10 @@ export interface FieldConfig extends DescribableConfig {
|
|
|
141
145
|
* Default value
|
|
142
146
|
*/
|
|
143
147
|
default?: Primitive;
|
|
148
|
+
/**
|
|
149
|
+
* Is the field readonly, or write only?, defaults to no restrictions
|
|
150
|
+
*/
|
|
151
|
+
access?: 'readonly' | 'writeonly';
|
|
144
152
|
}
|
|
145
153
|
|
|
146
154
|
export type ViewFieldsConfig<T> = { with: (keyof T)[] } | { without: (keyof T)[] };
|
|
@@ -38,7 +38,9 @@ export class SchemaValidator {
|
|
|
38
38
|
let errors: ValidationError[] = [];
|
|
39
39
|
|
|
40
40
|
for (const field of Object.keys(schema)) {
|
|
41
|
-
|
|
41
|
+
if (schema[field].access !== 'readonly') { // Do not validate readonly fields
|
|
42
|
+
errors = errors.concat(this.#validateFieldSchema(schema[field], o[field as keyof T], relative));
|
|
43
|
+
}
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
return errors;
|
|
@@ -223,7 +225,7 @@ export class SchemaValidator {
|
|
|
223
225
|
* @param view The optional view to limit the scope to
|
|
224
226
|
*/
|
|
225
227
|
static async validate<T>(cls: Class<T>, o: T, view?: string): Promise<T> {
|
|
226
|
-
if (!Util.isPlainObject(o) && !(o instanceof cls)) {
|
|
228
|
+
if (!Util.isPlainObject(o) && !(o instanceof cls || cls.ᚕid === (o as ClassInstance<T>).constructor.ᚕid)) {
|
|
227
229
|
throw new TypeMismatchError(cls.name, (o as unknown as ClassInstance).constructor.name);
|
|
228
230
|
}
|
|
229
231
|
cls = SchemaRegistry.resolveSubTypeForInstance(cls, o);
|
|
@@ -65,15 +65,33 @@ export class SchemaTransformUtil {
|
|
|
65
65
|
/**
|
|
66
66
|
* Compute property information from declaration
|
|
67
67
|
*/
|
|
68
|
-
static computeField<T extends ts.PropertyDeclaration | ts.ParameterDeclaration>(
|
|
68
|
+
static computeField<T extends ts.PropertyDeclaration | ts.ParameterDeclaration | ts.GetAccessorDeclaration | ts.SetAccessorDeclaration>(
|
|
69
69
|
state: TransformerState, node: T, config: { type?: AnyType, root?: ts.Node, name?: string } = { root: node }
|
|
70
70
|
): T {
|
|
71
71
|
|
|
72
|
-
const typeExpr = config.type ?? state.resolveType(node);
|
|
72
|
+
const typeExpr = config.type ?? state.resolveType(ts.isSetAccessor(node) ? node.parameters[0] : node);
|
|
73
73
|
const attrs: ts.PropertyAssignment[] = [];
|
|
74
74
|
|
|
75
|
-
if (!node
|
|
76
|
-
|
|
75
|
+
if (!ts.isGetAccessorDeclaration(node) && !ts.isSetAccessorDeclaration(node)) {
|
|
76
|
+
// eslint-disable-next-line no-bitwise
|
|
77
|
+
if ((ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Readonly) > 0) {
|
|
78
|
+
attrs.push(state.factory.createPropertyAssignment('mode', state.fromLiteral('readonly')));
|
|
79
|
+
} else if (!node.questionToken && !typeExpr.undefinable && !node.initializer) {
|
|
80
|
+
attrs.push(state.factory.createPropertyAssignment('required', state.fromLiteral({ active: true })));
|
|
81
|
+
}
|
|
82
|
+
if (node.initializer && ts.isLiteralExpression(node.initializer)) {
|
|
83
|
+
attrs.push(state.factory.createPropertyAssignment('default', node.initializer));
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
const acc = DeclarationUtil.getAccessorPair(node);
|
|
87
|
+
if (!acc.setter) {
|
|
88
|
+
attrs.push(state.factory.createPropertyAssignment('access', state.fromLiteral('readonly')));
|
|
89
|
+
}
|
|
90
|
+
if (!acc.getter) {
|
|
91
|
+
attrs.push(state.factory.createPropertyAssignment('mode', state.fromLiteral('writeonly')));
|
|
92
|
+
} else if (!typeExpr.undefinable) {
|
|
93
|
+
attrs.push(state.factory.createPropertyAssignment('required', state.fromLiteral({ active: true })));
|
|
94
|
+
}
|
|
77
95
|
}
|
|
78
96
|
|
|
79
97
|
if (ts.isParameter(node) || config.name !== undefined) {
|
|
@@ -82,10 +100,6 @@ export class SchemaTransformUtil {
|
|
|
82
100
|
));
|
|
83
101
|
}
|
|
84
102
|
|
|
85
|
-
if (node.initializer && ts.isLiteralExpression(node.initializer)) {
|
|
86
|
-
attrs.push(state.factory.createPropertyAssignment('default', node.initializer));
|
|
87
|
-
}
|
|
88
|
-
|
|
89
103
|
// If we have a union type
|
|
90
104
|
if (typeExpr.key === 'union') {
|
|
91
105
|
const values = typeExpr.subTypes.map(x => x.key === 'literal' ? x.value : undefined)
|
|
@@ -135,23 +149,16 @@ export class SchemaTransformUtil {
|
|
|
135
149
|
}
|
|
136
150
|
|
|
137
151
|
return state.factory.updatePropertyDeclaration(node as Exclude<typeof node, T>,
|
|
138
|
-
newDecs,
|
|
139
|
-
|
|
140
|
-
node.name,
|
|
141
|
-
node.questionToken,
|
|
142
|
-
node.type,
|
|
143
|
-
node.initializer
|
|
144
|
-
) as T;
|
|
145
|
-
} else {
|
|
152
|
+
newDecs, node.modifiers, node.name, node.questionToken, node.type, node.initializer) as T;
|
|
153
|
+
} else if (ts.isParameter(node)) {
|
|
146
154
|
return state.factory.updateParameterDeclaration(node as Exclude<typeof node, T>,
|
|
147
|
-
newDecs,
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
node.name,
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
node.
|
|
154
|
-
) as T;
|
|
155
|
+
newDecs, node.modifiers, node.dotDotDotToken, node.name, node.questionToken, node.type, node.initializer) as T;
|
|
156
|
+
} else if (ts.isGetAccessorDeclaration(node)) {
|
|
157
|
+
return state.factory.updateGetAccessorDeclaration(node as Exclude<typeof node, T>,
|
|
158
|
+
newDecs, node.modifiers, node.name, node.parameters, node.type, node.body) as T;
|
|
159
|
+
} else {
|
|
160
|
+
return state.factory.updateSetAccessorDeclaration(node as Exclude<typeof node, T>,
|
|
161
|
+
newDecs, node.modifiers, node.name, node.parameters, node.body) as T;
|
|
155
162
|
}
|
|
156
163
|
}
|
|
157
164
|
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
TransformerState, OnProperty, OnClass, AfterClass, DecoratorMeta, DocUtil, DeclarationUtil, TransformerId
|
|
4
|
+
TransformerState, OnProperty, OnClass, AfterClass, DecoratorMeta, DocUtil, DeclarationUtil, TransformerId, OnGetter, OnSetter
|
|
5
5
|
} from '@travetto/transformer';
|
|
6
|
+
|
|
6
7
|
import { SchemaTransformUtil } from './transform-util';
|
|
7
8
|
|
|
8
9
|
const inSchema = Symbol.for('@trv:schema/schema');
|
|
10
|
+
const accessors = Symbol.for('@trv:schema/schema');
|
|
9
11
|
|
|
10
12
|
interface AutoState {
|
|
11
13
|
[inSchema]?: boolean;
|
|
14
|
+
[accessors]?: Set<string>;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
const SCHEMA_MOD = '@travetto/schema/src/decorator/schema';
|
|
@@ -27,6 +30,7 @@ export class SchemaTransformer {
|
|
|
27
30
|
@OnClass('Schema')
|
|
28
31
|
static startSchema(state: AutoState & TransformerState, node: ts.ClassDeclaration, dec?: DecoratorMeta) {
|
|
29
32
|
state[inSchema] = true;
|
|
33
|
+
state[accessors] = new Set();
|
|
30
34
|
return node;
|
|
31
35
|
}
|
|
32
36
|
|
|
@@ -50,6 +54,7 @@ export class SchemaTransformer {
|
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
delete state[inSchema];
|
|
57
|
+
delete state[accessors];
|
|
53
58
|
|
|
54
59
|
return state.factory.updateClassDeclaration(
|
|
55
60
|
node,
|
|
@@ -71,4 +76,30 @@ export class SchemaTransformer {
|
|
|
71
76
|
return state[inSchema] && !ignore && DeclarationUtil.isPublic(node) ?
|
|
72
77
|
SchemaTransformUtil.computeField(state, node) : node;
|
|
73
78
|
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Handle getters
|
|
82
|
+
*/
|
|
83
|
+
@OnGetter()
|
|
84
|
+
static processSchemaGetter(state: TransformerState & AutoState, node: ts.GetAccessorDeclaration) {
|
|
85
|
+
const ignore = state.findDecorator(this, node, 'Ignore');
|
|
86
|
+
if (state[inSchema] && !ignore && DeclarationUtil.isPublic(node) && !state[accessors]?.has(node.name.getText())) {
|
|
87
|
+
state[accessors]?.add(node.name.getText());
|
|
88
|
+
return SchemaTransformUtil.computeField(state, node);
|
|
89
|
+
}
|
|
90
|
+
return node;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Handle setters
|
|
95
|
+
*/
|
|
96
|
+
@OnSetter()
|
|
97
|
+
static processSchemaSetter(state: TransformerState & AutoState, node: ts.SetAccessorDeclaration) {
|
|
98
|
+
const ignore = state.findDecorator(this, node, 'Ignore');
|
|
99
|
+
if (state[inSchema] && !ignore && DeclarationUtil.isPublic(node) && !state[accessors]?.has(node.name.getText())) {
|
|
100
|
+
state[accessors]?.add(node.name.getText());
|
|
101
|
+
return SchemaTransformUtil.computeField(state, node);
|
|
102
|
+
}
|
|
103
|
+
return node;
|
|
104
|
+
}
|
|
74
105
|
}
|