@naturalcycles/nodejs-lib 15.82.1 → 15.84.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/stream/stream.model.d.ts +2 -2
- package/dist/validation/ajv/getAjv.js +60 -10
- package/dist/validation/ajv/jsonSchemaBuilder.d.ts +41 -5
- package/dist/validation/ajv/jsonSchemaBuilder.js +77 -4
- package/package.json +1 -1
- package/src/stream/stream.model.ts +4 -2
- package/src/validation/ajv/getAjv.ts +68 -10
- package/src/validation/ajv/jsonSchemaBuilder.ts +122 -25
|
@@ -24,9 +24,9 @@ export interface ReadableTyped<T = unknown> extends Readable {
|
|
|
24
24
|
take: (limit: number, opt?: ReadableSignalOptions) => ReadableTyped<T>;
|
|
25
25
|
drop: (limit: number, opt?: ReadableSignalOptions) => ReadableTyped<T>;
|
|
26
26
|
}
|
|
27
|
-
export interface WritableTyped<
|
|
27
|
+
export interface WritableTyped<_T> extends Writable {
|
|
28
28
|
}
|
|
29
|
-
export interface TransformTyped<
|
|
29
|
+
export interface TransformTyped<_IN = unknown, _OUT = unknown> extends Transform {
|
|
30
30
|
}
|
|
31
31
|
export interface TransformOptions {
|
|
32
32
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { _isBetween, _lazyValue } from '@naturalcycles/js-lib';
|
|
2
|
-
import {
|
|
2
|
+
import { _assert } from '@naturalcycles/js-lib/error';
|
|
3
|
+
import { _deepCopy, _mapObject, Set2 } from '@naturalcycles/js-lib/object';
|
|
3
4
|
import { _substringAfterLast } from '@naturalcycles/js-lib/string';
|
|
4
5
|
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
5
6
|
import { validTLDs } from '../tlds.js';
|
|
@@ -165,7 +166,7 @@ export function createAjv(opt) {
|
|
|
165
166
|
}
|
|
166
167
|
idx++;
|
|
167
168
|
}
|
|
168
|
-
if (ctx?.parentData && ctx.parentDataProperty) {
|
|
169
|
+
if (ctx?.parentData && ctx.parentDataProperty !== undefined) {
|
|
169
170
|
ctx.parentData[ctx.parentDataProperty] = set;
|
|
170
171
|
}
|
|
171
172
|
return true;
|
|
@@ -215,7 +216,7 @@ export function createAjv(opt) {
|
|
|
215
216
|
];
|
|
216
217
|
return false;
|
|
217
218
|
}
|
|
218
|
-
if (ctx?.parentData && ctx.parentDataProperty) {
|
|
219
|
+
if (ctx?.parentData && ctx.parentDataProperty !== undefined) {
|
|
219
220
|
ctx.parentData[ctx.parentDataProperty] = buffer;
|
|
220
221
|
}
|
|
221
222
|
return true;
|
|
@@ -258,7 +259,7 @@ export function createAjv(opt) {
|
|
|
258
259
|
return false;
|
|
259
260
|
}
|
|
260
261
|
}
|
|
261
|
-
if (ctx?.parentData && ctx.parentDataProperty) {
|
|
262
|
+
if (ctx?.parentData && ctx.parentDataProperty !== undefined) {
|
|
262
263
|
ctx.parentData[ctx.parentDataProperty] = cleanData;
|
|
263
264
|
}
|
|
264
265
|
return true;
|
|
@@ -381,18 +382,17 @@ export function createAjv(opt) {
|
|
|
381
382
|
});
|
|
382
383
|
ajv.addKeyword({
|
|
383
384
|
keyword: 'optionalValues',
|
|
384
|
-
type: ['string', 'number', 'boolean'],
|
|
385
|
+
type: ['string', 'number', 'boolean', 'null'],
|
|
385
386
|
modifying: true,
|
|
386
387
|
errors: false,
|
|
387
388
|
schemaType: 'array',
|
|
388
389
|
validate: function validate(optionalValues, data, _schema, ctx) {
|
|
389
390
|
if (!optionalValues)
|
|
390
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.');
|
|
391
393
|
if (!optionalValues.includes(data))
|
|
392
394
|
return true;
|
|
393
|
-
|
|
394
|
-
delete ctx.parentData[ctx.parentDataProperty];
|
|
395
|
-
}
|
|
395
|
+
ctx.parentData[ctx.parentDataProperty] = undefined;
|
|
396
396
|
return true;
|
|
397
397
|
},
|
|
398
398
|
});
|
|
@@ -417,7 +417,7 @@ export function createAjv(opt) {
|
|
|
417
417
|
return validate;
|
|
418
418
|
},
|
|
419
419
|
});
|
|
420
|
-
// This
|
|
420
|
+
// This is added because Ajv validates the `min/maxProperties` before validating the properties.
|
|
421
421
|
// So, in case of `minProperties(1)` and `{ foo: 'bar' }` Ajv will let it pass, even
|
|
422
422
|
// if the property validation would strip `foo` from the data.
|
|
423
423
|
// And Ajv would return `{}` as a successful validation.
|
|
@@ -434,7 +434,7 @@ export function createAjv(opt) {
|
|
|
434
434
|
validate: function validate(minProperties, data, _schema, ctx) {
|
|
435
435
|
if (typeof data !== 'object')
|
|
436
436
|
return true;
|
|
437
|
-
const numberOfProperties = Object.
|
|
437
|
+
const numberOfProperties = Object.entries(data).filter(([, v]) => v !== undefined).length;
|
|
438
438
|
const isValid = numberOfProperties >= minProperties;
|
|
439
439
|
if (!isValid) {
|
|
440
440
|
;
|
|
@@ -513,6 +513,53 @@ export function createAjv(opt) {
|
|
|
513
513
|
return validate;
|
|
514
514
|
},
|
|
515
515
|
});
|
|
516
|
+
ajv.addKeyword({
|
|
517
|
+
keyword: 'anyOfThese',
|
|
518
|
+
modifying: true,
|
|
519
|
+
errors: true,
|
|
520
|
+
schemaType: 'array',
|
|
521
|
+
compile(schemas, _parentSchema, _it) {
|
|
522
|
+
const validators = schemas.map(schema => ajv.compile(schema));
|
|
523
|
+
function validate(data, ctx) {
|
|
524
|
+
let correctValidator;
|
|
525
|
+
let result = false;
|
|
526
|
+
let clonedData;
|
|
527
|
+
// Try each validator until we find one that works!
|
|
528
|
+
for (const validator of validators) {
|
|
529
|
+
clonedData = isPrimitive(data) ? _deepCopy(data) : data;
|
|
530
|
+
result = validator(clonedData);
|
|
531
|
+
if (result) {
|
|
532
|
+
correctValidator = validator;
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if (result && ctx?.parentData && ctx.parentDataProperty !== undefined) {
|
|
537
|
+
// If we found a validator and the data is valid and we are validating a property inside an object,
|
|
538
|
+
// then we can inject our result and be done with it.
|
|
539
|
+
ctx.parentData[ctx.parentDataProperty] = clonedData;
|
|
540
|
+
}
|
|
541
|
+
else if (result) {
|
|
542
|
+
// If we found a validator but we are not validating a property inside an object,
|
|
543
|
+
// then we must re-run the validation so that the mutations caused by Ajv
|
|
544
|
+
// will be done on the input data, not only on the clone.
|
|
545
|
+
result = correctValidator(data);
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
// If we didn't find a fitting schema,
|
|
549
|
+
// we add our own error.
|
|
550
|
+
;
|
|
551
|
+
validate.errors = [
|
|
552
|
+
{
|
|
553
|
+
instancePath: ctx?.instancePath ?? '',
|
|
554
|
+
message: `could not find a suitable schema to validate against`,
|
|
555
|
+
},
|
|
556
|
+
];
|
|
557
|
+
}
|
|
558
|
+
return result;
|
|
559
|
+
}
|
|
560
|
+
return validate;
|
|
561
|
+
},
|
|
562
|
+
});
|
|
516
563
|
return ajv;
|
|
517
564
|
}
|
|
518
565
|
const monthLengths = [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
@@ -652,3 +699,6 @@ function isIsoMonthValid(s) {
|
|
|
652
699
|
const month = (s.charCodeAt(5) - ZERO_CODE) * 10 + (s.charCodeAt(6) - ZERO_CODE);
|
|
653
700
|
return _isBetween(year, 1900, 2500, '[]') && _isBetween(month, 1, 12, '[]');
|
|
654
701
|
}
|
|
702
|
+
function isPrimitive(data) {
|
|
703
|
+
return data !== null && typeof data === 'object';
|
|
704
|
+
}
|
|
@@ -68,7 +68,7 @@ export declare const j: {
|
|
|
68
68
|
oneOf<B extends readonly JsonSchemaAnyBuilder<any, any, boolean>[], IN = BuilderInUnion<B>, OUT = BuilderOutUnion<B>>(items: [...B]): JsonSchemaAnyBuilder<IN, OUT, false>;
|
|
69
69
|
/**
|
|
70
70
|
* Use only with primitive values, otherwise this function will throw to avoid bugs.
|
|
71
|
-
* To validate objects, use `anyOfBy`.
|
|
71
|
+
* To validate objects, use `anyOfBy` or `anyOfThese`.
|
|
72
72
|
*
|
|
73
73
|
* Our Ajv is configured to strip unexpected properties from objects,
|
|
74
74
|
* and since Ajv is mutating the input, this means that it cannot
|
|
@@ -78,7 +78,28 @@ export declare const j: {
|
|
|
78
78
|
* Use `oneOf` when schemas are mutually exclusive.
|
|
79
79
|
*/
|
|
80
80
|
anyOf<B extends readonly JsonSchemaAnyBuilder<any, any, boolean>[], IN = BuilderInUnion<B>, OUT = BuilderOutUnion<B>>(items: [...B]): JsonSchemaAnyBuilder<IN, OUT, false>;
|
|
81
|
+
/**
|
|
82
|
+
* Pick validation schema for an object based on the value of a specific property.
|
|
83
|
+
*
|
|
84
|
+
* ```
|
|
85
|
+
* const schemaMap = {
|
|
86
|
+
* true: successSchema,
|
|
87
|
+
* false: errorSchema
|
|
88
|
+
* }
|
|
89
|
+
*
|
|
90
|
+
* const schema = j.anyOfBy('success', schemaMap)
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
81
93
|
anyOfBy<P extends string, D extends Record<PropertyKey, JsonSchemaTerminal<any, any, any>>, IN = AnyOfByIn<D>, OUT = AnyOfByOut<D>>(propertyName: P, schemaDictionary: D): JsonSchemaAnyOfByBuilder<IN, OUT, P>;
|
|
94
|
+
/**
|
|
95
|
+
* Custom version of `anyOf` which - in contrast to the original function - does not mutate the input.
|
|
96
|
+
* This comes with a performance penalty, so do not use it where performance matters.
|
|
97
|
+
*
|
|
98
|
+
* ```
|
|
99
|
+
* const schema = j.anyOfThese([successSchema, errorSchema])
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
anyOfThese<B extends readonly JsonSchemaAnyBuilder<any, any, boolean>[], IN = BuilderInUnion<B>, OUT = BuilderOutUnion<B>>(items: [...B]): JsonSchemaAnyBuilder<IN, OUT, false>;
|
|
82
103
|
and(): {
|
|
83
104
|
silentBob: () => never;
|
|
84
105
|
};
|
|
@@ -148,8 +169,13 @@ export declare class JsonSchemaStringBuilder<IN extends string | undefined = str
|
|
|
148
169
|
*
|
|
149
170
|
* This `optionalValues` feature only works when the current schema is nested in an object or array schema,
|
|
150
171
|
* due to how mutability works in Ajv.
|
|
172
|
+
*
|
|
173
|
+
* Make sure this `optional()` call is at the end of your call chain.
|
|
174
|
+
*
|
|
175
|
+
* When `null` is included in optionalValues, the return type becomes `JsonSchemaTerminal`
|
|
176
|
+
* (no further chaining allowed) because the schema is wrapped in an anyOf structure.
|
|
151
177
|
*/
|
|
152
|
-
optional(optionalValues?:
|
|
178
|
+
optional<T extends readonly (string | null)[] | undefined = undefined>(optionalValues?: T): T extends readonly (infer U)[] ? null extends U ? JsonSchemaTerminal<IN | undefined, OUT | undefined, true> : JsonSchemaStringBuilder<IN | undefined, OUT | undefined, true> : JsonSchemaStringBuilder<IN | undefined, OUT | undefined, true>;
|
|
153
179
|
regex(pattern: RegExp, opt?: JsonBuilderRuleOpt): this;
|
|
154
180
|
pattern(pattern: string, opt?: JsonBuilderRuleOpt): this;
|
|
155
181
|
minLength(minLength: number): this;
|
|
@@ -223,8 +249,13 @@ export declare class JsonSchemaNumberBuilder<IN extends number | undefined = num
|
|
|
223
249
|
*
|
|
224
250
|
* This `optionalValues` feature only works when the current schema is nested in an object or array schema,
|
|
225
251
|
* due to how mutability works in Ajv.
|
|
252
|
+
*
|
|
253
|
+
* Make sure this `optional()` call is at the end of your call chain.
|
|
254
|
+
*
|
|
255
|
+
* When `null` is included in optionalValues, the return type becomes `JsonSchemaTerminal`
|
|
256
|
+
* (no further chaining allowed) because the schema is wrapped in an anyOf structure.
|
|
226
257
|
*/
|
|
227
|
-
optional(optionalValues?:
|
|
258
|
+
optional<T extends readonly (number | null)[] | undefined = undefined>(optionalValues?: T): T extends readonly (infer U)[] ? null extends U ? JsonSchemaTerminal<IN | undefined, OUT | undefined, true> : JsonSchemaNumberBuilder<IN | undefined, OUT | undefined, true> : JsonSchemaNumberBuilder<IN | undefined, OUT | undefined, true>;
|
|
228
259
|
integer(): this;
|
|
229
260
|
branded<B extends number>(): JsonSchemaNumberBuilder<B, B, Opt>;
|
|
230
261
|
multipleOf(multipleOf: number): this;
|
|
@@ -369,6 +400,10 @@ export declare class JsonSchemaAnyOfByBuilder<IN, OUT, _P extends string = strin
|
|
|
369
400
|
in: IN;
|
|
370
401
|
constructor(propertyName: string, schemaDictionary: Record<PropertyKey, JsonSchemaTerminal<any, any, any>>);
|
|
371
402
|
}
|
|
403
|
+
export declare class JsonSchemaAnyOfTheseBuilder<IN, OUT, _P extends string = string> extends JsonSchemaAnyBuilder<AnyOfByInput<IN, _P> | IN, OUT, false> {
|
|
404
|
+
in: IN;
|
|
405
|
+
constructor(propertyName: string, schemaDictionary: Record<PropertyKey, JsonSchemaTerminal<any, any, any>>);
|
|
406
|
+
}
|
|
372
407
|
type EnumBaseType = 'string' | 'number' | 'other';
|
|
373
408
|
export interface JsonSchema<IN = unknown, OUT = IN> {
|
|
374
409
|
readonly in?: IN;
|
|
@@ -432,7 +467,7 @@ export interface JsonSchema<IN = unknown, OUT = IN> {
|
|
|
432
467
|
truncate?: number;
|
|
433
468
|
};
|
|
434
469
|
errorMessages?: StringMap<string>;
|
|
435
|
-
optionalValues?: (string | number | boolean)[];
|
|
470
|
+
optionalValues?: (string | number | boolean | null)[];
|
|
436
471
|
keySchema?: JsonSchema;
|
|
437
472
|
minProperties2?: number;
|
|
438
473
|
exclusiveProperties?: (readonly string[])[];
|
|
@@ -440,10 +475,11 @@ export interface JsonSchema<IN = unknown, OUT = IN> {
|
|
|
440
475
|
propertyName: string;
|
|
441
476
|
schemaDictionary: Record<string, JsonSchema>;
|
|
442
477
|
};
|
|
478
|
+
anyOfThese?: JsonSchema[];
|
|
443
479
|
}
|
|
444
480
|
declare function object(props: AnyObject): never;
|
|
445
481
|
declare function object<IN extends AnyObject>(props: {
|
|
446
|
-
[K in keyof Required<IN>]-?:
|
|
482
|
+
[K in keyof Required<IN>]-?: JsonSchemaTerminal<any, IN[K], any>;
|
|
447
483
|
}): JsonSchemaObjectBuilder<IN, IN, false>;
|
|
448
484
|
declare function objectInfer<P extends Record<string, JsonSchemaAnyBuilder<any, any, any>>>(props: P): JsonSchemaObjectInferringBuilder<P, false>;
|
|
449
485
|
declare function objectDbEntity(props: AnyObject): never;
|
|
@@ -4,7 +4,7 @@ import { _isUndefined, _numberEnumValues, _stringEnumValues, getEnumType, } from
|
|
|
4
4
|
import { _uniq } from '@naturalcycles/js-lib/array';
|
|
5
5
|
import { _assert } from '@naturalcycles/js-lib/error';
|
|
6
6
|
import { _deepCopy, _sortObject } from '@naturalcycles/js-lib/object';
|
|
7
|
-
import { _objectAssign, JWT_REGEX, } from '@naturalcycles/js-lib/types';
|
|
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';
|
|
10
10
|
import { isEveryItemNumber, isEveryItemPrimitive, isEveryItemString, JSON_SCHEMA_ORDER, mergeJsonSchemaObjects, } from './jsonSchemaBuilder.util.js';
|
|
@@ -133,7 +133,7 @@ export const j = {
|
|
|
133
133
|
},
|
|
134
134
|
/**
|
|
135
135
|
* Use only with primitive values, otherwise this function will throw to avoid bugs.
|
|
136
|
-
* To validate objects, use `anyOfBy`.
|
|
136
|
+
* To validate objects, use `anyOfBy` or `anyOfThese`.
|
|
137
137
|
*
|
|
138
138
|
* Our Ajv is configured to strip unexpected properties from objects,
|
|
139
139
|
* and since Ajv is mutating the input, this means that it cannot
|
|
@@ -149,9 +149,34 @@ export const j = {
|
|
|
149
149
|
anyOf: schemas,
|
|
150
150
|
});
|
|
151
151
|
},
|
|
152
|
+
/**
|
|
153
|
+
* Pick validation schema for an object based on the value of a specific property.
|
|
154
|
+
*
|
|
155
|
+
* ```
|
|
156
|
+
* const schemaMap = {
|
|
157
|
+
* true: successSchema,
|
|
158
|
+
* false: errorSchema
|
|
159
|
+
* }
|
|
160
|
+
*
|
|
161
|
+
* const schema = j.anyOfBy('success', schemaMap)
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
152
164
|
anyOfBy(propertyName, schemaDictionary) {
|
|
153
165
|
return new JsonSchemaAnyOfByBuilder(propertyName, schemaDictionary);
|
|
154
166
|
},
|
|
167
|
+
/**
|
|
168
|
+
* Custom version of `anyOf` which - in contrast to the original function - does not mutate the input.
|
|
169
|
+
* This comes with a performance penalty, so do not use it where performance matters.
|
|
170
|
+
*
|
|
171
|
+
* ```
|
|
172
|
+
* const schema = j.anyOfThese([successSchema, errorSchema])
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
anyOfThese(items) {
|
|
176
|
+
return new JsonSchemaAnyBuilder({
|
|
177
|
+
anyOfThese: items.map(b => b.build()),
|
|
178
|
+
});
|
|
179
|
+
},
|
|
155
180
|
and() {
|
|
156
181
|
return {
|
|
157
182
|
silentBob: () => {
|
|
@@ -301,17 +326,33 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
|
|
|
301
326
|
*
|
|
302
327
|
* This `optionalValues` feature only works when the current schema is nested in an object or array schema,
|
|
303
328
|
* due to how mutability works in Ajv.
|
|
329
|
+
*
|
|
330
|
+
* Make sure this `optional()` call is at the end of your call chain.
|
|
331
|
+
*
|
|
332
|
+
* When `null` is included in optionalValues, the return type becomes `JsonSchemaTerminal`
|
|
333
|
+
* (no further chaining allowed) because the schema is wrapped in an anyOf structure.
|
|
304
334
|
*/
|
|
305
335
|
optional(optionalValues) {
|
|
306
336
|
if (!optionalValues) {
|
|
307
337
|
return super.optional();
|
|
308
338
|
}
|
|
309
|
-
|
|
339
|
+
_typeCast(optionalValues);
|
|
340
|
+
let newBuilder = new JsonSchemaStringBuilder().optional();
|
|
310
341
|
const alternativesSchema = j.enum(optionalValues);
|
|
311
342
|
Object.assign(newBuilder.getSchema(), {
|
|
312
343
|
anyOf: [this.build(), alternativesSchema.build()],
|
|
313
344
|
optionalValues,
|
|
314
345
|
});
|
|
346
|
+
// When `null` is specified, we want `null` to be stripped and the value to become `undefined`,
|
|
347
|
+
// so we must allow `null` values to be parsed by Ajv,
|
|
348
|
+
// but the typing should not reflect that.
|
|
349
|
+
// We also cannot accept more rules attached, since we're not building a StringSchema anymore.
|
|
350
|
+
if (optionalValues.includes(null)) {
|
|
351
|
+
newBuilder = new JsonSchemaTerminal({
|
|
352
|
+
anyOf: [{ type: 'null', optionalValues }, newBuilder.build()],
|
|
353
|
+
optionalField: true,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
315
356
|
return newBuilder;
|
|
316
357
|
}
|
|
317
358
|
regex(pattern, opt) {
|
|
@@ -477,17 +518,33 @@ export class JsonSchemaNumberBuilder extends JsonSchemaAnyBuilder {
|
|
|
477
518
|
*
|
|
478
519
|
* This `optionalValues` feature only works when the current schema is nested in an object or array schema,
|
|
479
520
|
* due to how mutability works in Ajv.
|
|
521
|
+
*
|
|
522
|
+
* Make sure this `optional()` call is at the end of your call chain.
|
|
523
|
+
*
|
|
524
|
+
* When `null` is included in optionalValues, the return type becomes `JsonSchemaTerminal`
|
|
525
|
+
* (no further chaining allowed) because the schema is wrapped in an anyOf structure.
|
|
480
526
|
*/
|
|
481
527
|
optional(optionalValues) {
|
|
482
528
|
if (!optionalValues) {
|
|
483
529
|
return super.optional();
|
|
484
530
|
}
|
|
485
|
-
|
|
531
|
+
_typeCast(optionalValues);
|
|
532
|
+
let newBuilder = new JsonSchemaNumberBuilder().optional();
|
|
486
533
|
const alternativesSchema = j.enum(optionalValues);
|
|
487
534
|
Object.assign(newBuilder.getSchema(), {
|
|
488
535
|
anyOf: [this.build(), alternativesSchema.build()],
|
|
489
536
|
optionalValues,
|
|
490
537
|
});
|
|
538
|
+
// When `null` is specified, we want `null` to be stripped and the value to become `undefined`,
|
|
539
|
+
// so we must allow `null` values to be parsed by Ajv,
|
|
540
|
+
// but the typing should not reflect that.
|
|
541
|
+
// We also cannot accept more rules attached, since we're not building a NumberSchema anymore.
|
|
542
|
+
if (optionalValues.includes(null)) {
|
|
543
|
+
newBuilder = new JsonSchemaTerminal({
|
|
544
|
+
anyOf: [{ type: 'null', optionalValues }, newBuilder.build()],
|
|
545
|
+
optionalField: true,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
491
548
|
return newBuilder;
|
|
492
549
|
}
|
|
493
550
|
integer() {
|
|
@@ -837,6 +894,22 @@ export class JsonSchemaAnyOfByBuilder extends JsonSchemaAnyBuilder {
|
|
|
837
894
|
});
|
|
838
895
|
}
|
|
839
896
|
}
|
|
897
|
+
export class JsonSchemaAnyOfTheseBuilder extends JsonSchemaAnyBuilder {
|
|
898
|
+
constructor(propertyName, schemaDictionary) {
|
|
899
|
+
const builtSchemaDictionary = {};
|
|
900
|
+
for (const [key, schema] of Object.entries(schemaDictionary)) {
|
|
901
|
+
builtSchemaDictionary[key] = schema.build();
|
|
902
|
+
}
|
|
903
|
+
super({
|
|
904
|
+
type: 'object',
|
|
905
|
+
hasIsOfTypeCheck: true,
|
|
906
|
+
anyOfBy: {
|
|
907
|
+
propertyName,
|
|
908
|
+
schemaDictionary: builtSchemaDictionary,
|
|
909
|
+
},
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
}
|
|
840
913
|
function object(props) {
|
|
841
914
|
return new JsonSchemaObjectBuilder(props);
|
|
842
915
|
}
|
package/package.json
CHANGED
|
@@ -50,9 +50,11 @@ export interface ReadableTyped<T = unknown> extends Readable {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
// oxlint-disable no-unused-vars
|
|
53
|
-
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
54
|
+
export interface WritableTyped<_T> extends Writable {}
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
57
|
+
export interface TransformTyped<_IN = unknown, _OUT = unknown> extends Transform {}
|
|
56
58
|
// oxlint-enable
|
|
57
59
|
|
|
58
60
|
export interface TransformOptions {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { _isBetween, _lazyValue } from '@naturalcycles/js-lib'
|
|
2
|
-
import {
|
|
2
|
+
import { _assert } from '@naturalcycles/js-lib/error'
|
|
3
|
+
import { _deepCopy, _mapObject, Set2 } from '@naturalcycles/js-lib/object'
|
|
3
4
|
import { _substringAfterLast } from '@naturalcycles/js-lib/string'
|
|
4
5
|
import type { AnyObject } from '@naturalcycles/js-lib/types'
|
|
5
6
|
import { Ajv2020, type Options, type ValidateFunction } from 'ajv/dist/2020.js'
|
|
@@ -195,7 +196,7 @@ export function createAjv(opt?: Options): Ajv2020 {
|
|
|
195
196
|
idx++
|
|
196
197
|
}
|
|
197
198
|
|
|
198
|
-
if (ctx?.parentData && ctx.parentDataProperty) {
|
|
199
|
+
if (ctx?.parentData && ctx.parentDataProperty !== undefined) {
|
|
199
200
|
ctx.parentData[ctx.parentDataProperty] = set
|
|
200
201
|
}
|
|
201
202
|
|
|
@@ -248,7 +249,7 @@ export function createAjv(opt?: Options): Ajv2020 {
|
|
|
248
249
|
return false
|
|
249
250
|
}
|
|
250
251
|
|
|
251
|
-
if (ctx?.parentData && ctx.parentDataProperty) {
|
|
252
|
+
if (ctx?.parentData && ctx.parentDataProperty !== undefined) {
|
|
252
253
|
ctx.parentData[ctx.parentDataProperty] = buffer
|
|
253
254
|
}
|
|
254
255
|
|
|
@@ -297,7 +298,7 @@ export function createAjv(opt?: Options): Ajv2020 {
|
|
|
297
298
|
}
|
|
298
299
|
}
|
|
299
300
|
|
|
300
|
-
if (ctx?.parentData && ctx.parentDataProperty) {
|
|
301
|
+
if (ctx?.parentData && ctx.parentDataProperty !== undefined) {
|
|
301
302
|
ctx.parentData[ctx.parentDataProperty] = cleanData
|
|
302
303
|
}
|
|
303
304
|
|
|
@@ -435,7 +436,7 @@ export function createAjv(opt?: Options): Ajv2020 {
|
|
|
435
436
|
|
|
436
437
|
ajv.addKeyword({
|
|
437
438
|
keyword: 'optionalValues',
|
|
438
|
-
type: ['string', 'number', 'boolean'],
|
|
439
|
+
type: ['string', 'number', 'boolean', 'null'],
|
|
439
440
|
modifying: true,
|
|
440
441
|
errors: false,
|
|
441
442
|
schemaType: 'array',
|
|
@@ -447,11 +448,14 @@ export function createAjv(opt?: Options): Ajv2020 {
|
|
|
447
448
|
) {
|
|
448
449
|
if (!optionalValues) return true
|
|
449
450
|
|
|
451
|
+
_assert(
|
|
452
|
+
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.',
|
|
454
|
+
)
|
|
455
|
+
|
|
450
456
|
if (!optionalValues.includes(data)) return true
|
|
451
457
|
|
|
452
|
-
|
|
453
|
-
delete ctx.parentData[ctx.parentDataProperty]
|
|
454
|
-
}
|
|
458
|
+
ctx.parentData[ctx.parentDataProperty] = undefined
|
|
455
459
|
|
|
456
460
|
return true
|
|
457
461
|
},
|
|
@@ -482,7 +486,7 @@ export function createAjv(opt?: Options): Ajv2020 {
|
|
|
482
486
|
},
|
|
483
487
|
})
|
|
484
488
|
|
|
485
|
-
// This
|
|
489
|
+
// This is added because Ajv validates the `min/maxProperties` before validating the properties.
|
|
486
490
|
// So, in case of `minProperties(1)` and `{ foo: 'bar' }` Ajv will let it pass, even
|
|
487
491
|
// if the property validation would strip `foo` from the data.
|
|
488
492
|
// And Ajv would return `{}` as a successful validation.
|
|
@@ -499,7 +503,7 @@ export function createAjv(opt?: Options): Ajv2020 {
|
|
|
499
503
|
validate: function validate(minProperties: number, data: AnyObject, _schema, ctx) {
|
|
500
504
|
if (typeof data !== 'object') return true
|
|
501
505
|
|
|
502
|
-
const numberOfProperties = Object.
|
|
506
|
+
const numberOfProperties = Object.entries(data).filter(([, v]) => v !== undefined).length
|
|
503
507
|
const isValid = numberOfProperties >= minProperties
|
|
504
508
|
if (!isValid) {
|
|
505
509
|
;(validate as any).errors = [
|
|
@@ -585,6 +589,56 @@ export function createAjv(opt?: Options): Ajv2020 {
|
|
|
585
589
|
},
|
|
586
590
|
})
|
|
587
591
|
|
|
592
|
+
ajv.addKeyword({
|
|
593
|
+
keyword: 'anyOfThese',
|
|
594
|
+
modifying: true,
|
|
595
|
+
errors: true,
|
|
596
|
+
schemaType: 'array',
|
|
597
|
+
compile(schemas: JsonSchemaTerminal<any, any, any>[], _parentSchema, _it) {
|
|
598
|
+
const validators = schemas.map(schema => ajv.compile(schema))
|
|
599
|
+
|
|
600
|
+
function validate(data: AnyObject, ctx: any): boolean {
|
|
601
|
+
let correctValidator: ValidateFunction<unknown> | undefined
|
|
602
|
+
let result = false
|
|
603
|
+
let clonedData: any
|
|
604
|
+
|
|
605
|
+
// Try each validator until we find one that works!
|
|
606
|
+
for (const validator of validators) {
|
|
607
|
+
clonedData = isPrimitive(data) ? _deepCopy(data) : data
|
|
608
|
+
result = validator(clonedData)
|
|
609
|
+
if (result) {
|
|
610
|
+
correctValidator = validator
|
|
611
|
+
break
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (result && ctx?.parentData && ctx.parentDataProperty !== undefined) {
|
|
616
|
+
// If we found a validator and the data is valid and we are validating a property inside an object,
|
|
617
|
+
// then we can inject our result and be done with it.
|
|
618
|
+
ctx.parentData[ctx.parentDataProperty] = clonedData
|
|
619
|
+
} else if (result) {
|
|
620
|
+
// If we found a validator but we are not validating a property inside an object,
|
|
621
|
+
// then we must re-run the validation so that the mutations caused by Ajv
|
|
622
|
+
// will be done on the input data, not only on the clone.
|
|
623
|
+
result = correctValidator!(data)
|
|
624
|
+
} else {
|
|
625
|
+
// If we didn't find a fitting schema,
|
|
626
|
+
// we add our own error.
|
|
627
|
+
;(validate as any).errors = [
|
|
628
|
+
{
|
|
629
|
+
instancePath: ctx?.instancePath ?? '',
|
|
630
|
+
message: `could not find a suitable schema to validate against`,
|
|
631
|
+
},
|
|
632
|
+
]
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return result
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return validate
|
|
639
|
+
},
|
|
640
|
+
})
|
|
641
|
+
|
|
588
642
|
return ajv
|
|
589
643
|
}
|
|
590
644
|
|
|
@@ -728,3 +782,7 @@ function isIsoMonthValid(s: string): boolean {
|
|
|
728
782
|
|
|
729
783
|
return _isBetween(year, 1900, 2500, '[]') && _isBetween(month, 1, 12, '[]')
|
|
730
784
|
}
|
|
785
|
+
|
|
786
|
+
function isPrimitive(data: any): boolean {
|
|
787
|
+
return data !== null && typeof data === 'object'
|
|
788
|
+
}
|
|
@@ -13,6 +13,7 @@ import type { Set2 } from '@naturalcycles/js-lib/object'
|
|
|
13
13
|
import { _deepCopy, _sortObject } from '@naturalcycles/js-lib/object'
|
|
14
14
|
import {
|
|
15
15
|
_objectAssign,
|
|
16
|
+
_typeCast,
|
|
16
17
|
type AnyObject,
|
|
17
18
|
type BaseDBEntity,
|
|
18
19
|
type IANATimezone,
|
|
@@ -217,7 +218,7 @@ export const j = {
|
|
|
217
218
|
|
|
218
219
|
/**
|
|
219
220
|
* Use only with primitive values, otherwise this function will throw to avoid bugs.
|
|
220
|
-
* To validate objects, use `anyOfBy`.
|
|
221
|
+
* To validate objects, use `anyOfBy` or `anyOfThese`.
|
|
221
222
|
*
|
|
222
223
|
* Our Ajv is configured to strip unexpected properties from objects,
|
|
223
224
|
* and since Ajv is mutating the input, this means that it cannot
|
|
@@ -242,6 +243,18 @@ export const j = {
|
|
|
242
243
|
})
|
|
243
244
|
},
|
|
244
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Pick validation schema for an object based on the value of a specific property.
|
|
248
|
+
*
|
|
249
|
+
* ```
|
|
250
|
+
* const schemaMap = {
|
|
251
|
+
* true: successSchema,
|
|
252
|
+
* false: errorSchema
|
|
253
|
+
* }
|
|
254
|
+
*
|
|
255
|
+
* const schema = j.anyOfBy('success', schemaMap)
|
|
256
|
+
* ```
|
|
257
|
+
*/
|
|
245
258
|
anyOfBy<
|
|
246
259
|
P extends string,
|
|
247
260
|
D extends Record<PropertyKey, JsonSchemaTerminal<any, any, any>>,
|
|
@@ -251,6 +264,24 @@ export const j = {
|
|
|
251
264
|
return new JsonSchemaAnyOfByBuilder<IN, OUT, P>(propertyName, schemaDictionary)
|
|
252
265
|
},
|
|
253
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Custom version of `anyOf` which - in contrast to the original function - does not mutate the input.
|
|
269
|
+
* This comes with a performance penalty, so do not use it where performance matters.
|
|
270
|
+
*
|
|
271
|
+
* ```
|
|
272
|
+
* const schema = j.anyOfThese([successSchema, errorSchema])
|
|
273
|
+
* ```
|
|
274
|
+
*/
|
|
275
|
+
anyOfThese<
|
|
276
|
+
B extends readonly JsonSchemaAnyBuilder<any, any, boolean>[],
|
|
277
|
+
IN = BuilderInUnion<B>,
|
|
278
|
+
OUT = BuilderOutUnion<B>,
|
|
279
|
+
>(items: [...B]): JsonSchemaAnyBuilder<IN, OUT, false> {
|
|
280
|
+
return new JsonSchemaAnyBuilder<IN, OUT, false>({
|
|
281
|
+
anyOfThese: items.map(b => b.build()),
|
|
282
|
+
})
|
|
283
|
+
},
|
|
284
|
+
|
|
254
285
|
and() {
|
|
255
286
|
return {
|
|
256
287
|
silentBob: () => {
|
|
@@ -438,26 +469,45 @@ export class JsonSchemaStringBuilder<
|
|
|
438
469
|
*
|
|
439
470
|
* This `optionalValues` feature only works when the current schema is nested in an object or array schema,
|
|
440
471
|
* due to how mutability works in Ajv.
|
|
472
|
+
*
|
|
473
|
+
* Make sure this `optional()` call is at the end of your call chain.
|
|
474
|
+
*
|
|
475
|
+
* When `null` is included in optionalValues, the return type becomes `JsonSchemaTerminal`
|
|
476
|
+
* (no further chaining allowed) because the schema is wrapped in an anyOf structure.
|
|
441
477
|
*/
|
|
442
|
-
override optional(
|
|
443
|
-
optionalValues?:
|
|
444
|
-
):
|
|
478
|
+
override optional<T extends readonly (string | null)[] | undefined = undefined>(
|
|
479
|
+
optionalValues?: T,
|
|
480
|
+
): T extends readonly (infer U)[]
|
|
481
|
+
? null extends U
|
|
482
|
+
? JsonSchemaTerminal<IN | undefined, OUT | undefined, true>
|
|
483
|
+
: JsonSchemaStringBuilder<IN | undefined, OUT | undefined, true>
|
|
484
|
+
: JsonSchemaStringBuilder<IN | undefined, OUT | undefined, true> {
|
|
445
485
|
if (!optionalValues) {
|
|
446
|
-
return super.optional() as
|
|
447
|
-
IN | undefined,
|
|
448
|
-
OUT | undefined,
|
|
449
|
-
true
|
|
450
|
-
>
|
|
486
|
+
return super.optional() as any
|
|
451
487
|
}
|
|
452
488
|
|
|
453
|
-
|
|
489
|
+
_typeCast<(string | null)[]>(optionalValues)
|
|
490
|
+
|
|
491
|
+
let newBuilder: JsonSchemaTerminal<IN | undefined, OUT | undefined, true> =
|
|
492
|
+
new JsonSchemaStringBuilder<IN, OUT, Opt>().optional()
|
|
454
493
|
const alternativesSchema = j.enum(optionalValues)
|
|
455
494
|
Object.assign(newBuilder.getSchema(), {
|
|
456
495
|
anyOf: [this.build(), alternativesSchema.build()],
|
|
457
496
|
optionalValues,
|
|
458
497
|
})
|
|
459
498
|
|
|
460
|
-
|
|
499
|
+
// When `null` is specified, we want `null` to be stripped and the value to become `undefined`,
|
|
500
|
+
// so we must allow `null` values to be parsed by Ajv,
|
|
501
|
+
// but the typing should not reflect that.
|
|
502
|
+
// We also cannot accept more rules attached, since we're not building a StringSchema anymore.
|
|
503
|
+
if (optionalValues.includes(null)) {
|
|
504
|
+
newBuilder = new JsonSchemaTerminal({
|
|
505
|
+
anyOf: [{ type: 'null', optionalValues }, newBuilder.build()],
|
|
506
|
+
optionalField: true,
|
|
507
|
+
})
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return newBuilder as any
|
|
461
511
|
}
|
|
462
512
|
|
|
463
513
|
regex(pattern: RegExp, opt?: JsonBuilderRuleOpt): this {
|
|
@@ -686,27 +736,45 @@ export class JsonSchemaNumberBuilder<
|
|
|
686
736
|
*
|
|
687
737
|
* This `optionalValues` feature only works when the current schema is nested in an object or array schema,
|
|
688
738
|
* due to how mutability works in Ajv.
|
|
739
|
+
*
|
|
740
|
+
* Make sure this `optional()` call is at the end of your call chain.
|
|
741
|
+
*
|
|
742
|
+
* When `null` is included in optionalValues, the return type becomes `JsonSchemaTerminal`
|
|
743
|
+
* (no further chaining allowed) because the schema is wrapped in an anyOf structure.
|
|
689
744
|
*/
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
745
|
+
override optional<T extends readonly (number | null)[] | undefined = undefined>(
|
|
746
|
+
optionalValues?: T,
|
|
747
|
+
): T extends readonly (infer U)[]
|
|
748
|
+
? null extends U
|
|
749
|
+
? JsonSchemaTerminal<IN | undefined, OUT | undefined, true>
|
|
750
|
+
: JsonSchemaNumberBuilder<IN | undefined, OUT | undefined, true>
|
|
751
|
+
: JsonSchemaNumberBuilder<IN | undefined, OUT | undefined, true> {
|
|
694
752
|
if (!optionalValues) {
|
|
695
|
-
return super.optional() as
|
|
696
|
-
IN | undefined,
|
|
697
|
-
OUT | undefined,
|
|
698
|
-
true
|
|
699
|
-
>
|
|
753
|
+
return super.optional() as any
|
|
700
754
|
}
|
|
701
755
|
|
|
702
|
-
|
|
756
|
+
_typeCast<(number | null)[]>(optionalValues)
|
|
757
|
+
|
|
758
|
+
let newBuilder: JsonSchemaTerminal<IN | undefined, OUT | undefined, true> =
|
|
759
|
+
new JsonSchemaNumberBuilder<IN, OUT, Opt>().optional()
|
|
703
760
|
const alternativesSchema = j.enum(optionalValues)
|
|
704
761
|
Object.assign(newBuilder.getSchema(), {
|
|
705
762
|
anyOf: [this.build(), alternativesSchema.build()],
|
|
706
763
|
optionalValues,
|
|
707
764
|
})
|
|
708
765
|
|
|
709
|
-
|
|
766
|
+
// When `null` is specified, we want `null` to be stripped and the value to become `undefined`,
|
|
767
|
+
// so we must allow `null` values to be parsed by Ajv,
|
|
768
|
+
// but the typing should not reflect that.
|
|
769
|
+
// We also cannot accept more rules attached, since we're not building a NumberSchema anymore.
|
|
770
|
+
if (optionalValues.includes(null)) {
|
|
771
|
+
newBuilder = new JsonSchemaTerminal({
|
|
772
|
+
anyOf: [{ type: 'null', optionalValues }, newBuilder.build()],
|
|
773
|
+
optionalField: true,
|
|
774
|
+
})
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
return newBuilder as any
|
|
710
778
|
}
|
|
711
779
|
|
|
712
780
|
integer(): this {
|
|
@@ -1267,6 +1335,34 @@ export class JsonSchemaAnyOfByBuilder<
|
|
|
1267
1335
|
}
|
|
1268
1336
|
}
|
|
1269
1337
|
|
|
1338
|
+
export class JsonSchemaAnyOfTheseBuilder<
|
|
1339
|
+
IN,
|
|
1340
|
+
OUT,
|
|
1341
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
1342
|
+
_P extends string = string,
|
|
1343
|
+
> extends JsonSchemaAnyBuilder<AnyOfByInput<IN, _P> | IN, OUT, false> {
|
|
1344
|
+
declare in: IN
|
|
1345
|
+
|
|
1346
|
+
constructor(
|
|
1347
|
+
propertyName: string,
|
|
1348
|
+
schemaDictionary: Record<PropertyKey, JsonSchemaTerminal<any, any, any>>,
|
|
1349
|
+
) {
|
|
1350
|
+
const builtSchemaDictionary: Record<string, JsonSchema> = {}
|
|
1351
|
+
for (const [key, schema] of Object.entries(schemaDictionary)) {
|
|
1352
|
+
builtSchemaDictionary[key] = schema.build()
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
super({
|
|
1356
|
+
type: 'object',
|
|
1357
|
+
hasIsOfTypeCheck: true,
|
|
1358
|
+
anyOfBy: {
|
|
1359
|
+
propertyName,
|
|
1360
|
+
schemaDictionary: builtSchemaDictionary,
|
|
1361
|
+
},
|
|
1362
|
+
})
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1270
1366
|
type EnumBaseType = 'string' | 'number' | 'other'
|
|
1271
1367
|
|
|
1272
1368
|
export interface JsonSchema<IN = unknown, OUT = IN> {
|
|
@@ -1342,7 +1438,7 @@ export interface JsonSchema<IN = unknown, OUT = IN> {
|
|
|
1342
1438
|
instanceof?: string | string[]
|
|
1343
1439
|
transform?: { trim?: true; toLowerCase?: true; toUpperCase?: true; truncate?: number }
|
|
1344
1440
|
errorMessages?: StringMap<string>
|
|
1345
|
-
optionalValues?: (string | number | boolean)[]
|
|
1441
|
+
optionalValues?: (string | number | boolean | null)[]
|
|
1346
1442
|
keySchema?: JsonSchema
|
|
1347
1443
|
minProperties2?: number
|
|
1348
1444
|
exclusiveProperties?: (readonly string[])[]
|
|
@@ -1350,15 +1446,16 @@ export interface JsonSchema<IN = unknown, OUT = IN> {
|
|
|
1350
1446
|
propertyName: string
|
|
1351
1447
|
schemaDictionary: Record<string, JsonSchema>
|
|
1352
1448
|
}
|
|
1449
|
+
anyOfThese?: JsonSchema[]
|
|
1353
1450
|
}
|
|
1354
1451
|
|
|
1355
1452
|
function object(props: AnyObject): never
|
|
1356
1453
|
function object<IN extends AnyObject>(props: {
|
|
1357
|
-
[K in keyof Required<IN>]-?:
|
|
1454
|
+
[K in keyof Required<IN>]-?: JsonSchemaTerminal<any, IN[K], any>
|
|
1358
1455
|
}): JsonSchemaObjectBuilder<IN, IN, false>
|
|
1359
1456
|
|
|
1360
1457
|
function object<IN extends AnyObject>(props: {
|
|
1361
|
-
[key in keyof IN]:
|
|
1458
|
+
[key in keyof IN]: JsonSchemaTerminal<any, IN[key], any>
|
|
1362
1459
|
}): JsonSchemaObjectBuilder<IN, IN, false> {
|
|
1363
1460
|
return new JsonSchemaObjectBuilder<IN, IN, false>(props)
|
|
1364
1461
|
}
|