@naturalcycles/nodejs-lib 15.78.2 → 15.80.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 { _isBetween, _lazyValue } from '@naturalcycles/js-lib';
2
- import { Set2 } from '@naturalcycles/js-lib/object';
2
+ import { _mapObject, Set2 } from '@naturalcycles/js-lib/object';
3
3
  import { _substringAfterLast } from '@naturalcycles/js-lib/string';
4
4
  import { Ajv2020 } from 'ajv/dist/2020.js';
5
5
  import { validTLDs } from '../tlds.js';
@@ -477,6 +477,37 @@ export function createAjv(opt) {
477
477
  return true;
478
478
  },
479
479
  });
480
+ ajv.addKeyword({
481
+ keyword: 'anyOfBy',
482
+ type: 'object',
483
+ modifying: true,
484
+ errors: false,
485
+ schemaType: 'object',
486
+ compile(config, _parentSchema, _it) {
487
+ const { propertyName, schemaDictionary } = config;
488
+ const isValidFnByKey = _mapObject(schemaDictionary, (key, value) => {
489
+ return [key, ajv.compile(value)];
490
+ });
491
+ function validate(data, ctx) {
492
+ if (typeof data !== 'object' || data === null)
493
+ return true;
494
+ const determinant = data[propertyName];
495
+ const isValidFn = isValidFnByKey[determinant];
496
+ if (!isValidFn) {
497
+ ;
498
+ validate.errors = [
499
+ {
500
+ instancePath: ctx?.instancePath ?? '',
501
+ message: `could not find a suitable schema to validate against based on "${propertyName}"`,
502
+ },
503
+ ];
504
+ return false;
505
+ }
506
+ return isValidFn(data);
507
+ }
508
+ return validate;
509
+ },
510
+ });
480
511
  return ajv;
481
512
  }
