@naturalcycles/nodejs-lib 15.43.1 → 15.44.0

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.
@@ -1,5 +1,5 @@
1
1
  import type { Set2 } from '@naturalcycles/js-lib/object';
2
- import { type AnyObject, type IsoDate, type IsoDateTime, type NumberEnum, type StringEnum, type StringMap, type UnixTimestamp, type UnixTimestampMillis } from '@naturalcycles/js-lib/types';
2
+ import { type AnyObject, type IANATimezone, type IsoDate, type IsoDateTime, type NumberEnum, type StringEnum, type StringMap, type UnixTimestamp, type UnixTimestampMillis } from '@naturalcycles/js-lib/types';
3
3
  export declare const j: {
4
4
  string(): JsonSchemaStringBuilder<string, string, false>;
5
5
  number(): JsonSchemaNumberBuilder<number, number, false>;
@@ -12,7 +12,7 @@ export declare const j: {
12
12
  array<IN, OUT, Opt>(itemSchema: JsonSchemaAnyBuilder<IN, OUT, Opt>): JsonSchemaArrayBuilder<IN, OUT, Opt>;
13
13
  set<IN, OUT, Opt>(itemSchema: JsonSchemaAnyBuilder<IN, OUT, Opt>): JsonSchemaSet2Builder<IN, OUT, Opt>;
14
14
  buffer(): JsonSchemaBufferBuilder;
15
- enum<const T extends readonly (string | number | boolean | null)[] | StringEnum | NumberEnum>(input: T): JsonSchemaEnumBuilder<T extends readonly (infer U)[] ? U : T extends StringEnum ? T[keyof T] : T extends NumberEnum ? T[keyof T] : never>;
15
+ enum<const T extends readonly (string | number | boolean | null)[] | StringEnum | NumberEnum>(input: T, opt?: JsonBuilderRuleOpt): JsonSchemaEnumBuilder<T extends readonly (infer U)[] ? U : T extends StringEnum ? T[keyof T] : T extends NumberEnum ? T[keyof T] : never>;
16
16
  oneOf<B extends readonly JsonSchemaAnyBuilder<any, any, boolean>[], IN = BuilderInUnion<B>, OUT = BuilderOutUnion<B>>(items: [...B]): JsonSchemaAnyBuilder<IN, OUT, false>;
17
17
  };
18
18
  export declare class JsonSchemaAnyBuilder<IN, OUT, Opt> {
@@ -91,6 +91,13 @@ export declare class JsonSchemaStringBuilder<IN extends string = string, OUT = I
91
91
  languageTag(): this;
92
92
  countryCode(): this;
93
93
  currency(): this;
94
+ /**
95
+ * Validates that the input is a valid IANATimzone value.
96
+ *
97
+ * All previous expectations in the schema chain are dropped - including `.optional()` -
98
+ * because this call effectively starts a new schema chain as an `enum` validation.
99
+ */
100
+ ianaTimezone(): JsonSchemaEnumBuilder<string | IANATimezone, IANATimezone, false>;
94
101
  }
95
102
  export interface JsonSchemaStringEmailOptions {
96
103
  checkTLD: boolean;
@@ -177,7 +184,8 @@ export declare class JsonSchemaBufferBuilder extends JsonSchemaAnyBuilder<string
177
184
  constructor();
178
185
  }
179
186
  export declare class JsonSchemaEnumBuilder<IN extends string | number | boolean | null, OUT extends IN = IN, Opt extends boolean = false> extends JsonSchemaAnyBuilder<IN, OUT, Opt> {
180
- constructor(enumValues: readonly IN[]);
187
+ constructor(enumValues: readonly IN[], opt?: JsonBuilderRuleOpt);
188
+ branded<B extends IN>(): JsonSchemaEnumBuilder<B | IN, B, Opt>;
181
189
  }
182
190
  export interface JsonSchema<IN = unknown, OUT = IN> {
183
191
  readonly in?: IN;
@@ -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();
@@ -32,7 +32,7 @@ export const j = {
32
32
  buffer() {
33
33
  return new JsonSchemaBufferBuilder();
34
34
  },
35
- enum(input) {
35
+ enum(input, opt) {
36
36
  let enumValues;
37
37
  if (Array.isArray(input)) {
38
38
  enumValues = input;
@@ -47,7 +47,7 @@ export const j = {
47
47
  }
48
48
  }
49
49
  _assert(enumValues, 'Unsupported enum input');
50
- return new JsonSchemaEnumBuilder(enumValues);
50
+ return new JsonSchemaEnumBuilder(enumValues, opt);
51
51
  },
52
52
  oneOf(items) {
53
53
  const schemas = items.map(b => b.build());
@@ -275,6 +275,18 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
275
275
  const regex = /^[A-Z]{3}$/;
276
276
  return this.regex(regex, { msg: 'is not a valid currency format' });
277
277
  }
278
+ /**
279
+ * Validates that the input is a valid IANATimzone value.
280
+ *
281
+ * All previous expectations in the schema chain are dropped - including `.optional()` -
282
+ * because this call effectively starts a new schema chain as an `enum` validation.
283
+ */
284
+ ianaTimezone() {
285
+ // UTC is added to assist unit-testing, which uses UTC by default (not technically a valid Iana timezone identifier)
286
+ return j
287
+ .enum([...Intl.supportedValuesOf('timeZone'), 'UTC'], { msg: 'is an invalid IANA timezone' })
288
+ .branded();
289
+ }
278
290
  }
279
291
  export class JsonSchemaNumberBuilder extends JsonSchemaAnyBuilder {
280
292
  constructor() {
@@ -520,8 +532,13 @@ export class JsonSchemaBufferBuilder extends JsonSchemaAnyBuilder {
520
532
  }
521
533
  }
522
534
  export class JsonSchemaEnumBuilder extends JsonSchemaAnyBuilder {
523
- constructor(enumValues) {
535
+ constructor(enumValues, opt) {
524
536
  super({ enum: enumValues });
537
+ if (opt?.msg)
538
+ this.setErrorMessage('enum', opt.msg);
539
+ }
540
+ branded() {
541
+ return this;
525
542
  }
526
543
  }
527
544
  function object(props) {
@@ -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.1",
4
+ "version": "15.44.0",
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
@@ -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
- Yet another validation library. But why? Main reasons:
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,11 +9,11 @@ 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 {
16
15
  type AnyObject,
16
+ type IANATimezone,
17
17
  type IsoDate,
18
18
  type IsoDateTime,
19
19
  JWT_REGEX,
@@ -23,6 +23,7 @@ import {
23
23
  type UnixTimestamp,
24
24
  type UnixTimestampMillis,
25
25
  } from '@naturalcycles/js-lib/types'
26
+ import { JSON_SCHEMA_ORDER, mergeJsonSchemaObjects } from './jsonSchemaBuilder.util.js'
26
27
 
27
28
  export const j = {
28
29
  string(): JsonSchemaStringBuilder<string, string, false> {
@@ -63,6 +64,7 @@ export const j = {
63
64
 
64
65
  enum<const T extends readonly (string | number | boolean | null)[] | StringEnum | NumberEnum>(
65
66
  input: T,
67
+ opt?: JsonBuilderRuleOpt,
66
68
  ): JsonSchemaEnumBuilder<
67
69
  T extends readonly (infer U)[]
68
70
  ? U
@@ -86,7 +88,7 @@ export const j = {
86
88
  }
87
89
 
88
90
  _assert(enumValues, 'Unsupported enum input')
89
- return new JsonSchemaEnumBuilder(enumValues as any)
91
+ return new JsonSchemaEnumBuilder(enumValues as any, opt)
90
92
  },
91
93
 
92
94
  oneOf<
@@ -369,6 +371,19 @@ export class JsonSchemaStringBuilder<
369
371
  const regex = /^[A-Z]{3}$/
370
372
  return this.regex(regex, { msg: 'is not a valid currency format' })
371
373
  }
374
+
375
+ /**
376
+ * Validates that the input is a valid IANATimzone value.
377
+ *
378
+ * All previous expectations in the schema chain are dropped - including `.optional()` -
379
+ * because this call effectively starts a new schema chain as an `enum` validation.
380
+ */
381
+ ianaTimezone(): JsonSchemaEnumBuilder<string | IANATimezone, IANATimezone, false> {
382
+ // UTC is added to assist unit-testing, which uses UTC by default (not technically a valid Iana timezone identifier)
383
+ return j
384
+ .enum([...Intl.supportedValuesOf('timeZone'), 'UTC'], { msg: 'is an invalid IANA timezone' })
385
+ .branded<IANATimezone>()
386
+ }
372
387
  }
373
388
 
374
389
  export interface JsonSchemaStringEmailOptions {
@@ -750,8 +765,14 @@ export class JsonSchemaEnumBuilder<
750
765
  OUT extends IN = IN,
751
766
  Opt extends boolean = false,
752
767
  > extends JsonSchemaAnyBuilder<IN, OUT, Opt> {
753
- constructor(enumValues: readonly IN[]) {
768
+ constructor(enumValues: readonly IN[], opt?: JsonBuilderRuleOpt) {
754
769
  super({ enum: enumValues })
770
+
771
+ if (opt?.msg) this.setErrorMessage('enum', opt.msg)
772
+ }
773
+
774
+ branded<B extends IN>(): JsonSchemaEnumBuilder<B | IN, B, Opt> {
775
+ return this as unknown as JsonSchemaEnumBuilder<B | IN, B, Opt>
755
776
  }
756
777
  }
757
778
 
@@ -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
+ }