@naturalcycles/nodejs-lib 15.57.1 → 15.58.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.
@@ -374,6 +374,27 @@ export function createAjv(opt) {
374
374
  return true;
375
375
  },
376
376
  });
377
+ ajv.addKeyword({
378
+ keyword: 'keySchema',
379
+ type: 'object',
380
+ modifying: true,
381
+ errors: false,
382
+ schemaType: 'object',
383
+ compile(innerSchema, _parentSchema, _it) {
384
+ const isValidKeyFn = ajv.compile(innerSchema);
385
+ function validate(data, _ctx) {
386
+ if (typeof data !== 'object' || data === null)
387
+ return true;
388
+ for (const key of Object.keys(data)) {
389
+ if (!isValidKeyFn(key)) {
390
+ delete data[key];
391
+ }
392
+ }
393
+ return true;
394
+ }
395
+ return validate;
396
+ },
397
+ });
377
398
  return ajv;
378
399
  }
379
400
  const monthLengths = [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
@@ -9,7 +9,40 @@ export declare const j: {
9
9
  infer: typeof objectInfer;
10
10
  any(): JsonSchemaObjectBuilder<AnyObject, AnyObject, false>;
11
11
  stringMap<S extends JsonSchemaTerminal<any, any, any>>(schema: S): JsonSchemaObjectBuilder<StringMap<SchemaIn<S>>, StringMap<SchemaOut<S>>>;
12
+ /**
13
+ * @experimental Look around, maybe you find a rule that is better for your use-case.
14
+ *
15
+ * For Record<K, V> type of validations.
16
+ * ```ts
17
+ * const schema = j.object
18
+ * .record(
19
+ * j
20
+ * .string()
21
+ * .regex(/^\d{3,4}$/)
22
+ * .branded<B>(),
23
+ * j.number().nullable(),
24
+ * )
25
+ * .isOfType<Record<B, number | null>>()
26
+ * ```
27
+ *
28
+ * When the keys of the Record are values from an Enum, prefer `j.object.withEnumKeys`!
29
+ *
30
+ * Non-matching keys will be stripped from the object, i.e. they will not cause an error.
31
+ *
32
+ * Caveat: This rule first validates values of every properties of the object, and only then validates the keys.
33
+ * A consequence of that is that the validation will throw when there is an unexpected property with a value not matching the value schema.
34
+ */
35
+ record: typeof record;
36
+ /**
37
+ * For Record<ENUM, V> type of validations.
38
+ *
39
+ * When the keys of the Record are values from an Enum,
40
+ * this helper is more performant and behaves in a more conventional manner than `j.object.record` would.
41
+ *
42
+ *
43
+ */
12
44
  withEnumKeys: typeof withEnumKeys;
45
+ withRegexKeys: typeof withRegexKeys;
13
46
  };
14
47
  array<IN, OUT, Opt>(itemSchema: JsonSchemaAnyBuilder<IN, OUT, Opt>): JsonSchemaArrayBuilder<IN, OUT, Opt>;
15
48
  set<IN, OUT, Opt>(itemSchema: JsonSchemaAnyBuilder<IN, OUT, Opt>): JsonSchemaSet2Builder<IN, OUT, Opt>;
@@ -205,6 +238,7 @@ export declare class JsonSchemaObjectBuilder<IN extends AnyObject, OUT extends A
205
238
  interface JsonSchemaObjectBuilderOpts {
206
239
  hasIsOfTypeCheck?: false;
207
240
  patternProperties?: StringMap<JsonSchema<any, any>>;
241
+ keySchema?: JsonSchema;
208
242
  }
209
243
  export declare class JsonSchemaObjectInferringBuilder<PROPS extends Record<string, JsonSchemaAnyBuilder<any, any, any>>, Opt extends boolean = false> extends JsonSchemaAnyBuilder<Expand<{
210
244
  [K in keyof PROPS as PROPS[K] extends JsonSchemaAnyBuilder<any, any, infer IsOpt> ? IsOpt extends true ? never : K : never]: PROPS[K] extends JsonSchemaAnyBuilder<infer IN, any, any> ? IN : never;
@@ -316,6 +350,7 @@ export interface JsonSchema<IN = unknown, OUT = IN> {
316
350
  };
317
351
  errorMessages?: StringMap<string>;
318
352
  optionalValues?: (string | number | boolean)[];
353
+ keySchema?: JsonSchema;
319
354
  }
320
355
  declare function object(props: AnyObject): never;
321
356
  declare function object<IN extends AnyObject>(props: {
@@ -338,6 +373,8 @@ declare function objectDbEntity<IN extends BaseDBEntity & AnyObject, EXTRA_KEYS
338
373
  } : {
339
374
  updated: BuilderFor<IN['updated']>;
340
375
  })): JsonSchemaObjectBuilder<IN, IN, false>;
