@travetto/schema 8.0.0-alpha.21 → 8.0.0-alpha.23
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 +37 -38
- package/__index__.ts +13 -12
- package/package.json +3 -3
- package/src/bind-util.ts +23 -19
- package/src/data.ts +55 -38
- package/src/decorator/common.ts +3 -3
- package/src/decorator/field.ts +15 -7
- package/src/decorator/input.ts +63 -23
- package/src/decorator/method.ts +2 -2
- package/src/decorator/schema.ts +5 -4
- package/src/internal/types.ts +3 -3
- package/src/name.ts +1 -2
- package/src/service/registry-adapter.ts +69 -55
- package/src/service/registry-index.ts +19 -13
- package/src/service/types.ts +9 -9
- package/src/type-config.ts +3 -3
- package/src/types.ts +2 -2
- package/src/validate/error.ts +2 -1
- package/src/validate/messages.ts +17 -15
- package/src/validate/regex.ts +1 -2
- package/src/validate/validator.ts +44 -46
- package/support/transformer/util.ts +133 -92
- package/support/transformer.schema.ts +20 -23
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ npm install @travetto/schema
|
|
|
13
13
|
yarn add @travetto/schema
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
This module's purpose is to allow for proper declaration and validation of data types, in the course of running a program.
|
|
16
|
+
This module's purpose is to allow for proper declaration and validation of data types, in the course of running a program. The framework defined here, is leveraged in the [Configuration](https://github.com/travetto/travetto/tree/main/module/config#readme "Configuration support"), [Command Line Interface](https://github.com/travetto/travetto/tree/main/module/cli#readme "CLI infrastructure for Travetto framework"), [Web API](https://github.com/travetto/travetto/tree/main/module/web#readme "Declarative support for creating Web Applications"), [OpenAPI Specification](https://github.com/travetto/travetto/tree/main/module/openapi#readme "OpenAPI integration support for the Travetto framework") and [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") modules. The schema is the backbone of all data transfer, as it helps to provide validation on correctness of input, whether it is a web request, command line inputs, or a configuration file.
|
|
17
17
|
|
|
18
18
|
This module provides a mechanism for registering classes and field level information as well the ability to apply that information at runtime.
|
|
19
19
|
|
|
@@ -63,29 +63,29 @@ User:
|
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
### Fields
|
|
66
|
-
This schema provides a powerful base for data binding and validation at runtime.
|
|
67
|
-
* [@Field](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
68
|
-
* [@Required](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#
|
|
69
|
-
* [@Enum](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#
|
|
70
|
-
* [@Match](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#
|
|
71
|
-
* [@MinLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#
|
|
72
|
-
* [@MaxLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#
|
|
73
|
-
* [@Min](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#
|
|
74
|
-
* [@Max](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#
|
|
75
|
-
* [@Email](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#
|
|
76
|
-
* [@Telephone](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#
|
|
77
|
-
* [@Url](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#
|
|
66
|
+
This schema provides a powerful base for data binding and validation at runtime. Additionally there may be types that cannot be detected, or some information that the programmer would like to override. Below are the supported field decorators:
|
|
67
|
+
* [@Field](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L26) defines a field that will be serialized.
|
|
68
|
+
* [@Required](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L67) defines a that field should be required
|
|
69
|
+
* [@Enum](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L78) defines the allowable values that a field can have
|
|
70
|
+
* [@Match](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L108) defines a regular expression that the field value should match
|
|
71
|
+
* [@MinLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L119) enforces min length of a string
|
|
72
|
+
* [@MaxLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L130) enforces max length of a string
|
|
73
|
+
* [@Min](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L119) enforces min value for a date or a number
|
|
74
|
+
* [@Max](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L130) enforces max value for a date or a number
|
|
75
|
+
* [@Email](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L162) ensures string field matches basic email regex
|
|
76
|
+
* [@Telephone](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L172) ensures string field matches basic telephone regex
|
|
77
|
+
* [@Url](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L182) ensures string field matches basic url regex
|
|
78
78
|
* [@Ignore](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/common.ts#L41) exclude from auto schema registration
|
|
79
|
-
* [@Integer](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#
|
|
80
|
-
* [@Float](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#
|
|
81
|
-
* [@Currency](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#
|
|
82
|
-
* [@Text](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#
|
|
83
|
-
* [@LongText](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#
|
|
84
|
-
* [@Readonly](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
85
|
-
* [@Writeonly](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
86
|
-
* [@Secret](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
87
|
-
* [@Specifier](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#
|
|
88
|
-
* [@DiscriminatorField](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#
|
|
79
|
+
* [@Integer](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L202) ensures number passed in is only a whole number
|
|
80
|
+
* [@Float](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L211) ensures number passed in allows fractional values
|
|
81
|
+
* [@Currency](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L229) provides support for standard currency
|
|
82
|
+
* [@Text](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L88) 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/input.ts#L97) same as text, but expects longer form content
|
|
84
|
+
* [@Readonly](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L44) 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#L35) defines a that field should not be exported in serialization, but that it can be bound to
|
|
86
|
+
* [@Secret](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L54) marks a field as being sensitive. This is used by certain logging activities to ensure sensitive information is not logged out.
|
|
87
|
+
* [@Specifier](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L239) attributes additional specifiers to a field, allowing for more specification beyond just the field's type.
|
|
88
|
+
* [@DiscriminatorField](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L248) allows for promoting a given field as the owner of the sub type discriminator.
|
|
89
89
|
|
|
90
90
|
Additionally, schemas can be nested to form more complex data structures that are able to bound and validated.
|
|
91
91
|
|
|
@@ -96,7 +96,7 @@ Just like the class, all fields can be defined with
|
|
|
96
96
|
And similarly, the `description` will be picked up from the [JSDoc](http://usejsdoc.org/about-getting-started.html) comments, and additionally all fields can be set using the [@Describe](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/common.ts#L12) decorator.
|
|
97
97
|
|
|
98
98
|
### Parameters
|
|
99
|
-
Parameters are available in certain scenarios (e.g. [Web API](https://github.com/travetto/travetto/tree/main/module/web#readme "Declarative support for creating Web Applications") endpoints and [Command Line Interface](https://github.com/travetto/travetto/tree/main/module/cli#readme "CLI infrastructure for Travetto framework") main methods).
|
|
99
|
+
Parameters are available in certain scenarios (e.g. [Web API](https://github.com/travetto/travetto/tree/main/module/web#readme "Declarative support for creating Web Applications") endpoints and [Command Line Interface](https://github.com/travetto/travetto/tree/main/module/cli#readme "CLI infrastructure for Travetto framework") main methods). In these scenarios, all of the field decorators are valid, but need to be called slightly differently to pass the typechecker. The simple solution is to use the `Arg` field of the decorator to convince Typescript its the correct type.
|
|
100
100
|
|
|
101
101
|
**Code: Sample Parameter Usage**
|
|
102
102
|
```typescript
|
|
@@ -119,7 +119,7 @@ Binding is a very simple operation, as it takes in a class registered as as [@Sc
|
|
|
119
119
|
|
|
120
120
|
**Code: Sub Schemas via Address**
|
|
121
121
|
```typescript
|
|
122
|
-
import {
|
|
122
|
+
import { Integer, Schema } from '@travetto/schema';
|
|
123
123
|
|
|
124
124
|
@Schema()
|
|
125
125
|
export class Address {
|
|
@@ -173,7 +173,7 @@ Validation is very similar to binding, but instead of attempting to assign value
|
|
|
173
173
|
|
|
174
174
|
**Code: Sub Schemas via Address**
|
|
175
175
|
```typescript
|
|
176
|
-
import {
|
|
176
|
+
import { Integer, Schema } from '@travetto/schema';
|
|
177
177
|
|
|
178
178
|
@Schema()
|
|
179
179
|
export class Address {
|
|
@@ -198,7 +198,6 @@ import { SchemaValidator } from '@travetto/schema';
|
|
|
198
198
|
import { Person } from './person.ts';
|
|
199
199
|
|
|
200
200
|
export async function validate(): Promise<void> {
|
|
201
|
-
|
|
202
201
|
const person = Person.from({
|
|
203
202
|
name: 'Test',
|
|
204
203
|
age: 'abc',
|
|
@@ -243,11 +242,11 @@ Validation Failed {
|
|
|
243
242
|
```
|
|
244
243
|
|
|
245
244
|
### Custom Validators
|
|
246
|
-
Within the schema framework, it is possible to add custom validators class level.
|
|
245
|
+
Within the schema framework, it is possible to add custom validators class level. This allows for more flexibility when dealing with specific situations (e.g. password requirements or ensuring two fields match)
|
|
247
246
|
|
|
248
247
|
**Code: Password Validator**
|
|
249
248
|
```typescript
|
|
250
|
-
import { Schema,
|
|
249
|
+
import { Schema, type ValidationError, Validator } from '@travetto/schema';
|
|
251
250
|
|
|
252
251
|
const passwordValidator = (user: User): ValidationError | undefined => {
|
|
253
252
|
const password = user.password;
|
|
@@ -270,7 +269,7 @@ class User {
|
|
|
270
269
|
}
|
|
271
270
|
```
|
|
272
271
|
|
|
273
|
-
When the validator is executed, it has access to the entire object, and you can check any of the values.
|
|
272
|
+
When the validator is executed, it has access to the entire object, and you can check any of the values. The validator expects an object of a specific structure if you are looking to indicate an error has occurred.
|
|
274
273
|
|
|
275
274
|
**Code: Validation Error Structure**
|
|
276
275
|
```typescript
|
|
@@ -316,7 +315,7 @@ When working with the schema, the basic types are easily understood, but some of
|
|
|
316
315
|
To that end, the module supports two concepts:
|
|
317
316
|
|
|
318
317
|
### Type Adapters
|
|
319
|
-
This feature is meant to allow for simple Typescript types to be able to be backed by a proper class.
|
|
318
|
+
This feature is meant to allow for simple Typescript types to be able to be backed by a proper class. This is because all of the typescript type information disappears at runtime, and so only concrete types (like classes) remain. An example of this, can be found with how the [Data Model Querying](https://github.com/travetto/travetto/tree/main/module/model-query#readme "Datastore abstraction for advanced query support.") module handles geo data.
|
|
320
319
|
|
|
321
320
|
**Code: Point Contract**
|
|
322
321
|
```typescript
|
|
@@ -352,17 +351,17 @@ function bindPoint(input: unknown): [number, number] | typeof InvalidSymbol | un
|
|
|
352
351
|
*/
|
|
353
352
|
function validatePoint(input: unknown): 'type' | undefined {
|
|
354
353
|
const bound = bindPoint(input);
|
|
355
|
-
return bound !== InvalidSymbol && bound && !isNaN(bound[0]) && !isNaN(bound[1]) ? undefined : 'type';
|
|
354
|
+
return bound !== InvalidSymbol && bound && !Number.isNaN(bound[0]) && !Number.isNaN(bound[1]) ? undefined : 'type';
|
|
356
355
|
}
|
|
357
356
|
|
|
358
357
|
/**
|
|
359
358
|
* Point Contract
|
|
360
359
|
*/
|
|
361
|
-
export class PointContract {
|
|
360
|
+
export class PointContract {}
|
|
362
361
|
|
|
363
362
|
SchemaTypeUtil.setSchemaTypeConfig(PointContract, {
|
|
364
363
|
validate: validatePoint,
|
|
365
|
-
bind: bindPoint
|
|
364
|
+
bind: bindPoint
|
|
366
365
|
});
|
|
367
366
|
Object.defineProperty(PointContract, 'name', { value: 'Point' });
|
|
368
367
|
```
|
|
@@ -373,7 +372,7 @@ What you can see here is that the `Point` type is now backed by a class that sup
|
|
|
373
372
|
|
|
374
373
|
**Code: Simple Custom Type Usage**
|
|
375
374
|
```typescript
|
|
376
|
-
import {
|
|
375
|
+
import { type Point, Schema } from '@travetto/schema';
|
|
377
376
|
|
|
378
377
|
@Schema()
|
|
379
378
|
export class LocationAware {
|
|
@@ -382,7 +381,7 @@ export class LocationAware {
|
|
|
382
381
|
}
|
|
383
382
|
```
|
|
384
383
|
|
|
385
|
-
All that happens now, is the type is exported, and the class above is able to properly handle point as an `[x, y]` tuple.
|
|
384
|
+
All that happens now, is the type is exported, and the class above is able to properly handle point as an `[x, y]` tuple. All standard binding and validation patterns are supported, and type enforcement will work as expected.
|
|
386
385
|
|
|
387
386
|
**Terminal: Custom Type Validation**
|
|
388
387
|
```bash
|
|
@@ -408,8 +407,8 @@ Validation Failed {
|
|
|
408
407
|
```
|
|
409
408
|
|
|
410
409
|
## Data Utilities
|
|
411
|
-
Data utilities for binding values, and type conversion. Currently [DataUtil](https://github.com/travetto/travetto/tree/main/module/schema/src/data.ts#
|
|
412
|
-
* `deepAssign(a, b, mode ?)` which allows for deep assignment of `b` onto `a`, the `mode` determines how aggressive the assignment is, and how flexible it is.
|
|
410
|
+
Data utilities for binding values, and type conversion. Currently [DataUtil](https://github.com/travetto/travetto/tree/main/module/schema/src/data.ts#L12) includes:
|
|
411
|
+
* `deepAssign(a, b, mode ?)` which allows for deep assignment of `b` onto `a`, the `mode` determines how aggressive the assignment is, and how flexible it is. `mode` can have any of the following values:
|
|
413
412
|
* `loose`, which is the default is the most lenient. It will not error out, and overwrites will always happen
|
|
414
413
|
* `coerce`, will attempt to force values from `b` to fit the types of `a`, and if it can't it will error out
|
|
415
414
|
* `strict`, will error out if the types do not match
|
package/__index__.ts
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {} from './src/global.d.ts';
|
|
2
|
+
|
|
3
|
+
export * from './src/bind-util.ts';
|
|
4
|
+
export * from './src/data.ts';
|
|
5
|
+
export * from './src/decorator/common.ts';
|
|
2
6
|
export * from './src/decorator/field.ts';
|
|
3
7
|
export * from './src/decorator/input.ts';
|
|
4
|
-
export * from './src/decorator/schema.ts';
|
|
5
8
|
export * from './src/decorator/method.ts';
|
|
6
|
-
export * from './src/decorator/
|
|
9
|
+
export * from './src/decorator/schema.ts';
|
|
10
|
+
export * from './src/name.ts';
|
|
11
|
+
export * from './src/service/registry-adapter.ts';
|
|
12
|
+
export * from './src/service/registry-index.ts';
|
|
7
13
|
export * from './src/service/types.ts';
|
|
14
|
+
export * from './src/type-config.ts';
|
|
15
|
+
export * from './src/types.ts';
|
|
16
|
+
export * from './src/validate/error.ts';
|
|
8
17
|
export * from './src/validate/messages.ts';
|
|
9
18
|
export * from './src/validate/regex.ts';
|
|
10
|
-
export * from './src/validate/validator.ts';
|
|
11
|
-
export * from './src/validate/error.ts';
|
|
12
19
|
export * from './src/validate/types.ts';
|
|
13
|
-
export * from './src/
|
|
14
|
-
export * from './src/data.ts';
|
|
15
|
-
export * from './src/name.ts';
|
|
16
|
-
export * from './src/types.ts';
|
|
17
|
-
export * from './src/type-config.ts';
|
|
18
|
-
export * from './src/service/registry-index.ts';
|
|
19
|
-
export * from './src/service/registry-adapter.ts';
|
|
20
|
+
export * from './src/validate/validator.ts';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/schema",
|
|
3
|
-
"version": "8.0.0-alpha.
|
|
3
|
+
"version": "8.0.0-alpha.23",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Data type registry for runtime validation, reflection and binding.",
|
|
6
6
|
"keywords": [
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
"directory": "module/schema"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@travetto/registry": "^8.0.0-alpha.
|
|
31
|
+
"@travetto/registry": "^8.0.0-alpha.21"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@travetto/transformer": "^8.0.0-alpha.
|
|
34
|
+
"@travetto/transformer": "^8.0.0-alpha.15"
|
|
35
35
|
},
|
|
36
36
|
"peerDependenciesMeta": {
|
|
37
37
|
"@travetto/transformer": {
|
package/src/bind-util.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { asFull, type Class, castKey, castTo, classConstruct, TypedObject } from '@travetto/runtime';
|
|
2
2
|
|
|
3
3
|
import { DataUtil } from './data.ts';
|
|
4
|
-
import type { SchemaInputConfig, SchemaParameterConfig, SchemaFieldMap } from './service/types.ts';
|
|
5
4
|
import { SchemaRegistryIndex } from './service/registry-index.ts';
|
|
5
|
+
import type { SchemaFieldMap, SchemaInputConfig, SchemaParameterConfig } from './service/types.ts';
|
|
6
6
|
import { SchemaTypeUtil } from './type-config.ts';
|
|
7
7
|
|
|
8
8
|
type BindConfig = {
|
|
@@ -19,11 +19,10 @@ function isInstance<T>(value: unknown): value is T {
|
|
|
19
19
|
* Utilities for binding objects to schemas
|
|
20
20
|
*/
|
|
21
21
|
export class BindUtil {
|
|
22
|
-
|
|
23
22
|
/**
|
|
24
23
|
* Utility to make a property accessor enumerable at runtime
|
|
25
24
|
*/
|
|
26
|
-
static registerAccessor(instance:
|
|
25
|
+
static registerAccessor(instance: unknown, property: string): void {
|
|
27
26
|
Object.defineProperty(instance, property, {
|
|
28
27
|
...Object.getOwnPropertyDescriptor(Object.getPrototypeOf(instance), property),
|
|
29
28
|
enumerable: true
|
|
@@ -43,9 +42,11 @@ export class BindUtil {
|
|
|
43
42
|
value = DataUtil.coerceType(value, config.type, false);
|
|
44
43
|
|
|
45
44
|
if (config.type === Number && config.precision && typeof value === 'number') {
|
|
46
|
-
if (config.precision[1]) {
|
|
45
|
+
if (config.precision[1]) {
|
|
46
|
+
// Supports decimal
|
|
47
47
|
value = +value.toFixed(config.precision[1]);
|
|
48
|
-
} else {
|
|
48
|
+
} else {
|
|
49
|
+
// 0 digits
|
|
49
50
|
value = Math.trunc(value);
|
|
50
51
|
}
|
|
51
52
|
}
|
|
@@ -72,8 +73,9 @@ export class BindUtil {
|
|
|
72
73
|
const part = parts.shift()!;
|
|
73
74
|
const partArrayIndex = part.indexOf('[') > 0;
|
|
74
75
|
const name = part.split(/[^A-Za-z_0-9]/)[0];
|
|
76
|
+
// biome-ignore lint/complexity/noUselessEscapeInRegex: Consistent escaping for open and close
|
|
75
77
|
const idx = partArrayIndex ? part.split(/[\[\]]/)[1] : '';
|
|
76
|
-
const key = partArrayIndex ? (/^\d+$/.test(idx) ? parseInt(idx, 10) :
|
|
78
|
+
const key = partArrayIndex ? (/^\d+$/.test(idx) ? parseInt(idx, 10) : idx.trim() || undefined) : undefined;
|
|
77
79
|
|
|
78
80
|
if (!(name in sub)) {
|
|
79
81
|
sub[name] = typeof key === 'number' ? [] : {};
|
|
@@ -96,10 +98,11 @@ export class BindUtil {
|
|
|
96
98
|
}
|
|
97
99
|
} else {
|
|
98
100
|
const name = last.split(/[^A-Za-z_0-9]/)[0];
|
|
101
|
+
// biome-ignore lint/complexity/noUselessEscapeInRegex: Consistent escaping for open and close
|
|
99
102
|
const idx = last.split(/[\[\]]/)[1];
|
|
100
103
|
|
|
101
|
-
let key =
|
|
102
|
-
sub[name] ??=
|
|
104
|
+
let key = /^\d+$/.test(idx) ? parseInt(idx, 10) : idx.trim() || undefined;
|
|
105
|
+
sub[name] ??= typeof key === 'string' ? {} : [];
|
|
103
106
|
|
|
104
107
|
const arrSub: Record<string, unknown> & { length: number } = castTo(sub[name]);
|
|
105
108
|
if (key === undefined) {
|
|
@@ -125,10 +128,9 @@ export class BindUtil {
|
|
|
125
128
|
for (const [key, value] of Object.entries(data)) {
|
|
126
129
|
const pre = `${prefix}${key}`;
|
|
127
130
|
if (DataUtil.isPlainObject(value)) {
|
|
128
|
-
Object.assign(out, this.flattenPaths(value, `${pre}.`)
|
|
129
|
-
);
|
|
131
|
+
Object.assign(out, this.flattenPaths(value, `${pre}.`));
|
|
130
132
|
} else if (Array.isArray(value)) {
|
|
131
|
-
for (let i = 0; i < value.length; i
|
|
133
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
132
134
|
const element = value[i];
|
|
133
135
|
if (DataUtil.isPlainObject(element)) {
|
|
134
136
|
Object.assign(out, this.flattenPaths(element, `${pre}[${i}].`));
|
|
@@ -162,9 +164,10 @@ export class BindUtil {
|
|
|
162
164
|
const resolvedCls = SchemaRegistryIndex.resolveInstanceType<T>(cls, asFull<T>(data));
|
|
163
165
|
const instance = classConstruct<T & { type?: string }>(resolvedCls);
|
|
164
166
|
|
|
165
|
-
for (const key of TypedObject.keys(instance)) {
|
|
167
|
+
for (const key of TypedObject.keys(instance)) {
|
|
168
|
+
// Do not retain undefined fields
|
|
166
169
|
const descriptor = Object.getOwnPropertyDescriptor(instance, key);
|
|
167
|
-
if (descriptor?.writable === false) {
|
|
170
|
+
if (descriptor?.writable === false || descriptor?.get) {
|
|
168
171
|
continue;
|
|
169
172
|
}
|
|
170
173
|
if (instance[key] === undefined) {
|
|
@@ -189,7 +192,7 @@ export class BindUtil {
|
|
|
189
192
|
const view = config.view; // Does not convey
|
|
190
193
|
delete config.view;
|
|
191
194
|
|
|
192
|
-
if (
|
|
195
|
+
if (data && isInstance<T>(data)) {
|
|
193
196
|
const adapter = SchemaRegistryIndex.get(cls);
|
|
194
197
|
const schemaConfig = adapter.get();
|
|
195
198
|
|
|
@@ -208,14 +211,14 @@ export class BindUtil {
|
|
|
208
211
|
}
|
|
209
212
|
|
|
210
213
|
for (const [schemaFieldName, field] of Object.entries(schema)) {
|
|
211
|
-
let inboundField: string | undefined
|
|
214
|
+
let inboundField: string | undefined;
|
|
212
215
|
if (field.access === 'readonly' || config.filterInput?.(field) === false) {
|
|
213
216
|
continue; // Skip trying to write readonly fields
|
|
214
217
|
}
|
|
215
218
|
if (schemaFieldName in data) {
|
|
216
219
|
inboundField = schemaFieldName;
|
|
217
220
|
} else if (field.aliases) {
|
|
218
|
-
for (const aliasedField of
|
|
221
|
+
for (const aliasedField of field.aliases ?? []) {
|
|
219
222
|
if (aliasedField in data) {
|
|
220
223
|
inboundField = aliasedField;
|
|
221
224
|
break;
|
|
@@ -280,7 +283,8 @@ export class BindUtil {
|
|
|
280
283
|
return value;
|
|
281
284
|
}
|
|
282
285
|
const complex = SchemaRegistryIndex.has(config.type);
|
|
283
|
-
const bindConfig: BindConfig | undefined =
|
|
286
|
+
const bindConfig: BindConfig | undefined =
|
|
287
|
+
complex && 'view' in config && typeof config.view === 'string' ? { view: config.view } : undefined;
|
|
284
288
|
if (config.array) {
|
|
285
289
|
const subValue = !Array.isArray(value) ? [value] : value;
|
|
286
290
|
if (complex) {
|
|
@@ -324,4 +328,4 @@ export class BindUtil {
|
|
|
324
328
|
const paramConfigs = SchemaRegistryIndex.get(cls).getMethod(method).parameters;
|
|
325
329
|
return this.coerceParameters(paramConfigs, params, applyDefaults);
|
|
326
330
|
}
|
|
327
|
-
}
|
|
331
|
+
}
|
package/src/data.ts
CHANGED
|
@@ -1,38 +1,47 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isBooleanObject, isNumberObject, isStringObject } from 'node:util/types';
|
|
2
|
+
|
|
3
|
+
import { asConstructable, asFull, type Class, castTo, TypedObject } from '@travetto/runtime';
|
|
2
4
|
|
|
3
|
-
import { asConstructable, castTo, type Class, asFull, TypedObject } from '@travetto/runtime';
|
|
4
5
|
import { UnknownType } from './types.ts';
|
|
5
6
|
|
|
6
|
-
const REGEX_PATTERN = /[
|
|
7
|
+
const REGEX_PATTERN = /[/](.*)[/](i|g|m|s)?/;
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Utilities for data conversion and binding
|
|
10
11
|
*/
|
|
11
12
|
export class DataUtil {
|
|
12
|
-
|
|
13
13
|
/**
|
|
14
14
|
* Is a value a plain JS object, created using {}
|
|
15
15
|
* @param value Object to check
|
|
16
16
|
*/
|
|
17
17
|
static isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
18
|
-
return
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
return (
|
|
19
|
+
typeof value === 'object' && // separate from primitives
|
|
20
|
+
value !== undefined &&
|
|
21
|
+
value !== null && // is obvious
|
|
22
|
+
value.constructor === Object && // separate instances (Array, DOM, ...)
|
|
23
|
+
Object.prototype.toString.call(value) === '[object Object]'
|
|
24
|
+
); // separate build-in like Math
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/**
|
|
26
28
|
* Is a value of primitive type
|
|
27
29
|
* @param value Value to check
|
|
28
30
|
*/
|
|
29
|
-
static isPrimitive(value: unknown): value is
|
|
31
|
+
static isPrimitive(value: unknown): value is string | boolean | number | RegExp {
|
|
30
32
|
switch (typeof value) {
|
|
31
|
-
case 'string':
|
|
32
|
-
case '
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
case 'string':
|
|
34
|
+
case 'boolean':
|
|
35
|
+
case 'number':
|
|
36
|
+
case 'bigint':
|
|
37
|
+
return true;
|
|
38
|
+
case 'object':
|
|
39
|
+
return (
|
|
40
|
+
!!value &&
|
|
41
|
+
(value instanceof RegExp || value instanceof Date || isStringObject(value) || isNumberObject(value) || isBooleanObject(value))
|
|
42
|
+
);
|
|
43
|
+
default:
|
|
44
|
+
return false;
|
|
36
45
|
}
|
|
37
46
|
}
|
|
38
47
|
|
|
@@ -53,7 +62,8 @@ export class DataUtil {
|
|
|
53
62
|
|
|
54
63
|
let value: unknown;
|
|
55
64
|
|
|
56
|
-
if (isEmptyA || isEmptyB) {
|
|
65
|
+
if (isEmptyA || isEmptyB) {
|
|
66
|
+
// If no `a`, `b` always wins
|
|
57
67
|
if (mode === 'replace' || b === null || !isEmptyB) {
|
|
58
68
|
value = isEmptyB ? b : this.shallowClone(b);
|
|
59
69
|
} else if (!isEmptyA) {
|
|
@@ -65,29 +75,35 @@ export class DataUtil {
|
|
|
65
75
|
if (isArrA !== isArrB || isSimpA !== isSimpB) {
|
|
66
76
|
throw new Error(`Cannot merge differing types ${a} and ${b}`);
|
|
67
77
|
}
|
|
68
|
-
if (Array.isArray(b)) {
|
|
78
|
+
if (Array.isArray(b)) {
|
|
79
|
+
// Arrays
|
|
69
80
|
value = a; // Write onto A
|
|
70
81
|
if (mode === 'replace') {
|
|
71
82
|
value = b;
|
|
72
83
|
} else {
|
|
73
84
|
const valueArray: unknown[] = castTo(value);
|
|
74
85
|
const bArray = b;
|
|
75
|
-
for (let i = 0; i < bArray.length; i
|
|
86
|
+
for (let i = 0; i < bArray.length; i += 1) {
|
|
76
87
|
valueArray[i] = this.#deepAssignRaw(valueArray[i], bArray[i], mode);
|
|
77
88
|
}
|
|
78
89
|
}
|
|
79
|
-
} else if (isSimpB) {
|
|
90
|
+
} else if (isSimpB) {
|
|
91
|
+
// Scalars
|
|
80
92
|
const match = typeof a === typeof b;
|
|
81
93
|
value = b;
|
|
82
94
|
|
|
83
|
-
if (!match) {
|
|
84
|
-
|
|
95
|
+
if (!match) {
|
|
96
|
+
// If types do not match
|
|
97
|
+
if (mode === 'strict') {
|
|
98
|
+
// Bail on strict
|
|
85
99
|
throw new Error(`Cannot merge ${a} [${typeof a}] with ${b} [${typeof b}]`);
|
|
86
|
-
} else if (mode === 'coerce') {
|
|
100
|
+
} else if (mode === 'coerce') {
|
|
101
|
+
// Force on coerce
|
|
87
102
|
value = this.coerceType(b, asConstructable(a).constructor, false);
|
|
88
103
|
}
|
|
89
104
|
}
|
|
90
|
-
} else {
|
|
105
|
+
} else {
|
|
106
|
+
// Object merge
|
|
91
107
|
value = a;
|
|
92
108
|
const bObject: Record<string, unknown> = castTo(b);
|
|
93
109
|
const valueObject: Record<string, unknown> = castTo(value);
|
|
@@ -148,15 +164,16 @@ export class DataUtil {
|
|
|
148
164
|
if (typeof input === 'object' && 'toDate' in input && typeof input.toDate === 'function') {
|
|
149
165
|
value = castTo(input.toDate());
|
|
150
166
|
} else {
|
|
151
|
-
value =
|
|
152
|
-
input
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
167
|
+
value =
|
|
168
|
+
input instanceof Date
|
|
169
|
+
? input
|
|
170
|
+
: typeof input === 'number'
|
|
171
|
+
? new Date(input)
|
|
172
|
+
: typeof input === 'bigint'
|
|
173
|
+
? new Date(Number(input))
|
|
174
|
+
: typeof input === 'string' && /^[-]?\d+$/.test(input)
|
|
175
|
+
? new Date(parseInt(input, 10))
|
|
176
|
+
: new Date(input.toString());
|
|
160
177
|
}
|
|
161
178
|
if (strict && value && Number.isNaN(value.getTime())) {
|
|
162
179
|
throw new Error(`Invalid date value: ${input}`);
|
|
@@ -178,8 +195,7 @@ export class DataUtil {
|
|
|
178
195
|
return input;
|
|
179
196
|
}
|
|
180
197
|
try {
|
|
181
|
-
return BigInt(
|
|
182
|
-
input : `${input}`.replace(/n$/i, ''));
|
|
198
|
+
return BigInt(typeof input === 'boolean' || typeof input === 'number' ? input : `${input}`.replace(/n$/i, ''));
|
|
183
199
|
} catch {
|
|
184
200
|
if (strict) {
|
|
185
201
|
throw new Error(`Invalid numeric value: ${input}`);
|
|
@@ -222,7 +238,8 @@ export class DataUtil {
|
|
|
222
238
|
}
|
|
223
239
|
}
|
|
224
240
|
case undefined:
|
|
225
|
-
case String:
|
|
241
|
+
case String:
|
|
242
|
+
return `${input}`;
|
|
226
243
|
}
|
|
227
244
|
if (!strict || this.isPlainObject(input)) {
|
|
228
245
|
return input;
|
|
@@ -236,7 +253,7 @@ export class DataUtil {
|
|
|
236
253
|
* @param value Object to clone
|
|
237
254
|
*/
|
|
238
255
|
static shallowClone<T>(value: T): T {
|
|
239
|
-
return castTo(Array.isArray(value) ? value.slice(0) :
|
|
256
|
+
return castTo(Array.isArray(value) ? value.slice(0) : this.isSimpleValue(value) ? value : { ...castTo<object>(value) });
|
|
240
257
|
}
|
|
241
258
|
|
|
242
259
|
/**
|
|
@@ -245,7 +262,7 @@ export class DataUtil {
|
|
|
245
262
|
* @param b The source
|
|
246
263
|
* @param mode How the assignment should be handled
|
|
247
264
|
*/
|
|
248
|
-
static deepAssign<T, U>(a: T, b: U, mode:
|
|
265
|
+
static deepAssign<T, U>(a: T, b: U, mode: 'replace' | 'loose' | 'strict' | 'coerce' = 'loose'): T & U {
|
|
249
266
|
if (!a || this.isSimpleValue(a)) {
|
|
250
267
|
throw new Error(`Cannot merge onto a simple value, ${a}`);
|
|
251
268
|
}
|
|
@@ -278,4 +295,4 @@ export class DataUtil {
|
|
|
278
295
|
return input;
|
|
279
296
|
}
|
|
280
297
|
}
|
|
281
|
-
}
|
|
298
|
+
}
|
package/src/decorator/common.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Class, type ClassInstance, getClass } from '@travetto/runtime';
|
|
2
2
|
|
|
3
|
-
import type { SchemaCoreConfig } from '../service/types.ts';
|
|
4
3
|
import { SchemaRegistryIndex } from '../service/registry-index.ts';
|
|
4
|
+
import type { SchemaCoreConfig } from '../service/types.ts';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Describe a model or a field
|
|
@@ -31,7 +31,7 @@ export function Describe(config: Partial<Omit<SchemaCoreConfig, 'metadata'>>) {
|
|
|
31
31
|
* @augments `@travetto/schema:Input`
|
|
32
32
|
* @kind decorator
|
|
33
33
|
*/
|
|
34
|
-
export const IsPrivate = (): (instanceOrCls: Class | ClassInstance, property?: string) => void => Describe({ private: true });
|
|
34
|
+
export const IsPrivate = (): ((instanceOrCls: Class | ClassInstance, property?: string) => void) => Describe({ private: true });
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* Mark a field/method as ignored
|
|
@@ -39,5 +39,5 @@ export const IsPrivate = (): (instanceOrCls: Class | ClassInstance, property?: s
|
|
|
39
39
|
* @kind decorator
|
|
40
40
|
*/
|
|
41
41
|
export function Ignore(): PropertyDecorator {
|
|
42
|
-
return () => {
|
|
42
|
+
return () => {};
|
|
43
43
|
}
|
package/src/decorator/field.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { type Any, type ClassInstance, getClass } from '@travetto/runtime';
|
|
2
2
|
|
|
3
|
-
import type { SchemaFieldConfig } from '../service/types.ts';
|
|
4
3
|
import { SchemaRegistryIndex } from '../service/registry-index.ts';
|
|
4
|
+
import type { SchemaFieldConfig } from '../service/types.ts';
|
|
5
5
|
|
|
6
|
-
type PropType<V> =
|
|
7
|
-
instance: T,
|
|
8
|
-
|
|
6
|
+
type PropType<V> = <T extends Partial<Record<K, V | Function>>, K extends string>(
|
|
7
|
+
instance: T,
|
|
8
|
+
property: K,
|
|
9
|
+
idx?: TypedPropertyDescriptor<Any> | number
|
|
10
|
+
) => void;
|
|
9
11
|
|
|
10
12
|
function field<V>(...configs: Partial<SchemaFieldConfig>[]): PropType<V> {
|
|
11
13
|
return (instance: ClassInstance, property: string): void => {
|
|
@@ -30,14 +32,18 @@ export function Field(type?: Pick<SchemaFieldConfig, 'type' | 'array'>, ...confi
|
|
|
30
32
|
* @augments `@travetto/schema:Input`
|
|
31
33
|
* @kind decorator
|
|
32
34
|
*/
|
|
33
|
-
export function Writeonly(): PropType<unknown> {
|
|
35
|
+
export function Writeonly(): PropType<unknown> {
|
|
36
|
+
return field({ access: 'writeonly' });
|
|
37
|
+
}
|
|
34
38
|
|
|
35
39
|
/**
|
|
36
40
|
* Mark a field as readonly
|
|
37
41
|
* @augments `@travetto/schema:Input`
|
|
38
42
|
* @kind decorator
|
|
39
43
|
*/
|
|
40
|
-
export function Readonly(): PropType<unknown> {
|
|
44
|
+
export function Readonly(): PropType<unknown> {
|
|
45
|
+
return field({ access: 'readonly' });
|
|
46
|
+
}
|
|
41
47
|
|
|
42
48
|
/**
|
|
43
49
|
* Mark a field as sensitive
|
|
@@ -45,4 +51,6 @@ export function Readonly(): PropType<unknown> { return field({ access: 'readonly
|
|
|
45
51
|
* @augments `@travetto/schema:Input`
|
|
46
52
|
* @kind decorator
|
|
47
53
|
*/
|
|
48
|
-
export function Secret(active = true): PropType<unknown> {
|
|
54
|
+
export function Secret(active = true): PropType<unknown> {
|
|
55
|
+
return field({ secret: active });
|
|
56
|
+
}
|