@naturalcycles/nodejs-lib 15.87.0 → 15.89.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();
@@ -225,6 +243,18 @@ export interface JsonSchemaStringEmailOptions {
225
243
  }
226
244
  export declare class JsonSchemaIsoDateBuilder<Opt extends boolean = false> extends JsonSchemaAnyBuilder<string | IsoDate, IsoDate, Opt> {
227
245
  constructor();
246
+ /**
247
+ * @param optionalValues List of values that should be considered/converted as `undefined`.
248
+ *
249
+ * This `optionalValues` feature only works when the current schema is nested in an object or array schema,
250
+ * due to how mutability works in Ajv.
251
+ *
252
+ * Make sure this `optional()` call is at the end of your call chain.
253
+ *
254
+ * When `null` is included in optionalValues, the return type becomes `JsonSchemaTerminal`
255
+ * (no further chaining allowed) because the schema is wrapped in an anyOf structure.
256
+ */
257
+ optional<T extends readonly null[] | undefined = undefined>(optionalValues?: T): T extends readonly null[] ? JsonSchemaTerminal<string | IsoDate | undefined, IsoDate | undefined, true> : JsonSchemaIsoDateBuilder<true>;
228
258
  before(date: string): this;
229
259
  sameOrBefore(date: string): this;
230
260
  after(date: string): this;
@@ -520,6 +550,8 @@ export interface JsonSchema<IN = unknown, OUT = IN> {
520
550
  };
521
551
  anyOfThese?: JsonSchema[];
522
552
  precision?: number;
553
+ customValidations?: CustomValidatorFn[];
554
+ customConversions?: CustomConverterFn<any>[];
523
555
  }
524
556
  declare function object(props: AnyObject): never;
525
557
  declare function object<IN extends AnyObject>(props: {
@@ -618,4 +650,6 @@ type TupleIn<T extends readonly JsonSchemaAnyBuilder<any, any, any>[]> = {
618
650
  type TupleOut<T extends readonly JsonSchemaAnyBuilder<any, any, any>[]> = {
619
651
  [K in keyof T]: T[K] extends JsonSchemaAnyBuilder<any, infer O, any> ? O : never;
620
652
  };
653
+ export type CustomValidatorFn = (v: any) => string | undefined;
654
+ export type CustomConverterFn<OUT> = (v: any) => OUT;
621
655
  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() {
@@ -480,6 +508,31 @@ export class JsonSchemaIsoDateBuilder extends JsonSchemaAnyBuilder {
480
508
  IsoDate: {},
481
509
  });
482
510
  }
511
+ /**
512
+ * @param optionalValues List of values that should be considered/converted as `undefined`.
513
+ *
514
+ * This `optionalValues` feature only works when the current schema is nested in an object or array schema,
515
+ * due to how mutability works in Ajv.
516
+ *
517
+ * Make sure this `optional()` call is at the end of your call chain.
518
+ *
519
+ * When `null` is included in optionalValues, the return type becomes `JsonSchemaTerminal`
520
+ * (no further chaining allowed) because the schema is wrapped in an anyOf structure.
521
+ */
522
+ optional(optionalValues) {
523
+ if (!optionalValues) {
524
+ return super.optional();
525
+ }
526
+ _typeCast(optionalValues);
527
+ const newBuilder = new JsonSchemaTerminal({
528
+ anyOf: [
529
+ { type: 'null', optionalValues },
530
+ this.cloneAndUpdateSchema({ optionalField: true }).build(),
531
+ ],
532
+ optionalField: true,
533
+ });
534
+ return newBuilder;
535
+ }
483
536
  before(date) {
484
537
  return this.cloneAndUpdateSchema({ IsoDate: { before: date } });
485
538
  }
@@ -729,7 +782,7 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
729
782
  }
730
783
  extend(props) {
731
784
  const newBuilder = new JsonSchemaObjectBuilder();
732
- _objectAssign(newBuilder.schema, _deepCopy(this.schema));
785
+ _objectAssign(newBuilder.schema, deepCopyPreservingFunctions(this.schema));
733
786
  const incomingSchemaBuilder = new JsonSchemaObjectBuilder(props);
734
787
  mergeJsonSchemaObjects(newBuilder.schema, incomingSchemaBuilder.schema);
735
788
  _objectAssign(newBuilder.schema, { hasIsOfTypeCheck: false });
@@ -838,7 +891,7 @@ export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
838
891
  }
839
892
  extend(props) {
840
893
  const newBuilder = new JsonSchemaObjectInferringBuilder();
841
- _objectAssign(newBuilder.schema, _deepCopy(this.schema));
894
+ _objectAssign(newBuilder.schema, deepCopyPreservingFunctions(this.schema));
842
895
  const incomingSchemaBuilder = new JsonSchemaObjectInferringBuilder(props);
843
896
  mergeJsonSchemaObjects(newBuilder.schema, incomingSchemaBuilder.schema);
844
897
  // This extend function is not type-safe as it is inferring,
@@ -1057,3 +1110,22 @@ function hasNoObjectSchemas(schema) {
1057
1110
  }
1058
1111
  return false;
1059
1112
  }
1113
+ /**
1114
+ * Deep copy that preserves functions in customValidations/customConversions.
1115
+ * Unlike structuredClone, this handles function references (which only exist in those two properties).
1116
+ */
1117
+ function deepCopyPreservingFunctions(obj) {
1118
+ if (obj === null || typeof obj !== 'object')
1119
+ return obj;
1120
+ if (Array.isArray(obj))
1121
+ return obj.map(deepCopyPreservingFunctions);
1122
+ const copy = {};
1123
+ for (const key of Object.keys(obj)) {
1124
+ const value = obj[key];
1125
+ copy[key] =
1126
+ (key === 'customValidations' || key === 'customConversions') && Array.isArray(value)
1127
+ ? [...value]
1128
+ : deepCopyPreservingFunctions(value);
1129
+ }
1130
+ return copy;
1131
+ }
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.89.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<
@@ -673,6 +703,43 @@ export class JsonSchemaIsoDateBuilder<Opt extends boolean = false> extends JsonS
673
703
  })
