@travetto/schema 6.0.1 → 7.0.0-rc.1

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.
@@ -0,0 +1,207 @@
1
+ import { Any, Class, ClassInstance, getClass } from '@travetto/runtime';
2
+
3
+ import { CommonRegExp } from '../validate/regexp.ts';
4
+ import { CONSTRUCTOR_PROPERTY, SchemaInputConfig } from '../service/types.ts';
5
+ import { SchemaRegistryIndex } from '../service/registry-index.ts';
6
+
7
+ type PropType<V> = (<T extends Partial<Record<K, V | Function>>, K extends string>(t: T, k: K, idx?: TypedPropertyDescriptor<Any> | number) => void);
8
+
9
+ function input<V>(...obj: Partial<SchemaInputConfig>[]): PropType<V> {
10
+ return (instanceOrCls: ClassInstance | Class, property: string | symbol, idx?: number | TypedPropertyDescriptor<Any>): void => {
11
+ const adapter = SchemaRegistryIndex.getForRegister(getClass(instanceOrCls));
12
+ const propertyKey = property ?? CONSTRUCTOR_PROPERTY;
13
+ if (typeof idx === 'number') {
14
+ adapter.registerParameter(propertyKey, idx, ...obj);
15
+ } else {
16
+ adapter.registerField(propertyKey, ...obj);
17
+ }
18
+ };
19
+ }
20
+
21
+ /**
22
+ * Registering an input
23
+ * @param type The type for the input
24
+ * @param config The input configuration
25
+ * @augments `@travetto/schema:Input`
26
+ * @kind decorator
27
+ */
28
+ export function Input(type: Pick<SchemaInputConfig, 'type' | 'array'>, ...config: Partial<SchemaInputConfig>[]): PropType<unknown> {
29
+ return input(type, ...config);
30
+ }
31
+
32
+ /**
33
+ * Alias for the input
34
+ * @param aliases List of all aliases for a field
35
+ * @augments `@travetto/schema:Input`
36
+ * @kind decorator
37
+ */
38
+ export function Alias(...aliases: string[]): PropType<unknown> { return input({ aliases }); }
39
+
40
+ /**
41
+ * Mark an input as required
42
+ * @param active This determines if this field is required or not.
43
+ * @param message The error message when a the constraint fails.
44
+ * @augments `@travetto/schema:Input`
45
+ * @kind decorator
46
+ */
47
+ export function Required(active = true, message?: string): PropType<unknown> { return input({ required: { active, message } }); }
48
+
49
+ /**
50
+ * Define an input as a set of enumerated values
51
+ * @param values The list of values allowed for the enumeration
52
+ * @param message The error message to show when the constraint fails
53
+ * @augments `@travetto/schema:Input`
54
+ * @kind decorator
55
+ */
56
+ export function Enum(values: string[], message?: string): PropType<string | number> {
57
+ message = message || `{path} is only allowed to be "${values.join('" or "')}"`;
58
+ return input({ enum: { values, message } });
59
+ }
60
+
61
+ /**
62
+ * Mark the input as indicating it's storing textual data
63
+ * @augments `@travetto/schema:Input`
64
+ * @kind decorator
65
+ */
66
+ export function Text(): PropType<string | string[]> { return input({ specifiers: ['text'] }); }
67
+
68
+ /**
69
+ * Mark the input to indicate it's for long form text
70
+ * @augments `@travetto/schema:Input`
71
+ * @kind decorator
72
+ */
73
+ export function LongText(): PropType<string | string[]> { return input({ specifiers: ['text', 'long'] }); }
74
+
75
+ /**
76
+ * Require the input to match a specific RegExp
77
+ * @param re The regular expression to match against
78
+ * @param message The message to show when the constraint fails
79
+ * @augments `@travetto/schema:Input`
80
+ * @kind decorator
81
+ */
82
+ export function Match(re: RegExp, message?: string): PropType<string | string[]> { return input({ match: { re, message } }); }
83
+
84
+ /**
85
+ * The minimum length for the string or array
86
+ * @param n The minimum length
87
+ * @param message The message to show when the constraint fails
88
+ * @augments `@travetto/schema:Input`
89
+ * @kind decorator
90
+ */
91
+ export function MinLength(n: number, message?: string): PropType<string | unknown[]> {
92
+ return input({ minlength: { n, message }, ...(n === 0 ? { required: { active: false } } : {}) });
93
+ }
94
+
95
+ /**
96
+ * The maximum length for the string or array
97
+ * @param n The maximum length
98
+ * @param message The message to show when the constraint fails
99
+ * @augments `@travetto/schema:Input`
100
+ * @kind decorator
101
+ */
102
+ export function MaxLength(n: number, message?: string): PropType<string | unknown[]> { return input({ maxlength: { n, message } }); }
103
+
104
+ /**
105
+ * The minimum value
106
+ * @param n The minimum value
107
+ * @param message The message to show when the constraint fails
108
+ * @augments `@travetto/schema:Input`
109
+ * @kind decorator
110
+ */
111
+ export function Min<T extends number | Date>(n: T, message?: string): PropType<Date | number> {
112
+ return input({ min: { n, message } });
113
+ }
114
+
115
+ /**
116
+ * The maximum value
117
+ * @param n The maximum value
118
+ * @param message The message to show when the constraint fails
119
+ * @augments `@travetto/schema:Input`
120
+ * @kind decorator
121
+ */
122
+ export function Max<T extends number | Date>(n: T, message?: string): PropType<Date | number> {
123
+ return input({ max: { n, message } });
124
+ }
125
+
126
+ /**
127
+ * Mark an input as an email
128
+ * @param message The message to show when the constraint fails
129
+ * @augments `@travetto/schema:Input`
130
+ * @kind decorator
131
+ */
132
+ export function Email(message?: string): PropType<string | string[]> { return Match(CommonRegExp.email, message); }
133
+
134
+ /**
135
+ * Mark an input as an telephone number
136
+ * @param message The message to show when the constraint fails
137
+ * @augments `@travetto/schema:Input`
138
+ * @kind decorator
139
+ */
140
+ export function Telephone(message?: string): PropType<string | string[]> { return Match(CommonRegExp.telephone, message); }
141
+
142
+ /**
143
+ * Mark an input as a url
144
+ * @param message The message to show when the constraint fails
145
+ * @augments `@travetto/schema:Input`
146
+ * @kind decorator
147
+ */
148
+ export function Url(message?: string): PropType<string | string[]> { return Match(CommonRegExp.url, message); }
149
+
150
+ /**
151
+ * Determine the numeric precision of the value
152
+ * @param digits The number of digits a number should have
153
+ * @param decimals The number of decimal digits to support
154
+ * @augments `@travetto/schema:Input`
155
+ * @kind decorator
156
+ */
157
+ export function Precision(digits: number, decimals?: number): PropType<number> { return input({ precision: [digits, decimals] }); }
158
+
159
+ /**
160
+ * Mark a number as an integer
161
+ * @augments `@travetto/schema:Input`
162
+ * @kind decorator
163
+ */
164
+ export function Integer(): PropType<number> { return Precision(0); }
165
+
166
+ /**
167
+ * Mark a number as a float
168
+ * @augments `@travetto/schema:Input`
169
+ * @kind decorator
170
+ */
171
+ export function Float(): PropType<number> { return Precision(10, 7); }
172
+
173
+ /**
174
+ * Mark a number as a long value
175
+ * @augments `@travetto/schema:Input`
176
+ * @kind decorator
177
+ */
178
+ export function Long(): PropType<number> { return Precision(19, 0); }
179
+
180
+ /**
181
+ * Mark a number as a currency
182
+ * @augments `@travetto/schema:Input`
183
+ * @kind decorator
184
+ */
185
+ export function Currency(): PropType<number> { return Precision(13, 2); }
186
+
187
+ /**
188
+ * Specifier for the input
189
+ * @param specifiers The specifiers for an input
190
+ * @augments `@travetto/schema:Input`
191
+ * @kind decorator
192
+ */
193
+ export function Specifier(...specifiers: string[]): PropType<unknown> { return input({ specifiers }); }
194
+
195
+ /**
196
+ * Sets the discriminator field via a property decorator
197
+ * @augments `@travetto/schema:Input`
198
+ * @kind decorator
199
+ */
200
+ export function DiscriminatorField(): ((t: ClassInstance, k: string) => void) {
201
+ return (instance: ClassInstance, property: string): void => {
202
+ SchemaRegistryIndex.getForRegister(getClass(instance)).register({
203
+ discriminatedBase: true,
204
+ discriminatedField: property
205
+ });
206
+ };
207
+ }
@@ -0,0 +1,28 @@
1
+ import { Any, castTo, ClassInstance, getClass } from '@travetto/runtime';
2
+
3
+ import { SchemaMethodConfig } from '../service/types';
4
+ import { SchemaRegistryIndex } from '../service/registry-index';
5
+ import { MethodValidatorFn } from '../validate/types';
6
+
7
+ /**
8
+ * Registering a method
9
+ * @param config The method configuration
10
+ * @augments `@travetto/schema:Method`
11
+ * @kind decorator
12
+ */
13
+ export function Method(...config: Partial<SchemaMethodConfig>[]) {
14
+ return (instanceOrCls: ClassInstance, property: string | symbol): void => {
15
+ SchemaRegistryIndex.getForRegister(getClass(instanceOrCls)).registerMethod(property, ...config);
16
+ };
17
+ }
18
+
19
+ /**
20
+ * Add a custom validator for a given method
21
+ *
22
+ * @param fn The validator function
23
+ * @augments `@travetto/schema:Method`
24
+ * @kind decorator
25
+ */
26
+ export function MethodValidator<T extends (...args: Any[]) => Any>(fn: MethodValidatorFn<Parameters<T>>): MethodDecorator {
27
+ return Method({ validators: [castTo(fn)] });
28
+ }
@@ -1,22 +1,27 @@
1
- import { Any, castTo, Class, ClassInstance, DeepPartial } from '@travetto/runtime';
1
+ import { castTo, Class, DeepPartial } from '@travetto/runtime';
2
2
 
