@naturalcycles/nodejs-lib 15.43.1 → 15.43.2
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/dist/validation/ajv/jsonSchemaBuilder.js +1 -1
- package/dist/validation/ajv/jsonSchemaBuilder.util.d.ts +9 -0
- package/dist/validation/ajv/jsonSchemaBuilder.util.js +65 -0
- package/package.json +2 -2
- package/readme.md +0 -7
- package/src/validation/ajv/j.readme.md +116 -70
- package/src/validation/ajv/jsonSchemaBuilder.ts +1 -1
- package/src/validation/ajv/jsonSchemaBuilder.util.ts +78 -0
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
import { _isUndefined, _numberEnumValues, _stringEnumValues, getEnumType, } from '@naturalcycles/js-lib';
|
|
4
4
|
import { _uniq } from '@naturalcycles/js-lib/array';
|
|
5
5
|
import { _assert } from '@naturalcycles/js-lib/error';
|
|
6
|
-
import { JSON_SCHEMA_ORDER, mergeJsonSchemaObjects } from '@naturalcycles/js-lib/json-schema';
|
|
7
6
|
import { _deepCopy, _sortObject } from '@naturalcycles/js-lib/object';
|
|
8
7
|
import { JWT_REGEX, } from '@naturalcycles/js-lib/types';
|
|
8
|
+
import { JSON_SCHEMA_ORDER, mergeJsonSchemaObjects } from './jsonSchemaBuilder.util.js';
|
|
9
9
|
export const j = {
|
|
10
10
|
string() {
|
|
11
11
|
return new JsonSchemaStringBuilder();
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AnyObject } from '@naturalcycles/js-lib/types';
|
|
2
|
+
import type { JsonSchema } from './jsonSchemaBuilder.js';
|
|
3
|
+
export declare const JSON_SCHEMA_ORDER: string[];
|
|
4
|
+
/**
|
|
5
|
+
* Merges s2 into s1 (mutates s1) and returns s1.
|
|
6
|
+
* Does not mutate s2.
|
|
7
|
+
* API similar to Object.assign(s1, s2)
|
|
8
|
+
*/
|
|
9
|
+
export declare function mergeJsonSchemaObjects<T1 extends AnyObject, T2 extends AnyObject>(schema1: JsonSchema<T1>, schema2: JsonSchema<T2>): JsonSchema<T1 & T2>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { _uniq } from '@naturalcycles/js-lib/array';
|
|
2
|
+
import { _filterNullishValues } from '@naturalcycles/js-lib/object';
|
|
3
|
+
export const JSON_SCHEMA_ORDER = [
|
|
4
|
+
'$schema',
|
|
5
|
+
'$id',
|
|
6
|
+
'title',
|
|
7
|
+
'description',
|
|
8
|
+
'deprecated',
|
|
9
|
+
'readOnly',
|
|
10
|
+
'writeOnly',
|
|
11
|
+
'type',
|
|
12
|
+
'default',
|
|
13
|
+
// Object,
|
|
14
|
+
'properties',
|
|
15
|
+
'required',
|
|
16
|
+
'minProperties',
|
|
17
|
+
'maxProperties',
|
|
18
|
+
'patternProperties',
|
|
19
|
+
'propertyNames',
|
|
20
|
+
// Array
|
|
21
|
+
'properties',
|
|
22
|
+
'required',
|
|
23
|
+
'minProperties',
|
|
24
|
+
'maxProperties',
|
|
25
|
+
'patternProperties',
|
|
26
|
+
'propertyNames',
|
|
27
|
+
// String
|
|
28
|
+
'pattern',
|
|
29
|
+
'minLength',
|
|
30
|
+
'maxLength',
|
|
31
|
+
'format',
|
|
32
|
+
'transform',
|
|
33
|
+
// Number
|
|
34
|
+
'format',
|
|
35
|
+
'multipleOf',
|
|
36
|
+
'minimum',
|
|
37
|
+
'exclusiveMinimum',
|
|
38
|
+
'maximum',
|
|
39
|
+
'exclusiveMaximum',
|
|
40
|
+
];
|
|
41
|
+
/**
|
|
42
|
+
* Merges s2 into s1 (mutates s1) and returns s1.
|
|
43
|
+
* Does not mutate s2.
|
|
44
|
+
* API similar to Object.assign(s1, s2)
|
|
45
|
+
*/
|
|
46
|
+
export function mergeJsonSchemaObjects(schema1, schema2) {
|
|
47
|
+
const s1 = schema1;
|
|
48
|
+
const s2 = schema2;
|
|
49
|
+
// Merge `properties`
|
|
50
|
+
Object.entries(s2.properties).forEach(([k, v]) => {
|
|
51
|
+
s1.properties[k] = v;
|
|
52
|
+
});
|
|
53
|
+
// Merge `patternProperties`
|
|
54
|
+
Object.entries(s2.patternProperties || {}).forEach(([k, v]) => {
|
|
55
|
+
s1.patternProperties[k] = v;
|
|
56
|
+
});
|
|
57
|
+
s1.propertyNames = s2.propertyNames || s1.propertyNames;
|
|
58
|
+
s1.minProperties = s2.minProperties ?? s1.minProperties;
|
|
59
|
+
s1.maxProperties = s2.maxProperties ?? s1.maxProperties;
|
|
60
|
+
// Merge `required`
|
|
61
|
+
s1.required.push(...s2.required);
|
|
62
|
+
s1.required = _uniq(s1.required).sort();
|
|
63
|
+
// `additionalProperties` remains the same
|
|
64
|
+
return _filterNullishValues(s1, { mutate: true });
|
|
65
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naturalcycles/nodejs-lib",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "15.43.
|
|
4
|
+
"version": "15.43.2",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@naturalcycles/js-lib": "^15",
|
|
7
7
|
"@types/js-yaml": "^4",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"directory": "packages/nodejs-lib"
|
|
72
72
|
},
|
|
73
73
|
"engines": {
|
|
74
|
-
"node": ">=
|
|
74
|
+
"node": ">=24.10.0"
|
|
75
75
|
},
|
|
76
76
|
"description": "Standard library for Node.js",
|
|
77
77
|
"author": "Natural Cycles Team",
|
package/readme.md
CHANGED
|
@@ -11,10 +11,3 @@
|
|
|
11
11
|
[](https://github.com/NaturalCycles/nodejs-lib/actions)
|
|
12
12
|
|
|
13
13
|
# [Documentation](https://naturalcycles.github.io/nodejs-lib/)
|
|
14
|
-
|
|
15
|
-
# Packaging
|
|
16
|
-
|
|
17
|
-
- `engines.node`: Latest Node.js LTS
|
|
18
|
-
- `main: dist/index.js`: commonjs, es2020
|
|
19
|
-
- `types: dist/index.d.ts`: typescript types
|
|
20
|
-
- `/src` folder with source `*.ts` files included
|
|
@@ -6,9 +6,112 @@
|
|
|
6
6
|
|
|
7
7
|
In this document you can learn about how to use `j`, our new validation library.
|
|
8
8
|
|
|
9
|
+
A schema speaks louder than a thousand words:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
const dayInputSchema = j.object<DayInput>({
|
|
13
|
+
date: j.string().isoDate(),
|
|
14
|
+
isPeriod: j.boolean().optional(),
|
|
15
|
+
lhTest: j.enum(TestResult).nullable().optional(),
|
|
16
|
+
temp: j.integer().branded<CentiCelsius>().optional(),
|
|
17
|
+
})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### How to use `j` for validation?
|
|
21
|
+
|
|
22
|
+
While the API is very intuitive, there are some tips that can help with quick adoption:
|
|
23
|
+
|
|
24
|
+
1. When you think of our custom types (e.g. IsoDate, UnixTimestamp or just "email"), first think
|
|
25
|
+
about its underlying type:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
const timestamp = j.number().unixTimestamp2000() // start with ".number"
|
|
29
|
+
const email = j.string().email() // start with ".string"
|
|
30
|
+
const date = j.string().isoDate() // start with ".string"
|
|
31
|
+
const dbRow = j.object.dbEntity({}) // start with ".object"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
2. Probably the most important: object schemas must have a type
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
const schema1 = j.object({ foo: j.string() }) // ❌ Won't work.
|
|
38
|
+
const schema2 = j.object<SomeType>({ foo: j.string() }) // ✅ Works just fine.
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
But because we do not always want to create a type or interface for every object schema, in those
|
|
42
|
+
cases it's possible to use inference via `j.object.infer()`:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const schema3 = j.object.infer({ foo: j.string() }) // { foo: string } is inferred
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
⚠️ These inferred schemas cannot be used for validation - only to be passed into other schemas. If
|
|
49
|
+
you forget, there will be an error thrown when the first validation is about to happen.
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
const schema1 = j.object.infer({ foo: j.string() }) // ❌ Using `schema1` in validation would fail
|
|
53
|
+
|
|
54
|
+
// 💭 What this means is that you cannot use `schema1` to validate an input.
|
|
55
|
+
// But you can use it inside another schema:
|
|
56
|
+
|
|
57
|
+
const schema2 = j.object<SomeType>({ foo: schema1 }) // ✅ Using `schema1` inside another schema
|
|
58
|
+
|
|
59
|
+
const schema3 = j.object<SomeType>({
|
|
60
|
+
foo: j.object.infer({ bar: j.string() }),
|
|
61
|
+
}) // ✅ Using an inferred object inside another schema
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This requirement is in place to enforce that we 1) have types for data that we validate, and 2) that
|
|
65
|
+
mismatches between types and schemas become visible as soon as possible.
|
|
66
|
+
|
|
67
|
+
3. Use `j.object.dbEntity()` for validating an object to be saved in Datastore
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
interface DBRow extends BaseDBEntity {
|
|
71
|
+
foo: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const dbSchema = j.object.dbEntity<DBRow>({
|
|
75
|
+
foo: j.string(),
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// 👆 is a shortcut for
|
|
79
|
+
|
|
80
|
+
const dbSchema = j.object<DBRow>({
|
|
81
|
+
id: j.string(),
|
|
82
|
+
created: j.number().unixTimestamp2000(),
|
|
83
|
+
updated: j.number().unixTimestamp2000(),
|
|
84
|
+
foo: j.string(),
|
|
85
|
+
})
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The `dbEntity` helper also requires you to pass in a type. It will not work without it.
|
|
89
|
+
|
|
90
|
+
4. Many branded values have no shortcut (on purpose), usually those that come with no actual
|
|
91
|
+
validation:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
const accountId = j.string().accountId() // ❌
|
|
95
|
+
const accountId = j.string().branded<AccountId>() // ✅
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
5. In some cases you can specify a custom error message
|
|
99
|
+
|
|
100
|
+
When using regex validation, the resulting error message is generally not something we would want
|
|
101
|
+
the user to see. In many case, they are also not very helpful for developers either. So, when
|
|
102
|
+
running a regex validation, you can set a custom error message. This pattern can be extended to
|
|
103
|
+
other validator functions too, as we think it's necessary.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const schema = j.object({
|
|
107
|
+
foo: j.string().regex(/\[a-z]{2,}\d?.+/, { msg: 'not a valid OompaLoompa!' }),
|
|
108
|
+
})
|
|
109
|
+
// will produce an error like "Object.foo is not a valid OompaLoompa!"
|
|
110
|
+
```
|
|
111
|
+
|
|
9
112
|
### Why?
|
|
10
113
|
|
|
11
|
-
|
|
114
|
+
Why go into the trouble? Why not keep the JOI schemas? Well, the main reasons are:
|
|
12
115
|
|
|
13
116
|
1. Faster validation
|
|
14
117
|
2. Better DX
|
|
@@ -39,6 +142,18 @@ const newWay = j.object<SomeType>({
|
|
|
39
142
|
// ... knowing to import `j`, and the rest is aided by auto-completion.
|
|
40
143
|
```
|
|
41
144
|
|
|
145
|
+
Hopefully one welcomed change is how we handle `enum`s:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
const oldWay1 = numberEnumValueSchema(TestResult)
|
|
149
|
+
const newWay1 = j.enum(TestResult)
|
|
150
|
+
|
|
151
|
+
const oldWay2 = stringEnumValueSchema(SKU)
|
|
152
|
+
const newWay2 = j.enum(SKU)
|
|
153
|
+
|
|
154
|
+
const newWay3 = j.enum([1, 2, 'foo', false]) // newWay satisfies 1 | 2 | 'foo' | false
|
|
155
|
+
```
|
|
156
|
+
|
|
42
157
|
**Stricter type validation** (aka worse DX) means that the schema and the types need to match
|
|
43
158
|
exactly, unlike before where a required property could have had an optional schema.
|
|
44
159
|
|
|
@@ -65,75 +180,6 @@ const schema = j.object.infer({
|
|
|
65
180
|
})
|
|
66
181
|
```
|
|
67
182
|
|
|
68
|
-
### How to use `j` for validation?
|
|
69
|
-
|
|
70
|
-
While the API is very intuitive, there are some tips that can help with quick adoption:
|
|
71
|
-
|
|
72
|
-
1. When you want to use a specialized schema, first think about its underlying value:
|
|
73
|
-
|
|
74
|
-
```ts
|
|
75
|
-
const timestamp = j.number().unixTimestamp2000() // start with ".number"
|
|
76
|
-
const email = j.string().email() // start with ".string"
|
|
77
|
-
const date = j.string().isoDate() // start with ".string"
|
|
78
|
-
const dbRow = j.object.dbEntity({}) // start with ".object"
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
2. Many branded values have no shortcut (on purpose), usually those that come with no actual
|
|
82
|
-
validation:
|
|
83
|
-
|
|
84
|
-
```ts
|
|
85
|
-
const accountId = j.string().accountId() // ❌
|
|
86
|
-
const accountId = j.string().branded<AccountId>() // ✅
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
3. Probably the most important: object schemas must have a type
|
|
90
|
-
|
|
91
|
-
```ts
|
|
92
|
-
const schema1 = j.object({ foo: j.string() }) // ❌
|
|
93
|
-
const schema2 = j.object<SomeType>({ bar: j.string(), nested: objectSchema1 }) // ✅
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
But because we do not always want to create a type or interface for every object schema, in those
|
|
97
|
-
cases it's possible to use inference via `j.object.infer()`:
|
|
98
|
-
|
|
99
|
-
```ts
|
|
100
|
-
const schema3 = j.object.infer({ foo: j.string() }) // { foo: string } is inferred
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
But these inferred schemas cannot be used for validation - only to be passed into other schemas. To
|
|
104
|
-
use inferred schemas in validation, you need to call `.ofType<SomeType>()` on them. If you forget,
|
|
105
|
-
there will be an error thrown when the first validation is about to happen.
|
|
106
|
-
|
|
107
|
-
```ts
|
|
108
|
-
const schema1 = j.object.infer({ foo: j.string() }) // ❌ Using `schema1` in validation would fail
|
|
109
|
-
|
|
110
|
-
const schema2 = j.object<SomeType>({ nestedProperty: schema1 }) // ✅ Using `schema1` inside another schema
|
|
111
|
-
|
|
112
|
-
const schema3 = j.object.infer({ foo: j.string() }).isOfType<{ foo: string }>() // ✅ Using `schema3` for validation
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
This requirement is in place to enforce that we 1) have types for data that we validate, and 2) that
|
|
116
|
-
mismatches between types and schemas become visible as soon as possible.
|
|
117
|
-
|
|
118
|
-
If the typing has a mismatch, then the `schema`'s type will become `never`. When using
|
|
119
|
-
`j.object<SomeType>` the type error will be very helpful in identifying the mismatch. When using
|
|
120
|
-
`j.object.infer().isOfType()` the type error will be very unhelpful. Because of this,
|
|
121
|
-
`j.object<SomeType>` is the preferred choice.
|
|
122
|
-
|
|
123
|
-
4. In some cases you can specify a custom error message
|
|
124
|
-
|
|
125
|
-
When using regex validation, the resulting error message is generally not something we would want
|
|
126
|
-
the user to see. In many case, they are also not very helpful for developers either. So, when
|
|
127
|
-
running a regex validation, you can set a custom error message. This pattern can be extended to
|
|
128
|
-
other validator functions too, as we think it's necessary.
|
|
129
|
-
|
|
130
|
-
```ts
|
|
131
|
-
const schema = j.object({
|
|
132
|
-
foo: j.string().regex(/\[a-z]{2,}\d?.+/, { msg: 'not a valid OompaLoompa! ' }),
|
|
133
|
-
})
|
|
134
|
-
// will produce an error like "Object.foo is not a valid OompaLoompa!"
|
|
135
|
-
```
|
|
136
|
-
|
|
137
183
|
### More about `j`
|
|
138
184
|
|
|
139
185
|
`j` is a JSON Schema builder that is developed in-house.
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
} from '@naturalcycles/js-lib'
|
|
10
10
|
import { _uniq } from '@naturalcycles/js-lib/array'
|
|
11
11
|
import { _assert } from '@naturalcycles/js-lib/error'
|
|
12
|
-
import { JSON_SCHEMA_ORDER, mergeJsonSchemaObjects } from '@naturalcycles/js-lib/json-schema'
|
|
13
12
|
import type { Set2 } from '@naturalcycles/js-lib/object'
|
|
14
13
|
import { _deepCopy, _sortObject } from '@naturalcycles/js-lib/object'
|
|
15
14
|
import {
|
|
@@ -23,6 +22,7 @@ import {
|
|
|
23
22
|
type UnixTimestamp,
|
|
24
23
|
type UnixTimestampMillis,
|
|
25
24
|
} from '@naturalcycles/js-lib/types'
|
|
25
|
+
import { JSON_SCHEMA_ORDER, mergeJsonSchemaObjects } from './jsonSchemaBuilder.util.js'
|
|
26
26
|
|
|
27
27
|
export const j = {
|
|
28
28
|
string(): JsonSchemaStringBuilder<string, string, false> {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { _uniq } from '@naturalcycles/js-lib/array'
|
|
2
|
+
import { _filterNullishValues } from '@naturalcycles/js-lib/object'
|
|
3
|
+
import type { AnyObject } from '@naturalcycles/js-lib/types'
|
|
4
|
+
import type { JsonSchema } from './jsonSchemaBuilder.js'
|
|
5
|
+
|
|
6
|
+
export const JSON_SCHEMA_ORDER = [
|
|
7
|
+
'$schema',
|
|
8
|
+
'$id',
|
|
9
|
+
'title',
|
|
10
|
+
'description',
|
|
11
|
+
'deprecated',
|
|
12
|
+
'readOnly',
|
|
13
|
+
'writeOnly',
|
|
14
|
+
'type',
|
|
15
|
+
'default',
|
|
16
|
+
// Object,
|
|
17
|
+
'properties',
|
|
18
|
+
'required',
|
|
19
|
+
'minProperties',
|
|
20
|
+
'maxProperties',
|
|
21
|
+
'patternProperties',
|
|
22
|
+
'propertyNames',
|
|
23
|
+
// Array
|
|
24
|
+
'properties',
|
|
25
|
+
'required',
|
|
26
|
+
'minProperties',
|
|
27
|
+
'maxProperties',
|
|
28
|
+
'patternProperties',
|
|
29
|
+
'propertyNames',
|
|
30
|
+
// String
|
|
31
|
+
'pattern',
|
|
32
|
+
'minLength',
|
|
33
|
+
'maxLength',
|
|
34
|
+
'format',
|
|
35
|
+
'transform',
|
|
36
|
+
// Number
|
|
37
|
+
'format',
|
|
38
|
+
'multipleOf',
|
|
39
|
+
'minimum',
|
|
40
|
+
'exclusiveMinimum',
|
|
41
|
+
'maximum',
|
|
42
|
+
'exclusiveMaximum',
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Merges s2 into s1 (mutates s1) and returns s1.
|
|
47
|
+
* Does not mutate s2.
|
|
48
|
+
* API similar to Object.assign(s1, s2)
|
|
49
|
+
*/
|
|
50
|
+
export function mergeJsonSchemaObjects<T1 extends AnyObject, T2 extends AnyObject>(
|
|
51
|
+
schema1: JsonSchema<T1>,
|
|
52
|
+
schema2: JsonSchema<T2>,
|
|
53
|
+
): JsonSchema<T1 & T2> {
|
|
54
|
+
const s1 = schema1 as any
|
|
55
|
+
const s2 = schema2 as any
|
|
56
|
+
|
|
57
|
+
// Merge `properties`
|
|
58
|
+
Object.entries(s2.properties).forEach(([k, v]) => {
|
|
59
|
+
s1.properties[k] = v
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// Merge `patternProperties`
|
|
63
|
+
Object.entries(s2.patternProperties || {}).forEach(([k, v]) => {
|
|
64
|
+
s1.patternProperties[k] = v
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
s1.propertyNames = s2.propertyNames || s1.propertyNames
|
|
68
|
+
s1.minProperties = s2.minProperties ?? s1.minProperties
|
|
69
|
+
s1.maxProperties = s2.maxProperties ?? s1.maxProperties
|
|
70
|
+
|
|
71
|
+
// Merge `required`
|
|
72
|
+
s1.required.push(...s2.required)
|
|
73
|
+
s1.required = _uniq(s1.required).sort()
|
|
74
|
+
|
|
75
|
+
// `additionalProperties` remains the same
|
|
76
|
+
|
|
77
|
+
return _filterNullishValues(s1, { mutate: true })
|
|
78
|
+
}
|