@naturalcycles/nodejs-lib 15.86.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;
|
|
@@ -417,6 +417,15 @@ export function createAjv(opt) {
|
|
|
417
417
|
return validate;
|
|
418
418
|
},
|
|
419
419
|
});
|
|
420
|
+
// Validates that the value is undefined. Used in record/stringMap with optional value schemas
|
|
421
|
+
// to allow undefined values in patternProperties via anyOf.
|
|
422
|
+
ajv.addKeyword({
|
|
423
|
+
keyword: 'isUndefined',
|
|
424
|
+
modifying: false,
|
|
425
|
+
errors: false,
|
|
426
|
+
schemaType: 'boolean',
|
|
427
|
+
validate: (_schema, data) => data === undefined,
|
|
428
|
+
});
|
|
420
429
|
// This is added because Ajv validates the `min/maxProperties` before validating the properties.
|
|
421
430
|
// So, in case of `minProperties(1)` and `{ foo: 'bar' }` Ajv will let it pass, even
|
|
422
431
|
// if the property validation would strip `foo` from the data.
|
|
@@ -569,11 +578,51 @@ export function createAjv(opt) {
|
|
|
569
578
|
validate: function validate(numberOfDigits, data, _schema, ctx) {
|
|
570
579
|
if (!numberOfDigits)
|
|
571
580
|
return true;
|
|
572
|
-
_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.');
|
|
573
582
|
ctx.parentData[ctx.parentDataProperty] = _round(data, 10 ** (-1 * numberOfDigits));
|
|
574
583
|
return true;
|
|
575
584
|
},
|
|
576
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
|
+
});
|
|
577
626
|
return ajv;
|
|
578
627
|
}
|
|
579
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();
|
|
@@ -511,6 +529,7 @@ export interface JsonSchema<IN = unknown, OUT = IN> {
|
|
|
511
529
|
errorMessages?: StringMap<string>;
|
|
512
530
|
optionalValues?: (string | number | boolean | null)[];
|
|
513
531
|
keySchema?: JsonSchema;
|
|
532
|
+
isUndefined?: true;
|
|
514
533
|
minProperties2?: number;
|
|
515
534
|
exclusiveProperties?: (readonly string[])[];
|
|
516
535
|
anyOfBy?: {
|
|
@@ -519,6 +538,8 @@ export interface JsonSchema<IN = unknown, OUT = IN> {
|
|
|
519
538
|
};
|
|
520
539
|
anyOfThese?: JsonSchema[];
|
|
521
540
|
precision?: number;
|
|
541
|
+
customValidations?: CustomValidatorFn[];
|
|
542
|
+
customConversions?: CustomConverterFn<any>[];
|
|
522
543
|
}
|
|
523
544
|
declare function object(props: AnyObject): never;
|
|
524
545
|
declare function object<IN extends AnyObject>(props: {
|
|
@@ -617,4 +638,6 @@ type TupleIn<T extends readonly JsonSchemaAnyBuilder<any, any, any>[]> = {
|
|
|
617
638
|
type TupleOut<T extends readonly JsonSchemaAnyBuilder<any, any, any>[]> = {
|
|
618
639
|
[K in keyof T]: T[K] extends JsonSchemaAnyBuilder<any, infer O, any> ? O : never;
|
|
619
640
|
};
|
|
641
|
+
export type CustomValidatorFn = (v: any) => string | undefined;
|
|
642
|
+
export type CustomConverterFn<OUT> = (v: any) => OUT;
|
|
620
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 {
|
|
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';
|
|
@@ -32,11 +32,15 @@ export const j = {
|
|
|
32
32
|
return j.object({}).allowAdditionalProperties();
|
|
33
33
|
},
|
|
34
34
|
stringMap(schema) {
|
|
35
|
+
const isValueOptional = schema.getSchema().optionalField;
|
|
35
36
|
const builtSchema = schema.build();
|
|
37
|
+
const finalValueSchema = isValueOptional
|
|
38
|
+
? { anyOf: [{ isUndefined: true }, builtSchema] }
|
|
39
|
+
: builtSchema;
|
|
36
40
|
return new JsonSchemaObjectBuilder({}, {
|
|
37
41
|
hasIsOfTypeCheck: false,
|
|
38
42
|
patternProperties: {
|
|
39
|
-
'^.+$':
|
|
43
|
+
'^.+$': finalValueSchema,
|
|
40
44
|
},
|
|
41
45
|
});
|
|
42
46
|
},
|
|
@@ -219,13 +223,13 @@ export class JsonSchemaTerminal {
|
|
|
219
223
|
*/
|
|
220
224
|
build() {
|
|
221
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');
|
|
222
|
-
const jsonSchema = _sortObject(
|
|
226
|
+
const jsonSchema = _sortObject(deepCopyPreservingFunctions(this.schema), JSON_SCHEMA_ORDER);
|
|
223
227
|
delete jsonSchema.optionalField;
|
|
224
228
|
return jsonSchema;
|
|
225
229
|
}
|
|
226
230
|
clone() {
|
|
227
231
|
const cloned = Object.create(Object.getPrototypeOf(this));
|
|
228
|
-
cloned.schema =
|
|
232
|
+
cloned.schema = deepCopyPreservingFunctions(this.schema);
|
|
229
233
|
return cloned;
|
|
230
234
|
}
|
|
231
235
|
cloneAndUpdateSchema(schema) {
|
|
@@ -314,6 +318,34 @@ export class JsonSchemaAnyBuilder extends JsonSchemaTerminal {
|
|
|
314
318
|
final() {
|
|
315
319
|
return new JsonSchemaTerminal(this.schema);
|
|
316
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
|
+
}
|
|
317
349
|
}
|
|
318
350
|
export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
|
|
319
351
|
constructor() {
|
|
@@ -725,7 +757,7 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
|
|
|
725
757
|
}
|
|
726
758
|
extend(props) {
|
|
727
759
|
const newBuilder = new JsonSchemaObjectBuilder();
|
|
728
|
-
_objectAssign(newBuilder.schema,
|
|
760
|
+
_objectAssign(newBuilder.schema, deepCopyPreservingFunctions(this.schema));
|
|
729
761
|
const incomingSchemaBuilder = new JsonSchemaObjectBuilder(props);
|
|
730
762
|
mergeJsonSchemaObjects(newBuilder.schema, incomingSchemaBuilder.schema);
|
|
731
763
|
_objectAssign(newBuilder.schema, { hasIsOfTypeCheck: false });
|
|
@@ -834,7 +866,7 @@ export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
|
|
|
834
866
|
}
|
|
835
867
|
extend(props) {
|
|
836
868
|
const newBuilder = new JsonSchemaObjectInferringBuilder();
|
|
837
|
-
_objectAssign(newBuilder.schema,
|
|
869
|
+
_objectAssign(newBuilder.schema, deepCopyPreservingFunctions(this.schema));
|
|
838
870
|
const incomingSchemaBuilder = new JsonSchemaObjectInferringBuilder(props);
|
|
839
871
|
mergeJsonSchemaObjects(newBuilder.schema, incomingSchemaBuilder.schema);
|
|
840
872
|
// This extend function is not type-safe as it is inferring,
|
|
@@ -979,12 +1011,19 @@ function objectDbEntity(props) {
|
|
|
979
1011
|
}
|
|
980
1012
|
function record(keySchema, valueSchema) {
|
|
981
1013
|
const keyJsonSchema = keySchema.build();
|
|
1014
|
+
// Check if value schema is optional before build() strips the optionalField flag
|
|
1015
|
+
const isValueOptional = valueSchema.getSchema()
|
|
1016
|
+
.optionalField;
|
|
982
1017
|
const valueJsonSchema = valueSchema.build();
|
|
1018
|
+
// When value schema is optional, wrap in anyOf to allow undefined values
|
|
1019
|
+
const finalValueSchema = isValueOptional
|
|
1020
|
+
? { anyOf: [{ isUndefined: true }, valueJsonSchema] }
|
|
1021
|
+
: valueJsonSchema;
|
|
983
1022
|
return new JsonSchemaObjectBuilder([], {
|
|
984
1023
|
hasIsOfTypeCheck: false,
|
|
985
1024
|
keySchema: keyJsonSchema,
|
|
986
1025
|
patternProperties: {
|
|
987
|
-
['^.*$']:
|
|
1026
|
+
['^.*$']: finalValueSchema,
|
|
988
1027
|
},
|
|
989
1028
|
});
|
|
990
1029
|
}
|
|
@@ -1046,3 +1085,22 @@ function hasNoObjectSchemas(schema) {
|
|
|
1046
1085
|
}
|
|
1047
1086
|
return false;
|
|
1048
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
|
@@ -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
|
|
@@ -486,6 +488,16 @@ export function createAjv(opt?: Options): Ajv2020 {
|
|
|
486
488
|
},
|
|
487
489
|
})
|
|
488
490
|
|
|
491
|
+
// Validates that the value is undefined. Used in record/stringMap with optional value schemas
|
|
492
|
+
// to allow undefined values in patternProperties via anyOf.
|
|
493
|
+
ajv.addKeyword({
|
|
494
|
+
keyword: 'isUndefined',
|
|
495
|
+
modifying: false,
|
|
496
|
+
errors: false,
|
|
497
|
+
schemaType: 'boolean',
|
|
498
|
+
validate: (_schema: boolean, data: unknown) => data === undefined,
|
|
499
|
+
})
|
|
500
|
+
|
|
489
501
|
// This is added because Ajv validates the `min/maxProperties` before validating the properties.
|
|
490
502
|
// So, in case of `minProperties(1)` and `{ foo: 'bar' }` Ajv will let it pass, even
|
|
491
503
|
// if the property validation would strip `foo` from the data.
|
|
@@ -650,7 +662,7 @@ export function createAjv(opt?: Options): Ajv2020 {
|
|
|
650
662
|
|
|
651
663
|
_assert(
|
|
652
664
|
ctx?.parentData && ctx.parentDataProperty !== undefined,
|
|
653
|
-
'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.',
|
|
654
666
|
)
|
|
655
667
|
|
|
656
668
|
ctx.parentData[ctx.parentDataProperty] = _round(data, 10 ** (-1 * numberOfDigits))
|
|
@@ -659,6 +671,59 @@ export function createAjv(opt?: Options): Ajv2020 {
|
|
|
659
671
|
},
|
|
660
672
|
})
|
|
661
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
|
+
|
|
662
727
|
return ajv
|
|
663
728
|
}
|
|
664
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 {
|
|
13
|
+
import { _sortObject } from '@naturalcycles/js-lib/object'
|
|
14
14
|
import {
|
|
15
15
|
_objectAssign,
|
|
16
16
|
_typeCast,
|
|
@@ -80,14 +80,18 @@ export const j = {
|
|
|
80
80
|
stringMap<S extends JsonSchemaTerminal<any, any, any>>(
|
|
81
81
|
schema: S,
|
|
82
82
|
): JsonSchemaObjectBuilder<StringMap<SchemaIn<S>>, StringMap<SchemaOut<S>>> {
|
|
83
|
+
const isValueOptional = schema.getSchema().optionalField
|
|
83
84
|
const builtSchema = schema.build()
|
|
85
|
+
const finalValueSchema: JsonSchema = isValueOptional
|
|
86
|
+
? { anyOf: [{ isUndefined: true }, builtSchema] }
|
|
87
|
+
: builtSchema
|
|
84
88
|
|
|
85
89
|
return new JsonSchemaObjectBuilder<StringMap<SchemaIn<S>>, StringMap<SchemaOut<S>>>(
|
|
86
90
|
{},
|
|
87
91
|
{
|
|
88
92
|
hasIsOfTypeCheck: false,
|
|
89
93
|
patternProperties: {
|
|
90
|
-
'^.+$':
|
|
94
|
+
'^.+$': finalValueSchema,
|
|
91
95
|
},
|
|
92
96
|
},
|
|
93
97
|
)
|
|
@@ -334,7 +338,7 @@ export class JsonSchemaTerminal<IN, OUT, Opt> {
|
|
|
334
338
|
)
|
|
335
339
|
|
|
336
340
|
const jsonSchema = _sortObject(
|
|
337
|
-
|
|
341
|
+
deepCopyPreservingFunctions(this.schema) as AnyObject,
|
|
338
342
|
JSON_SCHEMA_ORDER,
|
|
339
343
|
) as JsonSchema<IN, OUT>
|
|
340
344
|
|
|
@@ -345,7 +349,7 @@ export class JsonSchemaTerminal<IN, OUT, Opt> {
|
|
|
345
349
|
|
|
346
350
|
clone(): this {
|
|
347
351
|
const cloned = Object.create(Object.getPrototypeOf(this))
|
|
348
|
-
cloned.schema =
|
|
352
|
+
cloned.schema = deepCopyPreservingFunctions(this.schema)
|
|
349
353
|
return cloned
|
|
350
354
|
}
|
|
351
355
|
|
|
@@ -451,6 +455,36 @@ export class JsonSchemaAnyBuilder<IN, OUT, Opt> extends JsonSchemaTerminal<IN, O
|
|
|
451
455
|
final(): JsonSchemaTerminal<IN, OUT, Opt> {
|
|
452
456
|
return new JsonSchemaTerminal<IN, OUT, Opt>(this.schema)
|
|
453
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
|
+
}
|
|
454
488
|
}
|
|
455
489
|
|
|
456
490
|
export class JsonSchemaStringBuilder<
|
|
@@ -1047,7 +1081,7 @@ export class JsonSchemaObjectBuilder<
|
|
|
1047
1081
|
false
|
|
1048
1082
|
> {
|
|
1049
1083
|
const newBuilder = new JsonSchemaObjectBuilder()
|
|
1050
|
-
_objectAssign(newBuilder.schema,
|
|
1084
|
+
_objectAssign(newBuilder.schema, deepCopyPreservingFunctions(this.schema))
|
|
1051
1085
|
|
|
1052
1086
|
const incomingSchemaBuilder = new JsonSchemaObjectBuilder(props)
|
|
1053
1087
|
mergeJsonSchemaObjects(newBuilder.schema as any, incomingSchemaBuilder.schema as any)
|
|
@@ -1301,7 +1335,7 @@ export class JsonSchemaObjectInferringBuilder<
|
|
|
1301
1335
|
Opt
|
|
1302
1336
|
> {
|
|
1303
1337
|
const newBuilder = new JsonSchemaObjectInferringBuilder<PROPS, Opt>()
|
|
1304
|
-
_objectAssign(newBuilder.schema,
|
|
1338
|
+
_objectAssign(newBuilder.schema, deepCopyPreservingFunctions(this.schema))
|
|
1305
1339
|
|
|
1306
1340
|
const incomingSchemaBuilder = new JsonSchemaObjectInferringBuilder<NEW_PROPS, false>(props)
|
|
1307
1341
|
mergeJsonSchemaObjects(newBuilder.schema as any, incomingSchemaBuilder.schema as any)
|
|
@@ -1574,6 +1608,7 @@ export interface JsonSchema<IN = unknown, OUT = IN> {
|
|
|
1574
1608
|
errorMessages?: StringMap<string>
|
|
1575
1609
|
optionalValues?: (string | number | boolean | null)[]
|
|
1576
1610
|
keySchema?: JsonSchema
|
|
1611
|
+
isUndefined?: true
|
|
1577
1612
|
minProperties2?: number
|
|
1578
1613
|
exclusiveProperties?: (readonly string[])[]
|
|
1579
1614
|
anyOfBy?: {
|
|
@@ -1582,6 +1617,8 @@ export interface JsonSchema<IN = unknown, OUT = IN> {
|
|
|
1582
1617
|
}
|
|
1583
1618
|
anyOfThese?: JsonSchema[]
|
|
1584
1619
|
precision?: number
|
|
1620
|
+
customValidations?: CustomValidatorFn[]
|
|
1621
|
+
customConversions?: CustomConverterFn<any>[]
|
|
1585
1622
|
}
|
|
1586
1623
|
|
|
1587
1624
|
function object(props: AnyObject): never
|
|
@@ -1648,8 +1685,16 @@ function record<
|
|
|
1648
1685
|
false
|
|
1649
1686
|
> {
|
|
1650
1687
|
const keyJsonSchema = keySchema.build()
|
|
1688
|
+
// Check if value schema is optional before build() strips the optionalField flag
|
|
1689
|
+
const isValueOptional = (valueSchema as JsonSchemaTerminal<any, any, any>).getSchema()
|
|
1690
|
+
.optionalField
|
|
1651
1691
|
const valueJsonSchema = valueSchema.build()
|
|
1652
1692
|
|
|
1693
|
+
// When value schema is optional, wrap in anyOf to allow undefined values
|
|
1694
|
+
const finalValueSchema: JsonSchema = isValueOptional
|
|
1695
|
+
? { anyOf: [{ isUndefined: true }, valueJsonSchema] }
|
|
1696
|
+
: valueJsonSchema
|
|
1697
|
+
|
|
1653
1698
|
return new JsonSchemaObjectBuilder<
|
|
1654
1699
|
Opt extends true
|
|
1655
1700
|
? Partial<Record<SchemaIn<KS>, SchemaIn<VS>>>
|
|
@@ -1662,7 +1707,7 @@ function record<
|
|
|
1662
1707
|
hasIsOfTypeCheck: false,
|
|
1663
1708
|
keySchema: keyJsonSchema,
|
|
1664
1709
|
patternProperties: {
|
|
1665
|
-
['^.*$']:
|
|
1710
|
+
['^.*$']: finalValueSchema,
|
|
1666
1711
|
},
|
|
1667
1712
|
})
|
|
1668
1713
|
}
|
|
@@ -1880,3 +1925,25 @@ type TupleIn<T extends readonly JsonSchemaAnyBuilder<any, any, any>[]> = {
|
|
|
1880
1925
|
type TupleOut<T extends readonly JsonSchemaAnyBuilder<any, any, any>[]> = {
|
|
1881
1926
|
[K in keyof T]: T[K] extends JsonSchemaAnyBuilder<any, infer O, any> ? O : never
|
|
1882
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
|
+
}
|