482
513
  const monthLengths = [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
@@ -54,17 +54,35 @@ export declare const j: {
54
54
  set<IN, OUT, Opt>(itemSchema: JsonSchemaAnyBuilder<IN, OUT, Opt>): JsonSchemaSet2Builder<IN, OUT, Opt>;
55
55
  buffer(): JsonSchemaBufferBuilder;
56
56
  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>;
57
+ /**
58
+ * Use only with primitive values, otherwise this function will throw to avoid bugs.
59
+ * To validate objects, use `anyOfBy`.
60
+ *
61
+ * Our Ajv is configured to strip unexpected properties from objects,
62
+ * and since Ajv is mutating the input, this means that it cannot
63
+ * properly validate the same data over multiple schemas.
64
+ *
65
+ * Use `anyOf` when schemas may overlap (e.g., AccountId | PartnerId with same format).
66
+ * Use `oneOf` when schemas are mutually exclusive.
67
+ */
57
68
  oneOf<B extends readonly JsonSchemaAnyBuilder<any, any, boolean>[], IN = BuilderInUnion<B>, OUT = BuilderOutUnion<B>>(items: [...B]): JsonSchemaAnyBuilder<IN, OUT, false>;
58
69
  /**
59
- * Value must match at least one of the provided schemas.
70
+ * Use only with primitive values, otherwise this function will throw to avoid bugs.
71
+ * To validate objects, use `anyOfBy`.
72
+ *
73
+ * Our Ajv is configured to strip unexpected properties from objects,
74
+ * and since Ajv is mutating the input, this means that it cannot
75
+ * properly validate the same data over multiple schemas.
60
76
  *
61
77
  * Use `anyOf` when schemas may overlap (e.g., AccountId | PartnerId with same format).
62
78
  * Use `oneOf` when schemas are mutually exclusive.
63
79
  */
64
80
  anyOf<B extends readonly JsonSchemaAnyBuilder<any, any, boolean>[], IN = BuilderInUnion<B>, OUT = BuilderOutUnion<B>>(items: [...B]): JsonSchemaAnyBuilder<IN, OUT, false>;
81
+ anyOfBy<P extends string, D extends Record<PropertyKey, JsonSchemaTerminal<any, any, any>>, IN = AnyOfByIn<D>, OUT = AnyOfByOut<D>>(propertyName: P, schemaDictionary: D): JsonSchemaAnyOfByBuilder<IN, OUT, P>;
65
82
  and(): {
66
83
  silentBob: () => never;
67
84
  };
85
+ literal<const V extends string | number | boolean | null>(v: V): JsonSchemaEnumBuilder<V, V, false>;
68
86
  };
69
87
  export declare class JsonSchemaTerminal<IN, OUT, Opt> {
70
88
  protected schema: JsonSchema;
@@ -347,6 +365,10 @@ export declare class JsonSchemaTupleBuilder<ITEMS extends JsonSchemaAnyBuilder<a
347
365
  private readonly _items;
348
366
  constructor(items: ITEMS);
349
367
  }
368
+ export declare class JsonSchemaAnyOfByBuilder<IN, OUT, _P extends string = string> extends JsonSchemaAnyBuilder<AnyOfByInput<IN, _P> | IN, OUT, false> {
369
+ in: IN;
370
+ constructor(propertyName: string, schemaDictionary: Record<PropertyKey, JsonSchemaTerminal<any, any, any>>);
371
+ }
350
372
  type EnumBaseType = 'string' | 'number' | 'other';
351
373
  export interface JsonSchema<IN = unknown, OUT = IN> {
352
374
  readonly in?: IN;
@@ -414,6 +436,10 @@ export interface JsonSchema<IN = unknown, OUT = IN> {
414
436
  keySchema?: JsonSchema;
415
437
  minProperties2?: number;
416
438
  exclusiveProperties?: (readonly string[])[];
439
+ anyOfBy?: {
440
+ propertyName: string;
441
+ schemaDictionary: Record<string, JsonSchema>;
442
+ };
417
443
  }
418
444
  declare function object(props: AnyObject): never;
419
445
  declare function object<IN extends AnyObject>(props: {
@@ -475,6 +501,18 @@ type BuilderOutUnion<B extends readonly JsonSchemaAnyBuilder<any, any, any>[]> =
475
501
  type BuilderInUnion<B extends readonly JsonSchemaAnyBuilder<any, any, any>[]> = {
476
502
  [K in keyof B]: B[K] extends JsonSchemaAnyBuilder<infer I, any, any> ? I : never;
477
503
  }[number];
504
+ type AnyOfByIn<D extends Record<PropertyKey, JsonSchemaTerminal<any, any, any>>> = {
505
+ [K in keyof D]: D[K] extends JsonSchemaTerminal<infer I, any, any> ? I : never;
506
+ }[keyof D];
507
+ type AnyOfByOut<D extends Record<PropertyKey, JsonSchemaTerminal<any, any, any>>> = {
508
+ [K in keyof D]: D[K] extends JsonSchemaTerminal<any, infer O, any> ? O : never;
509
+ }[keyof D];
510
+ type AnyOfByDiscriminant<IN, P extends string> = IN extends {
511
+ [K in P]: infer V;
512
+ } ? V : never;
513
+ type AnyOfByInput<IN, P extends string, D = AnyOfByDiscriminant<IN, P>> = IN extends unknown ? Omit<Partial<IN>, P> & {
514
+ [K in P]?: D;
515
+ } : never;
478
516
  type BuilderFor<T> = JsonSchemaAnyBuilder<any, T, any>;
479
517
  interface JsonBuilderRuleOpt {
480
518
  /**
@@ -113,24 +113,45 @@ export const j = {
113
113
  _assert(enumValues, 'Unsupported enum input');
114
114
  return new JsonSchemaEnumBuilder(enumValues, baseType, opt);
115
115
  },
116
+ /**
117
+ * Use only with primitive values, otherwise this function will throw to avoid bugs.
118
+ * To validate objects, use `anyOfBy`.
119
+ *
120
+ * Our Ajv is configured to strip unexpected properties from objects,
121
+ * and since Ajv is mutating the input, this means that it cannot
122
+ * properly validate the same data over multiple schemas.
123
+ *
124
+ * Use `anyOf` when schemas may overlap (e.g., AccountId | PartnerId with same format).
125
+ * Use `oneOf` when schemas are mutually exclusive.
126
+ */
116
127
  oneOf(items) {
117
128
  const schemas = items.map(b => b.build());
129
+ _assert(schemas.every(hasOnlySchemasForPrimitives), 'Do not use `oneOf` validation with non-primitive types!');
118
130
  return new JsonSchemaAnyBuilder({
119
131
  oneOf: schemas,
120
132
  });
121
133
  },
122
134
  /**
123
- * Value must match at least one of the provided schemas.
135
+ * Use only with primitive values, otherwise this function will throw to avoid bugs.
136
+ * To validate objects, use `anyOfBy`.
137
+ *
138
+ * Our Ajv is configured to strip unexpected properties from objects,
139
+ * and since Ajv is mutating the input, this means that it cannot
140
+ * properly validate the same data over multiple schemas.
124
141
  *
125
142
  * Use `anyOf` when schemas may overlap (e.g., AccountId | PartnerId with same format).
126
143
  * Use `oneOf` when schemas are mutually exclusive.
127
144
  */
128
145
  anyOf(items) {
129
146
  const schemas = items.map(b => b.build());
147
+ _assert(schemas.every(hasOnlySchemasForPrimitives), 'Do not use `anyOf` validation with non-primitive types!');
130
148
  return new JsonSchemaAnyBuilder({
131
149
  anyOf: schemas,
132
150
  });
133
151
  },
152
+ anyOfBy(propertyName, schemaDictionary) {
153
+ return new JsonSchemaAnyOfByBuilder(propertyName, schemaDictionary);
154
+ },
134
155
  and() {
135
156
  return {
136
157
  silentBob: () => {
@@ -138,6 +159,14 @@ export const j = {
138
159
  },
139
160
  };
140
161
  },
162
+ literal(v) {
163
+ let baseType = 'other';
164
+ if (typeof v === 'string')
165
+ baseType = 'string';
166
+ if (typeof v === 'number')
167
+ baseType = 'number';
168
+ return new JsonSchemaEnumBuilder([v], baseType);
169
+ },
141
170
  };
142
171
  const TS_2500 = 16725225600; // 2500-01-01
143
172
  const TS_2500_MILLIS = TS_2500 * 1000;
@@ -792,6 +821,22 @@ export class JsonSchemaTupleBuilder extends JsonSchemaAnyBuilder {
792
821
  this._items = items;
793
822
  }
794
823
  }
824
+ export class JsonSchemaAnyOfByBuilder extends JsonSchemaAnyBuilder {
825
+ constructor(propertyName, schemaDictionary) {
826
+ const builtSchemaDictionary = {};
827
+ for (const [key, schema] of Object.entries(schemaDictionary)) {
828
+ builtSchemaDictionary[key] = schema.build();
829
+ }
830
+ super({
831
+ type: 'object',
832
+ hasIsOfTypeCheck: true,
833
+ anyOfBy: {
834
+ propertyName,
835
+ schemaDictionary: builtSchemaDictionary,
836
+ },
837
+ });
838
+ }
839
+ }
795
840
  function object(props) {
796
841
  return new JsonSchemaObjectBuilder(props);
797
842
  }
@@ -854,3 +899,18 @@ function withEnumKeys(keys, schema) {
854
899
  const props = Object.fromEntries(typedValues.map(key => [key, schema]));
855
900
  return new JsonSchemaObjectBuilder(props, { hasIsOfTypeCheck: false });
856
901
  }
902
+ function hasOnlySchemasForPrimitives(schema) {
903
+ if (Array.isArray(schema.type)) {
904
+ schema.type.every(type => ['string', 'number', 'integer', 'boolean', 'null'].includes(type));
905
+ }
906
+ else if (schema.anyOf) {
907
+ return schema.anyOf.every(hasOnlySchemasForPrimitives);
908
+ }
909
+ else if (schema.oneOf) {
910
+ return schema.oneOf.every(hasOnlySchemasForPrimitives);
911
+ }
912
+ else {
913
+ return !!schema.type && ['string', 'number', 'integer', 'boolean', 'null'].includes(schema.type);
914
+ }
915
+ return false;
916
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/nodejs-lib",
3
3
  "type": "module",
4
- "version": "15.78.2",
4
+ "version": "15.80.0",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@types/js-yaml": "^4",
@@ -1,5 +1,5 @@
1
1
  import { _isBetween, _lazyValue } from '@naturalcycles/js-lib'
2
- import { Set2 } from '@naturalcycles/js-lib/object'
2
+ import { _mapObject, Set2 } from '@naturalcycles/js-lib/object'
3
3
  import { _substringAfterLast } from '@naturalcycles/js-lib/string'
4
4
  import type { AnyObject } from '@naturalcycles/js-lib/types'
5
5
  import { Ajv2020, type Options, type ValidateFunction } from 'ajv/dist/2020.js'
@@ -8,6 +8,7 @@ import type {
8
8
  JsonSchemaIsoDateOptions,
9
9
  JsonSchemaIsoMonthOptions,
10
10
  JsonSchemaStringEmailOptions,
11
+ JsonSchemaTerminal,
11
12
  } from './jsonSchemaBuilder.js'
12
13
 
13
14
  /* eslint-disable @typescript-eslint/prefer-string-starts-ends-with */
@@ -542,6 +543,44 @@ export function createAjv(opt?: Options): Ajv2020 {
542
543
  },
543
544
  })
544
545
 
546
+ ajv.addKeyword({
547
+ keyword: 'anyOfBy',
548
+ type: 'object',
549
+ modifying: true,
550
+ errors: false,
551
+ schemaType: 'object',
552
+ compile(config, _parentSchema, _it) {
553
+ const { propertyName, schemaDictionary } = config
554
+
555
+ const isValidFnByKey: Record<any, ValidateFunction> = _mapObject(
556
+ schemaDictionary as Record<any, JsonSchemaTerminal<any, any, any>>,
557
+ (key, value) => {
558
+ return [key, ajv.compile(value)]
559
+ },
560
+ )
561
+
562
+ function validate(data: AnyObject, ctx: any): boolean {
563
+ if (typeof data !== 'object' || data === null) return true
564
+
565
+ const determinant = data[propertyName]
566
+ const isValidFn = isValidFnByKey[determinant]
567
+ if (!isValidFn) {
568
+ ;(validate as any).errors = [
569
+ {
570
+ instancePath: ctx?.instancePath ?? '',
571
+ message: `could not find a suitable schema to validate against based on "${propertyName}"`,
572
+ },
573
+ ]
574
+ return false
575
+ }
576
+
577
+ return isValidFn(data)
578
+ }
579
+
580
+ return validate
581
+ },
582
+ })
583
+
545
584
  return ajv
546
585
  }
547
586
 
@@ -188,19 +188,40 @@ export const j = {
188
188
  return new JsonSchemaEnumBuilder(enumValues as any, baseType, opt)
189
189
  },
190
190
 
191
+ /**
192
+ * Use only with primitive values, otherwise this function will throw to avoid bugs.
193
+ * To validate objects, use `anyOfBy`.
194
+ *
195
+ * Our Ajv is configured to strip unexpected properties from objects,
196
+ * and since Ajv is mutating the input, this means that it cannot
197
+ * properly validate the same data over multiple schemas.
198
+ *
199
+ * Use `anyOf` when schemas may overlap (e.g., AccountId | PartnerId with same format).
200
+ * Use `oneOf` when schemas are mutually exclusive.
201
+ */
191
202
  oneOf<
192
203
  B extends readonly JsonSchemaAnyBuilder<any, any, boolean>[],
193
204
  IN = BuilderInUnion<B>,
194
205
  OUT = BuilderOutUnion<B>,
195
206
  >(items: [...B]): JsonSchemaAnyBuilder<IN, OUT, false> {
196
207
  const schemas = items.map(b => b.build())
208
+ _assert(
209
+ schemas.every(hasOnlySchemasForPrimitives),
210
+ 'Do not use `oneOf` validation with non-primitive types!',
211
+ )
212
+
197
213
  return new JsonSchemaAnyBuilder<IN, OUT, false>({
198
214
  oneOf: schemas,
199
215
  })
200
216
  },
201
217
 
202
218
  /**
203
- * Value must match at least one of the provided schemas.
219
+ * Use only with primitive values, otherwise this function will throw to avoid bugs.
220
+ * To validate objects, use `anyOfBy`.
221
+ *
222
+ * Our Ajv is configured to strip unexpected properties from objects,
223
+ * and since Ajv is mutating the input, this means that it cannot
224
+ * properly validate the same data over multiple schemas.
204
225
  *
205
226
  * Use `anyOf` when schemas may overlap (e.g., AccountId | PartnerId with same format).
206
227
  * Use `oneOf` when schemas are mutually exclusive.
@@ -211,11 +232,25 @@ export const j = {
211
232
  OUT = BuilderOutUnion<B>,
212
233
  >(items: [...B]): JsonSchemaAnyBuilder<IN, OUT, false> {
213
234
  const schemas = items.map(b => b.build())
235
+ _assert(
236
+ schemas.every(hasOnlySchemasForPrimitives),
237
+ 'Do not use `anyOf` validation with non-primitive types!',
238
+ )
239
+
214
240
  return new JsonSchemaAnyBuilder<IN, OUT, false>({
215
241
  anyOf: schemas,
216
242
  })
217
243
  },
218
244
 
245
+ anyOfBy<
246
+ P extends string,
247
+ D extends Record<PropertyKey, JsonSchemaTerminal<any, any, any>>,
248
+ IN = AnyOfByIn<D>,
249
+ OUT = AnyOfByOut<D>,
250
+ >(propertyName: P, schemaDictionary: D): JsonSchemaAnyOfByBuilder<IN, OUT, P> {
251
+ return new JsonSchemaAnyOfByBuilder<IN, OUT, P>(propertyName, schemaDictionary)
252
+ },
253
+
219
254
  and() {
220
255
  return {
221
256
  silentBob: () => {
@@ -223,6 +258,13 @@ export const j = {
223
258
  },
224
259
  }
225
260
  },
261
+
262
+ literal<const V extends string | number | boolean | null>(v: V) {
263
+ let baseType: EnumBaseType = 'other'
264
+ if (typeof v === 'string') baseType = 'string'
265
+ if (typeof v === 'number') baseType = 'number'
266
+ return new JsonSchemaEnumBuilder<V>([v], baseType)
267
+ },
226
268
  }
227
269
 
228
270
  const TS_2500 = 16725225600 // 2500-01-01
@@ -1197,6 +1239,34 @@ export class JsonSchemaTupleBuilder<
1197
1239
  }
1198
1240
  }
1199
1241
 
1242
+ export class JsonSchemaAnyOfByBuilder<
1243
+ IN,
1244
+ OUT,
1245
+ // eslint-disable-next-line @typescript-eslint/naming-convention
1246
+ _P extends string = string,
1247
+ > extends JsonSchemaAnyBuilder<AnyOfByInput<IN, _P> | IN, OUT, false> {
1248
+ declare in: IN
1249
+
1250
+ constructor(
1251
+ propertyName: string,
1252
+ schemaDictionary: Record<PropertyKey, JsonSchemaTerminal<any, any, any>>,
1253
+ ) {
1254
+ const builtSchemaDictionary: Record<string, JsonSchema> = {}
1255
+ for (const [key, schema] of Object.entries(schemaDictionary)) {
1256
+ builtSchemaDictionary[key] = schema.build()
1257
+ }
1258
+
1259
+ super({
1260
+ type: 'object',
1261
+ hasIsOfTypeCheck: true,
1262
+ anyOfBy: {
1263
+ propertyName,
1264
+ schemaDictionary: builtSchemaDictionary,
1265
+ },
1266
+ })
1267
+ }
1268
+ }
1269
+
1200
1270
  type EnumBaseType = 'string' | 'number' | 'other'
1201
1271
 
1202
1272
  export interface JsonSchema<IN = unknown, OUT = IN> {
@@ -1276,6 +1346,10 @@ export interface JsonSchema<IN = unknown, OUT = IN> {
1276
1346
  keySchema?: JsonSchema
1277
1347
  minProperties2?: number
1278
1348
  exclusiveProperties?: (readonly string[])[]
1349
+ anyOfBy?: {
1350
+ propertyName: string
1351
+ schemaDictionary: Record<string, JsonSchema>
1352
+ }
1279
1353
  }
1280
1354
 
1281
1355
  function object(props: AnyObject): never
@@ -1441,6 +1515,20 @@ function withEnumKeys<
1441
1515
  >(props, { hasIsOfTypeCheck: false })
1442
1516
  }
1443
1517
 
1518
+ function hasOnlySchemasForPrimitives(schema: JsonSchema): boolean {
1519
+ if (Array.isArray(schema.type)) {
1520
+ schema.type.every(type => ['string', 'number', 'integer', 'boolean', 'null'].includes(type))
1521
+ } else if (schema.anyOf) {
1522
+ return schema.anyOf.every(hasOnlySchemasForPrimitives)
1523
+ } else if (schema.oneOf) {
1524
+ return schema.oneOf.every(hasOnlySchemasForPrimitives)
1525
+ } else {
1526
+ return !!schema.type && ['string', 'number', 'integer', 'boolean', 'null'].includes(schema.type)
1527
+ }
1528
+
1529
+ return false
1530
+ }
1531
+
1444
1532
  type Expand<T> = { [K in keyof T]: T[K] }
1445
1533
 
1446
1534
  type StripIndexSignatureDeep<T> = T extends readonly unknown[]
@@ -1504,6 +1592,20 @@ type BuilderInUnion<B extends readonly JsonSchemaAnyBuilder<any, any, any>[]> =
1504
1592
  [K in keyof B]: B[K] extends JsonSchemaAnyBuilder<infer I, any, any> ? I : never
1505
1593
  }[number]
1506
1594
 
1595
+ type AnyOfByIn<D extends Record<PropertyKey, JsonSchemaTerminal<any, any, any>>> = {
1596
+ [K in keyof D]: D[K] extends JsonSchemaTerminal<infer I, any, any> ? I : never
1597
+ }[keyof D]
1598
+
1599
+ type AnyOfByOut<D extends Record<PropertyKey, JsonSchemaTerminal<any, any, any>>> = {
1600
+ [K in keyof D]: D[K] extends JsonSchemaTerminal<any, infer O, any> ? O : never
1601
+ }[keyof D]
1602
+
1603
+ type AnyOfByDiscriminant<IN, P extends string> = IN extends { [K in P]: infer V } ? V : never
1604
+
1605
+ type AnyOfByInput<IN, P extends string, D = AnyOfByDiscriminant<IN, P>> = IN extends unknown
1606
+ ? Omit<Partial<IN>, P> & { [K in P]?: D }
1607
+ : never
1608
+
1507
1609
  type BuilderFor<T> = JsonSchemaAnyBuilder<any, T, any>
1508
1610
 
1509
1611
  interface JsonBuilderRuleOpt {