3
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';
4
+ import { SchemaClassConfig, ViewFieldsConfig } from '../service/types.ts';
5
+ import { ValidatorFn } from '../validate/types.ts';
6
+ import { SchemaRegistryIndex } from '../service/registry-index.ts';
7
+
8
+ /**
9
+ * Provides all the valid string type fields from a given type T
10
+ */
11
+ type ValidStringField<T> = { [K in Extract<keyof T, string>]: T[K] extends string ? K : never }[Extract<keyof T, string>];
7
12
 
8
13
  /**
9
14
  * Register a class as a Schema
10
15
  *
11
16
  * @augments `@travetto/schema:Schema`
17
+ * @kind decorator
12
18
  */
13
- export function Schema(cfg?: Partial<Pick<ClassConfig, 'subTypeName' | 'subTypeField' | 'baseType'>>) { // Auto is used during compilation
14
- return <T, U extends Class<T>>(target: U): U => {
15
- target.from ??= function <V>(this: Class<V>, data: DeepPartial<V>, view?: string): V {
19
+ export function Schema(cfg?: Partial<Pick<SchemaClassConfig, 'validators' | 'methods'>>) {
20
+ return <T, U extends Class<T>>(cls: U): void => {
21
+ cls.from ??= function <V>(this: Class<V>, data: DeepPartial<V>, view?: string): V {
16
22
  return BindUtil.bindSchema(this, data, { view });
17
23
  };
18
- SchemaRegistry.register(target, cfg);
19
- return target;
24
+ SchemaRegistryIndex.getForRegister(cls).registerClass(cfg);
20
25
  };
21
26
  }
22
27
 
@@ -24,41 +29,43 @@ export function Schema(cfg?: Partial<Pick<ClassConfig, 'subTypeName' | 'subTypeF
24
29
  * Add a custom validator, can be at the class level
25
30
  *
26
31
  * @param fn The validator function
32
+ * @kind decorator
27
33
  */
28
34
  export const Validator = <T>(fn: ValidatorFn<T, string>) =>
29
- (target: Class<T>, _k?: string): void => {
30
- SchemaRegistry.getOrCreatePending(target).validators!.push(castTo(fn));
35
+ (cls: Class<T>): void => {
36
+ SchemaRegistryIndex.getForRegister(cls).register({ validators: [castTo(fn)] });
31
37
  };
32
38
 
33
39
  /**
34
- * Add a custom validator for a given method
35
- *
36
- * @param fn The validator function
40
+ * Register a specific view for a class
41
+ * @param name The name of the view
42
+ * @param fields The specific fields to add as part of a view
43
+ * @kind decorator
37
44
  */
38
- export function MethodValidator<T extends (...args: Any[]) => Any>(fn: MethodValidatorFn<Parameters<T>>) {
39
- return (target: ClassInstance, k: string, _prop: TypedPropertyDescriptor<T>): void => {
40
- SchemaRegistry.registerPendingMethod(target.constructor, k).validators!.push(castTo(fn));
45
+ export function View<T>(name: string, fields: ViewFieldsConfig<Partial<T>>) {
46
+ return (cls: Class<Partial<T>>): void => {
47
+ SchemaRegistryIndex.getForRegister(cls).register({ views: { [name]: fields } });
41
48
  };
42
49
  }
43
50
 
44
51
  /**
45
- * Register a specific view for a class
46
- * @param name The name of the view
47
- * @param fields The specific fields to add as part of a view
52
+ * Register a class as a discriminated class, by a specific type
53
+ * @param type The type to use for discrimination
54
+ * @kind decorator
48
55
  */
49
- export function View<T>(name: string, fields: ViewFieldsConfig<Partial<T>>) {
50
- return (target: Class<Partial<T>>): void => {
51
- SchemaRegistry.registerPendingView(target, name, fields);
56
+ export function SubType<T>(type?: string) {
57
+ return (cls: Class<Partial<T>>): void => {
58
+ SchemaRegistryIndex.getForRegister(cls).register({ discriminatedType: type });
52
59
  };
53
60
  }
54
61
 
55
62
  /**
56
- * Register a class as a subtype, with a specific discriminator
57
- * @param name
58
- * @returns
63
+ * Register a class as a discriminated class
64
+ * @param field The field to use for discrimination
65
+ * @kind decorator
59
66
  */
60
- export function SubType<T>(name: string) {
61
- return (target: Class<Partial<T>>): void => {
62
- SchemaRegistry.register(target, { subTypeName: name });
67
+ export function Discriminated<T>(field: ValidStringField<T>) {
68
+ return (cls: Class<Partial<T>>): void => {
69
+ SchemaRegistryIndex.getForRegister(cls).register({ discriminatedField: field, discriminatedBase: true });
63
70
  };
64
71
  }
package/src/name.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ClassConfig } from './service/types.ts';
1
+ import { SchemaClassConfig } from './service/types.ts';
2
2
 
3
3
  const ID_RE = /(\d{1,100})Δ$/;
4
4
 
@@ -14,7 +14,7 @@ export class SchemaNameResolver {
14
14
  this.#digits = digits;
15
15
  }
16
16
 
17
- getName(schema: ClassConfig): string {
17
+ getName(schema: SchemaClassConfig): string {
18
18
  const cls = schema.class;
19
19
  const id = cls.Ⲑid;
20
20
  if (ID_RE.test(cls.name)) {
@@ -3,27 +3,25 @@ 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.ts';
7
-
8
- const id = (c: Class | string): string => typeof c === 'string' ? c : c.Ⲑid;
6
+ import { SchemaFieldConfig, SchemaClassConfig } from './types.ts';
9
7
 
10
8
  interface FieldMapping {
11
- path: FieldConfig[];
12
- config: ClassConfig;
9
+ path: SchemaFieldConfig[];
10
+ config: SchemaClassConfig;
13
11
  }
14
12
 
15
13
  export interface FieldChangeEvent {
16
14
  cls: Class;
17
- changes: ChangeEvent<FieldConfig>[];
15
+ changes: ChangeEvent<SchemaFieldConfig>[];
18
16
  }
19
17
 
20
18
  interface SubSchemaChange {
21
- path: FieldConfig[];
22
- fields: ChangeEvent<FieldConfig>[];
19
+ path: SchemaFieldConfig[];
20
+ fields: ChangeEvent<SchemaFieldConfig>[];
23
21
  }
24
22
 
25
23
  export interface SchemaChange {
26
- config: ClassConfig;
24
+ config: SchemaClassConfig;
27
25
  subs: SubSchemaChange[];
28
26
  }
29
27
 
@@ -33,7 +31,7 @@ export interface SchemaChangeEvent {
33
31
  }
34
32
 
35
33
  /**
36
- * Schema change listener. Handles all changes that occur via the SchemaRegistry
34
+ * Schema change listener. Handles all changes that occur via the SchemaRegistryIndex
37
35
  */
38
36
  class $SchemaChangeListener {
39
37
 
@@ -60,7 +58,7 @@ class $SchemaChangeListener {
60
58
  * Clear dependency mappings for a given class
61
59
  */
62
60
  clearSchemaDependency(cls: Class): void {
63
- this.#mapping.delete(id(cls));
61
+ this.#mapping.delete(cls.Ⲑid);
64
62
  }
65
63
 
66
64
  /**
@@ -70,12 +68,12 @@ class $SchemaChangeListener {
70
68
  * @param path The path within the object hierarchy to arrive at the class
71
69
  * @param config The configuration or the class
72
70
  */
73
- trackSchemaDependency(src: Class, parent: Class, path: FieldConfig[], config: ClassConfig): void {
74
- const idValue = id(src);
71
+ trackSchemaDependency(src: Class, parent: Class, path: SchemaFieldConfig[], config: SchemaClassConfig): void {
72
+ const idValue = src.Ⲑid;
75
73
  if (!this.#mapping.has(idValue)) {
76
74
  this.#mapping.set(idValue, new Map());
77
75
  }
78
- this.#mapping.get(idValue)!.set(id(parent), { path, config });
76
+ this.#mapping.get(idValue)!.set(parent.Ⲑid, { path, config });
79
77
  }
80
78
 
81
79
  /**
@@ -85,7 +83,7 @@ class $SchemaChangeListener {
85
83
  */
86
84
  emitSchemaChanges({ cls, changes }: FieldChangeEvent): void {
87
85
  const updates = new Map<string, SchemaChange>();
88
- const clsId = id(cls);
86
+ const clsId = cls.Ⲑid;
89
87
 
90
88
  if (this.#mapping.has(clsId)) {
91
89
  const deps = this.#mapping.get(clsId)!;
@@ -108,25 +106,24 @@ class $SchemaChangeListener {
108
106
  * @param prev The previous class config
109
107
  * @param curr The current class config
110
108
  */
111
- emitFieldChanges({ prev, curr }: ChangeEvent<ClassConfig>): void {
112
-
113
- const prevView = prev?.totalView || { fields: [], schema: {} };
114
- const currView = curr!.totalView;
109
+ emitFieldChanges(ev: ChangeEvent<SchemaClassConfig>): void {
110
+ const prev = 'prev' in ev ? ev.prev : undefined;
111
+ const curr = 'curr' in ev ? ev.curr : undefined;
115
112
 
116
- const prevFields = new Set(prevView.fields);
117
- const currFields = new Set(currView.fields);
113
+ const prevFields = new Set(Object.keys(prev?.fields ?? {}));
114
+ const currFields = new Set(Object.keys(curr?.fields ?? {}));
118
115
 
119
- const changes: ChangeEvent<FieldConfig>[] = [];
116
+ const changes: ChangeEvent<SchemaFieldConfig>[] = [];
120
117
 
121
118
  for (const c of currFields) {
122
- if (!prevFields.has(c)) {
123
- changes.push({ curr: currView.schema[c], type: 'added' });
119
+ if (!prevFields.has(c) && curr) {
120
+ changes.push({ curr: curr.fields[c], type: 'added' });
124
121
  }
125
122
  }
126
123
 
127
124
  for (const c of prevFields) {
128
- if (!currFields.has(c)) {
129
- changes.push({ prev: prevView.schema[c], type: 'removing' });
125
+ if (!currFields.has(c) && prev) {
126
+ changes.push({ prev: prev.fields[c], type: 'removing' });
130
127
  }
131
128
  }
132
129
 
@@ -134,14 +131,14 @@ class $SchemaChangeListener {
134
131
  const compareTypes = (a: Class, b: Class): boolean => a.Ⲑid ? a.Ⲑid === b.Ⲑid : a === b;
135
132
 
136
133
  for (const c of currFields) {
137
- if (prevFields.has(c)) {
138
- const prevSchema = prevView.schema[c];
139
- const currSchema = currView.schema[c];
134
+ if (prevFields.has(c) && prev && curr) {
135
+ const prevSchema = prev.fields[c];
136
+ const currSchema = curr.fields[c];
140
137
  if (
141
138
  JSON.stringify(prevSchema) !== JSON.stringify(currSchema) ||
142
139
  !compareTypes(prevSchema.type, currSchema.type)
143
140
  ) {
144
- changes.push({ prev: prevView.schema[c], curr: currView.schema[c], type: 'changed' });
141
+ changes.push({ prev: prev.fields[c], curr: curr.fields[c], type: 'changed' });
145
142
  }
146
143
  }
147
144
  }