@naturalcycles/nodejs-lib 15.87.0 → 15.88.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.
@@ -389,7 +389,7 @@ export function createAjv(opt) {
389
389
  validate: function validate(optionalValues, data, _schema, ctx) {
390
390
  if (!optionalValues)
391
391
  return true;
392
- _assert(ctx?.parentData && ctx.parentDataProperty !== undefined, 'You should only use `optional([x, y, z]) on a property of an object, or on an element of an array due to Ajv mutation issues.');
392
+ _assert(ctx?.parentData && ctx.parentDataProperty !== undefined, 'You should only use `optional([x, y, z])` on a property of an object, or on an element of an array due to Ajv mutation issues.');
393
393
  if (!optionalValues.includes(data))
394
394
  return true;
395
395
  ctx.parentData[ctx.parentDataProperty] = undefined;
@@ -578,11 +578,51 @@ export function createAjv(opt) {
578
578
  validate: function validate(numberOfDigits, data, _schema, ctx) {
579
579
  if (!numberOfDigits)
580
580
  return true;
581
- _assert(ctx?.parentData && ctx.parentDataProperty !== undefined, 'You should only use `precision(n) on a property of an object, or on an element of an array due to Ajv mutation issues.');
581
+ _assert(ctx?.parentData && ctx.parentDataProperty !== undefined, 'You should only use `precision(n)` on a property of an object, or on an element of an array due to Ajv mutation issues.');
582
582
  ctx.parentData[ctx.parentDataProperty] = _round(data, 10 ** (-1 * numberOfDigits));
583
583
  return true;
584
584
  },
585
585
  });
586
+ ajv.addKeyword({
587
+ keyword: 'customValidations',
588
+ modifying: false,
589
+ errors: true,
590
+ schemaType: 'array',
591
+ validate: function validate(customValidations, data, _schema, ctx) {
592
+ if (!customValidations?.length)
593
+ return true;
594
+ for (const validator of customValidations) {
595
+ const error = validator(data);
596
+ if (error) {
597
+ ;
598
+ validate.errors = [
599
+ {
600
+ instancePath: ctx?.instancePath ?? '',
601
+ message: error,
602
+ },
603
+ ];
604
+ return false;
605
+ }
606
+ }
607
+ return true;
608
+ },
609
+ });
610
+ ajv.addKeyword({
611
+ keyword: 'customConversions',
612
+ modifying: true,
613
+ errors: false,
614
+ schemaType: 'array',
615
+ validate: function validate(customConversions, data, _schema, ctx) {
616
+ if (!customConversions?.length)
617
+ return true;
618
+ _assert(ctx?.parentData && ctx.parentDataProperty !== undefined, 'You should only use `convert()` on a property of an object, or on an element of an array due to Ajv mutation issues.');
619
+ for (const converter of customConversions) {
620
+ data = converter(data);
621
+ }
622
+ ctx.parentData[ctx.parentDataProperty] = data;
623
+ return true;
624
+ },
625
+ });
586
626
  return ajv;
587
627
  }
588
628
  const monthLengths = [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
@@ -161,6 +161,24 @@ export declare class JsonSchemaAnyBuilder<IN, OUT, Opt> extends JsonSchemaTermin
161
161
  * Locks the given schema chain and no other modification can be done to it.
162
162
  */
163
163
  final(): JsonSchemaTerminal<IN, OUT, Opt>;
164
+ /**
165
+ *
166
+ * @param validator A validator function that returns an error message or undefined.
167
+ *
168
+ * You may add multiple custom validators and they will be executed in the order you added them.
169
+ */
170
+ custom<OUT2 = OUT>(validator: CustomValidatorFn): JsonSchemaAnyBuilder<IN, OUT2, Opt>;
171
+ /**
172
+ *
173
+ * @param converter A converter function that returns a new value.
174
+ *
175
+ * You may add multiple converters and they will be executed in the order you added them,
176
+ * each converter receiving the result from the previous one.
177
+ *
178
+ * This feature only works when the current schema is nested in an object or array schema,
179
+ * due to how mutability works in Ajv.
180
+ */
181
+ convert<OUT2>(converter: CustomConverterFn<OUT2>): JsonSchemaAnyBuilder<IN, OUT2, Opt>;
164
182
  }
