@naturalcycles/nodejs-lib 15.79.0 → 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,14 +54,31 @@ 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
  };
@@ -348,6 +365,10 @@ export declare class JsonSchemaTupleBuilder<ITEMS extends JsonSchemaAnyBuilder<a
348
365
  private readonly _items;
349
366
  constructor(items: ITEMS);
350
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
+ }
351
372
  type EnumBaseType = 'string' | 'number' | 'other';
352
373
  export interface JsonSchema<IN = unknown, OUT = IN> {
353
374
  readonly in?: IN;
@@ -415,6 +436,10 @@ export interface JsonSchema<IN = unknown, OUT = IN> {
415
436
  keySchema?: JsonSchema;
416
437
  minProperties2?: number;
417
438
  exclusiveProperties?: (readonly string[])[];
439
+ anyOfBy?: {
440
+ propertyName: string;
441
+ schemaDictionary: Record<string, JsonSchema>;
442
+ };
418
443
  }
419
444
  declare function object(props: AnyObject): never;
420
445
  declare function object<IN extends AnyObject>(props: {
@@ -476,6 +501,18 @@ type BuilderOutUnion<B extends readonly JsonSchemaAnyBuilder<any, any, any>[]> =
476
501
  type BuilderInUnion<B extends readonly JsonSchemaAnyBuilder<any, any, any>[]> = {
477
502
  [K in keyof B]: B[K] extends JsonSchemaAnyBuilder<infer I, any, any> ? I : never;
478
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;
479
516
  type BuilderFor<T> = JsonSchemaAnyBuilder<any, T, any>;
480
517
  interface JsonBuilderRuleOpt {
481
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: () => {
@@ -800,6 +821,22 @@ export class JsonSchemaTupleBuilder extends JsonSchemaAnyBuilder {
800
821
  this._items = items;
801
822
  }
802
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
+ }
803
840
  function object(props) {
804
841
  return new JsonSchemaObjectBuilder(props);
805
842
  }
@@ -862,3 +899,18 @@ function withEnumKeys(keys, schema) {
862
899
  const props = Object.fromEntries(typedValues.map(key => [key, schema]));
863
900
  return new JsonSchemaObjectBuilder(props, { hasIsOfTypeCheck: false });
864
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.79.0",
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: () => {
@@ -1204,6 +1239,34 @@ export class JsonSchemaTupleBuilder<
1204
1239
  }
1205
1240
  }
1206
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
+
1207
1270
  type EnumBaseType = 'string' | 'number' | 'other'
1208
1271
 
1209
1272
  export interface JsonSchema<IN = unknown, OUT = IN> {
@@ -1283,6 +1346,10 @@ export interface JsonSchema<IN = unknown, OUT = IN> {
1283
1346
  keySchema?: JsonSchema
1284
1347
  minProperties2?: number
1285
1348
  exclusiveProperties?: (readonly string[])[]
1349
+ anyOfBy?: {
1350
+ propertyName: string
1351
+ schemaDictionary: Record<string, JsonSchema>
1352
+ }
1286
1353
  }
1287
1354
 
1288
1355
  function object(props: AnyObject): never
@@ -1448,6 +1515,20 @@ function withEnumKeys<
1448
1515
  >(props, { hasIsOfTypeCheck: false })
1449
1516
  }
1450
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
+
1451
1532
  type Expand<T> = { [K in keyof T]: T[K] }
1452
1533
 
1453
1534
  type StripIndexSignatureDeep<T> = T extends readonly unknown[]
@@ -1511,6 +1592,20 @@ type BuilderInUnion<B extends readonly JsonSchemaAnyBuilder<any, any, any>[]> =
1511
1592
  [K in keyof B]: B[K] extends JsonSchemaAnyBuilder<infer I, any, any> ? I : never
1512
1593
  }[number]
1513
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
+
1514
1609
  type BuilderFor<T> = JsonSchemaAnyBuilder<any, T, any>
1515
1610
 
1516
1611
  interface JsonBuilderRuleOpt {