376
+ declare function record<KS extends JsonSchemaAnyBuilder<any, any, any>, VS extends JsonSchemaAnyBuilder<any, any, any>, Opt extends boolean = SchemaOpt<VS>>(keySchema: KS, valueSchema: VS): JsonSchemaObjectBuilder<Opt extends true ? Partial<Record<SchemaIn<KS>, SchemaIn<VS>>> : Record<SchemaIn<KS>, SchemaIn<VS>>, Opt extends true ? Partial<Record<SchemaOut<KS>, SchemaOut<VS>>> : Record<SchemaOut<KS>, SchemaOut<VS>>, false>;
377
+ declare function withRegexKeys<S extends JsonSchemaAnyBuilder<any, any, any>, Opt extends boolean = SchemaOpt<S>>(keyRegex: RegExp | string, schema: S): JsonSchemaObjectBuilder<Opt extends true ? StringMap<SchemaIn<S>> : StringMap<SchemaIn<S>>, Opt extends true ? StringMap<SchemaOut<S>> : StringMap<SchemaOut<S>>, false>;
341
378
  /**
342
379
  * Builds the object schema with the indicated `keys` and uses the `schema` for their validation.
343
380
  */
@@ -378,5 +415,5 @@ interface JsonBuilderRuleOpt {
378
415
  type EnumKeyUnion<T> = T extends readonly (infer U)[] ? U : T extends StringEnum | NumberEnum ? T[keyof T] : never;
379
416
  type SchemaIn<S> = S extends JsonSchemaAnyBuilder<infer IN, any, any> ? IN : never;
380
417
  type SchemaOut<S> = S extends JsonSchemaAnyBuilder<any, infer OUT, any> ? OUT : never;
381
- type SchemaOpt<S> = S extends JsonSchemaAnyBuilder<any, any, infer Opt> ? Opt : false;
418
+ type SchemaOpt<S> = S extends JsonSchemaAnyBuilder<any, any, infer Opt> ? (Opt extends true ? true : false) : false;
382
419
  export {};
@@ -33,7 +33,40 @@ export const j = {
33
33
  },
34
34
  });
35
35
  },
36
+ /**
37
+ * @experimental Look around, maybe you find a rule that is better for your use-case.
38
+ *
39
+ * For Record<K, V> type of validations.
40
+ * ```ts
41
+ * const schema = j.object
42
+ * .record(
43
+ * j
44
+ * .string()
45
+ * .regex(/^\d{3,4}$/)
46
+ * .branded<B>(),
47
+ * j.number().nullable(),
48
+ * )
49
+ * .isOfType<Record<B, number | null>>()
50
+ * ```
51
+ *
52
+ * When the keys of the Record are values from an Enum, prefer `j.object.withEnumKeys`!
53
+ *
54
+ * Non-matching keys will be stripped from the object, i.e. they will not cause an error.
55
+ *
56
+ * Caveat: This rule first validates values of every properties of the object, and only then validates the keys.
57
+ * A consequence of that is that the validation will throw when there is an unexpected property with a value not matching the value schema.
58
+ */
59
+ record,
60
+ /**
61
+ * For Record<ENUM, V> type of validations.
62
+ *
63
+ * When the keys of the Record are values from an Enum,
64
+ * this helper is more performant and behaves in a more conventional manner than `j.object.record` would.
65
+ *
66
+ *
67
+ */
36
68
  withEnumKeys,
69
+ withRegexKeys,
37
70
  }),
38
71
  array(itemSchema) {
39
72
  return new JsonSchemaArrayBuilder(itemSchema);
@@ -527,6 +560,7 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
527
560
  additionalProperties: false,
528
561
  hasIsOfTypeCheck: opt?.hasIsOfTypeCheck ?? true,
529
562
  patternProperties: opt?.patternProperties ?? undefined,
563
+ keySchema: opt?.keySchema ?? undefined,
530
564
  });
531
565
  if (props)
532
566
  this.addProperties(props);
@@ -715,6 +749,27 @@ function objectDbEntity(props) {
715
749
  ...props,
716
750
  });
717
751
  }