674
704
  }
675
705
 
706
+ /**
707
+ * @param optionalValues List of values that should be considered/converted as `undefined`.
708
+ *
709
+ * This `optionalValues` feature only works when the current schema is nested in an object or array schema,
710
+ * due to how mutability works in Ajv.
711
+ *
712
+ * Make sure this `optional()` call is at the end of your call chain.
713
+ *
714
+ * When `null` is included in optionalValues, the return type becomes `JsonSchemaTerminal`
715
+ * (no further chaining allowed) because the schema is wrapped in an anyOf structure.
716
+ */
717
+ override optional<T extends readonly null[] | undefined = undefined>(
718
+ optionalValues?: T,
719
+ ): T extends readonly null[]
720
+ ? JsonSchemaTerminal<string | IsoDate | undefined, IsoDate | undefined, true>
721
+ : JsonSchemaIsoDateBuilder<true> {
722
+ if (!optionalValues) {
723
+ return super.optional() as any
724
+ }
725
+
726
+ _typeCast<null[]>(optionalValues)
727
+
728
+ const newBuilder = new JsonSchemaTerminal<
729
+ string | IsoDate | undefined,
730
+ IsoDate | undefined,
731
+ true
732
+ >({
733
+ anyOf: [
734
+ { type: 'null', optionalValues },
735
+ this.cloneAndUpdateSchema({ optionalField: true }).build(),
736
+ ],
737
+ optionalField: true,
738
+ })
739
+
740
+ return newBuilder as any
741
+ }
742
+
676
743
  before(date: string): this {
677
744
  return this.cloneAndUpdateSchema({ IsoDate: { before: date } })
678
745
  }
@@ -1051,7 +1118,7 @@ export class JsonSchemaObjectBuilder<
1051
1118
  false
1052
1119
  > {
1053
1120
  const newBuilder = new JsonSchemaObjectBuilder()
1054
- _objectAssign(newBuilder.schema, _deepCopy(this.schema))
1121
+ _objectAssign(newBuilder.schema, deepCopyPreservingFunctions(this.schema))
1055
1122
 
1056
1123
  const incomingSchemaBuilder = new JsonSchemaObjectBuilder(props)
1057
1124
  mergeJsonSchemaObjects(newBuilder.schema as any, incomingSchemaBuilder.schema as any)
@@ -1305,7 +1372,7 @@ export class JsonSchemaObjectInferringBuilder<
1305
1372
  Opt
1306
1373
  > {
1307
1374
  const newBuilder = new JsonSchemaObjectInferringBuilder<PROPS, Opt>()
1308
- _objectAssign(newBuilder.schema, _deepCopy(this.schema))
1375
+ _objectAssign(newBuilder.schema, deepCopyPreservingFunctions(this.schema))
1309
1376
 
1310
1377
  const incomingSchemaBuilder = new JsonSchemaObjectInferringBuilder<NEW_PROPS, false>(props)
1311
1378
  mergeJsonSchemaObjects(newBuilder.schema as any, incomingSchemaBuilder.schema as any)
@@ -1587,6 +1654,8 @@ export interface JsonSchema<IN = unknown, OUT = IN> {
1587
1654
  }
1588
1655
  anyOfThese?: JsonSchema[]
1589
1656
  precision?: number
1657
+ customValidations?: CustomValidatorFn[]
1658
+ customConversions?: CustomConverterFn<any>[]
1590
1659
  }
1591
1660
 
1592
1661
  function object(props: AnyObject): never
@@ -1893,3 +1962,25 @@ type TupleIn<T extends readonly JsonSchemaAnyBuilder<any, any, any>[]> = {
1893
1962
  type TupleOut<T extends readonly JsonSchemaAnyBuilder<any, any, any>[]> = {
1894
1963
  [K in keyof T]: T[K] extends JsonSchemaAnyBuilder<any, infer O, any> ? O : never
1895
1964
  }
1965
+
1966
+ export type CustomValidatorFn = (v: any) => string | undefined
1967
+ export type CustomConverterFn<OUT> = (v: any) => OUT
1968
+
1969
+ /**
1970
+ * Deep copy that preserves functions in customValidations/customConversions.
1971
+ * Unlike structuredClone, this handles function references (which only exist in those two properties).
1972
+ */
1973
+ function deepCopyPreservingFunctions<T>(obj: T): T {
1974
+ if (obj === null || typeof obj !== 'object') return obj
1975
+ if (Array.isArray(obj)) return obj.map(deepCopyPreservingFunctions) as T
1976
+ const copy = {} as T
1977
+ for (const key of Object.keys(obj)) {
1978
+ const value = (obj as any)[key]
1979
+ // customValidations/customConversions are arrays of functions - shallow copy the array
1980
+ ;(copy as any)[key] =
1981
+ (key === 'customValidations' || key === 'customConversions') && Array.isArray(value)
1982
+ ? [...value]
1983
+ : deepCopyPreservingFunctions(value)
1984
+ }
1985
+ return copy
1986
+ }