165
183
  export declare class JsonSchemaStringBuilder<IN extends string | undefined = string, OUT = IN, Opt extends boolean = false> extends JsonSchemaAnyBuilder<IN, OUT, Opt> {
166
184
  constructor();
@@ -520,6 +538,8 @@ export interface JsonSchema<IN = unknown, OUT = IN> {
520
538
  };
521
539
  anyOfThese?: JsonSchema[];
522
540
  precision?: number;
541
+ customValidations?: CustomValidatorFn[];
542
+ customConversions?: CustomConverterFn<any>[];
523
543
  }
524
544
  declare function object(props: AnyObject): never;
525
545
  declare function object<IN extends AnyObject>(props: {
@@ -618,4 +638,6 @@ type TupleIn<T extends readonly JsonSchemaAnyBuilder<any, any, any>[]> = {
618
638
  type TupleOut<T extends readonly JsonSchemaAnyBuilder<any, any, any>[]> = {
619
639
  [K in keyof T]: T[K] extends JsonSchemaAnyBuilder<any, infer O, any> ? O : never;
620
640
  };
641
+ export type CustomValidatorFn = (v: any) => string | undefined;
642
+ export type CustomConverterFn<OUT> = (v: any) => OUT;
621
643
  export {};
@@ -3,7 +3,7 @@
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 { _deepCopy, _sortObject } from '@naturalcycles/js-lib/object';
6
+ import { _sortObject } from '@naturalcycles/js-lib/object';
7
7
  import { _objectAssign, _typeCast, JWT_REGEX, } from '@naturalcycles/js-lib/types';
8
8
  import { BASE64URL_REGEX, COUNTRY_CODE_REGEX, CURRENCY_REGEX, IPV4_REGEX, IPV6_REGEX, LANGUAGE_TAG_REGEX, SEMVER_REGEX, SLUG_REGEX, URL_REGEX, UUID_REGEX, } from '../regexes.js';
9
9
  import { TIMEZONES } from '../timezones.js';
@@ -223,13 +223,13 @@ export class JsonSchemaTerminal {
223
223
  */
224
224
  build() {
225
225
  _assert(!(this.schema.optionalField && this.schema.default !== undefined), '.optional() and .default() should not be used together - the default value makes .optional() redundant and causes incorrect type inference');
226
- const jsonSchema = _sortObject(JSON.parse(JSON.stringify(this.schema)), JSON_SCHEMA_ORDER);
226
+ const jsonSchema = _sortObject(deepCopyPreservingFunctions(this.schema), JSON_SCHEMA_ORDER);
227
227
  delete jsonSchema.optionalField;
228
228
  return jsonSchema;
229
229
  }
230
230
  clone() {
231
231
  const cloned = Object.create(Object.getPrototypeOf(this));
232
- cloned.schema = _deepCopy(this.schema);
232
+ cloned.schema = deepCopyPreservingFunctions(this.schema);
233
233
  return cloned;
234
234
  }
235
235
  cloneAndUpdateSchema(schema) {
@@ -318,6 +318,34 @@ export class JsonSchemaAnyBuilder extends JsonSchemaTerminal {
318
318
  final() {
319
319
  return new JsonSchemaTerminal(this.schema);
320
320
  }
321
+ /**
322
+ *
323
+ * @param validator A validator function that returns an error message or undefined.
324
+ *
325
+ * You may add multiple custom validators and they will be executed in the order you added them.
326
+ */
327
+ custom(validator) {
328
+ const { customValidations = [] } = this.schema;
329
+ return this.cloneAndUpdateSchema({
330
+ customValidations: [...customValidations, validator],
331
+ });
332
+ }
333
+ /**
334
+ *
335
+ * @param converter A converter function that returns a new value.
336
+ *
337
+ * You may add multiple converters and they will be executed in the order you added them,
338
+ * each converter receiving the result from the previous one.
339
+ *
340
+ * This feature only works when the current schema is nested in an object or array schema,
341
+ * due to how mutability works in Ajv.
342
+ */
343
+ convert(converter) {
344
+ const { customConversions = [] } = this.schema;
345
+ return this.cloneAndUpdateSchema({
346
+ customConversions: [...customConversions, converter],
347
+ });
348
+ }
321
349
  }
322
350
  export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
323
351
  constructor() {
@@ -729,7 +757,7 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
729
757
  }
730
758
  extend(props) {
731
759
  const newBuilder = new JsonSchemaObjectBuilder();
732
- _objectAssign(newBuilder.schema, _deepCopy(this.schema));
760
+ _objectAssign(newBuilder.schema, deepCopyPreservingFunctions(this.schema));
733
761
  const incomingSchemaBuilder = new JsonSchemaObjectBuilder(props);
734
762
  mergeJsonSchemaObjects(newBuilder.schema, incomingSchemaBuilder.schema);
735
763
  _objectAssign(newBuilder.schema, { hasIsOfTypeCheck: false });
@@ -838,7 +866,7 @@ export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
838
866
  }
839
867
  extend(props) {
840
868
  const newBuilder = new JsonSchemaObjectInferringBuilder();
841
- _objectAssign(newBuilder.schema, _deepCopy(this.schema));
869
+ _objectAssign(newBuilder.schema, deepCopyPreservingFunctions(this.schema));
842
870
  const incomingSchemaBuilder = new JsonSchemaObjectInferringBuilder(props);
843
871
  mergeJsonSchemaObjects(newBuilder.schema, incomingSchemaBuilder.schema);
844
872
  // This extend function is not type-safe as it is inferring,
@@ -1057,3 +1085,22 @@ function hasNoObjectSchemas(schema) {
1057
1085
  }
1058
1086
  return false;
1059
1087
  }
1088
+ /**
1089
+ * Deep copy that preserves functions in customValidations/customConversions.
1090
+ * Unlike structuredClone, this handles function references (which only exist in those two properties).
1091
+ */
1092
+ function deepCopyPreservingFunctions(obj) {
1093
+ if (obj === null || typeof obj !== 'object')
1094
+ return obj;
1095
+ if (Array.isArray(obj))
1096
+ return obj.map(deepCopyPreservingFunctions);
1097
+ const copy = {};
1098
+ for (const key of Object.keys(obj)) {
1099
+ const value = obj[key];
1100
+ copy[key] =
1101
+ (key === 'customValidations' || key === 'customConversions') && Array.isArray(value)
1102
+ ? [...value]
1103
+ : deepCopyPreservingFunctions(value);
1104
+ }
1105
+ return copy;
1106
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/nodejs-lib",
3
3
  "type": "module",
4
- "version": "15.87.0",
4
+ "version": "15.88.0",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@types/js-yaml": "^4",
@@ -6,6 +6,8 @@ import type { AnyObject } from '@naturalcycles/js-lib/types'
6
6
  import { Ajv2020, type Options, type ValidateFunction } from 'ajv/dist/2020.js'
7
7
  import { validTLDs } from '../tlds.js'
8
8
  import type {
9
+ CustomConverterFn,
10
+ CustomValidatorFn,
9
11
  JsonSchemaIsoDateOptions,
10
12
  JsonSchemaIsoMonthOptions,
11
13
  JsonSchemaStringEmailOptions,
@@ -450,7 +452,7 @@ export function createAjv(opt?: Options): Ajv2020 {
450
452
 
451
453
  _assert(
452
454
  ctx?.parentData && ctx.parentDataProperty !== undefined,
453
- 'You should only use `optional([x, y, z]) on a property of an object, or on an element of an array due to Ajv mutation issues.',
455
+ 'You should only use `optional([x, y, z])` on a property of an object, or on an element of an array due to Ajv mutation issues.',
454
456
  )
455
457
 
456
458
  if (!optionalValues.includes(data)) return true
@@ -660,7 +662,7 @@ export function createAjv(opt?: Options): Ajv2020 {
660
662
 
661
663
  _assert(
662
664
  ctx?.parentData && ctx.parentDataProperty !== undefined,
663
- 'You should only use `precision(n) on a property of an object, or on an element of an array due to Ajv mutation issues.',
665
+ 'You should only use `precision(n)` on a property of an object, or on an element of an array due to Ajv mutation issues.',
664
666
  )
665
667
 
666
668
  ctx.parentData[ctx.parentDataProperty] = _round(data, 10 ** (-1 * numberOfDigits))
@@ -669,6 +671,59 @@ export function createAjv(opt?: Options): Ajv2020 {
669
671
  },
670
672
  })
671
673
 
674
+ ajv.addKeyword({
675
+ keyword: 'customValidations',
676
+ modifying: false,
677
+ errors: true,
678
+ schemaType: 'array',
679
+ validate: function validate(customValidations: CustomValidatorFn[], data: any, _schema, ctx) {
680
+ if (!customValidations?.length) return true
681
+
682
+ for (const validator of customValidations) {
683
+ const error = validator(data)
684
+ if (error) {
685
+ ;(validate as any).errors = [
686
+ {
687
+ instancePath: ctx?.instancePath ?? '',
688
+ message: error,
689
+ },
690
+ ]
691
+ return false
692
+ }
693
+ }
694
+
695
+ return true
696
+ },
697
+ })
698
+
699
+ ajv.addKeyword({
700
+ keyword: 'customConversions',
701
+ modifying: true,
702
+ errors: false,
703
+ schemaType: 'array',
704
+ validate: function validate(
705
+ customConversions: CustomConverterFn<any>[],
706
+ data: any,
707
+ _schema,
708
+ ctx,
709
+ ) {
710
+ if (!customConversions?.length) return true
711
+
712
+ _assert(
713
+ ctx?.parentData && ctx.parentDataProperty !== undefined,
714
+ 'You should only use `convert()` on a property of an object, or on an element of an array due to Ajv mutation issues.',
715
+ )
716
+
717
+ for (const converter of customConversions) {
718
+ data = converter(data)
719
+ }
720
+
721
+ ctx.parentData[ctx.parentDataProperty] = data
722
+
723
+ return true
724
+ },
725
+ })
726
+
672
727
  return ajv
673
728
  }
674
729
 
@@ -10,7 +10,7 @@ import {
10
10
  import { _uniq } from '@naturalcycles/js-lib/array'
11
11
  import { _assert } from '@naturalcycles/js-lib/error'
12
12
  import type { Set2 } from '@naturalcycles/js-lib/object'
13
- import { _deepCopy, _sortObject } from '@naturalcycles/js-lib/object'
13
+ import { _sortObject } from '@naturalcycles/js-lib/object'
14
14
  import {
15
15
  _objectAssign,
16
16
  _typeCast,
@@ -338,7 +338,7 @@ export class JsonSchemaTerminal<IN, OUT, Opt> {
338
338
  )
339
339
 
340
340
  const jsonSchema = _sortObject(
341
- JSON.parse(JSON.stringify(this.schema)),
341
+ deepCopyPreservingFunctions(this.schema) as AnyObject,
342
342
  JSON_SCHEMA_ORDER,
343
343
  ) as JsonSchema<IN, OUT>
344
344
 
@@ -349,7 +349,7 @@ export class JsonSchemaTerminal<IN, OUT, Opt> {
349
349
 
350
350
  clone(): this {
351
351
  const cloned = Object.create(Object.getPrototypeOf(this))
352
- cloned.schema = _deepCopy(this.schema)
352
+ cloned.schema = deepCopyPreservingFunctions(this.schema)
353
353
  return cloned
354
354
  }
355
355
 
@@ -455,6 +455,36 @@ export class JsonSchemaAnyBuilder<IN, OUT, Opt> extends JsonSchemaTerminal<IN, O
455
455
  final(): JsonSchemaTerminal<IN, OUT, Opt> {
456
456
  return new JsonSchemaTerminal<IN, OUT, Opt>(this.schema)
457
457
  }
458
+
459
+ /**
460
+ *
461
+ * @param validator A validator function that returns an error message or undefined.
462
+ *
463
+ * You may add multiple custom validators and they will be executed in the order you added them.
464
+ */
465
+ custom<OUT2 = OUT>(validator: CustomValidatorFn): JsonSchemaAnyBuilder<IN, OUT2, Opt> {
466
+ const { customValidations = [] } = this.schema
467
+ return this.cloneAndUpdateSchema({
468
+ customValidations: [...customValidations, validator],
469
+ }) as unknown as JsonSchemaAnyBuilder<IN, OUT2, Opt>
470
+ }
471
+
472
+ /**
473
+ *
474
+ * @param converter A converter function that returns a new value.
475
+ *
476
+ * You may add multiple converters and they will be executed in the order you added them,
477
+ * each converter receiving the result from the previous one.
478
+ *
479
+ * This feature only works when the current schema is nested in an object or array schema,
480
+ * due to how mutability works in Ajv.
481
+ */
482
+ convert<OUT2>(converter: CustomConverterFn<OUT2>): JsonSchemaAnyBuilder<IN, OUT2, Opt> {
483
+ const { customConversions = [] } = this.schema
484
+ return this.cloneAndUpdateSchema({
485
+ customConversions: [...customConversions, converter],
486
+ }) as unknown as JsonSchemaAnyBuilder<IN, OUT2, Opt>
487
+ }
458
488
  }
459
489
 
460
490
  export class JsonSchemaStringBuilder<
@@ -1051,7 +1081,7 @@ export class JsonSchemaObjectBuilder<
1051
1081
  false
1052
1082
  > {
1053
1083
  const newBuilder = new JsonSchemaObjectBuilder()
1054
- _objectAssign(newBuilder.schema, _deepCopy(this.schema))
1084
+ _objectAssign(newBuilder.schema, deepCopyPreservingFunctions(this.schema))
1055
1085
 
1056
1086
  const incomingSchemaBuilder = new JsonSchemaObjectBuilder(props)
1057
1087
  mergeJsonSchemaObjects(newBuilder.schema as any, incomingSchemaBuilder.schema as any)
@@ -1305,7 +1335,7 @@ export class JsonSchemaObjectInferringBuilder<
1305
1335
  Opt
1306
1336
  > {
1307
1337
  const newBuilder = new JsonSchemaObjectInferringBuilder<PROPS, Opt>()
1308
- _objectAssign(newBuilder.schema, _deepCopy(this.schema))
1338
+ _objectAssign(newBuilder.schema, deepCopyPreservingFunctions(this.schema))
1309
1339
 
1310
1340
  const incomingSchemaBuilder = new JsonSchemaObjectInferringBuilder<NEW_PROPS, false>(props)
1311
1341
  mergeJsonSchemaObjects(newBuilder.schema as any, incomingSchemaBuilder.schema as any)
@@ -1587,6 +1617,8 @@ export interface JsonSchema<IN = unknown, OUT = IN> {
1587
1617
  }
1588
1618
  anyOfThese?: JsonSchema[]
1589
1619
  precision?: number
1620
+ customValidations?: CustomValidatorFn[]
1621
+ customConversions?: CustomConverterFn<any>[]
1590
1622
  }
1591
1623
 
1592
1624
  function object(props: AnyObject): never
@@ -1893,3 +1925,25 @@ type TupleIn<T extends readonly JsonSchemaAnyBuilder<any, any, any>[]> = {
1893
1925
  type TupleOut<T extends readonly JsonSchemaAnyBuilder<any, any, any>[]> = {
1894
1926
  [K in keyof T]: T[K] extends JsonSchemaAnyBuilder<any, infer O, any> ? O : never
1895
1927
  }
1928
+
1929
+ export type CustomValidatorFn = (v: any) => string | undefined
1930
+ export type CustomConverterFn<OUT> = (v: any) => OUT
1931
+
1932
+ /**
1933
+ * Deep copy that preserves functions in customValidations/customConversions.
1934
+ * Unlike structuredClone, this handles function references (which only exist in those two properties).
1935
+ */
1936
+ function deepCopyPreservingFunctions<T>(obj: T): T {
1937
+ if (obj === null || typeof obj !== 'object') return obj
1938
+ if (Array.isArray(obj)) return obj.map(deepCopyPreservingFunctions) as T
1939
+ const copy = {} as T
1940
+ for (const key of Object.keys(obj)) {
1941
+ const value = (obj as any)[key]
1942
+ // customValidations/customConversions are arrays of functions - shallow copy the array
1943
+ ;(copy as any)[key] =
1944
+ (key === 'customValidations' || key === 'customConversions') && Array.isArray(value)
1945
+ ? [...value]
1946
+ : deepCopyPreservingFunctions(value)
1947
+ }
1948
+ return copy
1949
+ }