@naturalcycles/nodejs-lib 15.140.0 → 15.141.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.
- package/dist/validation/ajv/getAjv.js +113 -121
- package/dist/validation/ajv/jSchema.d.ts +10 -1
- package/dist/validation/ajv/jSchema.js +19 -20
- package/dist/validation/ajv/jsonSchemaBuilder.util.js +8 -4
- package/package.json +2 -2
- package/src/validation/ajv/getAjv.ts +128 -131
- package/src/validation/ajv/jSchema.ts +27 -21
- package/src/validation/ajv/jsonSchemaBuilder.util.ts +8 -4
|
@@ -2,7 +2,7 @@ import { _isBetween, _lazyValue, _round } from '@naturalcycles/js-lib';
|
|
|
2
2
|
import { _assert } from '@naturalcycles/js-lib/error';
|
|
3
3
|
import { _deepCopy, _mapObject, Set2 } from '@naturalcycles/js-lib/object';
|
|
4
4
|
import { _substringAfterLast } from '@naturalcycles/js-lib/string';
|
|
5
|
-
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
5
|
+
import { _, Ajv2020, str } from 'ajv/dist/2020.js';
|
|
6
6
|
import { validTLDs } from '../tlds.js';
|
|
7
7
|
import { jFormats } from './jsonSchemaBuilder.util.js';
|
|
8
8
|
// oxlint-disable unicorn/prefer-code-point
|
|
@@ -265,130 +265,70 @@ export function createAjv(opt) {
|
|
|
265
265
|
return true;
|
|
266
266
|
},
|
|
267
267
|
});
|
|
268
|
+
// The date/time keywords are `code` keywords, which Ajv inlines into the compiled validator.
|
|
269
|
+
// A `validate` keyword is a function call with a context object, whose instancePath Ajv builds
|
|
270
|
+
// on every call (escaping the key of every map entry), even for valid data.
|
|
268
271
|
ajv.addKeyword({
|
|
269
272
|
keyword: 'IsoDate',
|
|
270
273
|
type: 'string',
|
|
271
|
-
modifying: false,
|
|
272
|
-
errors: true,
|
|
273
274
|
schemaType: 'object',
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
instancePath: ctx?.instancePath ?? '',
|
|
297
|
-
message: `is not before ${before}`,
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
if (sameOrBefore) {
|
|
302
|
-
const isParamValid = isIsoDateValid(sameOrBefore);
|
|
303
|
-
const isRuleValid = isParamValid && sameOrBefore >= data;
|
|
304
|
-
if (!isRuleValid) {
|
|
305
|
-
errors.push({
|
|
306
|
-
instancePath: ctx?.instancePath ?? '',
|
|
307
|
-
message: `is not the same or before ${sameOrBefore}`,
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
if (after) {
|
|
312
|
-
const isParamValid = isIsoDateValid(after);
|
|
313
|
-
const isRuleValid = isParamValid && after < data;
|
|
314
|
-
if (!isRuleValid) {
|
|
315
|
-
errors.push({
|
|
316
|
-
instancePath: ctx?.instancePath ?? '',
|
|
317
|
-
message: `is not after ${after}`,
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
if (sameOrAfter) {
|
|
322
|
-
const isParamValid = isIsoDateValid(sameOrAfter);
|
|
323
|
-
const isRuleValid = isParamValid && sameOrAfter <= data;
|
|
324
|
-
if (!isRuleValid) {
|
|
325
|
-
errors.push({
|
|
326
|
-
instancePath: ctx?.instancePath ?? '',
|
|
327
|
-
message: `is not the same or after ${sameOrAfter}`,
|
|
328
|
-
});
|
|
275
|
+
error: {
|
|
276
|
+
message: ({ params }) => params['rule']
|
|
277
|
+
? str `is not ${isoDateRuleLabels[params['rule']]} ${params['limit']}`
|
|
278
|
+
: str `is an invalid IsoDate`,
|
|
279
|
+
params: ({ params }) => (params['rule'] ? _ `{${params['rule']}: ${params['limit']}}` : _ `{}`),
|
|
280
|
+
},
|
|
281
|
+
code(cxt) {
|
|
282
|
+
const { gen, data } = cxt;
|
|
283
|
+
const opt = cxt.schema;
|
|
284
|
+
const isValid = gen.scopeValue('func', { ref: isIsoDateValid });
|
|
285
|
+
gen.if(_ `!${isValid}(${data})`, () => cxt.error(), () => {
|
|
286
|
+
for (const rule of isoDateRules) {
|
|
287
|
+
const limit = opt[rule];
|
|
288
|
+
if (!limit)
|
|
289
|
+
continue;
|
|
290
|
+
const params = { rule, limit };
|
|
291
|
+
if (!isIsoDateValid(limit)) {
|
|
292
|
+
// An invalid limit rejects every date
|
|
293
|
+
cxt.error(false, params);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
gen.if(isoDateRuleFails(rule, data, limit), () => cxt.error(false, params));
|
|
329
297
|
}
|
|
330
|
-
}
|
|
331
|
-
if (errors.length === 0)
|
|
332
|
-
return true;
|
|
333
|
-
validate.errors = errors;
|
|
334
|
-
return false;
|
|
298
|
+
});
|
|
335
299
|
},
|
|
336
300
|
});
|
|
337
301
|
ajv.addKeyword({
|
|
338
302
|
keyword: 'IsoDateTime',
|
|
339
303
|
type: 'string',
|
|
340
|
-
modifying: false,
|
|
341
|
-
errors: true,
|
|
342
304
|
schemaType: 'boolean',
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
validate.errors = [
|
|
348
|
-
{
|
|
349
|
-
instancePath: ctx?.instancePath ?? '',
|
|
350
|
-
message: `is an invalid IsoDateTime`,
|
|
351
|
-
},
|
|
352
|
-
];
|
|
353
|
-
return false;
|
|
305
|
+
error: { message: 'is an invalid IsoDateTime' },
|
|
306
|
+
code(cxt) {
|
|
307
|
+
const isValid = cxt.gen.scopeValue('func', { ref: isIsoDateTimeValid });
|
|
308
|
+
cxt.fail(_ `!${isValid}(${cxt.data})`);
|
|
354
309
|
},
|
|
355
310
|
});
|
|
356
311
|
ajv.addKeyword({
|
|
357
312
|
keyword: 'IsoMonth',
|
|
358
313
|
type: 'string',
|
|
359
|
-
modifying: false,
|
|
360
|
-
errors: true,
|
|
361
314
|
schemaType: 'object',
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
validate.errors = [
|
|
367
|
-
{
|
|
368
|
-
instancePath: ctx?.instancePath ?? '',
|
|
369
|
-
message: `is an invalid IsoMonth`,
|
|
370
|
-
},
|
|
371
|
-
];
|
|
372
|
-
return false;
|
|
315
|
+
error: { message: 'is an invalid IsoMonth' },
|
|
316
|
+
code(cxt) {
|
|
317
|
+
const isValid = cxt.gen.scopeValue('func', { ref: isIsoMonthValid });
|
|
318
|
+
cxt.fail(_ `!${isValid}(${cxt.data})`);
|
|
373
319
|
},
|
|
374
320
|
});
|
|
375
321
|
ajv.addKeyword({
|
|
376
322
|
keyword: 'UtcOffset',
|
|
377
323
|
type: 'string',
|
|
378
|
-
modifying: false,
|
|
379
|
-
errors: true,
|
|
380
324
|
schemaType: 'object',
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
message: `is an invalid UtcOffset`,
|
|
389
|
-
},
|
|
390
|
-
];
|
|
391
|
-
return false;
|
|
325
|
+
error: { message: 'is an invalid UtcOffset' },
|
|
326
|
+
code(cxt) {
|
|
327
|
+
const opt = cxt.schema;
|
|
328
|
+
const isValid = cxt.gen.scopeValue('func', {
|
|
329
|
+
ref: opt.allowZ ? isUtcOffsetOrZValid : isUtcOffsetValid,
|
|
330
|
+
});
|
|
331
|
+
cxt.fail(_ `!${isValid}(${cxt.data})`);
|
|
392
332
|
},
|
|
393
333
|
});
|
|
394
334
|
// Annotation only (the values are validated by `enum`), registered so strict mode doesn't reject it
|
|
@@ -425,35 +365,62 @@ export function createAjv(opt) {
|
|
|
425
365
|
return true;
|
|
426
366
|
},
|
|
427
367
|
});
|
|
368
|
+
// Strips the keys that don't match the key schema; the values are validated by `additionalProperties`.
|
|
369
|
+
// The key schema is inlined per key with error reporting off, and the error count is reset after
|
|
370
|
+
// a mismatch, so a stripped key never surfaces as an error. Keys listed in `properties` are skipped.
|
|
428
371
|
ajv.addKeyword({
|
|
429
372
|
keyword: 'keySchema',
|
|
430
373
|
type: 'object',
|
|
431
|
-
modifying: true,
|
|
432
|
-
errors: false,
|
|
433
374
|
schemaType: 'object',
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
375
|
+
trackErrors: true,
|
|
376
|
+
code(cxt) {
|
|
377
|
+
const { gen, data, parentSchema } = cxt;
|
|
378
|
+
const propertyNames = Object.keys(parentSchema['properties'] || {});
|
|
379
|
+
const valid = gen.name('valid');
|
|
380
|
+
gen.forIn('key', data, key => {
|
|
381
|
+
const checkKey = () => {
|
|
382
|
+
cxt.subschema({
|
|
383
|
+
keyword: 'keySchema',
|
|
384
|
+
data: key,
|
|
385
|
+
dataTypes: ['string'],
|
|
386
|
+
compositeRule: true,
|
|
387
|
+
createErrors: false,
|
|
388
|
+
allErrors: false,
|
|
389
|
+
}, valid);
|
|
390
|
+
gen.if(_ `!${valid}`, () => {
|
|
391
|
+
cxt.reset();
|
|
392
|
+
gen.code(_ `delete ${data}[${key}]`);
|
|
393
|
+
});
|
|
394
|
+
};
|
|
395
|
+
if (!propertyNames.length)
|
|
396
|
+
return checkKey();
|
|
397
|
+
let isNotProperty = _ `${key} !== ${propertyNames[0]}`;
|
|
398
|
+
for (const name of propertyNames.slice(1)) {
|
|
399
|
+
isNotProperty = _ `${isNotProperty} && ${key} !== ${name}`;
|
|
443
400
|
}
|
|
444
|
-
|
|
445
|
-
}
|
|
446
|
-
return validate;
|
|
401
|
+
gen.if(isNotProperty, checkKey);
|
|
402
|
+
});
|
|
447
403
|
},
|
|
448
404
|
});
|
|
449
|
-
// Validates
|
|
450
|
-
//
|
|
405
|
+
// Validates the inner schema only when the value is defined. Used for the optional values of
|
|
406
|
+
// `stringMap`/`record`, whose `additionalProperties` loop visits the keys with undefined values too.
|
|
407
|
+
ajv.addKeyword({
|
|
408
|
+
keyword: 'ifDefined',
|
|
409
|
+
schemaType: ['object', 'boolean'],
|
|
410
|
+
code(cxt) {
|
|
411
|
+
const { gen, data } = cxt;
|
|
412
|
+
gen.if(_ `${data} !== undefined`, () => {
|
|
413
|
+
cxt.subschema({ keyword: 'ifDefined' }, gen.name('valid'));
|
|
414
|
+
});
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
// Validates that the value is undefined. Superseded by `ifDefined`, kept for raw JSON schemas.
|
|
451
418
|
ajv.addKeyword({
|
|
452
419
|
keyword: 'isUndefined',
|
|
453
|
-
modifying: false,
|
|
454
|
-
errors: false,
|
|
455
420
|
schemaType: 'boolean',
|
|
456
|
-
|
|
421
|
+
code(cxt) {
|
|
422
|
+
cxt.fail(_ `${cxt.data} !== undefined`);
|
|
423
|
+
},
|
|
457
424
|
});
|
|
458
425
|
// This is added because Ajv validates the `min/maxProperties` before validating the properties.
|
|
459
426
|
// So, in case of `minProperties(1)` and `{ foo: 'bar' }` Ajv will let it pass, even
|
|
@@ -660,6 +627,25 @@ export function createAjv(opt) {
|
|
|
660
627
|
});
|
|
661
628
|
return ajv;
|
|
662
629
|
}
|
|
630
|
+
const isoDateRules = ['before', 'sameOrBefore', 'after', 'sameOrAfter'];
|
|
631
|
+
const isoDateRuleLabels = {
|
|
632
|
+
before: 'before',
|
|
633
|
+
sameOrBefore: 'the same or before',
|
|
634
|
+
after: 'after',
|
|
635
|
+
sameOrAfter: 'the same or after',
|
|
636
|
+
};
|
|
637
|
+
/**
|
|
638
|
+
* IsoDate strings compare chronologically as plain strings.
|
|
639
|
+
*/
|
|
640
|
+
function isoDateRuleFails(rule, data, limit) {
|
|
641
|
+
if (rule === 'before')
|
|
642
|
+
return _ `${data} >= ${limit}`;
|
|
643
|
+
if (rule === 'sameOrBefore')
|
|
644
|
+
return _ `${data} > ${limit}`;
|
|
645
|
+
if (rule === 'after')
|
|
646
|
+
return _ `${data} <= ${limit}`;
|
|
647
|
+
return _ `${data} < ${limit}`;
|
|
648
|
+
}
|
|
663
649
|
const monthLengths = [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
664
650
|
const DASH_CODE = '-'.charCodeAt(0);
|
|
665
651
|
const ZERO_CODE = '0'.charCodeAt(0);
|
|
@@ -788,6 +774,12 @@ function isIsoTimezoneValid(s) {
|
|
|
788
774
|
return false; // min is -12:00
|
|
789
775
|
return true;
|
|
790
776
|
}
|
|
777
|
+
function isUtcOffsetValid(s) {
|
|
778
|
+
return s !== 'Z' && s !== '' && isIsoTimezoneValid(s);
|
|
779
|
+
}
|
|
780
|
+
function isUtcOffsetOrZValid(s) {
|
|
781
|
+
return s === 'Z' || (s !== '' && isIsoTimezoneValid(s));
|
|
782
|
+
}
|
|
791
783
|
/**
|
|
792
784
|
* This is a performance optimized correct validation
|
|
793
785
|
* for ISO month formatted as "YYYY-MM".
|
|
@@ -307,6 +307,7 @@ export declare class JIsoDate<Opt extends boolean = false> extends JBuilder<IsoD
|
|
|
307
307
|
after(date: string): this;
|
|
308
308
|
sameOrAfter(date: string): this;
|
|
309
309
|
between(fromDate: string, toDate: string, incl: Inclusiveness): this;
|
|
310
|
+
private addRules;
|
|
310
311
|
}
|
|
311
312
|
export interface JsonSchemaIsoDateOptions {
|
|
312
313
|
before?: string;
|
|
@@ -387,6 +388,10 @@ export declare class JObject<OUT extends AnyObject, Opt extends boolean = false>
|
|
|
387
388
|
}
|
|
388
389
|
interface JObjectOpts {
|
|
389
390
|
hasIsOfTypeCheck?: false;
|
|
391
|
+
/**
|
|
392
|
+
* Schema for every property not listed in `properties`, e.g the values of a string map.
|
|
393
|
+
*/
|
|
394
|
+
additionalProperties?: JsonSchema<any>;
|
|
390
395
|
patternProperties?: StringMap<JsonSchema<any>>;
|
|
391
396
|
keySchema?: JsonSchema;
|
|
392
397
|
}
|
|
@@ -574,7 +579,7 @@ export interface JsonSchema<OUT = unknown> {
|
|
|
574
579
|
};
|
|
575
580
|
patternProperties?: StringMap<JsonSchema<any>>;
|
|
576
581
|
required?: string[];
|
|
577
|
-
additionalProperties?: boolean
|
|
582
|
+
additionalProperties?: boolean | JsonSchema<any>;
|
|
578
583
|
minProperties?: number;
|
|
579
584
|
maxProperties?: number;
|
|
580
585
|
default?: OUT;
|
|
@@ -626,6 +631,10 @@ export interface JsonSchema<OUT = unknown> {
|
|
|
626
631
|
errorMessages?: StringMap<string>;
|
|
627
632
|
optionalValues?: (string | number | boolean | null)[];
|
|
628
633
|
keySchema?: JsonSchema;
|
|
634
|
+
/**
|
|
635
|
+
* Validates the value against the given schema only when it's defined.
|
|
636
|
+
*/
|
|
637
|
+
ifDefined?: JsonSchema;
|
|
629
638
|
isUndefined?: true;
|
|
630
639
|
minProperties2?: number;
|
|
631
640
|
exclusiveProperties?: (readonly string[])[];
|
|
@@ -38,13 +38,13 @@ export const j = {
|
|
|
38
38
|
const isValueOptional = schema.getSchema().optionalField;
|
|
39
39
|
const builtSchema = schema.build();
|
|
40
40
|
const finalValueSchema = isValueOptional
|
|
41
|
-
? {
|
|
41
|
+
? { ifDefined: builtSchema }
|
|
42
42
|
: builtSchema;
|
|
43
|
+
// `additionalProperties` instead of `patternProperties: { '^.+$': ... }`, which made Ajv
|
|
44
|
+
// regex-test every key twice (once more for `additionalProperties: false`)
|
|
43
45
|
return new JObject({}, {
|
|
44
46
|
hasIsOfTypeCheck: false,
|
|
45
|
-
|
|
46
|
-
'^.+$': finalValueSchema,
|
|
47
|
-
},
|
|
47
|
+
additionalProperties: finalValueSchema,
|
|
48
48
|
});
|
|
49
49
|
},
|
|
50
50
|
/**
|
|
@@ -695,26 +695,25 @@ export class JIsoDate extends JBuilder {
|
|
|
695
695
|
});
|
|
696
696
|
}
|
|
697
697
|
before(date) {
|
|
698
|
-
return this.
|
|
698
|
+
return this.addRules({ before: date });
|
|
699
699
|
}
|
|
700
700
|
sameOrBefore(date) {
|
|
701
|
-
return this.
|
|
701
|
+
return this.addRules({ sameOrBefore: date });
|
|
702
702
|
}
|
|
703
703
|
after(date) {
|
|
704
|
-
return this.
|
|
704
|
+
return this.addRules({ after: date });
|
|
705
705
|
}
|
|
706
706
|
sameOrAfter(date) {
|
|
707
|
-
return this.
|
|
707
|
+
return this.addRules({ sameOrAfter: date });
|
|
708
708
|
}
|
|
709
709
|
between(fromDate, toDate, incl) {
|
|
710
|
-
let schemaPatch = {};
|
|
711
710
|
if (incl === '[)') {
|
|
712
|
-
|
|
713
|
-
}
|
|
714
|
-
else if (incl === '[]') {
|
|
715
|
-
schemaPatch = { IsoDate: { sameOrAfter: fromDate, sameOrBefore: toDate } };
|
|
711
|
+
return this.addRules({ sameOrAfter: fromDate, before: toDate });
|
|
716
712
|
}
|
|
717
|
-
return this.
|
|
713
|
+
return this.addRules({ sameOrAfter: fromDate, sameOrBefore: toDate });
|
|
714
|
+
}
|
|
715
|
+
addRules(rules) {
|
|
716
|
+
return this.cloneAndUpdateSchema({ IsoDate: { ...this.schema.IsoDate, ...rules } });
|
|
718
717
|
}
|
|
719
718
|
}
|
|
720
719
|
export class JNumber extends JBuilder {
|
|
@@ -832,7 +831,7 @@ export class JObject extends JBuilder {
|
|
|
832
831
|
type: 'object',
|
|
833
832
|
properties: {},
|
|
834
833
|
required: [],
|
|
835
|
-
additionalProperties: false,
|
|
834
|
+
additionalProperties: opt?.additionalProperties ?? false,
|
|
836
835
|
hasIsOfTypeCheck: opt?.hasIsOfTypeCheck ?? true,
|
|
837
836
|
patternProperties: opt?.patternProperties ?? undefined,
|
|
838
837
|
keySchema: opt?.keySchema ?? undefined,
|
|
@@ -844,6 +843,9 @@ export class JObject extends JBuilder {
|
|
|
844
843
|
* When set, the validation will not strip away properties that are not specified explicitly in the schema.
|
|
845
844
|
*/
|
|
846
845
|
allowAdditionalProperties() {
|
|
846
|
+
// A string map already accepts every property, validated by its value schema
|
|
847
|
+
if (typeof this.schema.additionalProperties === 'object')
|
|
848
|
+
return this.clone();
|
|
847
849
|
return this.cloneAndUpdateSchema({ additionalProperties: true });
|
|
848
850
|
}
|
|
849
851
|
extend(props) {
|
|
@@ -1024,16 +1026,13 @@ function record(keySchema, valueSchema) {
|
|
|
1024
1026
|
// Check if value schema is optional before build() strips the optionalField flag
|
|
1025
1027
|
const isValueOptional = valueSchema.getSchema().optionalField;
|
|
1026
1028
|
const valueJsonSchema = valueSchema.build();
|
|
1027
|
-
// When value schema is optional, wrap in anyOf to allow undefined values
|
|
1028
1029
|
const finalValueSchema = isValueOptional
|
|
1029
|
-
? {
|
|
1030
|
+
? { ifDefined: valueJsonSchema }
|
|
1030
1031
|
: valueJsonSchema;
|
|
1031
1032
|
return new JObject([], {
|
|
1032
1033
|
hasIsOfTypeCheck: false,
|
|
1033
1034
|
keySchema: keyJsonSchema,
|
|
1034
|
-
|
|
1035
|
-
['^.*$']: finalValueSchema,
|
|
1036
|
-
},
|
|
1035
|
+
additionalProperties: finalValueSchema,
|
|
1037
1036
|
});
|
|
1038
1037
|
}
|
|
1039
1038
|
function withRegexKeys(keyRegex, schema) {
|
|
@@ -53,16 +53,20 @@ export function mergeJsonSchemaObjects(schema1, schema2) {
|
|
|
53
53
|
s1.properties[k] = v;
|
|
54
54
|
});
|
|
55
55
|
// Merge `patternProperties`
|
|
56
|
-
|
|
57
|
-
s1.patternProperties
|
|
58
|
-
}
|
|
56
|
+
if (s2.patternProperties) {
|
|
57
|
+
s1.patternProperties = { ...s1.patternProperties, ...s2.patternProperties };
|
|
58
|
+
}
|
|
59
59
|
s1.propertyNames = s2.propertyNames || s1.propertyNames;
|
|
60
|
+
s1.keySchema = s2.keySchema || s1.keySchema;
|
|
60
61
|
s1.minProperties = s2.minProperties ?? s1.minProperties;
|
|
61
62
|
s1.maxProperties = s2.maxProperties ?? s1.maxProperties;
|
|
62
63
|
// Merge `required`
|
|
63
64
|
s1.required.push(...s2.required);
|
|
64
65
|
s1.required = _uniq(s1.required).sort();
|
|
65
|
-
// `additionalProperties` remains the same
|
|
66
|
+
// `additionalProperties` remains the same, unless s2 is a string map (schema-valued)
|
|
67
|
+
if (typeof s2.additionalProperties === 'object') {
|
|
68
|
+
s1.additionalProperties = s2.additionalProperties;
|
|
69
|
+
}
|
|
66
70
|
return _filterNullishValues(s1, { mutate: true });
|
|
67
71
|
}
|
|
68
72
|
export function isEveryItemString(arr) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naturalcycles/nodejs-lib",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "15.
|
|
4
|
+
"version": "15.141.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@naturalcycles/js-lib": "^15",
|
|
7
7
|
"@standard-schema/spec": "^1",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"yaml": "^2"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"@naturalcycles/dev-lib": "
|
|
17
|
+
"@naturalcycles/dev-lib": "0.0.0",
|
|
18
18
|
"typescript": "^7"
|
|
19
19
|
},
|
|
20
20
|
"exports": {
|
|
@@ -3,15 +3,14 @@ import { _assert } from '@naturalcycles/js-lib/error'
|
|
|
3
3
|
import { _deepCopy, _mapObject, Set2 } from '@naturalcycles/js-lib/object'
|
|
4
4
|
import { _substringAfterLast } from '@naturalcycles/js-lib/string'
|
|
5
5
|
import type { AnyObject } from '@naturalcycles/js-lib/types'
|
|
6
|
-
import type { Options, ValidateFunction } from 'ajv/dist/2020.js'
|
|
7
|
-
import { Ajv2020 } from 'ajv/dist/2020.js'
|
|
6
|
+
import type { Code, KeywordCxt, Options, ValidateFunction } from 'ajv/dist/2020.js'
|
|
7
|
+
import { _, Ajv2020, str } from 'ajv/dist/2020.js'
|
|
8
8
|
import { validTLDs } from '../tlds.js'
|
|
9
9
|
import type {
|
|
10
10
|
CustomConverterFn,
|
|
11
11
|
CustomValidatorFn,
|
|
12
12
|
JSchema,
|
|
13
13
|
JsonSchemaIsoDateOptions,
|
|
14
|
-
JsonSchemaIsoMonthOptions,
|
|
15
14
|
JsonSchemaStringEmailOptions,
|
|
16
15
|
JsonSchemaUtcOffsetOptions,
|
|
17
16
|
} from './jSchema.js'
|
|
@@ -310,140 +309,79 @@ export function createAjv(opt?: Options): Ajv2020 {
|
|
|
310
309
|
},
|
|
311
310
|
})
|
|
312
311
|
|
|
312
|
+
// The date/time keywords are `code` keywords, which Ajv inlines into the compiled validator.
|
|
313
|
+
// A `validate` keyword is a function call with a context object, whose instancePath Ajv builds
|
|
314
|
+
// on every call (escaping the key of every map entry), even for valid data.
|
|
313
315
|
ajv.addKeyword({
|
|
314
316
|
keyword: 'IsoDate',
|
|
315
317
|
type: 'string',
|
|
316
|
-
modifying: false,
|
|
317
|
-
errors: true,
|
|
318
318
|
schemaType: 'object',
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if (sameOrBefore) {
|
|
352
|
-
const isParamValid = isIsoDateValid(sameOrBefore)
|
|
353
|
-
const isRuleValid = isParamValid && sameOrBefore >= data
|
|
354
|
-
|
|
355
|
-
if (!isRuleValid) {
|
|
356
|
-
errors.push({
|
|
357
|
-
instancePath: ctx?.instancePath ?? '',
|
|
358
|
-
message: `is not the same or before ${sameOrBefore}`,
|
|
359
|
-
})
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
if (after) {
|
|
364
|
-
const isParamValid = isIsoDateValid(after)
|
|
365
|
-
const isRuleValid = isParamValid && after < data
|
|
366
|
-
|
|
367
|
-
if (!isRuleValid) {
|
|
368
|
-
errors.push({
|
|
369
|
-
instancePath: ctx?.instancePath ?? '',
|
|
370
|
-
message: `is not after ${after}`,
|
|
371
|
-
})
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
if (sameOrAfter) {
|
|
376
|
-
const isParamValid = isIsoDateValid(sameOrAfter)
|
|
377
|
-
const isRuleValid = isParamValid && sameOrAfter <= data
|
|
378
|
-
|
|
379
|
-
if (!isRuleValid) {
|
|
380
|
-
errors.push({
|
|
381
|
-
instancePath: ctx?.instancePath ?? '',
|
|
382
|
-
message: `is not the same or after ${sameOrAfter}`,
|
|
383
|
-
})
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
if (errors.length === 0) return true
|
|
388
|
-
;(validate as any).errors = errors
|
|
389
|
-
return false
|
|
319
|
+
error: {
|
|
320
|
+
message: ({ params }) =>
|
|
321
|
+
params['rule']
|
|
322
|
+
? str`is not ${isoDateRuleLabels[params['rule'] as IsoDateRule]} ${params['limit']}`
|
|
323
|
+
: str`is an invalid IsoDate`,
|
|
324
|
+
params: ({ params }) => (params['rule'] ? _`{${params['rule']}: ${params['limit']}}` : _`{}`),
|
|
325
|
+
},
|
|
326
|
+
code(cxt: KeywordCxt) {
|
|
327
|
+
const { gen, data } = cxt
|
|
328
|
+
const opt: JsonSchemaIsoDateOptions = cxt.schema
|
|
329
|
+
const isValid = gen.scopeValue('func', { ref: isIsoDateValid })
|
|
330
|
+
|
|
331
|
+
gen.if(
|
|
332
|
+
_`!${isValid}(${data})`,
|
|
333
|
+
() => cxt.error(),
|
|
334
|
+
() => {
|
|
335
|
+
for (const rule of isoDateRules) {
|
|
336
|
+
const limit = opt[rule]
|
|
337
|
+
if (!limit) continue
|
|
338
|
+
|
|
339
|
+
const params = { rule, limit }
|
|
340
|
+
if (!isIsoDateValid(limit)) {
|
|
341
|
+
// An invalid limit rejects every date
|
|
342
|
+
cxt.error(false, params)
|
|
343
|
+
continue
|
|
344
|
+
}
|
|
345
|
+
gen.if(isoDateRuleFails(rule, data, limit), () => cxt.error(false, params))
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
)
|
|
390
349
|
},
|
|
391
350
|
})
|
|
392
351
|
|
|
393
352
|
ajv.addKeyword({
|
|
394
353
|
keyword: 'IsoDateTime',
|
|
395
354
|
type: 'string',
|
|
396
|
-
modifying: false,
|
|
397
|
-
errors: true,
|
|
398
355
|
schemaType: 'boolean',
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
{
|
|
404
|
-
instancePath: ctx?.instancePath ?? '',
|
|
405
|
-
message: `is an invalid IsoDateTime`,
|
|
406
|
-
},
|
|
407
|
-
]
|
|
408
|
-
return false
|
|
356
|
+
error: { message: 'is an invalid IsoDateTime' },
|
|
357
|
+
code(cxt: KeywordCxt) {
|
|
358
|
+
const isValid = cxt.gen.scopeValue('func', { ref: isIsoDateTimeValid })
|
|
359
|
+
cxt.fail(_`!${isValid}(${cxt.data})`)
|
|
409
360
|
},
|
|
410
361
|
})
|
|
411
362
|
|
|
412
363
|
ajv.addKeyword({
|
|
413
364
|
keyword: 'IsoMonth',
|
|
414
365
|
type: 'string',
|
|
415
|
-
modifying: false,
|
|
416
|
-
errors: true,
|
|
417
366
|
schemaType: 'object',
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
{
|
|
423
|
-
instancePath: ctx?.instancePath ?? '',
|
|
424
|
-
message: `is an invalid IsoMonth`,
|
|
425
|
-
},
|
|
426
|
-
]
|
|
427
|
-
return false
|
|
367
|
+
error: { message: 'is an invalid IsoMonth' },
|
|
368
|
+
code(cxt: KeywordCxt) {
|
|
369
|
+
const isValid = cxt.gen.scopeValue('func', { ref: isIsoMonthValid })
|
|
370
|
+
cxt.fail(_`!${isValid}(${cxt.data})`)
|
|
428
371
|
},
|
|
429
372
|
})
|
|
430
373
|
|
|
431
374
|
ajv.addKeyword({
|
|
432
375
|
keyword: 'UtcOffset',
|
|
433
376
|
type: 'string',
|
|
434
|
-
modifying: false,
|
|
435
|
-
errors: true,
|
|
436
377
|
schemaType: 'object',
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
},
|
|
445
|
-
]
|
|
446
|
-
return false
|
|
378
|
+
error: { message: 'is an invalid UtcOffset' },
|
|
379
|
+
code(cxt: KeywordCxt) {
|
|
380
|
+
const opt: JsonSchemaUtcOffsetOptions = cxt.schema
|
|
381
|
+
const isValid = cxt.gen.scopeValue('func', {
|
|
382
|
+
ref: opt.allowZ ? isUtcOffsetOrZValid : isUtcOffsetValid,
|
|
383
|
+
})
|
|
384
|
+
cxt.fail(_`!${isValid}(${cxt.data})`)
|
|
447
385
|
},
|
|
448
386
|
})
|
|
449
387
|
|
|
@@ -496,39 +434,69 @@ export function createAjv(opt?: Options): Ajv2020 {
|
|
|
496
434
|
},
|
|
497
435
|
})
|
|
498
436
|
|
|
437
|
+
// Strips the keys that don't match the key schema; the values are validated by `additionalProperties`.
|
|
438
|
+
// The key schema is inlined per key with error reporting off, and the error count is reset after
|
|
439
|
+
// a mismatch, so a stripped key never surfaces as an error. Keys listed in `properties` are skipped.
|
|
499
440
|
ajv.addKeyword({
|
|
500
441
|
keyword: 'keySchema',
|
|
501
442
|
type: 'object',
|
|
502
|
-
modifying: true,
|
|
503
|
-
errors: false,
|
|
504
443
|
schemaType: 'object',
|
|
505
|
-
|
|
506
|
-
|
|
444
|
+
trackErrors: true,
|
|
445
|
+
code(cxt: KeywordCxt) {
|
|
446
|
+
const { gen, data, parentSchema } = cxt
|
|
447
|
+
const propertyNames = Object.keys(parentSchema['properties'] || {})
|
|
448
|
+
const valid = gen.name('valid')
|
|
449
|
+
|
|
450
|
+
gen.forIn('key', data, key => {
|
|
451
|
+
const checkKey = (): void => {
|
|
452
|
+
cxt.subschema(
|
|
453
|
+
{
|
|
454
|
+
keyword: 'keySchema',
|
|
455
|
+
data: key,
|
|
456
|
+
dataTypes: ['string'],
|
|
457
|
+
compositeRule: true,
|
|
458
|
+
createErrors: false,
|
|
459
|
+
allErrors: false,
|
|
460
|
+
},
|
|
461
|
+
valid,
|
|
462
|
+
)
|
|
463
|
+
gen.if(_`!${valid}`, () => {
|
|
464
|
+
cxt.reset()
|
|
465
|
+
gen.code(_`delete ${data}[${key}]`)
|
|
466
|
+
})
|
|
467
|
+
}
|
|
507
468
|
|
|
508
|
-
|
|
509
|
-
if (typeof data !== 'object' || data === null) return true
|
|
469
|
+
if (!propertyNames.length) return checkKey()
|
|
510
470
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
}
|
|
471
|
+
let isNotProperty: Code = _`${key} !== ${propertyNames[0]}`
|
|
472
|
+
for (const name of propertyNames.slice(1)) {
|
|
473
|
+
isNotProperty = _`${isNotProperty} && ${key} !== ${name}`
|
|
515
474
|
}
|
|
475
|
+
gen.if(isNotProperty, checkKey)
|
|
476
|
+
})
|
|
477
|
+
},
|
|
478
|
+
})
|
|
516
479
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
480
|
+
// Validates the inner schema only when the value is defined. Used for the optional values of
|
|
481
|
+
// `stringMap`/`record`, whose `additionalProperties` loop visits the keys with undefined values too.
|
|
482
|
+
ajv.addKeyword({
|
|
483
|
+
keyword: 'ifDefined',
|
|
484
|
+
schemaType: ['object', 'boolean'],
|
|
485
|
+
code(cxt: KeywordCxt) {
|
|
486
|
+
const { gen, data } = cxt
|
|
487
|
+
gen.if(_`${data} !== undefined`, () => {
|
|
488
|
+
cxt.subschema({ keyword: 'ifDefined' }, gen.name('valid'))
|
|
489
|
+
})
|
|
521
490
|
},
|
|
522
491
|
})
|
|
523
492
|
|
|
524
|
-
// Validates that the value is undefined.
|
|
525
|
-
// to allow undefined values in patternProperties via anyOf.
|
|
493
|
+
// Validates that the value is undefined. Superseded by `ifDefined`, kept for raw JSON schemas.
|
|
526
494
|
ajv.addKeyword({
|
|
527
495
|
keyword: 'isUndefined',
|
|
528
|
-
modifying: false,
|
|
529
|
-
errors: false,
|
|
530
496
|
schemaType: 'boolean',
|
|
531
|
-
|
|
497
|
+
code(cxt: KeywordCxt) {
|
|
498
|
+
cxt.fail(_`${cxt.data} !== undefined`)
|
|
499
|
+
},
|
|
532
500
|
})
|
|
533
501
|
|
|
534
502
|
// This is added because Ajv validates the `min/maxProperties` before validating the properties.
|
|
@@ -777,6 +745,27 @@ export function createAjv(opt?: Options): Ajv2020 {
|
|
|
777
745
|
return ajv
|
|
778
746
|
}
|
|
779
747
|
|
|
748
|
+
type IsoDateRule = keyof JsonSchemaIsoDateOptions
|
|
749
|
+
|
|
750
|
+
const isoDateRules: IsoDateRule[] = ['before', 'sameOrBefore', 'after', 'sameOrAfter']
|
|
751
|
+
|
|
752
|
+
const isoDateRuleLabels: Record<IsoDateRule, string> = {
|
|
753
|
+
before: 'before',
|
|
754
|
+
sameOrBefore: 'the same or before',
|
|
755
|
+
after: 'after',
|
|
756
|
+
sameOrAfter: 'the same or after',
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* IsoDate strings compare chronologically as plain strings.
|
|
761
|
+
*/
|
|
762
|
+
function isoDateRuleFails(rule: IsoDateRule, data: Code, limit: string): Code {
|
|
763
|
+
if (rule === 'before') return _`${data} >= ${limit}`
|
|
764
|
+
if (rule === 'sameOrBefore') return _`${data} > ${limit}`
|
|
765
|
+
if (rule === 'after') return _`${data} <= ${limit}`
|
|
766
|
+
return _`${data} < ${limit}`
|
|
767
|
+
}
|
|
768
|
+
|
|
780
769
|
const monthLengths = [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
781
770
|
|
|
782
771
|
const DASH_CODE = '-'.charCodeAt(0)
|
|
@@ -904,6 +893,14 @@ function isIsoTimezoneValid(s: string): boolean {
|
|
|
904
893
|
return true
|
|
905
894
|
}
|
|
906
895
|
|
|
896
|
+
function isUtcOffsetValid(s: string): boolean {
|
|
897
|
+
return s !== 'Z' && s !== '' && isIsoTimezoneValid(s)
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function isUtcOffsetOrZValid(s: string): boolean {
|
|
901
|
+
return s === 'Z' || (s !== '' && isIsoTimezoneValid(s))
|
|
902
|
+
}
|
|
903
|
+
|
|
907
904
|
/**
|
|
908
905
|
* This is a performance optimized correct validation
|
|
909
906
|
* for ISO month formatted as "YYYY-MM".
|
|
@@ -89,16 +89,16 @@ export const j = {
|
|
|
89
89
|
const isValueOptional = schema.getSchema().optionalField
|
|
90
90
|
const builtSchema = schema.build()
|
|
91
91
|
const finalValueSchema: JsonSchema = isValueOptional
|
|
92
|
-
? {
|
|
92
|
+
? { ifDefined: builtSchema }
|
|
93
93
|
: builtSchema
|
|
94
94
|
|
|
95
|
+
// `additionalProperties` instead of `patternProperties: { '^.+$': ... }`, which made Ajv
|
|
96
|
+
// regex-test every key twice (once more for `additionalProperties: false`)
|
|
95
97
|
return new JObject<StringMap<SchemaOut<S>>>(
|
|
96
98
|
{},
|
|
97
99
|
{
|
|
98
100
|
hasIsOfTypeCheck: false,
|
|
99
|
-
|
|
100
|
-
'^.+$': finalValueSchema,
|
|
101
|
-
},
|
|
101
|
+
additionalProperties: finalValueSchema,
|
|
102
102
|
},
|
|
103
103
|
)
|
|
104
104
|
},
|
|
@@ -917,31 +917,30 @@ export class JIsoDate<Opt extends boolean = false> extends JBuilder<IsoDate, Opt
|
|
|
917
917
|
}
|
|
918
918
|
|
|
919
919
|
before(date: string): this {
|
|
920
|
-
return this.
|
|
920
|
+
return this.addRules({ before: date })
|
|
921
921
|
}
|
|
922
922
|
|
|
923
923
|
sameOrBefore(date: string): this {
|
|
924
|
-
return this.
|
|
924
|
+
return this.addRules({ sameOrBefore: date })
|
|
925
925
|
}
|
|
926
926
|
|
|
927
927
|
after(date: string): this {
|
|
928
|
-
return this.
|
|
928
|
+
return this.addRules({ after: date })
|
|
929
929
|
}
|
|
930
930
|
|
|
931
931
|
sameOrAfter(date: string): this {
|
|
932
|
-
return this.
|
|
932
|
+
return this.addRules({ sameOrAfter: date })
|
|
933
933
|
}
|
|
934
934
|
|
|
935
935
|
between(fromDate: string, toDate: string, incl: Inclusiveness): this {
|
|
936
|
-
let schemaPatch: Partial<JsonSchema> = {}
|
|
937
|
-
|
|
938
936
|
if (incl === '[)') {
|
|
939
|
-
|
|
940
|
-
} else if (incl === '[]') {
|
|
941
|
-
schemaPatch = { IsoDate: { sameOrAfter: fromDate, sameOrBefore: toDate } }
|
|
937
|
+
return this.addRules({ sameOrAfter: fromDate, before: toDate })
|
|
942
938
|
}
|
|
939
|
+
return this.addRules({ sameOrAfter: fromDate, sameOrBefore: toDate })
|
|
940
|
+
}
|
|
943
941
|
|
|
944
|
-
|
|
942
|
+
private addRules(rules: JsonSchemaIsoDateOptions): this {
|
|
943
|
+
return this.cloneAndUpdateSchema({ IsoDate: { ...this.schema.IsoDate, ...rules } })
|
|
945
944
|
}
|
|
946
945
|
}
|
|
947
946
|
|
|
@@ -1108,7 +1107,7 @@ export class JObject<OUT extends AnyObject, Opt extends boolean = false> extends
|
|
|
1108
1107
|
type: 'object',
|
|
1109
1108
|
properties: {},
|
|
1110
1109
|
required: [],
|
|
1111
|
-
additionalProperties: false,
|
|
1110
|
+
additionalProperties: opt?.additionalProperties ?? false,
|
|
1112
1111
|
hasIsOfTypeCheck: opt?.hasIsOfTypeCheck ?? true,
|
|
1113
1112
|
patternProperties: opt?.patternProperties ?? undefined,
|
|
1114
1113
|
keySchema: opt?.keySchema ?? undefined,
|
|
@@ -1122,6 +1121,8 @@ export class JObject<OUT extends AnyObject, Opt extends boolean = false> extends
|
|
|
1122
1121
|
*/
|
|
1123
1122
|
|
|
1124
1123
|
allowAdditionalProperties(): this {
|
|
1124
|
+
// A string map already accepts every property, validated by its value schema
|
|
1125
|
+
if (typeof this.schema.additionalProperties === 'object') return this.clone()
|
|
1125
1126
|
return this.cloneAndUpdateSchema({ additionalProperties: true })
|
|
1126
1127
|
}
|
|
1127
1128
|
|
|
@@ -1207,6 +1208,10 @@ export class JObject<OUT extends AnyObject, Opt extends boolean = false> extends
|
|
|
1207
1208
|
|
|
1208
1209
|
interface JObjectOpts {
|
|
1209
1210
|
hasIsOfTypeCheck?: false
|
|
1211
|
+
/**
|
|
1212
|
+
* Schema for every property not listed in `properties`, e.g the values of a string map.
|
|
1213
|
+
*/
|
|
1214
|
+
additionalProperties?: JsonSchema<any>
|
|
1210
1215
|
patternProperties?: StringMap<JsonSchema<any>>
|
|
1211
1216
|
keySchema?: JsonSchema
|
|
1212
1217
|
}
|
|
@@ -1477,17 +1482,14 @@ function record<
|
|
|
1477
1482
|
const isValueOptional = (valueSchema as JSchema<any, any>).getSchema().optionalField
|
|
1478
1483
|
const valueJsonSchema = valueSchema.build()
|
|
1479
1484
|
|
|
1480
|
-
// When value schema is optional, wrap in anyOf to allow undefined values
|
|
1481
1485
|
const finalValueSchema: JsonSchema = isValueOptional
|
|
1482
|
-
? {
|
|
1486
|
+
? { ifDefined: valueJsonSchema }
|
|
1483
1487
|
: valueJsonSchema
|
|
1484
1488
|
|
|
1485
1489
|
return new JObject([], {
|
|
1486
1490
|
hasIsOfTypeCheck: false,
|
|
1487
1491
|
keySchema: keyJsonSchema,
|
|
1488
|
-
|
|
1489
|
-
['^.*$']: finalValueSchema,
|
|
1490
|
-
},
|
|
1492
|
+
additionalProperties: finalValueSchema,
|
|
1491
1493
|
}) as any
|
|
1492
1494
|
}
|
|
1493
1495
|
|
|
@@ -2086,7 +2088,7 @@ export interface JsonSchema<OUT = unknown> {
|
|
|
2086
2088
|
}
|
|
2087
2089
|
patternProperties?: StringMap<JsonSchema<any>>
|
|
2088
2090
|
required?: string[]
|
|
2089
|
-
additionalProperties?: boolean
|
|
2091
|
+
additionalProperties?: boolean | JsonSchema<any>
|
|
2090
2092
|
minProperties?: number
|
|
2091
2093
|
maxProperties?: number
|
|
2092
2094
|
|
|
@@ -2146,6 +2148,10 @@ export interface JsonSchema<OUT = unknown> {
|
|
|
2146
2148
|
errorMessages?: StringMap<string>
|
|
2147
2149
|
optionalValues?: (string | number | boolean | null)[]
|
|
2148
2150
|
keySchema?: JsonSchema
|
|
2151
|
+
/**
|
|
2152
|
+
* Validates the value against the given schema only when it's defined.
|
|
2153
|
+
*/
|
|
2154
|
+
ifDefined?: JsonSchema
|
|
2149
2155
|
isUndefined?: true
|
|
2150
2156
|
minProperties2?: number
|
|
2151
2157
|
exclusiveProperties?: (readonly string[])[]
|
|
@@ -62,11 +62,12 @@ export function mergeJsonSchemaObjects<T1 extends AnyObject, T2 extends AnyObjec
|
|
|
62
62
|
})
|
|
63
63
|
|
|
64
64
|
// Merge `patternProperties`
|
|
65
|
-
|
|
66
|
-
s1.patternProperties
|
|
67
|
-
}
|
|
65
|
+
if (s2.patternProperties) {
|
|
66
|
+
s1.patternProperties = { ...s1.patternProperties, ...s2.patternProperties }
|
|
67
|
+
}
|
|
68
68
|
|
|
69
69
|
s1.propertyNames = s2.propertyNames || s1.propertyNames
|
|
70
|
+
s1.keySchema = s2.keySchema || s1.keySchema
|
|
70
71
|
s1.minProperties = s2.minProperties ?? s1.minProperties
|
|
71
72
|
s1.maxProperties = s2.maxProperties ?? s1.maxProperties
|
|
72
73
|
|
|
@@ -74,7 +75,10 @@ export function mergeJsonSchemaObjects<T1 extends AnyObject, T2 extends AnyObjec
|
|
|
74
75
|
s1.required.push(...s2.required)
|
|
75
76
|
s1.required = _uniq(s1.required).sort()
|
|
76
77
|
|
|
77
|
-
// `additionalProperties` remains the same
|
|
78
|
+
// `additionalProperties` remains the same, unless s2 is a string map (schema-valued)
|
|
79
|
+
if (typeof s2.additionalProperties === 'object') {
|
|
80
|
+
s1.additionalProperties = s2.additionalProperties
|
|
81
|
+
}
|
|
78
82
|
|
|
79
83
|
return _filterNullishValues(s1, { mutate: true })
|
|
80
84
|
}
|