@travetto/schema 7.0.0-rc.2 → 7.0.0-rc.3
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 +6 -6
- package/__index__.ts +3 -3
- package/package.json +3 -3
- package/src/decorator/input.ts +17 -17
- package/src/internal/types.ts +3 -3
- package/src/service/registry-adapter.ts +9 -6
- package/src/service/registry-index.ts +7 -62
- package/src/service/types.ts +9 -13
- package/src/types.ts +1 -1
- package/src/validate/messages.ts +10 -10
- package/src/validate/regex.ts +22 -0
- package/src/validate/types.ts +4 -4
- package/src/validate/validator.ts +9 -9
- package/support/transformer/util.ts +3 -3
- package/src/service/changes.ts +0 -152
- package/src/validate/regexp.ts +0 -22
package/README.md
CHANGED
|
@@ -293,11 +293,11 @@ export interface ValidationError {
|
|
|
293
293
|
/**
|
|
294
294
|
* Regular expression to match
|
|
295
295
|
*/
|
|
296
|
-
|
|
296
|
+
regex?: string;
|
|
297
297
|
/**
|
|
298
298
|
* Number to compare against
|
|
299
299
|
*/
|
|
300
|
-
|
|
300
|
+
limit?: number | Date;
|
|
301
301
|
/**
|
|
302
302
|
* The type of the field
|
|
303
303
|
*/
|
|
@@ -322,7 +322,7 @@ This feature is meant to allow for simple Typescript types to be able to be back
|
|
|
322
322
|
/**
|
|
323
323
|
* Geometric Point as [number,number] with validation and binding support
|
|
324
324
|
*
|
|
325
|
-
* @concrete ./internal/types.ts#
|
|
325
|
+
* @concrete ./internal/types.ts#PointContract
|
|
326
326
|
*/
|
|
327
327
|
export type Point = [number, number];
|
|
328
328
|
```
|
|
@@ -334,9 +334,9 @@ import { DataUtil } from '../data.ts';
|
|
|
334
334
|
const InvalidSymbol = Symbol();
|
|
335
335
|
|
|
336
336
|
/**
|
|
337
|
-
* Point
|
|
337
|
+
* Point Contract
|
|
338
338
|
*/
|
|
339
|
-
export class
|
|
339
|
+
export class PointContract {
|
|
340
340
|
|
|
341
341
|
/**
|
|
342
342
|
* Validate we have an actual point
|
|
@@ -359,7 +359,7 @@ export class PointImplementation {
|
|
|
359
359
|
}
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
Object.defineProperty(
|
|
362
|
+
Object.defineProperty(PointContract, 'name', { value: 'Point' });
|
|
363
363
|
```
|
|
364
364
|
|
|
365
365
|
What you can see here is that the `Point` type is now backed by a class that supports:
|
package/__index__.ts
CHANGED
|
@@ -4,10 +4,9 @@ export * from './src/decorator/input.ts';
|
|
|
4
4
|
export * from './src/decorator/schema.ts';
|
|
5
5
|
export * from './src/decorator/method.ts';
|
|
6
6
|
export * from './src/decorator/common.ts';
|
|
7
|
-
export * from './src/service/changes.ts';
|
|
8
7
|
export * from './src/service/types.ts';
|
|
9
8
|
export * from './src/validate/messages.ts';
|
|
10
|
-
export * from './src/validate/
|
|
9
|
+
export * from './src/validate/regex.ts';
|
|
11
10
|
export * from './src/validate/validator.ts';
|
|
12
11
|
export * from './src/validate/error.ts';
|
|
13
12
|
export * from './src/validate/types.ts';
|
|
@@ -15,4 +14,5 @@ export * from './src/bind-util.ts';
|
|
|
15
14
|
export * from './src/data.ts';
|
|
16
15
|
export * from './src/name.ts';
|
|
17
16
|
export * from './src/types.ts';
|
|
18
|
-
export * from './src/service/registry-index.ts';
|
|
17
|
+
export * from './src/service/registry-index.ts';
|
|
18
|
+
export * from './src/service/registry-adapter.ts';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/schema",
|
|
3
|
-
"version": "7.0.0-rc.
|
|
3
|
+
"version": "7.0.0-rc.3",
|
|
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": "^7.0.0-rc.
|
|
30
|
+
"@travetto/registry": "^7.0.0-rc.3"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@travetto/transformer": "^7.0.0-rc.
|
|
33
|
+
"@travetto/transformer": "^7.0.0-rc.3"
|
|
34
34
|
},
|
|
35
35
|
"peerDependenciesMeta": {
|
|
36
36
|
"@travetto/transformer": {
|
package/src/decorator/input.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Any, Class, ClassInstance, getClass } from '@travetto/runtime';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { CommonRegex } from '../validate/regex.ts';
|
|
4
4
|
import { CONSTRUCTOR_PROPERTY, SchemaInputConfig } from '../service/types.ts';
|
|
5
5
|
import { SchemaRegistryIndex } from '../service/registry-index.ts';
|
|
6
6
|
|
|
@@ -76,53 +76,53 @@ export function LongText(): PropType<string | string[]> { return input({ specifi
|
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
78
|
* Require the input to match a specific RegExp
|
|
79
|
-
* @param
|
|
79
|
+
* @param regex The regular expression to match against
|
|
80
80
|
* @param message The message to show when the constraint fails
|
|
81
81
|
* @augments `@travetto/schema:Input`
|
|
82
82
|
* @kind decorator
|
|
83
83
|
*/
|
|
84
|
-
export function Match(
|
|
84
|
+
export function Match(regex: RegExp, message?: string): PropType<string | string[]> { return input({ match: { regex, message } }); }
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
87
|
* The minimum length for the string or array
|
|
88
|
-
* @param
|
|
88
|
+
* @param limit The minimum length
|
|
89
89
|
* @param message The message to show when the constraint fails
|
|
90
90
|
* @augments `@travetto/schema:Input`
|
|
91
91
|
* @kind decorator
|
|
92
92
|
*/
|
|
93
|
-
export function MinLength(
|
|
94
|
-
return input({ minlength: {
|
|
93
|
+
export function MinLength(limit: number, message?: string): PropType<string | unknown[]> {
|
|
94
|
+
return input({ minlength: { limit, message }, ...(limit === 0 ? { required: { active: false } } : {}) });
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
98
|
* The maximum length for the string or array
|
|
99
|
-
* @param
|
|
99
|
+
* @param limit The maximum length
|
|
100
100
|
* @param message The message to show when the constraint fails
|
|
101
101
|
* @augments `@travetto/schema:Input`
|
|
102
102
|
* @kind decorator
|
|
103
103
|
*/
|
|
104
|
-
export function MaxLength(
|
|
104
|
+
export function MaxLength(limit: number, message?: string): PropType<string | unknown[]> { return input({ maxlength: { limit, message } }); }
|
|
105
105
|
|
|
106
106
|
/**
|
|
107
107
|
* The minimum value
|
|
108
|
-
* @param
|
|
108
|
+
* @param limit The minimum value
|
|
109
109
|
* @param message The message to show when the constraint fails
|
|
110
110
|
* @augments `@travetto/schema:Input`
|
|
111
111
|
* @kind decorator
|
|
112
112
|
*/
|
|
113
|
-
export function Min<T extends number | Date>(
|
|
114
|
-
return input({ min: {
|
|
113
|
+
export function Min<T extends number | Date>(limit: T, message?: string): PropType<Date | number> {
|
|
114
|
+
return input({ min: { limit, message } });
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
/**
|
|
118
118
|
* The maximum value
|
|
119
|
-
* @param
|
|
119
|
+
* @param limit The maximum value
|
|
120
120
|
* @param message The message to show when the constraint fails
|
|
121
121
|
* @augments `@travetto/schema:Input`
|
|
122
122
|
* @kind decorator
|
|
123
123
|
*/
|
|
124
|
-
export function Max<T extends number | Date>(
|
|
125
|
-
return input({ max: {
|
|
124
|
+
export function Max<T extends number | Date>(limit: T, message?: string): PropType<Date | number> {
|
|
125
|
+
return input({ max: { limit, message } });
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
/**
|
|
@@ -131,7 +131,7 @@ export function Max<T extends number | Date>(n: T, message?: string): PropType<D
|
|
|
131
131
|
* @augments `@travetto/schema:Input`
|
|
132
132
|
* @kind decorator
|
|
133
133
|
*/
|
|
134
|
-
export function Email(message?: string): PropType<string | string[]> { return Match(
|
|
134
|
+
export function Email(message?: string): PropType<string | string[]> { return Match(CommonRegex.email, message); }
|
|
135
135
|
|
|
136
136
|
/**
|
|
137
137
|
* Mark an input as an telephone number
|
|
@@ -139,7 +139,7 @@ export function Email(message?: string): PropType<string | string[]> { return Ma
|
|
|
139
139
|
* @augments `@travetto/schema:Input`
|
|
140
140
|
* @kind decorator
|
|
141
141
|
*/
|
|
142
|
-
export function Telephone(message?: string): PropType<string | string[]> { return Match(
|
|
142
|
+
export function Telephone(message?: string): PropType<string | string[]> { return Match(CommonRegex.telephone, message); }
|
|
143
143
|
|
|
144
144
|
/**
|
|
145
145
|
* Mark an input as a url
|
|
@@ -147,7 +147,7 @@ export function Telephone(message?: string): PropType<string | string[]> { retur
|
|
|
147
147
|
* @augments `@travetto/schema:Input`
|
|
148
148
|
* @kind decorator
|
|
149
149
|
*/
|
|
150
|
-
export function Url(message?: string): PropType<string | string[]> { return Match(
|
|
150
|
+
export function Url(message?: string): PropType<string | string[]> { return Match(CommonRegex.url, message); }
|
|
151
151
|
|
|
152
152
|
/**
|
|
153
153
|
* Determine the numeric precision of the value
|
package/src/internal/types.ts
CHANGED
|
@@ -3,9 +3,9 @@ import { DataUtil } from '../data.ts';
|
|
|
3
3
|
const InvalidSymbol = Symbol();
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Point
|
|
6
|
+
* Point Contract
|
|
7
7
|
*/
|
|
8
|
-
export class
|
|
8
|
+
export class PointContract {
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Validate we have an actual point
|
|
@@ -28,4 +28,4 @@ export class PointImplementation {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
Object.defineProperty(
|
|
31
|
+
Object.defineProperty(PointContract, 'name', { value: 'Point' });
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
CONSTRUCTOR_PROPERTY
|
|
8
8
|
} from './types';
|
|
9
9
|
|
|
10
|
+
export type SchemaDiscriminatedInfo = Required<Pick<SchemaClassConfig, 'discriminatedType' | 'discriminatedField' | 'discriminatedBase'>>;
|
|
11
|
+
|
|
10
12
|
const classToDiscriminatedType = (cls: Class): string => cls.name
|
|
11
13
|
.replace(/([A-Z])([A-Z][a-z])/g, (all, left, right) => `${left}_${right.toLowerCase()}`)
|
|
12
14
|
.replace(/([a-z]|\b)([A-Z])/g, (all, left, right) => left ? `${left}_${right.toLowerCase()}` : right.toLowerCase())
|
|
@@ -69,6 +71,7 @@ function getConstructorConfig<T extends SchemaClassConfig>(base: Partial<T>, par
|
|
|
69
71
|
const parentCons = parent?.methods?.[CONSTRUCTOR_PROPERTY];
|
|
70
72
|
const baseCons = base.methods?.[CONSTRUCTOR_PROPERTY];
|
|
71
73
|
return {
|
|
74
|
+
class: base.class!,
|
|
72
75
|
parameters: [],
|
|
73
76
|
validators: [],
|
|
74
77
|
...parentCons,
|
|
@@ -168,7 +171,7 @@ export class SchemaRegistryAdapter implements RegistryAdapter<SchemaClassConfig>
|
|
|
168
171
|
|
|
169
172
|
registerField(field: string, ...data: Partial<SchemaFieldConfig>[]): SchemaFieldConfig {
|
|
170
173
|
const classConfig = this.register({});
|
|
171
|
-
const config = classConfig.fields[field] ??= { name: field,
|
|
174
|
+
const config = classConfig.fields[field] ??= { name: field, class: this.#cls, type: null! };
|
|
172
175
|
const combined = combineInputs(config, data);
|
|
173
176
|
return combined;
|
|
174
177
|
}
|
|
@@ -197,7 +200,7 @@ export class SchemaRegistryAdapter implements RegistryAdapter<SchemaClassConfig>
|
|
|
197
200
|
|
|
198
201
|
registerMethod(method: string, ...data: Partial<SchemaMethodConfig>[]): SchemaMethodConfig {
|
|
199
202
|
const classConfig = this.register();
|
|
200
|
-
const config = classConfig.methods[method] ??= { parameters: [], validators: [] };
|
|
203
|
+
const config = classConfig.methods[method] ??= { class: this.#cls, parameters: [], validators: [] };
|
|
201
204
|
return combineMethods(config, data);
|
|
202
205
|
}
|
|
203
206
|
|
|
@@ -213,7 +216,7 @@ export class SchemaRegistryAdapter implements RegistryAdapter<SchemaClassConfig>
|
|
|
213
216
|
|
|
214
217
|
registerParameter(method: string, idx: number, ...data: Partial<SchemaParameterConfig>[]): SchemaParameterConfig {
|
|
215
218
|
const params = this.registerMethod(method, {}).parameters;
|
|
216
|
-
const config = params[idx] ??= { method, index: idx,
|
|
219
|
+
const config = params[idx] ??= { method, index: idx, class: this.#cls, array: false, type: null! };
|
|
217
220
|
return combineInputs(config, data);
|
|
218
221
|
}
|
|
219
222
|
|
|
@@ -305,8 +308,8 @@ export class SchemaRegistryAdapter implements RegistryAdapter<SchemaClassConfig>
|
|
|
305
308
|
}
|
|
306
309
|
|
|
307
310
|
/**
|
|
308
|
-
|
|
309
|
-
|
|
311
|
+
* Provides the prototype-derived descriptor for a property
|
|
312
|
+
*/
|
|
310
313
|
getAccessorDescriptor(field: string): PropertyDescriptor {
|
|
311
314
|
if (!this.#accessorDescriptors.has(field)) {
|
|
312
315
|
let proto = this.#cls.prototype;
|
|
@@ -330,7 +333,7 @@ export class SchemaRegistryAdapter implements RegistryAdapter<SchemaClassConfig>
|
|
|
330
333
|
return value;
|
|
331
334
|
}
|
|
332
335
|
|
|
333
|
-
getDiscriminatedConfig():
|
|
336
|
+
getDiscriminatedConfig(): SchemaDiscriminatedInfo | undefined {
|
|
334
337
|
const { discriminatedField, discriminatedType, discriminatedBase } = this.#config;
|
|
335
338
|
if (discriminatedType && discriminatedField) {
|
|
336
339
|
return { discriminatedType, discriminatedField, discriminatedBase: !!discriminatedBase };
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { AppError, castKey, castTo, Class, classConstruct, getParentClass
|
|
1
|
+
import { RegistrationMethods, RegistryIndex, RegistryIndexStore, Registry } from '@travetto/registry';
|
|
2
|
+
import { AppError, castKey, castTo, Class, classConstruct, getParentClass } from '@travetto/runtime';
|
|
3
3
|
|
|
4
4
|
import { SchemaFieldConfig, SchemaClassConfig } from './types.ts';
|
|
5
|
-
import { SchemaRegistryAdapter } from './registry-adapter.ts';
|
|
6
|
-
import { SchemaChangeListener } from './changes.ts';
|
|
5
|
+
import { SchemaDiscriminatedInfo, SchemaRegistryAdapter } from './registry-adapter.ts';
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* Schema registry index for managing schema configurations across classes
|
|
@@ -20,7 +19,7 @@ export class SchemaRegistryIndex implements RegistryIndex {
|
|
|
20
19
|
return this.#instance.store.get(cls).get();
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
static getDiscriminatedConfig<T>(cls: Class<T>):
|
|
22
|
+
static getDiscriminatedConfig<T>(cls: Class<T>): SchemaDiscriminatedInfo | undefined {
|
|
24
23
|
return this.#instance.store.get(cls).getDiscriminatedConfig();
|
|
25
24
|
}
|
|
26
25
|
|
|
@@ -28,10 +27,6 @@ export class SchemaRegistryIndex implements RegistryIndex {
|
|
|
28
27
|
return this.#instance.store.has(cls);
|
|
29
28
|
}
|
|
30
29
|
|
|
31
|
-
static getClassById(classId: string): Class {
|
|
32
|
-
return this.#instance.store.getClassById(classId);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
30
|
static getDiscriminatedTypes(cls: Class): string[] | undefined {
|
|
36
31
|
return this.#instance.getDiscriminatedTypes(cls);
|
|
37
32
|
}
|
|
@@ -68,6 +63,8 @@ export class SchemaRegistryIndex implements RegistryIndex {
|
|
|
68
63
|
#baseSchema = new Map<Class, Class>();
|
|
69
64
|
#byDiscriminatedTypes = new Map<Class, Map<string, Class>>();
|
|
70
65
|
|
|
66
|
+
/** @private */ constructor(source: unknown) { Registry.validateConstructor(source); }
|
|
67
|
+
|
|
71
68
|
/**
|
|
72
69
|
* Register discriminated types for a class
|
|
73
70
|
*/
|
|
@@ -84,40 +81,7 @@ export class SchemaRegistryIndex implements RegistryIndex {
|
|
|
84
81
|
this.#byDiscriminatedTypes.get(base)!.set(config.discriminatedType, cls);
|
|
85
82
|
}
|
|
86
83
|
|
|
87
|
-
|
|
88
|
-
Util.queueMacroTask().then(() => {
|
|
89
|
-
SchemaChangeListener.emitFieldChanges({
|
|
90
|
-
type: 'changed',
|
|
91
|
-
current: this.getClassConfig(event.current),
|
|
92
|
-
previous: this.getClassConfig(event.previous)
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
#onRemoving(event: ChangeEvent<Class> & { type: 'removing' }): void {
|
|
98
|
-
SchemaChangeListener.clearSchemaDependency(event.previous);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
#onAdded(event: ChangeEvent<Class> & { type: 'added' }): void {
|
|
102
|
-
Util.queueMacroTask().then(() => {
|
|
103
|
-
SchemaChangeListener.emitFieldChanges({
|
|
104
|
-
type: 'added',
|
|
105
|
-
current: this.getClassConfig(event.current)
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
process(events: ChangeEvent<Class>[]): void {
|
|
111
|
-
for (const event of events) {
|
|
112
|
-
if (event.type === 'changed') {
|
|
113
|
-
this.#onChanged(event);
|
|
114
|
-
} else if (event.type === 'removing') {
|
|
115
|
-
this.#onRemoving(event);
|
|
116
|
-
} else if (event.type === 'added') {
|
|
117
|
-
this.#onAdded(event);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
84
|
+
beforeChangeSetComplete(): void {
|
|
121
85
|
// Rebuild indices after every "process" batch
|
|
122
86
|
this.#byDiscriminatedTypes.clear();
|
|
123
87
|
for (const cls of this.store.getClasses()) {
|
|
@@ -174,25 +138,6 @@ export class SchemaRegistryIndex implements RegistryIndex {
|
|
|
174
138
|
}
|
|
175
139
|
}
|
|
176
140
|
|
|
177
|
-
/**
|
|
178
|
-
* Track changes to schemas, and track the dependent changes
|
|
179
|
-
* @param cls The root class of the hierarchy
|
|
180
|
-
* @param current The new class
|
|
181
|
-
* @param path The path within the object hierarchy
|
|
182
|
-
*/
|
|
183
|
-
trackSchemaDependencies(cls: Class, current: Class = cls, path: SchemaFieldConfig[] = []): void {
|
|
184
|
-
const config = this.getClassConfig(current);
|
|
185
|
-
|
|
186
|
-
SchemaChangeListener.trackSchemaDependency(current, cls, path, this.getClassConfig(cls));
|
|
187
|
-
|
|
188
|
-
// Read children
|
|
189
|
-
for (const field of Object.values(config.fields)) {
|
|
190
|
-
if (SchemaRegistryIndex.has(field.type) && field.type !== cls) {
|
|
191
|
-
this.trackSchemaDependencies(cls, field.type, [...path, field]);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
141
|
/**
|
|
197
142
|
* Visit fields recursively
|
|
198
143
|
*/
|
package/src/service/types.ts
CHANGED
|
@@ -36,6 +36,10 @@ export type SchemaBasicType = {
|
|
|
36
36
|
* Basic schema configuration
|
|
37
37
|
*/
|
|
38
38
|
export interface SchemaCoreConfig {
|
|
39
|
+
/**
|
|
40
|
+
* Schema class
|
|
41
|
+
*/
|
|
42
|
+
class: Class;
|
|
39
43
|
/**
|
|
40
44
|
* Description
|
|
41
45
|
*/
|
|
@@ -90,10 +94,6 @@ export interface SchemaFieldMap {
|
|
|
90
94
|
* Class configuration
|
|
91
95
|
*/
|
|
92
96
|
export interface SchemaClassConfig extends SchemaCoreConfig {
|
|
93
|
-
/**
|
|
94
|
-
* Target class
|
|
95
|
-
*/
|
|
96
|
-
class: Class;
|
|
97
97
|
/**
|
|
98
98
|
* List of all views
|
|
99
99
|
*/
|
|
@@ -144,10 +144,6 @@ export interface SchemaInputConfig extends SchemaCoreConfig, SchemaBasicType {
|
|
|
144
144
|
* Key name for validation when dealing with complex types
|
|
145
145
|
*/
|
|
146
146
|
view?: string;
|
|
147
|
-
/**
|
|
148
|
-
* Owner class
|
|
149
|
-
*/
|
|
150
|
-
owner: Class;
|
|
151
147
|
/**
|
|
152
148
|
* List of aliases
|
|
153
149
|
*/
|
|
@@ -167,23 +163,23 @@ export interface SchemaInputConfig extends SchemaCoreConfig, SchemaBasicType {
|
|
|
167
163
|
/**
|
|
168
164
|
* Does the field expect a match
|
|
169
165
|
*/
|
|
170
|
-
match?: {
|
|
166
|
+
match?: { regex: RegExp, message?: string, template?: TemplateLiteral };
|
|
171
167
|
/**
|
|
172
168
|
* Minimum value configuration
|
|
173
169
|
*/
|
|
174
|
-
min?: {
|
|
170
|
+
min?: { limit: number | Date, message?: string };
|
|
175
171
|
/**
|
|
176
172
|
* Maximum value configuration
|
|
177
173
|
*/
|
|
178
|
-
max?: {
|
|
174
|
+
max?: { limit: number | Date, message?: string };
|
|
179
175
|
/**
|
|
180
176
|
* Minimum length configuration
|
|
181
177
|
*/
|
|
182
|
-
minlength?: {
|
|
178
|
+
minlength?: { limit: number, message?: string };
|
|
183
179
|
/**
|
|
184
180
|
* Maximum length configuration
|
|
185
181
|
*/
|
|
186
|
-
maxlength?: {
|
|
182
|
+
maxlength?: { limit: number, message?: string };
|
|
187
183
|
/**
|
|
188
184
|
* Enumerated values
|
|
189
185
|
*/
|
package/src/types.ts
CHANGED
package/src/validate/messages.ts
CHANGED
|
@@ -5,14 +5,14 @@ export const Messages = new Map<string, string>(Object.entries({
|
|
|
5
5
|
default: '{path} is not valid',
|
|
6
6
|
type: '{path} is not a valid {type}',
|
|
7
7
|
required: '{path} is required',
|
|
8
|
-
minlength: '{path} is not long enough ({
|
|
9
|
-
maxlength: '{path} is too long ({
|
|
10
|
-
match: '{path} should match {
|
|
11
|
-
min: '{path} is less than ({
|
|
12
|
-
max: '{path} is greater than ({
|
|
13
|
-
telephone: '{path} is not a valid phone number',
|
|
14
|
-
url: '{path} is not a valid url',
|
|
15
|
-
simpleName: '{path} is not a proper name',
|
|
16
|
-
postalCode: '{path} is not a valid postal code',
|
|
17
|
-
email: '{path} is not a valid email address'
|
|
8
|
+
minlength: '{path} is not long enough ({limit})',
|
|
9
|
+
maxlength: '{path} is too long ({limit})',
|
|
10
|
+
match: '{path} should match {regex}',
|
|
11
|
+
min: '{path} is less than ({limit})',
|
|
12
|
+
max: '{path} is greater than ({limit})',
|
|
13
|
+
'[[:telephone:]]': '{path} is not a valid phone number',
|
|
14
|
+
'[[:url:]]': '{path} is not a valid url',
|
|
15
|
+
'[[:simpleName:]]': '{path} is not a proper name',
|
|
16
|
+
'[[:postalCode:]]': '{path} is not a valid postal code',
|
|
17
|
+
'[[:email:]]': '{path} is not a valid email address'
|
|
18
18
|
}));
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { TypedObject } from '@travetto/runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* List of common regular expressions for fields
|
|
5
|
+
*/
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
7
|
+
export const [CommonRegex, CommonRegexToName] = (() => {
|
|
8
|
+
const regexToName = new Map<RegExp, string>();
|
|
9
|
+
const regexes = {
|
|
10
|
+
email: /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/,
|
|
11
|
+
telephone: /^(\+?\d{1,3}(\s*-?\s*|\s+))?((\(\d{3}\))|\d{3})(\s*|-|[.])(\d{3})(\s*|-|[.])(\d{4})(\s+(x|ext[.]?)\s*\d+)?$/,
|
|
12
|
+
url: /^(https?|ftp):\/\/[^\s/$.?#].[^\s]*$/,
|
|
13
|
+
simpleName: /^([a-zA-Z\u0080-\u024F]{0,100}(?:. |-| |')){0,10}[a-zA-Z\u0080-\u024F]+$/,
|
|
14
|
+
postalCode: /^\d{5}(?:[-\s]\d{4})?$/
|
|
15
|
+
};
|
|
16
|
+
// Rebind regexes
|
|
17
|
+
for (const key of TypedObject.keys(regexes)) {
|
|
18
|
+
const name = `[[:${key}:]]`;
|
|
19
|
+
regexToName.set(regexes[key], name);
|
|
20
|
+
}
|
|
21
|
+
return [regexes, regexToName];
|
|
22
|
+
})();
|
package/src/validate/types.ts
CHANGED
|
@@ -25,11 +25,11 @@ export interface ValidationError {
|
|
|
25
25
|
/**
|
|
26
26
|
* Regular expression to match
|
|
27
27
|
*/
|
|
28
|
-
|
|
28
|
+
regex?: string;
|
|
29
29
|
/**
|
|
30
30
|
* Number to compare against
|
|
31
31
|
*/
|
|
32
|
-
|
|
32
|
+
limit?: number | Date;
|
|
33
33
|
/**
|
|
34
34
|
* The type of the field
|
|
35
35
|
*/
|
|
@@ -63,11 +63,11 @@ export interface ValidationResult {
|
|
|
63
63
|
/**
|
|
64
64
|
* Potential regular expression for the result
|
|
65
65
|
*/
|
|
66
|
-
|
|
66
|
+
regex?: RegExp;
|
|
67
67
|
/**
|
|
68
68
|
* Number to compare against
|
|
69
69
|
*/
|
|
70
|
-
|
|
70
|
+
limit?: number | Date;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
type OrPromise<T> = T | Promise<T>;
|
|
@@ -5,7 +5,7 @@ import { ValidationError, ValidationKindCore, ValidationResult } from './types.t
|
|
|
5
5
|
import { Messages } from './messages.ts';
|
|
6
6
|
import { isValidationError, TypeMismatchError, ValidationResultError } from './error.ts';
|
|
7
7
|
import { DataUtil } from '../data.ts';
|
|
8
|
-
import {
|
|
8
|
+
import { CommonRegexToName } from './regex.ts';
|
|
9
9
|
import { SchemaRegistryIndex } from '../service/registry-index.ts';
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -111,7 +111,7 @@ export class SchemaValidator {
|
|
|
111
111
|
(input.type === Date ? Date.parse(value) : parseInt(value, 10)) :
|
|
112
112
|
(value instanceof Date ? value.getTime() : value);
|
|
113
113
|
|
|
114
|
-
const boundary = (typeof config.
|
|
114
|
+
const boundary = (typeof config.limit === 'number' ? config.limit : config.limit.getTime());
|
|
115
115
|
return key === 'min' ? parsed < boundary : parsed > boundary;
|
|
116
116
|
}
|
|
117
117
|
|
|
@@ -144,15 +144,15 @@ export class SchemaValidator {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
if (input.match && !input.match.
|
|
147
|
+
if (input.match && !input.match.regex.test(`${value}`)) {
|
|
148
148
|
criteria.push(['match', input.match]);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
if (input.minlength && `${value}`.length < input.minlength.
|
|
151
|
+
if (input.minlength && `${value}`.length < input.minlength.limit) {
|
|
152
152
|
criteria.push(['minlength', input.minlength]);
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
if (input.maxlength && `${value}`.length > input.maxlength.
|
|
155
|
+
if (input.maxlength && `${value}`.length > input.maxlength.limit) {
|
|
156
156
|
criteria.push(['maxlength', input.maxlength]);
|
|
157
157
|
}
|
|
158
158
|
|
|
@@ -189,17 +189,17 @@ export class SchemaValidator {
|
|
|
189
189
|
kind: result.kind,
|
|
190
190
|
value: result.value,
|
|
191
191
|
message: '',
|
|
192
|
-
|
|
192
|
+
regex: CommonRegexToName.get(result.regex!) ?? result.regex?.source ?? '',
|
|
193
193
|
path,
|
|
194
194
|
type: (typeof result.type === 'function' ? result.type.name : result.type)
|
|
195
195
|
};
|
|
196
196
|
|
|
197
|
-
if (!error.
|
|
198
|
-
delete error.
|
|
197
|
+
if (!error.regex) {
|
|
198
|
+
delete error.regex;
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
const msg = result.message ?? (
|
|
202
|
-
Messages.get(error.
|
|
202
|
+
Messages.get(error.regex ?? '') ??
|
|
203
203
|
Messages.get(error.kind) ??
|
|
204
204
|
Messages.get('default')!
|
|
205
205
|
);
|
|
@@ -171,11 +171,11 @@ class ${uniqueId} extends ${type.mappedClassName} {
|
|
|
171
171
|
};
|
|
172
172
|
}
|
|
173
173
|
} else if (primaryExpr.key === 'template' && primaryExpr.template) {
|
|
174
|
-
const
|
|
174
|
+
const regex = LiteralUtil.templateLiteralToRegex(primaryExpr.template);
|
|
175
175
|
attrs.match = {
|
|
176
|
-
|
|
176
|
+
regex: new RegExp(regex),
|
|
177
177
|
template: primaryExpr.template,
|
|
178
|
-
message: `{path} must match "${
|
|
178
|
+
message: `{path} must match "${regex}"`
|
|
179
179
|
};
|
|
180
180
|
}
|
|
181
181
|
|
package/src/service/changes.ts
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from 'node:events';
|
|
2
|
-
|
|
3
|
-
import { Class } from '@travetto/runtime';
|
|
4
|
-
import { ChangeEvent } from '@travetto/registry';
|
|
5
|
-
|
|
6
|
-
import { SchemaFieldConfig, SchemaClassConfig } from './types.ts';
|
|
7
|
-
|
|
8
|
-
interface FieldMapping {
|
|
9
|
-
path: SchemaFieldConfig[];
|
|
10
|
-
config: SchemaClassConfig;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface FieldChangeEvent {
|
|
14
|
-
cls: Class;
|
|
15
|
-
changes: ChangeEvent<SchemaFieldConfig>[];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface SubSchemaChange {
|
|
19
|
-
path: SchemaFieldConfig[];
|
|
20
|
-
fields: ChangeEvent<SchemaFieldConfig>[];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface SchemaChange {
|
|
24
|
-
config: SchemaClassConfig;
|
|
25
|
-
subs: SubSchemaChange[];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface SchemaChangeEvent {
|
|
29
|
-
cls: Class;
|
|
30
|
-
change: SchemaChange;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Schema change listener. Handles all changes that occur via the SchemaRegistryIndex
|
|
35
|
-
*/
|
|
36
|
-
class $SchemaChangeListener {
|
|
37
|
-
|
|
38
|
-
#emitter = new EventEmitter();
|
|
39
|
-
#mapping = new Map<string, Map<string, FieldMapping>>();
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* On schema change, emit the change event for the whole schema
|
|
43
|
-
* @param handler The function to call on schema change
|
|
44
|
-
*/
|
|
45
|
-
onSchemaChange(handler: (event: SchemaChangeEvent) => void): void {
|
|
46
|
-
this.#emitter.on('schema', handler);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* On schema field change, emit the change event for the whole schema
|
|
51
|
-
* @param handler The function to call on schema field change
|
|
52
|
-
*/
|
|
53
|
-
onFieldChange(handler: (event: FieldChangeEvent) => void): void {
|
|
54
|
-
this.#emitter.on('field', handler);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Clear dependency mappings for a given class
|
|
59
|
-
*/
|
|
60
|
-
clearSchemaDependency(cls: Class): void {
|
|
61
|
-
this.#mapping.delete(cls.Ⲑid);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Track a specific class for dependencies
|
|
66
|
-
* @param cls The target class
|
|
67
|
-
* @param parent The parent class
|
|
68
|
-
* @param path The path within the object hierarchy to arrive at the class
|
|
69
|
-
* @param config The configuration or the class
|
|
70
|
-
*/
|
|
71
|
-
trackSchemaDependency(cls: Class, parent: Class, path: SchemaFieldConfig[], config: SchemaClassConfig): void {
|
|
72
|
-
const idValue = cls.Ⲑid;
|
|
73
|
-
if (!this.#mapping.has(idValue)) {
|
|
74
|
-
this.#mapping.set(idValue, new Map());
|
|
75
|
-
}
|
|
76
|
-
this.#mapping.get(idValue)!.set(parent.Ⲑid, { path, config });
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Emit changes to the schema
|
|
81
|
-
* @param cls The class of the event
|
|
82
|
-
* @param changes The changes to send
|
|
83
|
-
*/
|
|
84
|
-
emitSchemaChanges({ cls, changes }: FieldChangeEvent): void {
|
|
85
|
-
const updates = new Map<string, SchemaChange>();
|
|
86
|
-
const clsId = cls.Ⲑid;
|
|
87
|
-
|
|
88
|
-
if (this.#mapping.has(clsId)) {
|
|
89
|
-
const dependencies = this.#mapping.get(clsId)!;
|
|
90
|
-
for (const dependencyClsId of dependencies.keys()) {
|
|
91
|
-
if (!updates.has(dependencyClsId)) {
|
|
92
|
-
updates.set(dependencyClsId, { config: dependencies.get(dependencyClsId)!.config, subs: [] });
|
|
93
|
-
}
|
|
94
|
-
const childDependency = dependencies.get(dependencyClsId)!;
|
|
95
|
-
updates.get(dependencyClsId)!.subs.push({ path: [...childDependency.path], fields: changes });
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
for (const key of updates.keys()) {
|
|
100
|
-
this.#emitter.emit('schema', { cls: updates.get(key)!.config.class, change: updates.get(key)! });
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Emit field level changes in the schema
|
|
106
|
-
* @param previous The previous class config
|
|
107
|
-
* @param current The current class config
|
|
108
|
-
*/
|
|
109
|
-
emitFieldChanges(event: ChangeEvent<SchemaClassConfig>): void {
|
|
110
|
-
const previous = 'previous' in event ? event.previous : undefined;
|
|
111
|
-
const current = 'current' in event ? event.current : undefined;
|
|
112
|
-
|
|
113
|
-
const previousFields = new Set(Object.keys(previous?.fields ?? {}));
|
|
114
|
-
const currentFields = new Set(Object.keys(current?.fields ?? {}));
|
|
115
|
-
|
|
116
|
-
const changes: ChangeEvent<SchemaFieldConfig>[] = [];
|
|
117
|
-
|
|
118
|
-
for (const field of currentFields) {
|
|
119
|
-
if (!previousFields.has(field) && current) {
|
|
120
|
-
changes.push({ current: current.fields[field], type: 'added' });
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
for (const field of previousFields) {
|
|
125
|
-
if (!currentFields.has(field) && previous) {
|
|
126
|
-
changes.push({ previous: previous.fields[field], type: 'removing' });
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Handle class references changing, but keeping same id
|
|
131
|
-
const compareTypes = (a: Class, b: Class): boolean => a.Ⲑid ? a.Ⲑid === b.Ⲑid : a === b;
|
|
132
|
-
|
|
133
|
-
for (const field of currentFields) {
|
|
134
|
-
if (previousFields.has(field) && previous && current) {
|
|
135
|
-
const prevSchema = previous.fields[field];
|
|
136
|
-
const currSchema = current.fields[field];
|
|
137
|
-
if (
|
|
138
|
-
JSON.stringify(prevSchema) !== JSON.stringify(currSchema) ||
|
|
139
|
-
!compareTypes(prevSchema.type, currSchema.type)
|
|
140
|
-
) {
|
|
141
|
-
changes.push({ previous: previous.fields[field], current: current.fields[field], type: 'changed' });
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Send field changes
|
|
147
|
-
this.#emitter.emit('field', { cls: current!.class, changes });
|
|
148
|
-
this.emitSchemaChanges({ cls: current!.class, changes });
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export const SchemaChangeListener = new $SchemaChangeListener();
|
package/src/validate/regexp.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { TypedObject } from '@travetto/runtime';
|
|
2
|
-
import { Messages } from './messages.ts';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* List of common regular expressions for fields
|
|
6
|
-
*/
|
|
7
|
-
export const CommonRegExp = {
|
|
8
|
-
email: /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/,
|
|
9
|
-
telephone: /^(\+?\d{1,3}(\s*-?\s*|\s+))?((\(\d{3}\))|\d{3})(\s*|-|[.])(\d{3})(\s*|-|[.])(\d{4})(\s+(x|ext[.]?)\s*\d+)?$/,
|
|
10
|
-
url: /^(https?|ftp):\/\/[^\s/$.?#].[^\s]*$/,
|
|
11
|
-
simpleName: /^([a-zA-Z\u0080-\u024F]{0,100}(?:. |-| |')){0,10}[a-zA-Z\u0080-\u024F]+$/,
|
|
12
|
-
postalCode: /^\d{5}(?:[-\s]\d{4})?$/
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export const CommonRegExpToName = new Map<RegExp, string>();
|
|
16
|
-
|
|
17
|
-
// Rebind regexes
|
|
18
|
-
for (const key of TypedObject.keys(CommonRegExp)) {
|
|
19
|
-
const name = `[[:${key}:]]`;
|
|
20
|
-
CommonRegExpToName.set(CommonRegExp[key], name);
|
|
21
|
-
Messages.set(name, Messages.get(key)!);
|
|
22
|
-
}
|