@naturalcycles/nodejs-lib 15.46.3 → 15.47.1
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.d.ts +7 -0
- package/dist/validation/ajv/getAjv.js +70 -9
- package/dist/validation/ajv/jsonSchemaBuilder.d.ts +30 -5
- package/dist/validation/ajv/jsonSchemaBuilder.js +97 -42
- package/dist/validation/ajv/jsonSchemaBuilder.util.d.ts +2 -0
- package/dist/validation/ajv/jsonSchemaBuilder.util.js +14 -0
- package/package.json +1 -1
- package/src/validation/ajv/getAjv.ts +84 -10
- package/src/validation/ajv/jsonSchemaBuilder.ts +127 -44
- package/src/validation/ajv/jsonSchemaBuilder.util.ts +14 -0
|
@@ -13,6 +13,13 @@ export declare const getAjv: any;
|
|
|
13
13
|
* and are not interested in transforming the data.
|
|
14
14
|
*/
|
|
15
15
|
export declare const getNonMutatingAjv: any;
|
|
16
|
+
/**
|
|
17
|
+
* Returns cached instance of Ajv, which is coercing data.
|
|
18
|
+
*
|
|
19
|
+
* To be used in places where we know that we are going to receive data with the wrong type,
|
|
20
|
+
* typically: request path params and request query params.
|
|
21
|
+
*/
|
|
22
|
+
export declare const getCoercingAjv: any;
|
|
16
23
|
/**
|
|
17
24
|
* Create Ajv with modified defaults.
|
|
18
25
|
*
|
|
@@ -19,6 +19,10 @@ const AJV_NON_MUTATING_OPTIONS = {
|
|
|
19
19
|
removeAdditional: false,
|
|
20
20
|
useDefaults: false,
|
|
21
21
|
};
|
|
22
|
+
const AJV_MUTATING_COERCING_OPTIONS = {
|
|
23
|
+
...AJV_OPTIONS,
|
|
24
|
+
coerceTypes: true,
|
|
25
|
+
};
|
|
22
26
|
/**
|
|
23
27
|
* Return cached instance of Ajv with default (recommended) options.
|
|
24
28
|
*
|
|
@@ -33,6 +37,13 @@ export const getAjv = _lazyValue(createAjv);
|
|
|
33
37
|
* and are not interested in transforming the data.
|
|
34
38
|
*/
|
|
35
39
|
export const getNonMutatingAjv = _lazyValue(() => createAjv(AJV_NON_MUTATING_OPTIONS));
|
|
40
|
+
/**
|
|
41
|
+
* Returns cached instance of Ajv, which is coercing data.
|
|
42
|
+
*
|
|
43
|
+
* To be used in places where we know that we are going to receive data with the wrong type,
|
|
44
|
+
* typically: request path params and request query params.
|
|
45
|
+
*/
|
|
46
|
+
export const getCoercingAjv = _lazyValue(() => createAjv(AJV_MUTATING_COERCING_OPTIONS));
|
|
36
47
|
/**
|
|
37
48
|
* Create Ajv with modified defaults.
|
|
38
49
|
*
|
|
@@ -233,17 +244,67 @@ export function createAjv(opt) {
|
|
|
233
244
|
type: 'string',
|
|
234
245
|
modifying: false,
|
|
235
246
|
errors: true,
|
|
236
|
-
schemaType: '
|
|
237
|
-
validate: function validate(
|
|
247
|
+
schemaType: 'object',
|
|
248
|
+
validate: function validate(opt, data, _schema, ctx) {
|
|
249
|
+
const hasOptions = Object.keys(opt).length > 0;
|
|
238
250
|
const isValid = isIsoDateValid(data);
|
|
239
|
-
if (isValid)
|
|
251
|
+
if (!isValid) {
|
|
252
|
+
;
|
|
253
|
+
validate.errors = [
|
|
254
|
+
{
|
|
255
|
+
instancePath: ctx?.instancePath ?? '',
|
|
256
|
+
message: `is an invalid IsoDate`,
|
|
257
|
+
},
|
|
258
|
+
];
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
if (!hasOptions)
|
|
240
262
|
return true;
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
263
|
+
const { before, sameOrBefore, after, sameOrAfter } = opt;
|
|
264
|
+
const errors = [];
|
|
265
|
+
if (before) {
|
|
266
|
+
const isParamValid = isIsoDateValid(before);
|
|
267
|
+
const isRuleValid = isParamValid && before > data;
|
|
268
|
+
if (!isRuleValid) {
|
|
269
|
+
errors.push({
|
|
270
|
+
instancePath: ctx?.instancePath ?? '',
|
|
271
|
+
message: `is not before ${before}`,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (sameOrBefore) {
|
|
276
|
+
const isParamValid = isIsoDateValid(sameOrBefore);
|
|
277
|
+
const isRuleValid = isParamValid && sameOrBefore >= data;
|
|
278
|
+
if (!isRuleValid) {
|
|
279
|
+
errors.push({
|
|
280
|
+
instancePath: ctx?.instancePath ?? '',
|
|
281
|
+
message: `is not the same or before ${sameOrBefore}`,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (after) {
|
|
286
|
+
const isParamValid = isIsoDateValid(after);
|
|
287
|
+
const isRuleValid = isParamValid && after < data;
|
|
288
|
+
if (!isRuleValid) {
|
|
289
|
+
errors.push({
|
|
290
|
+
instancePath: ctx?.instancePath ?? '',
|
|
291
|
+
message: `is not after ${after}`,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (sameOrAfter) {
|
|
296
|
+
const isParamValid = isIsoDateValid(sameOrAfter);
|
|
297
|
+
const isRuleValid = isParamValid && sameOrAfter <= data;
|
|
298
|
+
if (!isRuleValid) {
|
|
299
|
+
errors.push({
|
|
300
|
+
instancePath: ctx?.instancePath ?? '',
|
|
301
|
+
message: `is not the same or after ${sameOrAfter}`,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (errors.length === 0)
|
|
306
|
+
return true;
|
|
307
|
+
validate.errors = errors;
|
|
247
308
|
return false;
|
|
248
309
|
},
|
|
249
310
|
});
|
|
@@ -75,7 +75,13 @@ export declare class JsonSchemaStringBuilder<IN extends string = string, OUT = I
|
|
|
75
75
|
toUpperCase(): this;
|
|
76
76
|
truncate(toLength: number): this;
|
|
77
77
|
branded<B extends string>(): JsonSchemaStringBuilder<B, B, Opt>;
|
|
78
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Validates that the input is a fully-specified YYYY-MM-DD formatted valid IsoDate value.
|
|
80
|
+
*
|
|
81
|
+
* All previous expectations in the schema chain are dropped - including `.optional()` -
|
|
82
|
+
* because this call effectively starts a new schema chain.
|
|
83
|
+
*/
|
|
84
|
+
isoDate(): JsonSchemaIsoDateBuilder;
|
|
79
85
|
isoDateTime(): JsonSchemaStringBuilder<IsoDateTime | IN, IsoDateTime, Opt>;
|
|
80
86
|
/**
|
|
81
87
|
* Validates the string format to be JWT.
|
|
@@ -102,6 +108,20 @@ export declare class JsonSchemaStringBuilder<IN extends string = string, OUT = I
|
|
|
102
108
|
export interface JsonSchemaStringEmailOptions {
|
|
103
109
|
checkTLD: boolean;
|
|
104
110
|
}
|
|
111
|
+
export declare class JsonSchemaIsoDateBuilder<Opt extends boolean = false> extends JsonSchemaAnyBuilder<string | IsoDate, IsoDate, Opt> {
|
|
112
|
+
constructor();
|
|
113
|
+
before(date: string): this;
|
|
114
|
+
sameOrBefore(date: string): this;
|
|
115
|
+
after(date: string): this;
|
|
116
|
+
sameOrAfter(date: string): this;
|
|
117
|
+
between(fromDate: string, toDate: string, incl: Inclusiveness): this;
|
|
118
|
+
}
|
|
119
|
+
export interface JsonSchemaIsoDateOptions {
|
|
120
|
+
before?: string;
|
|
121
|
+
sameOrBefore?: string;
|
|
122
|
+
after?: string;
|
|
123
|
+
sameOrAfter?: string;
|
|
124
|
+
}
|
|
105
125
|
export declare class JsonSchemaNumberBuilder<IN extends number = number, OUT = IN, Opt extends boolean = false> extends JsonSchemaAnyBuilder<IN, OUT, Opt> {
|
|
106
126
|
constructor();
|
|
107
127
|
integer(): this;
|
|
@@ -177,7 +197,7 @@ export declare class JsonSchemaArrayBuilder<IN, OUT, Opt> extends JsonSchemaAnyB
|
|
|
177
197
|
maxLength(maxItems: number): this;
|
|
178
198
|
length(minItems: number, maxItems: number): this;
|
|
179
199
|
exactLength(length: number): this;
|
|
180
|
-
unique(
|
|
200
|
+
unique(): this;
|
|
181
201
|
}
|
|
182
202
|
export declare class JsonSchemaSet2Builder<IN, OUT, Opt> extends JsonSchemaAnyBuilder<Iterable<IN>, Set2<OUT>, Opt> {
|
|
183
203
|
constructor(itemsSchema: JsonSchemaAnyBuilder<IN, OUT, Opt>);
|
|
@@ -188,13 +208,14 @@ export declare class JsonSchemaBufferBuilder extends JsonSchemaAnyBuilder<string
|
|
|
188
208
|
constructor();
|
|
189
209
|
}
|
|
190
210
|
export declare class JsonSchemaEnumBuilder<IN extends string | number | boolean | null, OUT extends IN = IN, Opt extends boolean = false> extends JsonSchemaAnyBuilder<IN, OUT, Opt> {
|
|
191
|
-
constructor(enumValues: readonly IN[], opt?: JsonBuilderRuleOpt);
|
|
211
|
+
constructor(enumValues: readonly IN[], baseType: EnumBaseType, opt?: JsonBuilderRuleOpt);
|
|
192
212
|
branded<B extends IN>(): JsonSchemaEnumBuilder<B | IN, B, Opt>;
|
|
193
213
|
}
|
|
214
|
+
type EnumBaseType = 'string' | 'number' | 'other';
|
|
194
215
|
export interface JsonSchema<IN = unknown, OUT = IN> {
|
|
195
216
|
readonly in?: IN;
|
|
196
217
|
readonly out?: OUT;
|
|
197
|
-
$schema?:
|
|
218
|
+
$schema?: string;
|
|
198
219
|
$id?: string;
|
|
199
220
|
title?: string;
|
|
200
221
|
description?: string;
|
|
@@ -232,10 +253,14 @@ export interface JsonSchema<IN = unknown, OUT = IN> {
|
|
|
232
253
|
exclusiveMinimum?: number;
|
|
233
254
|
maximum?: number;
|
|
234
255
|
exclusiveMaximum?: number;
|
|
256
|
+
minItems?: number;
|
|
257
|
+
maxItems?: number;
|
|
258
|
+
uniqueItems?: boolean;
|
|
235
259
|
enum?: any;
|
|
260
|
+
email?: JsonSchemaStringEmailOptions;
|
|
236
261
|
Set2?: JsonSchema;
|
|
237
262
|
Buffer?: true;
|
|
238
|
-
IsoDate?:
|
|
263
|
+
IsoDate?: JsonSchemaIsoDateOptions;
|
|
239
264
|
IsoDateTime?: true;
|
|
240
265
|
instanceof?: string | string[];
|
|
241
266
|
transform?: {
|
|
@@ -4,9 +4,9 @@ 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 { JWT_REGEX, } from '@naturalcycles/js-lib/types';
|
|
7
|
+
import { _objectAssign, JWT_REGEX, } from '@naturalcycles/js-lib/types';
|
|
8
8
|
import { TIMEZONES } from '../timezones.js';
|
|
9
|
-
import { JSON_SCHEMA_ORDER, mergeJsonSchemaObjects } from './jsonSchemaBuilder.util.js';
|
|
9
|
+
import { isEveryItemNumber, isEveryItemString, JSON_SCHEMA_ORDER, mergeJsonSchemaObjects, } from './jsonSchemaBuilder.util.js';
|
|
10
10
|
export const j = {
|
|
11
11
|
string() {
|
|
12
12
|
return new JsonSchemaStringBuilder();
|
|
@@ -35,20 +35,29 @@ export const j = {
|
|
|
35
35
|
},
|
|
36
36
|
enum(input, opt) {
|
|
37
37
|
let enumValues;
|
|
38
|
+
let baseType = 'other';
|
|
38
39
|
if (Array.isArray(input)) {
|
|
39
40
|
enumValues = input;
|
|
41
|
+
if (isEveryItemNumber(input)) {
|
|
42
|
+
baseType = 'number';
|
|
43
|
+
}
|
|
44
|
+
else if (isEveryItemString(input)) {
|
|
45
|
+
baseType = 'string';
|
|
46
|
+
}
|
|
40
47
|
}
|
|
41
48
|
else if (typeof input === 'object') {
|
|
42
49
|
const enumType = getEnumType(input);
|
|
43
50
|
if (enumType === 'NumberEnum') {
|
|
44
51
|
enumValues = _numberEnumValues(input);
|
|
52
|
+
baseType = 'number';
|
|
45
53
|
}
|
|
46
54
|
else if (enumType === 'StringEnum') {
|
|
47
55
|
enumValues = _stringEnumValues(input);
|
|
56
|
+
baseType = 'string';
|
|
48
57
|
}
|
|
49
58
|
}
|
|
50
59
|
_assert(enumValues, 'Unsupported enum input');
|
|
51
|
-
return new JsonSchemaEnumBuilder(enumValues, opt);
|
|
60
|
+
return new JsonSchemaEnumBuilder(enumValues, baseType, opt);
|
|
52
61
|
},
|
|
53
62
|
oneOf(items) {
|
|
54
63
|
const schemas = items.map(b => b.build());
|
|
@@ -94,14 +103,14 @@ export class JsonSchemaAnyBuilder {
|
|
|
94
103
|
* ```
|
|
95
104
|
*/
|
|
96
105
|
isOfType() {
|
|
97
|
-
|
|
106
|
+
_objectAssign(this.schema, { hasIsOfTypeCheck: true });
|
|
98
107
|
return this;
|
|
99
108
|
}
|
|
100
109
|
getSchema() {
|
|
101
110
|
return this.schema;
|
|
102
111
|
}
|
|
103
112
|
$schema($schema) {
|
|
104
|
-
|
|
113
|
+
_objectAssign(this.schema, { $schema });
|
|
105
114
|
return this;
|
|
106
115
|
}
|
|
107
116
|
$schemaDraft7() {
|
|
@@ -109,31 +118,31 @@ export class JsonSchemaAnyBuilder {
|
|
|
109
118
|
return this;
|
|
110
119
|
}
|
|
111
120
|
$id($id) {
|
|
112
|
-
|
|
121
|
+
_objectAssign(this.schema, { $id });
|
|
113
122
|
return this;
|
|
114
123
|
}
|
|
115
124
|
title(title) {
|
|
116
|
-
|
|
125
|
+
_objectAssign(this.schema, { title });
|
|
117
126
|
return this;
|
|
118
127
|
}
|
|
119
128
|
description(description) {
|
|
120
|
-
|
|
129
|
+
_objectAssign(this.schema, { description });
|
|
121
130
|
return this;
|
|
122
131
|
}
|
|
123
132
|
deprecated(deprecated = true) {
|
|
124
|
-
|
|
133
|
+
_objectAssign(this.schema, { deprecated });
|
|
125
134
|
return this;
|
|
126
135
|
}
|
|
127
136
|
type(type) {
|
|
128
|
-
|
|
137
|
+
_objectAssign(this.schema, { type });
|
|
129
138
|
return this;
|
|
130
139
|
}
|
|
131
140
|
default(v) {
|
|
132
|
-
|
|
141
|
+
_objectAssign(this.schema, { default: v });
|
|
133
142
|
return this;
|
|
134
143
|
}
|
|
135
144
|
instanceof(of) {
|
|
136
|
-
|
|
145
|
+
_objectAssign(this.schema, { type: 'object', instanceof: of });
|
|
137
146
|
return this;
|
|
138
147
|
}
|
|
139
148
|
optional() {
|
|
@@ -182,53 +191,59 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
|
|
|
182
191
|
this.setErrorMessage('pattern', `is not a valid ${opt.name}`);
|
|
183
192
|
if (opt?.msg)
|
|
184
193
|
this.setErrorMessage('pattern', opt.msg);
|
|
185
|
-
|
|
194
|
+
_objectAssign(this.schema, { pattern });
|
|
186
195
|
return this;
|
|
187
196
|
}
|
|
188
197
|
minLength(minLength) {
|
|
189
|
-
|
|
198
|
+
_objectAssign(this.schema, { minLength });
|
|
190
199
|
return this;
|
|
191
200
|
}
|
|
192
201
|
maxLength(maxLength) {
|
|
193
|
-
|
|
202
|
+
_objectAssign(this.schema, { maxLength });
|
|
194
203
|
return this;
|
|
195
204
|
}
|
|
196
205
|
length(minLength, maxLength) {
|
|
197
|
-
|
|
206
|
+
_objectAssign(this.schema, { minLength, maxLength });
|
|
198
207
|
return this;
|
|
199
208
|
}
|
|
200
209
|
email(opt) {
|
|
201
210
|
const defaultOptions = { checkTLD: true };
|
|
202
|
-
|
|
211
|
+
_objectAssign(this.schema, { email: { ...defaultOptions, ...opt } });
|
|
203
212
|
// from `ajv-formats`
|
|
204
213
|
const regex = /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i;
|
|
205
214
|
return this.regex(regex, { msg: 'is not a valid email address' }).trim().toLowerCase();
|
|
206
215
|
}
|
|
207
216
|
trim() {
|
|
208
|
-
|
|
217
|
+
_objectAssign(this.schema, { transform: { ...this.schema.transform, trim: true } });
|
|
209
218
|
return this;
|
|
210
219
|
}
|
|
211
220
|
toLowerCase() {
|
|
212
|
-
|
|
221
|
+
_objectAssign(this.schema, { transform: { ...this.schema.transform, toLowerCase: true } });
|
|
213
222
|
return this;
|
|
214
223
|
}
|
|
215
224
|
toUpperCase() {
|
|
216
|
-
|
|
225
|
+
_objectAssign(this.schema, { transform: { ...this.schema.transform, toUpperCase: true } });
|
|
217
226
|
return this;
|
|
218
227
|
}
|
|
219
228
|
truncate(toLength) {
|
|
220
|
-
|
|
229
|
+
_objectAssign(this.schema, { transform: { ...this.schema.transform, truncate: toLength } });
|
|
221
230
|
return this;
|
|
222
231
|
}
|
|
223
232
|
branded() {
|
|
224
233
|
return this;
|
|
225
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Validates that the input is a fully-specified YYYY-MM-DD formatted valid IsoDate value.
|
|
237
|
+
*
|
|
238
|
+
* All previous expectations in the schema chain are dropped - including `.optional()` -
|
|
239
|
+
* because this call effectively starts a new schema chain.
|
|
240
|
+
*/
|
|
226
241
|
isoDate() {
|
|
227
|
-
|
|
228
|
-
return
|
|
242
|
+
_objectAssign(this.schema, { IsoDate: {} });
|
|
243
|
+
return new JsonSchemaIsoDateBuilder();
|
|
229
244
|
}
|
|
230
245
|
isoDateTime() {
|
|
231
|
-
|
|
246
|
+
_objectAssign(this.schema, { IsoDateTime: true });
|
|
232
247
|
return this.branded();
|
|
233
248
|
}
|
|
234
249
|
/**
|
|
@@ -289,6 +304,39 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
|
|
|
289
304
|
return j.enum(TIMEZONES, { msg: 'is an invalid IANA timezone' }).branded();
|
|
290
305
|
}
|
|
291
306
|
}
|
|
307
|
+
export class JsonSchemaIsoDateBuilder extends JsonSchemaAnyBuilder {
|
|
308
|
+
constructor() {
|
|
309
|
+
super({
|
|
310
|
+
type: 'string',
|
|
311
|
+
IsoDate: {},
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
before(date) {
|
|
315
|
+
_objectAssign(this.schema.IsoDate, { before: date });
|
|
316
|
+
return this;
|
|
317
|
+
}
|
|
318
|
+
sameOrBefore(date) {
|
|
319
|
+
_objectAssign(this.schema.IsoDate, { sameOrBefore: date });
|
|
320
|
+
return this;
|
|
321
|
+
}
|
|
322
|
+
after(date) {
|
|
323
|
+
_objectAssign(this.schema.IsoDate, { after: date });
|
|
324
|
+
return this;
|
|
325
|
+
}
|
|
326
|
+
sameOrAfter(date) {
|
|
327
|
+
_objectAssign(this.schema.IsoDate, { sameOrAfter: date });
|
|
328
|
+
return this;
|
|
329
|
+
}
|
|
330
|
+
between(fromDate, toDate, incl) {
|
|
331
|
+
if (incl === '[)') {
|
|
332
|
+
_objectAssign(this.schema.IsoDate, { sameOrAfter: fromDate, before: toDate });
|
|
333
|
+
}
|
|
334
|
+
else if (incl === '[]') {
|
|
335
|
+
_objectAssign(this.schema.IsoDate, { sameOrAfter: fromDate, sameOrBefore: toDate });
|
|
336
|
+
}
|
|
337
|
+
return this;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
292
340
|
export class JsonSchemaNumberBuilder extends JsonSchemaAnyBuilder {
|
|
293
341
|
constructor() {
|
|
294
342
|
super({
|
|
@@ -296,30 +344,30 @@ export class JsonSchemaNumberBuilder extends JsonSchemaAnyBuilder {
|
|
|
296
344
|
});
|
|
297
345
|
}
|
|
298
346
|
integer() {
|
|
299
|
-
|
|
347
|
+
_objectAssign(this.schema, { type: 'integer' });
|
|
300
348
|
return this;
|
|
301
349
|
}
|
|
302
350
|
branded() {
|
|
303
351
|
return this;
|
|
304
352
|
}
|
|
305
353
|
multipleOf(multipleOf) {
|
|
306
|
-
|
|
354
|
+
_objectAssign(this.schema, { multipleOf });
|
|
307
355
|
return this;
|
|
308
356
|
}
|
|
309
357
|
min(minimum) {
|
|
310
|
-
|
|
358
|
+
_objectAssign(this.schema, { minimum });
|
|
311
359
|
return this;
|
|
312
360
|
}
|
|
313
361
|
exclusiveMin(exclusiveMinimum) {
|
|
314
|
-
|
|
362
|
+
_objectAssign(this.schema, { exclusiveMinimum });
|
|
315
363
|
return this;
|
|
316
364
|
}
|
|
317
365
|
max(maximum) {
|
|
318
|
-
|
|
366
|
+
_objectAssign(this.schema, { maximum });
|
|
319
367
|
return this;
|
|
320
368
|
}
|
|
321
369
|
exclusiveMax(exclusiveMaximum) {
|
|
322
|
-
|
|
370
|
+
_objectAssign(this.schema, { exclusiveMaximum });
|
|
323
371
|
return this;
|
|
324
372
|
}
|
|
325
373
|
lessThan(value) {
|
|
@@ -427,12 +475,12 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
|
|
|
427
475
|
* When set, the validation will not strip away properties that are not specified explicitly in the schema.
|
|
428
476
|
*/
|
|
429
477
|
allowAdditionalProperties() {
|
|
430
|
-
|
|
478
|
+
_objectAssign(this.schema, { additionalProperties: true });
|
|
431
479
|
return this;
|
|
432
480
|
}
|
|
433
481
|
extend(props) {
|
|
434
482
|
const newBuilder = new JsonSchemaObjectBuilder();
|
|
435
|
-
|
|
483
|
+
_objectAssign(newBuilder.schema, _deepCopy(this.schema));
|
|
436
484
|
const incomingSchemaBuilder = new JsonSchemaObjectBuilder(props);
|
|
437
485
|
mergeJsonSchemaObjects(newBuilder.schema, incomingSchemaBuilder.schema);
|
|
438
486
|
return newBuilder;
|
|
@@ -481,12 +529,12 @@ export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
|
|
|
481
529
|
* When set, the validation will not strip away properties that are not specified explicitly in the schema.
|
|
482
530
|
*/
|
|
483
531
|
allowAdditionalProperties() {
|
|
484
|
-
|
|
532
|
+
_objectAssign(this.schema, { additionalProperties: true });
|
|
485
533
|
return this;
|
|
486
534
|
}
|
|
487
535
|
extend(props) {
|
|
488
536
|
const newBuilder = new JsonSchemaObjectInferringBuilder();
|
|
489
|
-
|
|
537
|
+
_objectAssign(newBuilder.schema, _deepCopy(this.schema));
|
|
490
538
|
const incomingSchemaBuilder = new JsonSchemaObjectInferringBuilder(props);
|
|
491
539
|
mergeJsonSchemaObjects(newBuilder.schema, incomingSchemaBuilder.schema);
|
|
492
540
|
return newBuilder;
|
|
@@ -511,11 +559,11 @@ export class JsonSchemaArrayBuilder extends JsonSchemaAnyBuilder {
|
|
|
511
559
|
});
|
|
512
560
|
}
|
|
513
561
|
minLength(minItems) {
|
|
514
|
-
|
|
562
|
+
_objectAssign(this.schema, { minItems });
|
|
515
563
|
return this;
|
|
516
564
|
}
|
|
517
565
|
maxLength(maxItems) {
|
|
518
|
-
|
|
566
|
+
_objectAssign(this.schema, { maxItems });
|
|
519
567
|
return this;
|
|
520
568
|
}
|
|
521
569
|
length(minItems, maxItems) {
|
|
@@ -524,8 +572,8 @@ export class JsonSchemaArrayBuilder extends JsonSchemaAnyBuilder {
|
|
|
524
572
|
exactLength(length) {
|
|
525
573
|
return this.minLength(length).maxLength(length);
|
|
526
574
|
}
|
|
527
|
-
unique(
|
|
528
|
-
|
|
575
|
+
unique() {
|
|
576
|
+
_objectAssign(this.schema, { uniqueItems: true });
|
|
529
577
|
return this;
|
|
530
578
|
}
|
|
531
579
|
}
|
|
@@ -537,11 +585,11 @@ export class JsonSchemaSet2Builder extends JsonSchemaAnyBuilder {
|
|
|
537
585
|
});
|
|
538
586
|
}
|
|
539
587
|
min(minItems) {
|
|
540
|
-
|
|
588
|
+
_objectAssign(this.schema, { minItems });
|
|
541
589
|
return this;
|
|
542
590
|
}
|
|
543
591
|
max(maxItems) {
|
|
544
|
-
|
|
592
|
+
_objectAssign(this.schema, { maxItems });
|
|
545
593
|
return this;
|
|
546
594
|
}
|
|
547
595
|
}
|
|
@@ -553,8 +601,15 @@ export class JsonSchemaBufferBuilder extends JsonSchemaAnyBuilder {
|
|
|
553
601
|
}
|
|
554
602
|
}
|
|
555
603
|
export class JsonSchemaEnumBuilder extends JsonSchemaAnyBuilder {
|
|
556
|
-
constructor(enumValues, opt) {
|
|
557
|
-
|
|
604
|
+
constructor(enumValues, baseType, opt) {
|
|
605
|
+
const jsonSchema = { enum: enumValues };
|
|
606
|
+
// Specifying the base type helps in cases when we ask Ajv to coerce the types.
|
|
607
|
+
// Having only the `enum` in the schema does not trigger a coercion in Ajv.
|
|
608
|
+
if (baseType === 'string')
|
|
609
|
+
jsonSchema.type = 'string';
|
|
610
|
+
if (baseType === 'number')
|
|
611
|
+
jsonSchema.type = 'number';
|
|
612
|
+
super(jsonSchema);
|
|
558
613
|
if (opt?.name)
|
|
559
614
|
this.setErrorMessage('pattern', `is not a valid ${opt.name}`);
|
|
560
615
|
if (opt?.msg)
|
|
@@ -7,3 +7,5 @@ export declare const JSON_SCHEMA_ORDER: string[];
|
|
|
7
7
|
* API similar to Object.assign(s1, s2)
|
|
8
8
|
*/
|
|
9
9
|
export declare function mergeJsonSchemaObjects<T1 extends AnyObject, T2 extends AnyObject>(schema1: JsonSchema<T1>, schema2: JsonSchema<T2>): JsonSchema<T1 & T2>;
|
|
10
|
+
export declare function isEveryItemString(arr: any[]): boolean;
|
|
11
|
+
export declare function isEveryItemNumber(arr: any[]): boolean;
|
|
@@ -63,3 +63,17 @@ export function mergeJsonSchemaObjects(schema1, schema2) {
|
|
|
63
63
|
// `additionalProperties` remains the same
|
|
64
64
|
return _filterNullishValues(s1, { mutate: true });
|
|
65
65
|
}
|
|
66
|
+
export function isEveryItemString(arr) {
|
|
67
|
+
for (let i = 0; i <= arr.length; ++i) {
|
|
68
|
+
if (typeof arr[i] !== 'string')
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
export function isEveryItemNumber(arr) {
|
|
74
|
+
for (let i = 0; i <= arr.length; ++i) {
|
|
75
|
+
if (typeof arr[i] !== 'number')
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@ import { Set2 } from '@naturalcycles/js-lib/object'
|
|
|
3
3
|
import { _substringAfterLast } from '@naturalcycles/js-lib/string'
|
|
4
4
|
import { _, Ajv, type Options, type ValidateFunction } from 'ajv'
|
|
5
5
|
import { validTLDs } from '../tlds.js'
|
|
6
|
-
import type { JsonSchemaStringEmailOptions } from './jsonSchemaBuilder.js'
|
|
6
|
+
import type { JsonSchemaIsoDateOptions, JsonSchemaStringEmailOptions } from './jsonSchemaBuilder.js'
|
|
7
7
|
|
|
8
8
|
/* eslint-disable @typescript-eslint/prefer-string-starts-ends-with */
|
|
9
9
|
// oxlint-disable unicorn/prefer-code-point
|
|
@@ -24,6 +24,11 @@ const AJV_NON_MUTATING_OPTIONS: Options = {
|
|
|
24
24
|
useDefaults: false,
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
const AJV_MUTATING_COERCING_OPTIONS: Options = {
|
|
28
|
+
...AJV_OPTIONS,
|
|
29
|
+
coerceTypes: true,
|
|
30
|
+
}
|
|
31
|
+
|
|
27
32
|
/**
|
|
28
33
|
* Return cached instance of Ajv with default (recommended) options.
|
|
29
34
|
*
|
|
@@ -40,6 +45,14 @@ export const getAjv = _lazyValue(createAjv)
|
|
|
40
45
|
*/
|
|
41
46
|
export const getNonMutatingAjv = _lazyValue(() => createAjv(AJV_NON_MUTATING_OPTIONS))
|
|
42
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Returns cached instance of Ajv, which is coercing data.
|
|
50
|
+
*
|
|
51
|
+
* To be used in places where we know that we are going to receive data with the wrong type,
|
|
52
|
+
* typically: request path params and request query params.
|
|
53
|
+
*/
|
|
54
|
+
export const getCoercingAjv = _lazyValue(() => createAjv(AJV_MUTATING_COERCING_OPTIONS))
|
|
55
|
+
|
|
43
56
|
/**
|
|
44
57
|
* Create Ajv with modified defaults.
|
|
45
58
|
*
|
|
@@ -255,16 +268,77 @@ export function createAjv(opt?: Options): Ajv {
|
|
|
255
268
|
type: 'string',
|
|
256
269
|
modifying: false,
|
|
257
270
|
errors: true,
|
|
258
|
-
schemaType: '
|
|
259
|
-
validate: function validate(
|
|
271
|
+
schemaType: 'object',
|
|
272
|
+
validate: function validate(opt: JsonSchemaIsoDateOptions, data: string, _schema, ctx) {
|
|
273
|
+
const hasOptions = Object.keys(opt).length > 0
|
|
274
|
+
|
|
260
275
|
const isValid = isIsoDateValid(data)
|
|
261
|
-
if (isValid)
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
276
|
+
if (!isValid) {
|
|
277
|
+
;(validate as any).errors = [
|
|
278
|
+
{
|
|
279
|
+
instancePath: ctx?.instancePath ?? '',
|
|
280
|
+
message: `is an invalid IsoDate`,
|
|
281
|
+
},
|
|
282
|
+
]
|
|
283
|
+
|
|
284
|
+
return false
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (!hasOptions) return true
|
|
288
|
+
|
|
289
|
+
const { before, sameOrBefore, after, sameOrAfter } = opt
|
|
290
|
+
const errors: any[] = []
|
|
291
|
+
|
|
292
|
+
if (before) {
|
|
293
|
+
const isParamValid = isIsoDateValid(before)
|
|
294
|
+
const isRuleValid = isParamValid && before > data
|
|
295
|
+
|
|
296
|
+
if (!isRuleValid) {
|
|
297
|
+
errors.push({
|
|
298
|
+
instancePath: ctx?.instancePath ?? '',
|
|
299
|
+
message: `is not before ${before}`,
|
|
300
|
+
})
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (sameOrBefore) {
|
|
305
|
+
const isParamValid = isIsoDateValid(sameOrBefore)
|
|
306
|
+
const isRuleValid = isParamValid && sameOrBefore >= data
|
|
307
|
+
|
|
308
|
+
if (!isRuleValid) {
|
|
309
|
+
errors.push({
|
|
310
|
+
instancePath: ctx?.instancePath ?? '',
|
|
311
|
+
message: `is not the same or before ${sameOrBefore}`,
|
|
312
|
+
})
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (after) {
|
|
317
|
+
const isParamValid = isIsoDateValid(after)
|
|
318
|
+
const isRuleValid = isParamValid && after < data
|
|
319
|
+
|
|
320
|
+
if (!isRuleValid) {
|
|
321
|
+
errors.push({
|
|
322
|
+
instancePath: ctx?.instancePath ?? '',
|
|
323
|
+
message: `is not after ${after}`,
|
|
324
|
+
})
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (sameOrAfter) {
|
|
329
|
+
const isParamValid = isIsoDateValid(sameOrAfter)
|
|
330
|
+
const isRuleValid = isParamValid && sameOrAfter <= data
|
|
331
|
+
|
|
332
|
+
if (!isRuleValid) {
|
|
333
|
+
errors.push({
|
|
334
|
+
instancePath: ctx?.instancePath ?? '',
|
|
335
|
+
message: `is not the same or after ${sameOrAfter}`,
|
|
336
|
+
})
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (errors.length === 0) return true
|
|
341
|
+
;(validate as any).errors = errors
|
|
268
342
|
return false
|
|
269
343
|
},
|
|
270
344
|
})
|
|
@@ -12,6 +12,7 @@ import { _assert } from '@naturalcycles/js-lib/error'
|
|
|
12
12
|
import type { Set2 } from '@naturalcycles/js-lib/object'
|
|
13
13
|
import { _deepCopy, _sortObject } from '@naturalcycles/js-lib/object'
|
|
14
14
|
import {
|
|
15
|
+
_objectAssign,
|
|
15
16
|
type AnyObject,
|
|
16
17
|
type BaseDBEntity,
|
|
17
18
|
type IANATimezone,
|
|
@@ -26,7 +27,12 @@ import {
|
|
|
26
27
|
type UnixTimestampMillis,
|
|
27
28
|
} from '@naturalcycles/js-lib/types'
|
|
28
29
|
import { TIMEZONES } from '../timezones.js'
|
|
29
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
isEveryItemNumber,
|
|
32
|
+
isEveryItemString,
|
|
33
|
+
JSON_SCHEMA_ORDER,
|
|
34
|
+
mergeJsonSchemaObjects,
|
|
35
|
+
} from './jsonSchemaBuilder.util.js'
|
|
30
36
|
|
|
31
37
|
export const j = {
|
|
32
38
|
string(): JsonSchemaStringBuilder<string, string, false> {
|
|
@@ -78,20 +84,28 @@ export const j = {
|
|
|
78
84
|
: never
|
|
79
85
|
> {
|
|
80
86
|
let enumValues: readonly (string | number | boolean | null)[] | undefined
|
|
87
|
+
let baseType: EnumBaseType = 'other'
|
|
81
88
|
|
|
82
89
|
if (Array.isArray(input)) {
|
|
83
90
|
enumValues = input
|
|
91
|
+
if (isEveryItemNumber(input)) {
|
|
92
|
+
baseType = 'number'
|
|
93
|
+
} else if (isEveryItemString(input)) {
|
|
94
|
+
baseType = 'string'
|
|
95
|
+
}
|
|
84
96
|
} else if (typeof input === 'object') {
|
|
85
97
|
const enumType = getEnumType(input)
|
|
86
98
|
if (enumType === 'NumberEnum') {
|
|
87
99
|
enumValues = _numberEnumValues(input as NumberEnum)
|
|
100
|
+
baseType = 'number'
|
|
88
101
|
} else if (enumType === 'StringEnum') {
|
|
89
102
|
enumValues = _stringEnumValues(input as StringEnum)
|
|
103
|
+
baseType = 'string'
|
|
90
104
|
}
|
|
91
105
|
}
|
|
92
106
|
|
|
93
107
|
_assert(enumValues, 'Unsupported enum input')
|
|
94
|
-
return new JsonSchemaEnumBuilder(enumValues as any, opt)
|
|
108
|
+
return new JsonSchemaEnumBuilder(enumValues as any, baseType, opt)
|
|
95
109
|
},
|
|
96
110
|
|
|
97
111
|
oneOf<
|
|
@@ -144,7 +158,7 @@ export class JsonSchemaAnyBuilder<IN, OUT, Opt> {
|
|
|
144
158
|
* ```
|
|
145
159
|
*/
|
|
146
160
|
isOfType<ExpectedType>(): ExactMatch<ExpectedType, OUT> extends true ? this : never {
|
|
147
|
-
|
|
161
|
+
_objectAssign(this.schema, { hasIsOfTypeCheck: true })
|
|
148
162
|
return this as any
|
|
149
163
|
}
|
|
150
164
|
|
|
@@ -153,7 +167,7 @@ export class JsonSchemaAnyBuilder<IN, OUT, Opt> {
|
|
|
153
167
|
}
|
|
154
168
|
|
|
155
169
|
$schema($schema: string): this {
|
|
156
|
-
|
|
170
|
+
_objectAssign(this.schema, { $schema })
|
|
157
171
|
return this
|
|
158
172
|
}
|
|
159
173
|
|
|
@@ -163,37 +177,37 @@ export class JsonSchemaAnyBuilder<IN, OUT, Opt> {
|
|
|
163
177
|
}
|
|
164
178
|
|
|
165
179
|
$id($id: string): this {
|
|
166
|
-
|
|
180
|
+
_objectAssign(this.schema, { $id })
|
|
167
181
|
return this
|
|
168
182
|
}
|
|
169
183
|
|
|
170
184
|
title(title: string): this {
|
|
171
|
-
|
|
185
|
+
_objectAssign(this.schema, { title })
|
|
172
186
|
return this
|
|
173
187
|
}
|
|
174
188
|
|
|
175
189
|
description(description: string): this {
|
|
176
|
-
|
|
190
|
+
_objectAssign(this.schema, { description })
|
|
177
191
|
return this
|
|
178
192
|
}
|
|
179
193
|
|
|
180
194
|
deprecated(deprecated = true): this {
|
|
181
|
-
|
|
195
|
+
_objectAssign(this.schema, { deprecated })
|
|
182
196
|
return this
|
|
183
197
|
}
|
|
184
198
|
|
|
185
199
|
type(type: string): this {
|
|
186
|
-
|
|
200
|
+
_objectAssign(this.schema, { type })
|
|
187
201
|
return this
|
|
188
202
|
}
|
|
189
203
|
|
|
190
204
|
default(v: any): this {
|
|
191
|
-
|
|
205
|
+
_objectAssign(this.schema, { default: v })
|
|
192
206
|
return this
|
|
193
207
|
}
|
|
194
208
|
|
|
195
209
|
instanceof(of: string): this {
|
|
196
|
-
|
|
210
|
+
_objectAssign(this.schema, { type: 'object', instanceof: of })
|
|
197
211
|
return this
|
|
198
212
|
}
|
|
199
213
|
|
|
@@ -253,28 +267,28 @@ export class JsonSchemaStringBuilder<
|
|
|
253
267
|
pattern(pattern: string, opt?: JsonBuilderRuleOpt): this {
|
|
254
268
|
if (opt?.name) this.setErrorMessage('pattern', `is not a valid ${opt.name}`)
|
|
255
269
|
if (opt?.msg) this.setErrorMessage('pattern', opt.msg)
|
|
256
|
-
|
|
270
|
+
_objectAssign(this.schema, { pattern })
|
|
257
271
|
return this
|
|
258
272
|
}
|
|
259
273
|
|
|
260
274
|
minLength(minLength: number): this {
|
|
261
|
-
|
|
275
|
+
_objectAssign(this.schema, { minLength })
|
|
262
276
|
return this
|
|
263
277
|
}
|
|
264
278
|
|
|
265
279
|
maxLength(maxLength: number): this {
|
|
266
|
-
|
|
280
|
+
_objectAssign(this.schema, { maxLength })
|
|
267
281
|
return this
|
|
268
282
|
}
|
|
269
283
|
|
|
270
284
|
length(minLength: number, maxLength: number): this {
|
|
271
|
-
|
|
285
|
+
_objectAssign(this.schema, { minLength, maxLength })
|
|
272
286
|
return this
|
|
273
287
|
}
|
|
274
288
|
|
|
275
289
|
email(opt?: Partial<JsonSchemaStringEmailOptions>): this {
|
|
276
290
|
const defaultOptions: JsonSchemaStringEmailOptions = { checkTLD: true }
|
|
277
|
-
|
|
291
|
+
_objectAssign(this.schema, { email: { ...defaultOptions, ...opt } })
|
|
278
292
|
|
|
279
293
|
// from `ajv-formats`
|
|
280
294
|
const regex =
|
|
@@ -283,22 +297,22 @@ export class JsonSchemaStringBuilder<
|
|
|
283
297
|
}
|
|
284
298
|
|
|
285
299
|
trim(): this {
|
|
286
|
-
|
|
300
|
+
_objectAssign(this.schema, { transform: { ...this.schema.transform, trim: true } })
|
|
287
301
|
return this
|
|
288
302
|
}
|
|
289
303
|
|
|
290
304
|
toLowerCase(): this {
|
|
291
|
-
|
|
305
|
+
_objectAssign(this.schema, { transform: { ...this.schema.transform, toLowerCase: true } })
|
|
292
306
|
return this
|
|
293
307
|
}
|
|
294
308
|
|
|
295
309
|
toUpperCase(): this {
|
|
296
|
-
|
|
310
|
+
_objectAssign(this.schema, { transform: { ...this.schema.transform, toUpperCase: true } })
|
|
297
311
|
return this
|
|
298
312
|
}
|
|
299
313
|
|
|
300
314
|
truncate(toLength: number): this {
|
|
301
|
-
|
|
315
|
+
_objectAssign(this.schema, { transform: { ...this.schema.transform, truncate: toLength } })
|
|
302
316
|
return this
|
|
303
317
|
}
|
|
304
318
|
|
|
@@ -306,13 +320,19 @@ export class JsonSchemaStringBuilder<
|
|
|
306
320
|
return this as unknown as JsonSchemaStringBuilder<B, B, Opt>
|
|
307
321
|
}
|
|
308
322
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
323
|
+
/**
|
|
324
|
+
* Validates that the input is a fully-specified YYYY-MM-DD formatted valid IsoDate value.
|
|
325
|
+
*
|
|
326
|
+
* All previous expectations in the schema chain are dropped - including `.optional()` -
|
|
327
|
+
* because this call effectively starts a new schema chain.
|
|
328
|
+
*/
|
|
329
|
+
isoDate(): JsonSchemaIsoDateBuilder {
|
|
330
|
+
_objectAssign(this.schema, { IsoDate: {} })
|
|
331
|
+
return new JsonSchemaIsoDateBuilder()
|
|
312
332
|
}
|
|
313
333
|
|
|
314
334
|
isoDateTime(): JsonSchemaStringBuilder<IsoDateTime | IN, IsoDateTime, Opt> {
|
|
315
|
-
|
|
335
|
+
_objectAssign(this.schema, { IsoDateTime: true })
|
|
316
336
|
return this.branded<IsoDateTime>()
|
|
317
337
|
}
|
|
318
338
|
|
|
@@ -392,6 +412,56 @@ export interface JsonSchemaStringEmailOptions {
|
|
|
392
412
|
checkTLD: boolean
|
|
393
413
|
}
|
|
394
414
|
|
|
415
|
+
export class JsonSchemaIsoDateBuilder<Opt extends boolean = false> extends JsonSchemaAnyBuilder<
|
|
416
|
+
string | IsoDate,
|
|
417
|
+
IsoDate,
|
|
418
|
+
Opt
|
|
419
|
+
> {
|
|
420
|
+
constructor() {
|
|
421
|
+
super({
|
|
422
|
+
type: 'string',
|
|
423
|
+
IsoDate: {},
|
|
424
|
+
})
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
before(date: string): this {
|
|
428
|
+
_objectAssign(this.schema.IsoDate!, { before: date })
|
|
429
|
+
return this
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
sameOrBefore(date: string): this {
|
|
433
|
+
_objectAssign(this.schema.IsoDate!, { sameOrBefore: date })
|
|
434
|
+
return this
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
after(date: string): this {
|
|
438
|
+
_objectAssign(this.schema.IsoDate!, { after: date })
|
|
439
|
+
return this
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
sameOrAfter(date: string): this {
|
|
443
|
+
_objectAssign(this.schema.IsoDate!, { sameOrAfter: date })
|
|
444
|
+
return this
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
between(fromDate: string, toDate: string, incl: Inclusiveness): this {
|
|
448
|
+
if (incl === '[)') {
|
|
449
|
+
_objectAssign(this.schema.IsoDate!, { sameOrAfter: fromDate, before: toDate })
|
|
450
|
+
} else if (incl === '[]') {
|
|
451
|
+
_objectAssign(this.schema.IsoDate!, { sameOrAfter: fromDate, sameOrBefore: toDate })
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return this
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export interface JsonSchemaIsoDateOptions {
|
|
459
|
+
before?: string
|
|
460
|
+
sameOrBefore?: string
|
|
461
|
+
after?: string
|
|
462
|
+
sameOrAfter?: string
|
|
463
|
+
}
|
|
464
|
+
|
|
395
465
|
export class JsonSchemaNumberBuilder<
|
|
396
466
|
IN extends number = number,
|
|
397
467
|
OUT = IN,
|
|
@@ -404,7 +474,7 @@ export class JsonSchemaNumberBuilder<
|
|
|
404
474
|
}
|
|
405
475
|
|
|
406
476
|
integer(): this {
|
|
407
|
-
|
|
477
|
+
_objectAssign(this.schema, { type: 'integer' })
|
|
408
478
|
return this
|
|
409
479
|
}
|
|
410
480
|
|
|
@@ -413,27 +483,27 @@ export class JsonSchemaNumberBuilder<
|
|
|
413
483
|
}
|
|
414
484
|
|
|
415
485
|
multipleOf(multipleOf: number): this {
|
|
416
|
-
|
|
486
|
+
_objectAssign(this.schema, { multipleOf })
|
|
417
487
|
return this
|
|
418
488
|
}
|
|
419
489
|
|
|
420
490
|
min(minimum: number): this {
|
|
421
|
-
|
|
491
|
+
_objectAssign(this.schema, { minimum })
|
|
422
492
|
return this
|
|
423
493
|
}
|
|
424
494
|
|
|
425
495
|
exclusiveMin(exclusiveMinimum: number): this {
|
|
426
|
-
|
|
496
|
+
_objectAssign(this.schema, { exclusiveMinimum })
|
|
427
497
|
return this
|
|
428
498
|
}
|
|
429
499
|
|
|
430
500
|
max(maximum: number): this {
|
|
431
|
-
|
|
501
|
+
_objectAssign(this.schema, { maximum })
|
|
432
502
|
return this
|
|
433
503
|
}
|
|
434
504
|
|
|
435
505
|
exclusiveMax(exclusiveMaximum: number): this {
|
|
436
|
-
|
|
506
|
+
_objectAssign(this.schema, { exclusiveMaximum })
|
|
437
507
|
return this
|
|
438
508
|
}
|
|
439
509
|
|
|
@@ -575,7 +645,7 @@ export class JsonSchemaObjectBuilder<
|
|
|
575
645
|
* When set, the validation will not strip away properties that are not specified explicitly in the schema.
|
|
576
646
|
*/
|
|
577
647
|
allowAdditionalProperties(): this {
|
|
578
|
-
|
|
648
|
+
_objectAssign(this.schema, { additionalProperties: true })
|
|
579
649
|
return this
|
|
580
650
|
}
|
|
581
651
|
|
|
@@ -583,7 +653,7 @@ export class JsonSchemaObjectBuilder<
|
|
|
583
653
|
props: AnyObject,
|
|
584
654
|
): JsonSchemaObjectBuilder<IN & IN2, OUT & IN2, Opt> {
|
|
585
655
|
const newBuilder = new JsonSchemaObjectBuilder<IN & IN2, OUT & IN2, Opt>()
|
|
586
|
-
|
|
656
|
+
_objectAssign(newBuilder.schema, _deepCopy(this.schema))
|
|
587
657
|
|
|
588
658
|
const incomingSchemaBuilder = new JsonSchemaObjectBuilder<IN2, IN2, false>(props)
|
|
589
659
|
mergeJsonSchemaObjects(newBuilder.schema as any, incomingSchemaBuilder.schema as any)
|
|
@@ -675,7 +745,7 @@ export class JsonSchemaObjectInferringBuilder<
|
|
|
675
745
|
* When set, the validation will not strip away properties that are not specified explicitly in the schema.
|
|
676
746
|
*/
|
|
677
747
|
allowAdditionalProperties(): this {
|
|
678
|
-
|
|
748
|
+
_objectAssign(this.schema, { additionalProperties: true })
|
|
679
749
|
return this
|
|
680
750
|
}
|
|
681
751
|
|
|
@@ -692,7 +762,7 @@ export class JsonSchemaObjectInferringBuilder<
|
|
|
692
762
|
Opt
|
|
693
763
|
> {
|
|
694
764
|
const newBuilder = new JsonSchemaObjectInferringBuilder<PROPS, Opt>()
|
|
695
|
-
|
|
765
|
+
_objectAssign(newBuilder.schema, _deepCopy(this.schema))
|
|
696
766
|
|
|
697
767
|
const incomingSchemaBuilder = new JsonSchemaObjectInferringBuilder<NEW_PROPS, false>(props)
|
|
698
768
|
mergeJsonSchemaObjects(newBuilder.schema as any, incomingSchemaBuilder.schema as any)
|
|
@@ -731,12 +801,12 @@ export class JsonSchemaArrayBuilder<IN, OUT, Opt> extends JsonSchemaAnyBuilder<I
|
|
|
731
801
|
}
|
|
732
802
|
|
|
733
803
|
minLength(minItems: number): this {
|
|
734
|
-
|
|
804
|
+
_objectAssign(this.schema, { minItems })
|
|
735
805
|
return this
|
|
736
806
|
}
|
|
737
807
|
|
|
738
808
|
maxLength(maxItems: number): this {
|
|
739
|
-
|
|
809
|
+
_objectAssign(this.schema, { maxItems })
|
|
740
810
|
return this
|
|
741
811
|
}
|
|
742
812
|
|
|
@@ -748,8 +818,8 @@ export class JsonSchemaArrayBuilder<IN, OUT, Opt> extends JsonSchemaAnyBuilder<I
|
|
|
748
818
|
return this.minLength(length).maxLength(length)
|
|
749
819
|
}
|
|
750
820
|
|
|
751
|
-
unique(
|
|
752
|
-
|
|
821
|
+
unique(): this {
|
|
822
|
+
_objectAssign(this.schema, { uniqueItems: true })
|
|
753
823
|
return this
|
|
754
824
|
}
|
|
755
825
|
}
|
|
@@ -767,12 +837,12 @@ export class JsonSchemaSet2Builder<IN, OUT, Opt> extends JsonSchemaAnyBuilder<
|
|
|
767
837
|
}
|
|
768
838
|
|
|
769
839
|
min(minItems: number): this {
|
|
770
|
-
|
|
840
|
+
_objectAssign(this.schema, { minItems })
|
|
771
841
|
return this
|
|
772
842
|
}
|
|
773
843
|
|
|
774
844
|
max(maxItems: number): this {
|
|
775
|
-
|
|
845
|
+
_objectAssign(this.schema, { maxItems })
|
|
776
846
|
return this
|
|
777
847
|
}
|
|
778
848
|
}
|
|
@@ -794,8 +864,15 @@ export class JsonSchemaEnumBuilder<
|
|
|
794
864
|
OUT extends IN = IN,
|
|
795
865
|
Opt extends boolean = false,
|
|
796
866
|
> extends JsonSchemaAnyBuilder<IN, OUT, Opt> {
|
|
797
|
-
constructor(enumValues: readonly IN[], opt?: JsonBuilderRuleOpt) {
|
|
798
|
-
|
|
867
|
+
constructor(enumValues: readonly IN[], baseType: EnumBaseType, opt?: JsonBuilderRuleOpt) {
|
|
868
|
+
const jsonSchema: JsonSchema = { enum: enumValues }
|
|
869
|
+
// Specifying the base type helps in cases when we ask Ajv to coerce the types.
|
|
870
|
+
// Having only the `enum` in the schema does not trigger a coercion in Ajv.
|
|
871
|
+
if (baseType === 'string') jsonSchema.type = 'string'
|
|
872
|
+
if (baseType === 'number') jsonSchema.type = 'number'
|
|
873
|
+
|
|
874
|
+
super(jsonSchema)
|
|
875
|
+
|
|
799
876
|
if (opt?.name) this.setErrorMessage('pattern', `is not a valid ${opt.name}`)
|
|
800
877
|
if (opt?.msg) this.setErrorMessage('enum', opt.msg)
|
|
801
878
|
}
|
|
@@ -805,11 +882,13 @@ export class JsonSchemaEnumBuilder<
|
|
|
805
882
|
}
|
|
806
883
|
}
|
|
807
884
|
|
|
885
|
+
type EnumBaseType = 'string' | 'number' | 'other'
|
|
886
|
+
|
|
808
887
|
export interface JsonSchema<IN = unknown, OUT = IN> {
|
|
809
888
|
readonly in?: IN
|
|
810
889
|
readonly out?: OUT
|
|
811
890
|
|
|
812
|
-
$schema?:
|
|
891
|
+
$schema?: string
|
|
813
892
|
$id?: string
|
|
814
893
|
title?: string
|
|
815
894
|
description?: string
|
|
@@ -857,14 +936,18 @@ export interface JsonSchema<IN = unknown, OUT = IN> {
|
|
|
857
936
|
exclusiveMinimum?: number
|
|
858
937
|
maximum?: number
|
|
859
938
|
exclusiveMaximum?: number
|
|
939
|
+
minItems?: number
|
|
940
|
+
maxItems?: number
|
|
941
|
+
uniqueItems?: boolean
|
|
860
942
|
|
|
861
943
|
enum?: any
|
|
862
944
|
|
|
863
945
|
// Below we add custom Ajv keywords
|
|
864
946
|
|
|
947
|
+
email?: JsonSchemaStringEmailOptions
|
|
865
948
|
Set2?: JsonSchema
|
|
866
949
|
Buffer?: true
|
|
867
|
-
IsoDate?:
|
|
950
|
+
IsoDate?: JsonSchemaIsoDateOptions
|
|
868
951
|
IsoDateTime?: true
|
|
869
952
|
instanceof?: string | string[]
|
|
870
953
|
transform?: { trim?: true; toLowerCase?: true; toUpperCase?: true; truncate?: number }
|
|
@@ -76,3 +76,17 @@ export function mergeJsonSchemaObjects<T1 extends AnyObject, T2 extends AnyObjec
|
|
|
76
76
|
|
|
77
77
|
return _filterNullishValues(s1, { mutate: true })
|
|
78
78
|
}
|
|
79
|
+
|
|
80
|
+
export function isEveryItemString(arr: any[]): boolean {
|
|
81
|
+
for (let i = 0; i <= arr.length; ++i) {
|
|
82
|
+
if (typeof arr[i] !== 'string') return false
|
|
83
|
+
}
|
|
84
|
+
return true
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function isEveryItemNumber(arr: any[]): boolean {
|
|
88
|
+
for (let i = 0; i <= arr.length; ++i) {
|
|
89
|
+
if (typeof arr[i] !== 'number') return false
|
|
90
|
+
}
|
|
91
|
+
return true
|
|
92
|
+
}
|