752
+ function record(keySchema, valueSchema) {
753
+ const keyJsonSchema = keySchema.build();
754
+ const valueJsonSchema = valueSchema.build();
755
+ return new JsonSchemaObjectBuilder([], {
756
+ hasIsOfTypeCheck: false,
757
+ keySchema: keyJsonSchema,
758
+ patternProperties: {
759
+ ['^.*$']: valueJsonSchema,
760
+ },
761
+ });
762
+ }
763
+ function withRegexKeys(keyRegex, schema) {
764
+ const pattern = keyRegex instanceof RegExp ? keyRegex.source : keyRegex;
765
+ const jsonSchema = schema.build();
766
+ return new JsonSchemaObjectBuilder([], {
767
+ hasIsOfTypeCheck: false,
768
+ patternProperties: {
769
+ [pattern]: jsonSchema,
770
+ },
771
+ });
772
+ }
718
773
  /**
719
774
  * Builds the object schema with the indicated `keys` and uses the `schema` for their validation.
720
775
  */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/nodejs-lib",
3
3
  "type": "module",
4
- "version": "15.57.1",
4
+ "version": "15.58.0",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@types/js-yaml": "^4",
@@ -1,6 +1,7 @@
1
1
  import { _lazyValue } from '@naturalcycles/js-lib'
2
2
  import { Set2 } from '@naturalcycles/js-lib/object'
3
3
  import { _substringAfterLast } from '@naturalcycles/js-lib/string'
4
+ import type { AnyObject } from '@naturalcycles/js-lib/types'
4
5
  import { Ajv, type Options, type ValidateFunction } from 'ajv'
5
6
  import { validTLDs } from '../tlds.js'
6
7
  import type { JsonSchemaIsoDateOptions, JsonSchemaStringEmailOptions } from './jsonSchemaBuilder.js'
@@ -429,6 +430,31 @@ export function createAjv(opt?: Options): Ajv {
429
430
  },
430
431
  })
431
432
 
433
+ ajv.addKeyword({
434
+ keyword: 'keySchema',
435
+ type: 'object',
436
+ modifying: true,
437
+ errors: false,
438
+ schemaType: 'object',
439
+ compile(innerSchema, _parentSchema, _it) {
440
+ const isValidKeyFn: ValidateFunction = ajv.compile(innerSchema)
441
+
442
+ function validate(data: AnyObject, _ctx: any): boolean {
443
+ if (typeof data !== 'object' || data === null) return true
444
+
445
+ for (const key of Object.keys(data)) {
446
+ if (!isValidKeyFn(key)) {
447
+ delete data[key]
448
+ }
449
+ }
450
+
451
+ return true
452
+ }
453
+
454
+ return validate
455
+ },
456
+ })
457
+
432
458
  return ajv
433
459
  }
434
460
 
@@ -82,7 +82,41 @@ export const j = {
82
82
  )
83
83
  },
84
84
 
85
+ /**
86
+ * @experimental Look around, maybe you find a rule that is better for your use-case.
87
+ *
88
+ * For Record<K, V> type of validations.
89
+ * ```ts
90
+ * const schema = j.object
91
+ * .record(
92
+ * j
93
+ * .string()
94
+ * .regex(/^\d{3,4}$/)
95
+ * .branded<B>(),
96
+ * j.number().nullable(),
97
+ * )
98
+ * .isOfType<Record<B, number | null>>()
99
+ * ```
100
+ *
101
+ * When the keys of the Record are values from an Enum, prefer `j.object.withEnumKeys`!
102
+ *
103
+ * Non-matching keys will be stripped from the object, i.e. they will not cause an error.
104
+ *
105
+ * Caveat: This rule first validates values of every properties of the object, and only then validates the keys.
106
+ * A consequence of that is that the validation will throw when there is an unexpected property with a value not matching the value schema.
107
+ */
108
+ record,
109
+
110
+ /**
111
+ * For Record<ENUM, V> type of validations.
112
+ *
113
+ * When the keys of the Record are values from an Enum,
114
+ * this helper is more performant and behaves in a more conventional manner than `j.object.record` would.
115
+ *
116
+ *
117
+ */
85
118
  withEnumKeys,
119
+ withRegexKeys,
86
120
  }),
87
121
 
88
122
  array<IN, OUT, Opt>(
@@ -749,6 +783,7 @@ export class JsonSchemaObjectBuilder<
749
783
  additionalProperties: false,
750
784
  hasIsOfTypeCheck: opt?.hasIsOfTypeCheck ?? true,
751
785
  patternProperties: opt?.patternProperties ?? undefined,
786
+ keySchema: opt?.keySchema ?? undefined,
752
787
  })
753
788
 
754
789
  if (props) this.addProperties(props)
@@ -820,6 +855,7 @@ export class JsonSchemaObjectBuilder<
820
855
  interface JsonSchemaObjectBuilderOpts {
821
856
  hasIsOfTypeCheck?: false
822
857
  patternProperties?: StringMap<JsonSchema<any, any>>
858
+ keySchema?: JsonSchema
823
859
  }
