@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 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#L60) defines a that field should be required
69
- * [@Enum](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L67) defines the allowable values that a field can have
70
- * [@Trimmed](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L76) augments binding to remove leading and trailing whitespace from string values
71
- * [@Match](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L94) defines a regular expression that the field value should match
72
- * [@MinLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L102) enforces min length of a string
73
- * [@MaxLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L110) enforces max length of a string
74
- * [@Min](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L102) enforces min value for a date or a number
75
- * [@Max](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L110) enforces max value for a date or a number
76
- * [@Email](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L133) ensures string field matches basic email regex
77
- * [@Telephone](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L140) ensures string field matches basic telephone regex
78
- * [@Url](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L147) ensures string field matches basic url regex
79
- * [@Ignore](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L186) exclude from auto schema registration
80
- * [@Integer](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L161) ensures number passed in is only a whole number
81
- * [@Float](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L167) ensures number passed in allows fractional values
82
- * [@Currency](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L179) provides support for standard currency
83
- * [@Text](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L81) indicates that a field is expecting natural language input, not just discrete values
84
- * [@LongText](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L86) same as text, but expects longer form content
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": "2021-03-14T05:00:00.618Z",
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": "2021-03-14T05:00:00.837Z",
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.2",
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.1",
33
- "@travetto/transformer": "^2.0.1"
32
+ "@travetto/registry": "^2.1.0",
33
+ "@travetto/transformer": "^2.1.0"
34
34
  },
35
35
  "optionalPeerDependencies": {
36
- "@types/faker": "^5.5.6",
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) {
@@ -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: Record<string, unknown>) {
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: Record<string, unknown>) => <T extends Partial<Record<K, string | unknown[]>>, K extends string>(t: T, k: K, idx?: number) => void;
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: Record<string, unknown>) => <T extends Partial<Record<K, string | string[]>>, K extends string>(t: T, k: K, idx?: number) => void;
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: Record<string, unknown>) => <T extends Partial<Record<K, number>>, K extends string>(t: T, k: K, idx?: number) => void;
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: Record<string, unknown>) => <T extends Partial<Record<K, string | number>>, K extends string>(t: T, k: K, idx?: number) => void;
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: Record<string, unknown>) => <T extends Partial<Record<K, Date | number>>, K extends string>(t: T, k: K, idx?: number) => void;
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`
@@ -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
  }
@@ -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: string) {
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, this.getSubTypeName(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, this.getSubTypeName(clz));
370
+ this.registerSubTypes(clz);
339
371
  }
340
372
 
341
373
  SchemaChangeListener.clearSchemaDependency(cls);
@@ -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
- errors = errors.concat(this.#validateFieldSchema(schema[field], o[field as keyof T], relative));
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.questionToken && !typeExpr.undefinable && !node.initializer) {
76
- attrs.push(state.factory.createPropertyAssignment('required', state.fromLiteral({ active: true })));
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
- node.modifiers,
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
- node.modifiers,
149
- node.dotDotDotToken,
150
- node.name,
151
- node.questionToken,
152
- node.type,
153
- node.initializer
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
  }