@naturalcycles/nodejs-lib 15.43.0 → 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.
@@ -241,11 +241,14 @@ declare function object<IN extends AnyObject>(props: {
241
241
  }): JsonSchemaObjectBuilder<IN, IN, false>;
242
242
  declare function objectInfer<P extends Record<string, JsonSchemaAnyBuilder<any, any, any>>>(props: P): JsonSchemaObjectInferringBuilder<P, false>;
243
243
  declare function objectDbEntity(props?: AnyObject): never;
244
- declare function objectDbEntity<IN extends AnyObject>(props?: AnyObject): JsonSchemaObjectBuilder<IN, IN, false>;
244
+ declare function objectDbEntity<IN extends AnyObject>(props?: {
245
+ [key in keyof PartiallyOptional<IN, 'id' | 'created' | 'updated'>]: JsonSchemaAnyBuilder<any, IN[key], any>;
246
+ }): JsonSchemaObjectBuilder<IN, IN, false>;
245
247
  type Expand<T> = T extends infer O ? {
246
248
  [K in keyof O]: O[K];
247
249
  } : never;
248
250
  type ExactMatch<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
251
+ type PartiallyOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
249
252
  type BuilderOutUnion<B extends readonly JsonSchemaAnyBuilder<any, any, any>[]> = {
250
253
  [K in keyof B]: B[K] extends JsonSchemaAnyBuilder<any, infer O, any> ? O : never;
251
254
  }[number];
@@ -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.0",
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": ">=22.12.0"
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
  [![Actions](https://github.com/NaturalCycles/nodejs-lib/workflows/ci/badge.svg)](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
@@ -0,0 +1,190 @@
1
+ # `j`
2
+
3
+ ## and Silent Bob
4
+
5
+ ### validate The Data
6
+
7
+ In this document you can learn about how to use `j`, our new validation library.
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
+
112
+ ### Why?
113
+
114
+ Why go into the trouble? Why not keep the JOI schemas? Well, the main reasons are:
115
+
116
+ 1. Faster validation
117
+ 2. Better DX
118
+ 3. Stricter type validation
119
+ 4. New types
120
+
121
+ **Faster validation** means that we can now start validating data that we used to ignore, because
122
+ validating them were very-very slow. For example: OuraSleepData.
123
+
124
+ It also means that we are more prepared for the accumulation of data that will happen with our own
125
+ devices like B1 and R1.
126
+
127
+ **Better DX** comes from the discoverable API, which means that one does not need to remember what
128
+ kind of schemas we usually import or use.
129
+
130
+ ```ts
131
+ const oldWay = objectSchema<SomeType>({
132
+ unix: unixTimestamp2000Schema(),
133
+ })
134
+
135
+ // 👆 You needed to know about importing `objectSchema` and `unixTimestamp2000Schema`
136
+ // as opposed to... 👇
137
+
138
+ const newWay = j.object<SomeType>({
139
+ unix: j.number().unixTimestamp2000(),
140
+ })
141
+
142
+ // ... knowing to import `j`, and the rest is aided by auto-completion.
143
+ ```
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
+
157
+ **Stricter type validation** (aka worse DX) means that the schema and the types need to match
158
+ exactly, unlike before where a required property could have had an optional schema.
159
+
160
+ ```ts
161
+ interface Foo {
162
+ foo: string
163
+ }
164
+
165
+ const oldWay = objectSchema<Foo>({ foo: stringSchema.optional() }) // ✅ Worked
166
+ const newWay = j.object<Foo>({ foo: j.string().optional() }) // ❌ Does not work anymore
167
+ ```
168
+
169
+ And we also have **new types** in the schema, e.g.: Buffer, Set.
170
+
171
+ The novelty is that the new types support serialization and de-serialization, i.e. you can use
172
+ `j.set()` and when you know that the incoming data (from Datastore or from a Request) is an array,
173
+ it will convert the incoming data to a Set. And the same for `j.buffer()` - should you ever need
174
+ that.
175
+
176
+ ```ts
177
+ const schema = j.object.infer({
178
+ set: j.set(j.enum(DataFlags)), // accepts any Iterable input, output is Set<DataFlags> instance
179
+ buffer: j.buffer(), // accepts any valid input for Buffer, output is a Buffer instance
180
+ })
181
+ ```
182
+
183
+ ### More about `j`
184
+
185
+ `j` is a JSON Schema builder that is developed in-house.
186
+
187
+ The validation is done by `ajv` which stands for Another JsonSchema Validator, an insanely fast
188
+ validation library.
189
+
190
+ `ajv` is hidden under the hood, and developers will mostly interact with `j`.
@@ -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> {
@@ -840,13 +840,21 @@ function objectInfer<P extends Record<string, JsonSchemaAnyBuilder<any, any, any
840
840
  }
841
841
 
842
842
  function objectDbEntity(props?: AnyObject): never
843
- function objectDbEntity<IN extends AnyObject>(
844
- props?: AnyObject,
845
- ): JsonSchemaObjectBuilder<IN, IN, false>
843
+ function objectDbEntity<IN extends AnyObject>(props?: {
844
+ [key in keyof PartiallyOptional<IN, 'id' | 'created' | 'updated'>]: JsonSchemaAnyBuilder<
845
+ any,
846
+ IN[key],
847
+ any
848
+ >
849
+ }): JsonSchemaObjectBuilder<IN, IN, false>
846
850
 
847
- function objectDbEntity<IN extends AnyObject>(
848
- props?: AnyObject,
849
- ): JsonSchemaObjectBuilder<IN, IN, false> {
851
+ function objectDbEntity<IN extends AnyObject>(props?: {
852
+ [key in keyof PartiallyOptional<IN, 'id' | 'created' | 'updated'>]: JsonSchemaAnyBuilder<
853
+ any,
854
+ IN[key],
855
+ any
856
+ >
857
+ }): JsonSchemaObjectBuilder<IN, IN, false> {
850
858
  return j.object({
851
859
  id: j.string(),
852
860
  created: j.number().unixTimestamp2000(),
@@ -860,6 +868,8 @@ type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never
860
868
  type ExactMatch<A, B> =
861
869
  (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false
862
870
 
871
+ type PartiallyOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
872
+
863
873
  type BuilderOutUnion<B extends readonly JsonSchemaAnyBuilder<any, any, any>[]> = {
864
874
  [K in keyof B]: B[K] extends JsonSchemaAnyBuilder<any, infer O, any> ? O : never
865
875
  }[number]
@@ -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
+ }