824
860
 
825
861
  export class JsonSchemaObjectInferringBuilder<
@@ -1107,6 +1143,7 @@ export interface JsonSchema<IN = unknown, OUT = IN> {
1107
1143
  transform?: { trim?: true; toLowerCase?: true; toUpperCase?: true; truncate?: number }
1108
1144
  errorMessages?: StringMap<string>
1109
1145
  optionalValues?: (string | number | boolean)[]
1146
+ keySchema?: JsonSchema
1110
1147
  }
1111
1148
 
1112
1149
  function object(props: AnyObject): never
@@ -1156,6 +1193,68 @@ function objectDbEntity(props: AnyObject): any {
1156
1193
  })
1157
1194
  }
1158
1195
 
1196
+ function record<
1197
+ KS extends JsonSchemaAnyBuilder<any, any, any>,
1198
+ VS extends JsonSchemaAnyBuilder<any, any, any>,
1199
+ Opt extends boolean = SchemaOpt<VS>,
1200
+ >(
1201
+ keySchema: KS,
1202
+ valueSchema: VS,
1203
+ ): JsonSchemaObjectBuilder<
1204
+ Opt extends true
1205
+ ? Partial<Record<SchemaIn<KS>, SchemaIn<VS>>>
1206
+ : Record<SchemaIn<KS>, SchemaIn<VS>>,
1207
+ Opt extends true
1208
+ ? Partial<Record<SchemaOut<KS>, SchemaOut<VS>>>
1209
+ : Record<SchemaOut<KS>, SchemaOut<VS>>,
1210
+ false
1211
+ > {
1212
+ const keyJsonSchema = keySchema.build()
1213
+ const valueJsonSchema = valueSchema.build()
1214
+
1215
+ return new JsonSchemaObjectBuilder<
1216
+ Opt extends true
1217
+ ? Partial<Record<SchemaIn<KS>, SchemaIn<VS>>>
1218
+ : Record<SchemaIn<KS>, SchemaIn<VS>>,
1219
+ Opt extends true
1220
+ ? Partial<Record<SchemaOut<KS>, SchemaOut<VS>>>
1221
+ : Record<SchemaOut<KS>, SchemaOut<VS>>,
1222
+ false
1223
+ >([], {
1224
+ hasIsOfTypeCheck: false,
1225
+ keySchema: keyJsonSchema,
1226
+ patternProperties: {
1227
+ ['^.*$']: valueJsonSchema,
1228
+ },
1229
+ })
1230
+ }
1231
+
1232
+ function withRegexKeys<
1233
+ S extends JsonSchemaAnyBuilder<any, any, any>,
1234
+ Opt extends boolean = SchemaOpt<S>,
1235
+ >(
1236
+ keyRegex: RegExp | string,
1237
+ schema: S,
1238
+ ): JsonSchemaObjectBuilder<
1239
+ Opt extends true ? StringMap<SchemaIn<S>> : StringMap<SchemaIn<S>>,
1240
+ Opt extends true ? StringMap<SchemaOut<S>> : StringMap<SchemaOut<S>>,
1241
+ false
1242
+ > {
1243
+ const pattern = keyRegex instanceof RegExp ? keyRegex.source : keyRegex
1244
+ const jsonSchema = schema.build()
1245
+
1246
+ return new JsonSchemaObjectBuilder<
1247
+ Opt extends true ? StringMap<SchemaIn<S>> : StringMap<SchemaIn<S>>,
1248
+ Opt extends true ? StringMap<SchemaOut<S>> : StringMap<SchemaOut<S>>,
1249
+ false
1250
+ >([], {
1251
+ hasIsOfTypeCheck: false,
1252
+ patternProperties: {
1253
+ [pattern]: jsonSchema,
1254
+ },
1255
+ })
1256
+ }
1257
+
1159
1258
  /**
1160
1259
  * Builds the object schema with the indicated `keys` and uses the `schema` for their validation.
1161
1260
  */
@@ -1245,4 +1344,5 @@ type EnumKeyUnion<T> =
1245
1344
 
1246
1345
  type SchemaIn<S> = S extends JsonSchemaAnyBuilder<infer IN, any, any> ? IN : never
1247
1346
  type SchemaOut<S> = S extends JsonSchemaAnyBuilder<any, infer OUT, any> ? OUT : never
1248
- type SchemaOpt<S> = S extends JsonSchemaAnyBuilder<any, any, infer Opt> ? Opt : false
1347
+ type SchemaOpt<S> =
1348
+ S extends JsonSchemaAnyBuilder<any, any, infer Opt> ? (Opt extends true ? true : false) : false