@naturalcycles/nodejs-lib 15.93.0 → 15.95.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/jwt/jwt.service.d.ts +4 -4
- package/dist/validation/ajv/from-data/generateJsonSchemaFromData.d.ts +1 -1
- package/dist/validation/ajv/getAjv.d.ts +1 -1
- package/dist/validation/ajv/index.d.ts +1 -1
- package/dist/validation/ajv/index.js +1 -1
- package/dist/validation/ajv/{ajvSchema.d.ts → jSchema.d.ts} +233 -272
- package/dist/validation/ajv/{ajvSchema.js → jSchema.js} +455 -461
- package/dist/validation/ajv/jsonSchemaBuilder.util.d.ts +1 -1
- package/package.json +1 -1
- package/src/jwt/jwt.service.ts +8 -4
- package/src/validation/ajv/from-data/generateJsonSchemaFromData.ts +2 -2
- package/src/validation/ajv/getAjv.ts +5 -5
- package/src/validation/ajv/index.ts +1 -1
- package/src/validation/ajv/{ajvSchema.ts → jSchema.ts} +1453 -1474
- package/src/validation/ajv/jsonSchemaBuilder.util.ts +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint-disable id-denylist */
|
|
2
2
|
// oxlint-disable max-lines
|
|
3
|
-
import { _isObject, _isUndefined,
|
|
3
|
+
import { _isObject, _isUndefined, _numberEnumValues, _stringEnumValues, getEnumType, } from '@naturalcycles/js-lib';
|
|
4
4
|
import { _uniq } from '@naturalcycles/js-lib/array';
|
|
5
5
|
import { _assert, _try } from '@naturalcycles/js-lib/error';
|
|
6
6
|
import { _deepCopy, _filterNullishValues, _sortObject } from '@naturalcycles/js-lib/object';
|
|
@@ -12,314 +12,23 @@ import { TIMEZONES } from '../timezones.js';
|
|
|
12
12
|
import { AjvValidationError } from './ajvValidationError.js';
|
|
13
13
|
import { getAjv } from './getAjv.js';
|
|
14
14
|
import { isEveryItemNumber, isEveryItemPrimitive, isEveryItemString, JSON_SCHEMA_ORDER, mergeJsonSchemaObjects, } from './jsonSchemaBuilder.util.js';
|
|
15
|
-
// ====
|
|
16
|
-
/**
|
|
17
|
-
* On creation - compiles ajv validation function.
|
|
18
|
-
* Provides convenient methods, error reporting, etc.
|
|
19
|
-
*/
|
|
20
|
-
export class AjvSchema {
|
|
21
|
-
schema;
|
|
22
|
-
constructor(schema, cfg = {}) {
|
|
23
|
-
this.schema = schema;
|
|
24
|
-
this.cfg = {
|
|
25
|
-
lazy: false,
|
|
26
|
-
...cfg,
|
|
27
|
-
ajv: cfg.ajv || getAjv(),
|
|
28
|
-
// Auto-detecting "InputName" from $id of the schema (e.g "Address.schema.json")
|
|
29
|
-
inputName: cfg.inputName || (schema.$id ? _substringBefore(schema.$id, '.') : undefined),
|
|
30
|
-
};
|
|
31
|
-
if (!cfg.lazy) {
|
|
32
|
-
this.getAJVValidateFunction(); // compile eagerly
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Shortcut for AjvSchema.create(schema, { lazy: true })
|
|
37
|
-
*/
|
|
38
|
-
static createLazy(schema, cfg) {
|
|
39
|
-
return AjvSchema.create(schema, {
|
|
40
|
-
lazy: true,
|
|
41
|
-
...cfg,
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Conveniently allows to pass either JsonSchema or JsonSchemaBuilder, or existing AjvSchema.
|
|
46
|
-
* If it's already an AjvSchema - it'll just return it without any processing.
|
|
47
|
-
* If it's a Builder - will call `build` before proceeding.
|
|
48
|
-
* Otherwise - will construct AjvSchema instance ready to be used.
|
|
49
|
-
*
|
|
50
|
-
* Implementation note: JsonSchemaBuilder goes first in the union type, otherwise TypeScript fails to infer <T> type
|
|
51
|
-
* correctly for some reason.
|
|
52
|
-
*/
|
|
53
|
-
static create(schema, cfg) {
|
|
54
|
-
if (schema instanceof AjvSchema)
|
|
55
|
-
return schema;
|
|
56
|
-
if (AjvSchema.isSchemaWithCachedAjvSchema(schema)) {
|
|
57
|
-
return AjvSchema.requireCachedAjvSchema(schema);
|
|
58
|
-
}
|
|
59
|
-
let jsonSchema;
|
|
60
|
-
if (AjvSchema.isJsonSchemaBuilder(schema)) {
|
|
61
|
-
// oxlint-disable typescript-eslint(no-unnecessary-type-assertion)
|
|
62
|
-
jsonSchema = schema.build();
|
|
63
|
-
AjvSchema.requireValidJsonSchema(jsonSchema);
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
jsonSchema = schema;
|
|
67
|
-
}
|
|
68
|
-
// This is our own helper which marks a schema as optional
|
|
69
|
-
// in case it is going to be used in an object schema,
|
|
70
|
-
// where we need to mark the given property as not-required.
|
|
71
|
-
// But once all compilation is done, the presence of this field
|
|
72
|
-
// really upsets Ajv.
|
|
73
|
-
delete jsonSchema.optionalField;
|
|
74
|
-
const ajvSchema = new AjvSchema(jsonSchema, cfg);
|
|
75
|
-
AjvSchema.cacheAjvSchema(schema, ajvSchema);
|
|
76
|
-
return ajvSchema;
|
|
77
|
-
}
|
|
78
|
-
static isJsonSchemaBuilder(schema) {
|
|
79
|
-
return schema instanceof JsonSchemaTerminal;
|
|
80
|
-
}
|
|
81
|
-
cfg;
|
|
82
|
-
/**
|
|
83
|
-
* It returns the original object just for convenience.
|
|
84
|
-
* Reminder: Ajv will MUTATE your object under 2 circumstances:
|
|
85
|
-
* 1. `useDefaults` option (enabled by default!), which will set missing/empty values that have `default` set in the schema.
|
|
86
|
-
* 2. `coerceTypes` (false by default).
|
|
87
|
-
*
|
|
88
|
-
* Returned object is always the same object (`===`) that was passed, so it is returned just for convenience.
|
|
89
|
-
*/
|
|
90
|
-
validate(input, opt = {}) {
|
|
91
|
-
const [err, output] = this.getValidationResult(input, opt);
|
|
92
|
-
if (err)
|
|
93
|
-
throw err;
|
|
94
|
-
return output;
|
|
95
|
-
}
|
|
96
|
-
isValid(input, opt) {
|
|
97
|
-
// todo: we can make it both fast and non-mutating by using Ajv
|
|
98
|
-
// with "removeAdditional" and "useDefaults" disabled.
|
|
99
|
-
const [err] = this.getValidationResult(input, opt);
|
|
100
|
-
return !err;
|
|
101
|
-
}
|
|
102
|
-
getValidationResult(input, opt = {}) {
|
|
103
|
-
const fn = this.getAJVValidateFunction();
|
|
104
|
-
const item = opt.mutateInput !== false || typeof input !== 'object'
|
|
105
|
-
? input // mutate
|
|
106
|
-
: _deepCopy(input); // not mutate
|
|
107
|
-
let valid = fn(item); // mutates item, but not input
|
|
108
|
-
_typeCast(item);
|
|
109
|
-
let output = item;
|
|
110
|
-
if (valid && this.schema.postValidation) {
|
|
111
|
-
const [err, result] = _try(() => this.schema.postValidation(output));
|
|
112
|
-
if (err) {
|
|
113
|
-
valid = false;
|
|
114
|
-
fn.errors = [
|
|
115
|
-
{
|
|
116
|
-
instancePath: '',
|
|
117
|
-
message: err.message,
|
|
118
|
-
},
|
|
119
|
-
];
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
output = result;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
if (valid)
|
|
126
|
-
return [null, output];
|
|
127
|
-
const errors = fn.errors;
|
|
128
|
-
const { inputId = _isObject(input) ? input['id'] : undefined, inputName = this.cfg.inputName || 'Object', } = opt;
|
|
129
|
-
const dataVar = [inputName, inputId].filter(Boolean).join('.');
|
|
130
|
-
this.applyImprovementsOnErrorMessages(errors);
|
|
131
|
-
let message = this.cfg.ajv.errorsText(errors, {
|
|
132
|
-
dataVar,
|
|
133
|
-
separator,
|
|
134
|
-
});
|
|
135
|
-
// Note: if we mutated the input already, e.g stripped unknown properties,
|
|
136
|
-
// the error message Input would contain already mutated object print, such as Input: {}
|
|
137
|
-
// Unless `getOriginalInput` function is provided - then it will be used to preserve the Input pureness.
|
|
138
|
-
const inputStringified = _inspect(opt.getOriginalInput?.() || input, { maxLen: 4000 });
|
|
139
|
-
message = [message, 'Input: ' + inputStringified].join(separator);
|
|
140
|
-
const err = new AjvValidationError(message, _filterNullishValues({
|
|
141
|
-
errors,
|
|
142
|
-
inputName,
|
|
143
|
-
inputId,
|
|
144
|
-
}));
|
|
145
|
-
return [err, output];
|
|
146
|
-
}
|
|
147
|
-
getValidationFunction() {
|
|
148
|
-
return (input, opt) => {
|
|
149
|
-
return this.getValidationResult(input, {
|
|
150
|
-
mutateInput: opt?.mutateInput,
|
|
151
|
-
inputName: opt?.inputName,
|
|
152
|
-
inputId: opt?.inputId,
|
|
153
|
-
});
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
static isSchemaWithCachedAjvSchema(schema) {
|
|
157
|
-
return !!schema?.[HIDDEN_AJV_SCHEMA];
|
|
158
|
-
}
|
|
159
|
-
static cacheAjvSchema(schema, ajvSchema) {
|
|
160
|
-
return Object.assign(schema, { [HIDDEN_AJV_SCHEMA]: ajvSchema });
|
|
161
|
-
}
|
|
162
|
-
static requireCachedAjvSchema(schema) {
|
|
163
|
-
return schema[HIDDEN_AJV_SCHEMA];
|
|
164
|
-
}
|
|
165
|
-
getAJVValidateFunction = _lazyValue(() => this.cfg.ajv.compile(this.schema));
|
|
166
|
-
static requireValidJsonSchema(schema) {
|
|
167
|
-
// For object schemas we require that it is type checked against an external type, e.g.:
|
|
168
|
-
// interface Foo { name: string }
|
|
169
|
-
// const schema = j.object({ name: j.string() }).ofType<Foo>()
|
|
170
|
-
_assert(schema.type !== 'object' || schema.hasIsOfTypeCheck, 'The schema must be type checked against a type or interface, using the `.isOfType()` helper in `j`.');
|
|
171
|
-
}
|
|
172
|
-
applyImprovementsOnErrorMessages(errors) {
|
|
173
|
-
if (!errors)
|
|
174
|
-
return;
|
|
175
|
-
this.filterNullableAnyOfErrors(errors);
|
|
176
|
-
const { errorMessages } = this.schema;
|
|
177
|
-
for (const error of errors) {
|
|
178
|
-
const errorMessage = this.getErrorMessageForInstancePath(this.schema, error.instancePath, error.keyword);
|
|
179
|
-
if (errorMessage) {
|
|
180
|
-
error.message = errorMessage;
|
|
181
|
-
}
|
|
182
|
-
else if (errorMessages?.[error.keyword]) {
|
|
183
|
-
error.message = errorMessages[error.keyword];
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
const unwrapped = unwrapNullableAnyOf(this.schema);
|
|
187
|
-
if (unwrapped?.errorMessages?.[error.keyword]) {
|
|
188
|
-
error.message = unwrapped.errorMessages[error.keyword];
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
error.instancePath = error.instancePath.replaceAll(/\/(\d+)/g, `[$1]`).replaceAll('/', '.');
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
/**
|
|
195
|
-
* Filters out noisy errors produced by nullable anyOf patterns.
|
|
196
|
-
* When `nullable()` wraps a schema in `anyOf: [realSchema, { type: 'null' }]`,
|
|
197
|
-
* AJV produces "must be null" and "must match a schema in anyOf" errors
|
|
198
|
-
* that are confusing. This method splices them out, keeping only the real errors.
|
|
199
|
-
*/
|
|
200
|
-
filterNullableAnyOfErrors(errors) {
|
|
201
|
-
// Collect exact schemaPaths to remove (anyOf aggregates) and prefixes (null branches)
|
|
202
|
-
const exactPaths = [];
|
|
203
|
-
const nullBranchPrefixes = [];
|
|
204
|
-
for (const error of errors) {
|
|
205
|
-
if (error.keyword !== 'anyOf')
|
|
206
|
-
continue;
|
|
207
|
-
const parentSchema = this.resolveSchemaPath(error.schemaPath);
|
|
208
|
-
if (!parentSchema)
|
|
209
|
-
continue;
|
|
210
|
-
const nullIndex = unwrapNullableAnyOfIndex(parentSchema);
|
|
211
|
-
if (nullIndex === -1)
|
|
212
|
-
continue;
|
|
213
|
-
exactPaths.push(error.schemaPath); // e.g. "#/anyOf"
|
|
214
|
-
const anyOfBase = error.schemaPath.slice(0, -'anyOf'.length);
|
|
215
|
-
nullBranchPrefixes.push(`${anyOfBase}anyOf/${nullIndex}/`); // e.g. "#/anyOf/1/"
|
|
216
|
-
}
|
|
217
|
-
if (!exactPaths.length)
|
|
218
|
-
return;
|
|
219
|
-
for (let i = errors.length - 1; i >= 0; i--) {
|
|
220
|
-
const sp = errors[i].schemaPath;
|
|
221
|
-
if (exactPaths.includes(sp) || nullBranchPrefixes.some(p => sp.startsWith(p))) {
|
|
222
|
-
errors.splice(i, 1);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Navigates the schema tree using an AJV schemaPath (e.g. "#/properties/foo/anyOf")
|
|
228
|
-
* and returns the parent schema containing the last keyword.
|
|
229
|
-
*/
|
|
230
|
-
resolveSchemaPath(schemaPath) {
|
|
231
|
-
// schemaPath looks like "#/properties/foo/anyOf" or "#/anyOf"
|
|
232
|
-
// We want the schema that contains the final keyword (e.g. "anyOf")
|
|
233
|
-
const segments = schemaPath.replace(/^#\//, '').split('/');
|
|
234
|
-
// Remove the last segment (the keyword itself, e.g. "anyOf")
|
|
235
|
-
segments.pop();
|
|
236
|
-
let current = this.schema;
|
|
237
|
-
for (const segment of segments) {
|
|
238
|
-
if (!current || typeof current !== 'object')
|
|
239
|
-
return undefined;
|
|
240
|
-
current = current[segment];
|
|
241
|
-
}
|
|
242
|
-
return current;
|
|
243
|
-
}
|
|
244
|
-
getErrorMessageForInstancePath(schema, instancePath, keyword) {
|
|
245
|
-
if (!schema || !instancePath)
|
|
246
|
-
return undefined;
|
|
247
|
-
const segments = instancePath.split('/').filter(Boolean);
|
|
248
|
-
return this.traverseSchemaPath(schema, segments, keyword);
|
|
249
|
-
}
|
|
250
|
-
traverseSchemaPath(schema, segments, keyword) {
|
|
251
|
-
if (!segments.length)
|
|
252
|
-
return undefined;
|
|
253
|
-
const [currentSegment, ...remainingSegments] = segments;
|
|
254
|
-
const nextSchema = this.getChildSchema(schema, currentSegment);
|
|
255
|
-
if (!nextSchema)
|
|
256
|
-
return undefined;
|
|
257
|
-
if (nextSchema.errorMessages?.[keyword]) {
|
|
258
|
-
return nextSchema.errorMessages[keyword];
|
|
259
|
-
}
|
|
260
|
-
// Check through nullable wrapper
|
|
261
|
-
const unwrapped = unwrapNullableAnyOf(nextSchema);
|
|
262
|
-
if (unwrapped?.errorMessages?.[keyword]) {
|
|
263
|
-
return unwrapped.errorMessages[keyword];
|
|
264
|
-
}
|
|
265
|
-
if (remainingSegments.length) {
|
|
266
|
-
return this.traverseSchemaPath(nextSchema, remainingSegments, keyword);
|
|
267
|
-
}
|
|
268
|
-
return undefined;
|
|
269
|
-
}
|
|
270
|
-
getChildSchema(schema, segment) {
|
|
271
|
-
if (!segment)
|
|
272
|
-
return undefined;
|
|
273
|
-
// Unwrap nullable anyOf to find properties/items through nullable wrappers
|
|
274
|
-
const effectiveSchema = unwrapNullableAnyOf(schema) ?? schema;
|
|
275
|
-
if (/^\d+$/.test(segment) && effectiveSchema.items) {
|
|
276
|
-
return this.getArrayItemSchema(effectiveSchema, segment);
|
|
277
|
-
}
|
|
278
|
-
return this.getObjectPropertySchema(effectiveSchema, segment);
|
|
279
|
-
}
|
|
280
|
-
getArrayItemSchema(schema, indexSegment) {
|
|
281
|
-
if (!schema.items)
|
|
282
|
-
return undefined;
|
|
283
|
-
if (Array.isArray(schema.items)) {
|
|
284
|
-
return schema.items[Number(indexSegment)];
|
|
285
|
-
}
|
|
286
|
-
return schema.items;
|
|
287
|
-
}
|
|
288
|
-
getObjectPropertySchema(schema, segment) {
|
|
289
|
-
return schema.properties?.[segment];
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
function unwrapNullableAnyOf(schema) {
|
|
293
|
-
const nullIndex = unwrapNullableAnyOfIndex(schema);
|
|
294
|
-
if (nullIndex === -1)
|
|
295
|
-
return undefined;
|
|
296
|
-
return schema.anyOf[1 - nullIndex];
|
|
297
|
-
}
|
|
298
|
-
function unwrapNullableAnyOfIndex(schema) {
|
|
299
|
-
if (schema.anyOf?.length !== 2)
|
|
300
|
-
return -1;
|
|
301
|
-
const nullIndex = schema.anyOf.findIndex(s => s.type === 'null');
|
|
302
|
-
return nullIndex;
|
|
303
|
-
}
|
|
304
|
-
const separator = '\n';
|
|
305
|
-
export const HIDDEN_AJV_SCHEMA = Symbol('HIDDEN_AJV_SCHEMA');
|
|
306
|
-
// ===== JsonSchemaBuilders ===== //
|
|
15
|
+
// ==== j (factory object) ====
|
|
307
16
|
export const j = {
|
|
308
17
|
/**
|
|
309
18
|
* Matches literally any value - equivalent to TypeScript's `any` type.
|
|
310
19
|
* Use sparingly, as it bypasses type validation entirely.
|
|
311
20
|
*/
|
|
312
21
|
any() {
|
|
313
|
-
return new
|
|
22
|
+
return new JBuilder({});
|
|
314
23
|
},
|
|
315
24
|
string() {
|
|
316
|
-
return new
|
|
25
|
+
return new JString();
|
|
317
26
|
},
|
|
318
27
|
number() {
|
|
319
|
-
return new
|
|
28
|
+
return new JNumber();
|
|
320
29
|
},
|
|
321
30
|
boolean() {
|
|
322
|
-
return new
|
|
31
|
+
return new JBoolean();
|
|
323
32
|
},
|
|
324
33
|
object: Object.assign(object, {
|
|
325
34
|
dbEntity: objectDbEntity,
|
|
@@ -333,7 +42,7 @@ export const j = {
|
|
|
333
42
|
const finalValueSchema = isValueOptional
|
|
334
43
|
? { anyOf: [{ isUndefined: true }, builtSchema] }
|
|
335
44
|
: builtSchema;
|
|
336
|
-
return new
|
|
45
|
+
return new JObject({}, {
|
|
337
46
|
hasIsOfTypeCheck: false,
|
|
338
47
|
patternProperties: {
|
|
339
48
|
'^.+$': finalValueSchema,
|
|
@@ -376,16 +85,18 @@ export const j = {
|
|
|
376
85
|
withRegexKeys,
|
|
377
86
|
}),
|
|
378
87
|
array(itemSchema) {
|
|
379
|
-
return new
|
|
88
|
+
return new JArray(itemSchema);
|
|
380
89
|
},
|
|
381
90
|
tuple(items) {
|
|
382
|
-
return new
|
|
91
|
+
return new JTuple(items);
|
|
383
92
|
},
|
|
384
93
|
set(itemSchema) {
|
|
385
|
-
return new
|
|
94
|
+
return new JSet2Builder(itemSchema);
|
|
386
95
|
},
|
|
387
96
|
buffer() {
|
|
388
|
-
return new
|
|
97
|
+
return new JBuilder({
|
|
98
|
+
Buffer: true,
|
|
99
|
+
});
|
|
389
100
|
},
|
|
390
101
|
enum(input, opt) {
|
|
391
102
|
let enumValues;
|
|
@@ -411,7 +122,7 @@ export const j = {
|
|
|
411
122
|
}
|
|
412
123
|
}
|
|
413
124
|
_assert(enumValues, 'Unsupported enum input');
|
|
414
|
-
return new
|
|
125
|
+
return new JEnum(enumValues, baseType, opt);
|
|
415
126
|
},
|
|
416
127
|
/**
|
|
417
128
|
* Use only with primitive values, otherwise this function will throw to avoid bugs.
|
|
@@ -427,7 +138,7 @@ export const j = {
|
|
|
427
138
|
oneOf(items) {
|
|
428
139
|
const schemas = items.map(b => b.build());
|
|
429
140
|
_assert(schemas.every(hasNoObjectSchemas), 'Do not use `oneOf` validation with non-primitive types!');
|
|
430
|
-
return new
|
|
141
|
+
return new JBuilder({
|
|
431
142
|
oneOf: schemas,
|
|
432
143
|
});
|
|
433
144
|
},
|
|
@@ -445,7 +156,7 @@ export const j = {
|
|
|
445
156
|
anyOf(items) {
|
|
446
157
|
const schemas = items.map(b => b.build());
|
|
447
158
|
_assert(schemas.every(hasNoObjectSchemas), 'Do not use `anyOf` validation with non-primitive types!');
|
|
448
|
-
return new
|
|
159
|
+
return new JBuilder({
|
|
449
160
|
anyOf: schemas,
|
|
450
161
|
});
|
|
451
162
|
},
|
|
@@ -462,7 +173,18 @@ export const j = {
|
|
|
462
173
|
* ```
|
|
463
174
|
*/
|
|
464
175
|
anyOfBy(propertyName, schemaDictionary) {
|
|
465
|
-
|
|
176
|
+
const builtSchemaDictionary = {};
|
|
177
|
+
for (const [key, schema] of Object.entries(schemaDictionary)) {
|
|
178
|
+
builtSchemaDictionary[key] = schema.build();
|
|
179
|
+
}
|
|
180
|
+
return new JBuilder({
|
|
181
|
+
type: 'object',
|
|
182
|
+
hasIsOfTypeCheck: true,
|
|
183
|
+
anyOfBy: {
|
|
184
|
+
propertyName,
|
|
185
|
+
schemaDictionary: builtSchemaDictionary,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
466
188
|
},
|
|
467
189
|
/**
|
|
468
190
|
* Custom version of `anyOf` which - in contrast to the original function - does not mutate the input.
|
|
@@ -473,7 +195,7 @@ export const j = {
|
|
|
473
195
|
* ```
|
|
474
196
|
*/
|
|
475
197
|
anyOfThese(items) {
|
|
476
|
-
return new
|
|
198
|
+
return new JBuilder({
|
|
477
199
|
anyOfThese: items.map(b => b.build()),
|
|
478
200
|
});
|
|
479
201
|
},
|
|
@@ -490,13 +212,21 @@ export const j = {
|
|
|
490
212
|
baseType = 'string';
|
|
491
213
|
if (typeof v === 'number')
|
|
492
214
|
baseType = 'number';
|
|
493
|
-
return new
|
|
215
|
+
return new JEnum([v], baseType);
|
|
216
|
+
},
|
|
217
|
+
/**
|
|
218
|
+
* Create a JSchema from a plain JsonSchema object.
|
|
219
|
+
* Useful when the schema is loaded from a JSON file or generated externally.
|
|
220
|
+
*
|
|
221
|
+
* Optionally accepts a custom Ajv instance and/or inputName for error messages.
|
|
222
|
+
*/
|
|
223
|
+
fromSchema(schema, cfg) {
|
|
224
|
+
return new JSchema(schema, cfg);
|
|
494
225
|
},
|
|
495
226
|
};
|
|
496
|
-
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
const TS_2000_MILLIS = TS_2000 * 1000;
|
|
227
|
+
// ==== Symbol for caching compiled AjvSchema ====
|
|
228
|
+
export const HIDDEN_AJV_SCHEMA = Symbol('HIDDEN_AJV_SCHEMA');
|
|
229
|
+
// ==== JSchema (locked base) ====
|
|
500
230
|
/*
|
|
501
231
|
Notes for future reference
|
|
502
232
|
|
|
@@ -505,17 +235,41 @@ const TS_2000_MILLIS = TS_2000 * 1000;
|
|
|
505
235
|
which means that the `foo` property would be mandatory, it's just that its value can be `undefined` as well.
|
|
506
236
|
With `Opt`, we can infer it as `{ foo?: string | undefined }`.
|
|
507
237
|
*/
|
|
508
|
-
export class
|
|
238
|
+
export class JSchema {
|
|
509
239
|
[HIDDEN_AJV_SCHEMA];
|
|
510
240
|
schema;
|
|
511
|
-
|
|
241
|
+
_cfg;
|
|
242
|
+
constructor(schema, cfg) {
|
|
512
243
|
this.schema = schema;
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
244
|
+
this._cfg = cfg;
|
|
245
|
+
}
|
|
246
|
+
_builtSchema;
|
|
247
|
+
_compiledFns;
|
|
248
|
+
_getBuiltSchema() {
|
|
249
|
+
if (!this._builtSchema) {
|
|
250
|
+
const builtSchema = this.build();
|
|
251
|
+
if (this instanceof JBuilder) {
|
|
252
|
+
_assert(builtSchema.type !== 'object' || builtSchema.hasIsOfTypeCheck, 'The schema must be type checked against a type or interface, using the `.isOfType()` helper in `j`.');
|
|
253
|
+
}
|
|
254
|
+
delete builtSchema.optionalField;
|
|
255
|
+
this._builtSchema = builtSchema;
|
|
256
|
+
}
|
|
257
|
+
return this._builtSchema;
|
|
258
|
+
}
|
|
259
|
+
_getCompiled(overrideAjv) {
|
|
260
|
+
const builtSchema = this._getBuiltSchema();
|
|
261
|
+
const ajv = overrideAjv ?? this._cfg?.ajv ?? getAjv();
|
|
262
|
+
this._compiledFns ??= new WeakMap();
|
|
263
|
+
let fn = this._compiledFns.get(ajv);
|
|
264
|
+
if (!fn) {
|
|
265
|
+
fn = ajv.compile(builtSchema);
|
|
266
|
+
this._compiledFns.set(ajv, fn);
|
|
267
|
+
// Cache AjvSchema wrapper for HIDDEN_AJV_SCHEMA backward compat (default ajv only)
|
|
268
|
+
if (!overrideAjv) {
|
|
269
|
+
this[HIDDEN_AJV_SCHEMA] = AjvSchema._wrap(builtSchema, fn);
|
|
270
|
+
}
|
|
517
271
|
}
|
|
518
|
-
return
|
|
272
|
+
return { fn, builtSchema };
|
|
519
273
|
}
|
|
520
274
|
getSchema() {
|
|
521
275
|
return this.schema;
|
|
@@ -533,6 +287,7 @@ export class JsonSchemaTerminal {
|
|
|
533
287
|
clone() {
|
|
534
288
|
const cloned = Object.create(Object.getPrototypeOf(this));
|
|
535
289
|
cloned.schema = deepCopyPreservingFunctions(this.schema);
|
|
290
|
+
cloned._cfg = this._cfg;
|
|
536
291
|
return cloned;
|
|
537
292
|
}
|
|
538
293
|
cloneAndUpdateSchema(schema) {
|
|
@@ -541,16 +296,29 @@ export class JsonSchemaTerminal {
|
|
|
541
296
|
return clone;
|
|
542
297
|
}
|
|
543
298
|
validate(input, opt) {
|
|
544
|
-
|
|
299
|
+
const [err, output] = this.getValidationResult(input, opt);
|
|
300
|
+
if (err)
|
|
301
|
+
throw err;
|
|
302
|
+
return output;
|
|
545
303
|
}
|
|
546
304
|
isValid(input, opt) {
|
|
547
|
-
|
|
305
|
+
const [err] = this.getValidationResult(input, opt);
|
|
306
|
+
return !err;
|
|
548
307
|
}
|
|
549
308
|
getValidationResult(input, opt = {}) {
|
|
550
|
-
|
|
309
|
+
const { fn, builtSchema } = this._getCompiled(opt.ajv);
|
|
310
|
+
const inputName = this._cfg?.inputName || (builtSchema.$id ? _substringBefore(builtSchema.$id, '.') : undefined);
|
|
311
|
+
return executeValidation(fn, builtSchema, input, opt, inputName);
|
|
551
312
|
}
|
|
552
|
-
getValidationFunction() {
|
|
553
|
-
return
|
|
313
|
+
getValidationFunction(opt = {}) {
|
|
314
|
+
return (input, opt2) => {
|
|
315
|
+
return this.getValidationResult(input, {
|
|
316
|
+
ajv: opt.ajv,
|
|
317
|
+
mutateInput: opt2?.mutateInput ?? opt.mutateInput,
|
|
318
|
+
inputName: opt2?.inputName ?? opt.inputName,
|
|
319
|
+
inputId: opt2?.inputId ?? opt.inputId,
|
|
320
|
+
});
|
|
321
|
+
};
|
|
554
322
|
}
|
|
555
323
|
/**
|
|
556
324
|
* Specify a function to be called after the normal validation is finished.
|
|
@@ -573,7 +341,8 @@ export class JsonSchemaTerminal {
|
|
|
573
341
|
out;
|
|
574
342
|
opt;
|
|
575
343
|
}
|
|
576
|
-
|
|
344
|
+
// ==== JBuilder (chainable base) ====
|
|
345
|
+
export class JBuilder extends JSchema {
|
|
577
346
|
setErrorMessage(ruleName, errorMessage) {
|
|
578
347
|
if (_isUndefined(errorMessage))
|
|
579
348
|
return;
|
|
@@ -585,15 +354,6 @@ export class JsonSchemaAnyBuilder extends JsonSchemaTerminal {
|
|
|
585
354
|
*
|
|
586
355
|
* When the type inferred from the schema differs from the passed-in type,
|
|
587
356
|
* the schema becomes unusable, by turning its type into `never`.
|
|
588
|
-
*
|
|
589
|
-
* ```ts
|
|
590
|
-
* const schemaGood = j.string().isOfType<string>() // ✅
|
|
591
|
-
*
|
|
592
|
-
* const schemaBad = j.string().isOfType<number>() // ❌
|
|
593
|
-
* schemaBad.build() // TypeError: property "build" does not exist on type "never"
|
|
594
|
-
*
|
|
595
|
-
* const result = ajvValidateRequest.body(req, schemaBad) // result will have `unknown` type
|
|
596
|
-
* ```
|
|
597
357
|
*/
|
|
598
358
|
isOfType() {
|
|
599
359
|
return this.cloneAndUpdateSchema({ hasIsOfTypeCheck: true });
|
|
@@ -630,7 +390,7 @@ export class JsonSchemaAnyBuilder extends JsonSchemaTerminal {
|
|
|
630
390
|
return clone;
|
|
631
391
|
}
|
|
632
392
|
nullable() {
|
|
633
|
-
return new
|
|
393
|
+
return new JBuilder({
|
|
634
394
|
anyOf: [this.build(), { type: 'null' }],
|
|
635
395
|
});
|
|
636
396
|
}
|
|
@@ -645,7 +405,7 @@ export class JsonSchemaAnyBuilder extends JsonSchemaTerminal {
|
|
|
645
405
|
* Locks the given schema chain and no other modification can be done to it.
|
|
646
406
|
*/
|
|
647
407
|
final() {
|
|
648
|
-
return new
|
|
408
|
+
return new JSchema(this.schema);
|
|
649
409
|
}
|
|
650
410
|
/**
|
|
651
411
|
*
|
|
@@ -676,7 +436,13 @@ export class JsonSchemaAnyBuilder extends JsonSchemaTerminal {
|
|
|
676
436
|
});
|
|
677
437
|
}
|
|
678
438
|
}
|
|
679
|
-
|
|
439
|
+
// ==== Consts
|
|
440
|
+
const TS_2500 = 16725225600; // 2500-01-01
|
|
441
|
+
const TS_2500_MILLIS = TS_2500 * 1000;
|
|
442
|
+
const TS_2000 = 946684800; // 2000-01-01
|
|
443
|
+
const TS_2000_MILLIS = TS_2000 * 1000;
|
|
444
|
+
// ==== Type-specific builders ====
|
|
445
|
+
export class JString extends JBuilder {
|
|
680
446
|
constructor() {
|
|
681
447
|
super({
|
|
682
448
|
type: 'string',
|
|
@@ -690,7 +456,7 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
|
|
|
690
456
|
*
|
|
691
457
|
* Make sure this `optional()` call is at the end of your call chain.
|
|
692
458
|
*
|
|
693
|
-
* When `null` is included in optionalValues, the return type becomes `
|
|
459
|
+
* When `null` is included in optionalValues, the return type becomes `JSchema`
|
|
694
460
|
* (no further chaining allowed) because the schema is wrapped in an anyOf structure.
|
|
695
461
|
*/
|
|
696
462
|
optional(optionalValues) {
|
|
@@ -698,7 +464,7 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
|
|
|
698
464
|
return super.optional();
|
|
699
465
|
}
|
|
700
466
|
_typeCast(optionalValues);
|
|
701
|
-
let newBuilder = new
|
|
467
|
+
let newBuilder = new JString().optional();
|
|
702
468
|
const alternativesSchema = j.enum(optionalValues);
|
|
703
469
|
Object.assign(newBuilder.getSchema(), {
|
|
704
470
|
anyOf: [this.build(), alternativesSchema.build()],
|
|
@@ -709,7 +475,7 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
|
|
|
709
475
|
// but the typing should not reflect that.
|
|
710
476
|
// We also cannot accept more rules attached, since we're not building a StringSchema anymore.
|
|
711
477
|
if (optionalValues.includes(null)) {
|
|
712
|
-
newBuilder = new
|
|
478
|
+
newBuilder = new JSchema({
|
|
713
479
|
anyOf: [{ type: 'null', optionalValues }, newBuilder.build()],
|
|
714
480
|
optionalField: true,
|
|
715
481
|
});
|
|
@@ -772,13 +538,16 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
|
|
|
772
538
|
* because this call effectively starts a new schema chain.
|
|
773
539
|
*/
|
|
774
540
|
isoDate() {
|
|
775
|
-
return new
|
|
541
|
+
return new JIsoDate();
|
|
776
542
|
}
|
|
777
543
|
isoDateTime() {
|
|
778
544
|
return this.cloneAndUpdateSchema({ IsoDateTime: true }).branded();
|
|
779
545
|
}
|
|
780
546
|
isoMonth() {
|
|
781
|
-
return new
|
|
547
|
+
return new JBuilder({
|
|
548
|
+
type: 'string',
|
|
549
|
+
IsoMonth: {},
|
|
550
|
+
});
|
|
782
551
|
}
|
|
783
552
|
/**
|
|
784
553
|
* Validates the string format to be JWT.
|
|
@@ -830,7 +599,7 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
|
|
|
830
599
|
return this.regex(UUID_REGEX, { msg: 'is an invalid UUID' });
|
|
831
600
|
}
|
|
832
601
|
}
|
|
833
|
-
export class
|
|
602
|
+
export class JIsoDate extends JBuilder {
|
|
834
603
|
constructor() {
|
|
835
604
|
super({
|
|
836
605
|
type: 'string',
|
|
@@ -843,7 +612,7 @@ export class JsonSchemaIsoDateBuilder extends JsonSchemaAnyBuilder {
|
|
|
843
612
|
* This `null` feature only works when the current schema is nested in an object or array schema,
|
|
844
613
|
* due to how mutability works in Ajv.
|
|
845
614
|
*
|
|
846
|
-
* When `null` is passed, the return type becomes `
|
|
615
|
+
* When `null` is passed, the return type becomes `JSchema`
|
|
847
616
|
* (no further chaining allowed) because the schema is wrapped in an anyOf structure.
|
|
848
617
|
*/
|
|
849
618
|
optional(nullValue) {
|
|
@@ -854,7 +623,7 @@ export class JsonSchemaIsoDateBuilder extends JsonSchemaAnyBuilder {
|
|
|
854
623
|
// so we must allow `null` values to be parsed by Ajv,
|
|
855
624
|
// but the typing should not reflect that.
|
|
856
625
|
// We also cannot accept more rules attached, since we're not building an ObjectSchema anymore.
|
|
857
|
-
return new
|
|
626
|
+
return new JSchema({
|
|
858
627
|
anyOf: [{ type: 'null', optionalValues: [null] }, this.build()],
|
|
859
628
|
optionalField: true,
|
|
860
629
|
});
|
|
@@ -882,15 +651,7 @@ export class JsonSchemaIsoDateBuilder extends JsonSchemaAnyBuilder {
|
|
|
882
651
|
return this.cloneAndUpdateSchema(schemaPatch);
|
|
883
652
|
}
|
|
884
653
|
}
|
|
885
|
-
export class
|
|
886
|
-
constructor() {
|
|
887
|
-
super({
|
|
888
|
-
type: 'string',
|
|
889
|
-
IsoMonth: {},
|
|
890
|
-
});
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
export class JsonSchemaNumberBuilder extends JsonSchemaAnyBuilder {
|
|
654
|
+
export class JNumber extends JBuilder {
|
|
894
655
|
constructor() {
|
|
895
656
|
super({
|
|
896
657
|
type: 'number',
|
|
@@ -904,7 +665,7 @@ export class JsonSchemaNumberBuilder extends JsonSchemaAnyBuilder {
|
|
|
904
665
|
*
|
|
905
666
|
* Make sure this `optional()` call is at the end of your call chain.
|
|
906
667
|
*
|
|
907
|
-
* When `null` is included in optionalValues, the return type becomes `
|
|
668
|
+
* When `null` is included in optionalValues, the return type becomes `JSchema`
|
|
908
669
|
* (no further chaining allowed) because the schema is wrapped in an anyOf structure.
|
|
909
670
|
*/
|
|
910
671
|
optional(optionalValues) {
|
|
@@ -912,7 +673,7 @@ export class JsonSchemaNumberBuilder extends JsonSchemaAnyBuilder {
|
|
|
912
673
|
return super.optional();
|
|
913
674
|
}
|
|
914
675
|
_typeCast(optionalValues);
|
|
915
|
-
let newBuilder = new
|
|
676
|
+
let newBuilder = new JNumber().optional();
|
|
916
677
|
const alternativesSchema = j.enum(optionalValues);
|
|
917
678
|
Object.assign(newBuilder.getSchema(), {
|
|
918
679
|
anyOf: [this.build(), alternativesSchema.build()],
|
|
@@ -923,7 +684,7 @@ export class JsonSchemaNumberBuilder extends JsonSchemaAnyBuilder {
|
|
|
923
684
|
// but the typing should not reflect that.
|
|
924
685
|
// We also cannot accept more rules attached, since we're not building a NumberSchema anymore.
|
|
925
686
|
if (optionalValues.includes(null)) {
|
|
926
|
-
newBuilder = new
|
|
687
|
+
newBuilder = new JSchema({
|
|
927
688
|
anyOf: [{ type: 'null', optionalValues }, newBuilder.build()],
|
|
928
689
|
optionalField: true,
|
|
929
690
|
});
|
|
@@ -1024,7 +785,7 @@ export class JsonSchemaNumberBuilder extends JsonSchemaAnyBuilder {
|
|
|
1024
785
|
return this.cloneAndUpdateSchema({ precision: numberOfDigits });
|
|
1025
786
|
}
|
|
1026
787
|
}
|
|
1027
|
-
export class
|
|
788
|
+
export class JBoolean extends JBuilder {
|
|
1028
789
|
constructor() {
|
|
1029
790
|
super({
|
|
1030
791
|
type: 'boolean',
|
|
@@ -1040,7 +801,7 @@ export class JsonSchemaBooleanBuilder extends JsonSchemaAnyBuilder {
|
|
|
1040
801
|
if (typeof optionalValue === 'undefined') {
|
|
1041
802
|
return super.optional();
|
|
1042
803
|
}
|
|
1043
|
-
const newBuilder = new
|
|
804
|
+
const newBuilder = new JBoolean().optional();
|
|
1044
805
|
const alternativesSchema = j.enum([optionalValue]);
|
|
1045
806
|
Object.assign(newBuilder.getSchema(), {
|
|
1046
807
|
anyOf: [this.build(), alternativesSchema.build()],
|
|
@@ -1049,7 +810,7 @@ export class JsonSchemaBooleanBuilder extends JsonSchemaAnyBuilder {
|
|
|
1049
810
|
return newBuilder;
|
|
1050
811
|
}
|
|
1051
812
|
}
|
|
1052
|
-
export class
|
|
813
|
+
export class JObject extends JBuilder {
|
|
1053
814
|
constructor(props, opt) {
|
|
1054
815
|
super({
|
|
1055
816
|
type: 'object',
|
|
@@ -1061,22 +822,7 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
|
|
|
1061
822
|
keySchema: opt?.keySchema ?? undefined,
|
|
1062
823
|
});
|
|
1063
824
|
if (props)
|
|
1064
|
-
this.
|
|
1065
|
-
}
|
|
1066
|
-
addProperties(props) {
|
|
1067
|
-
const properties = {};
|
|
1068
|
-
const required = [];
|
|
1069
|
-
for (const [key, builder] of Object.entries(props)) {
|
|
1070
|
-
const isOptional = builder.getSchema().optionalField;
|
|
1071
|
-
if (!isOptional) {
|
|
1072
|
-
required.push(key);
|
|
1073
|
-
}
|
|
1074
|
-
const schema = builder.build();
|
|
1075
|
-
properties[key] = schema;
|
|
1076
|
-
}
|
|
1077
|
-
this.schema.properties = properties;
|
|
1078
|
-
this.schema.required = _uniq(required).sort();
|
|
1079
|
-
return this;
|
|
825
|
+
addPropertiesToSchema(this.schema, props);
|
|
1080
826
|
}
|
|
1081
827
|
/**
|
|
1082
828
|
* @param nullValue Pass `null` to have `null` values be considered/converted as `undefined`.
|
|
@@ -1084,7 +830,7 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
|
|
|
1084
830
|
* This `null` feature only works when the current schema is nested in an object or array schema,
|
|
1085
831
|
* due to how mutability works in Ajv.
|
|
1086
832
|
*
|
|
1087
|
-
* When `null` is passed, the return type becomes `
|
|
833
|
+
* When `null` is passed, the return type becomes `JSchema`
|
|
1088
834
|
* (no further chaining allowed) because the schema is wrapped in an anyOf structure.
|
|
1089
835
|
*/
|
|
1090
836
|
optional(nullValue) {
|
|
@@ -1095,7 +841,7 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
|
|
|
1095
841
|
// so we must allow `null` values to be parsed by Ajv,
|
|
1096
842
|
// but the typing should not reflect that.
|
|
1097
843
|
// We also cannot accept more rules attached, since we're not building an ObjectSchema anymore.
|
|
1098
|
-
return new
|
|
844
|
+
return new JSchema({
|
|
1099
845
|
anyOf: [{ type: 'null', optionalValues: [null] }, this.build()],
|
|
1100
846
|
optionalField: true,
|
|
1101
847
|
});
|
|
@@ -1107,9 +853,9 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
|
|
|
1107
853
|
return this.cloneAndUpdateSchema({ additionalProperties: true });
|
|
1108
854
|
}
|
|
1109
855
|
extend(props) {
|
|
1110
|
-
const newBuilder = new
|
|
856
|
+
const newBuilder = new JObject();
|
|
1111
857
|
_objectAssign(newBuilder.schema, deepCopyPreservingFunctions(this.schema));
|
|
1112
|
-
const incomingSchemaBuilder = new
|
|
858
|
+
const incomingSchemaBuilder = new JObject(props);
|
|
1113
859
|
mergeJsonSchemaObjects(newBuilder.schema, incomingSchemaBuilder.schema);
|
|
1114
860
|
_objectAssign(newBuilder.schema, { hasIsOfTypeCheck: false });
|
|
1115
861
|
return newBuilder;
|
|
@@ -1120,17 +866,6 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
|
|
|
1120
866
|
* It expects you to use `isOfType<T>()` in the chain,
|
|
1121
867
|
* otherwise the validation will throw. This is to ensure
|
|
1122
868
|
* that the schemas you concatenated match the intended final type.
|
|
1123
|
-
*
|
|
1124
|
-
* ```ts
|
|
1125
|
-
* interface Foo { foo: string }
|
|
1126
|
-
* const fooSchema = j.object<Foo>({ foo: j.string() })
|
|
1127
|
-
*
|
|
1128
|
-
* interface Bar { bar: number }
|
|
1129
|
-
* const barSchema = j.object<Bar>({ bar: j.number() })
|
|
1130
|
-
*
|
|
1131
|
-
* interface Shu { foo: string, bar: number }
|
|
1132
|
-
* const shuSchema = fooSchema.concat(barSchema).isOfType<Shu>() // important
|
|
1133
|
-
* ```
|
|
1134
869
|
*/
|
|
1135
870
|
concat(other) {
|
|
1136
871
|
const clone = this.clone();
|
|
@@ -1160,7 +895,7 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
|
|
|
1160
895
|
return this.cloneAndUpdateSchema({ exclusiveProperties: [...exclusiveProperties, propNames] });
|
|
1161
896
|
}
|
|
1162
897
|
}
|
|
1163
|
-
export class
|
|
898
|
+
export class JObjectInfer extends JBuilder {
|
|
1164
899
|
constructor(props) {
|
|
1165
900
|
super({
|
|
1166
901
|
type: 'object',
|
|
@@ -1169,22 +904,7 @@ export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
|
|
|
1169
904
|
additionalProperties: false,
|
|
1170
905
|
});
|
|
1171
906
|
if (props)
|
|
1172
|
-
this.
|
|
1173
|
-
}
|
|
1174
|
-
addProperties(props) {
|
|
1175
|
-
const properties = {};
|
|
1176
|
-
const required = [];
|
|
1177
|
-
for (const [key, builder] of Object.entries(props)) {
|
|
1178
|
-
const isOptional = builder.getSchema().optionalField;
|
|
1179
|
-
if (!isOptional) {
|
|
1180
|
-
required.push(key);
|
|
1181
|
-
}
|
|
1182
|
-
const schema = builder.build();
|
|
1183
|
-
properties[key] = schema;
|
|
1184
|
-
}
|
|
1185
|
-
this.schema.properties = properties;
|
|
1186
|
-
this.schema.required = _uniq(required).sort();
|
|
1187
|
-
return this;
|
|
907
|
+
addPropertiesToSchema(this.schema, props);
|
|
1188
908
|
}
|
|
1189
909
|
/**
|
|
1190
910
|
* @param nullValue Pass `null` to have `null` values be considered/converted as `undefined`.
|
|
@@ -1192,7 +912,7 @@ export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
|
|
|
1192
912
|
* This `null` feature only works when the current schema is nested in an object or array schema,
|
|
1193
913
|
* due to how mutability works in Ajv.
|
|
1194
914
|
*
|
|
1195
|
-
* When `null` is passed, the return type becomes `
|
|
915
|
+
* When `null` is passed, the return type becomes `JSchema`
|
|
1196
916
|
* (no further chaining allowed) because the schema is wrapped in an anyOf structure.
|
|
1197
917
|
*/
|
|
1198
918
|
// @ts-expect-error override adds optional parameter which is compatible but TS can't verify complex mapped types
|
|
@@ -1204,7 +924,7 @@ export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
|
|
|
1204
924
|
// so we must allow `null` values to be parsed by Ajv,
|
|
1205
925
|
// but the typing should not reflect that.
|
|
1206
926
|
// We also cannot accept more rules attached, since we're not building an ObjectSchema anymore.
|
|
1207
|
-
return new
|
|
927
|
+
return new JSchema({
|
|
1208
928
|
anyOf: [{ type: 'null', optionalValues: [null] }, this.build()],
|
|
1209
929
|
optionalField: true,
|
|
1210
930
|
});
|
|
@@ -1216,9 +936,9 @@ export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
|
|
|
1216
936
|
return this.cloneAndUpdateSchema({ additionalProperties: true });
|
|
1217
937
|
}
|
|
1218
938
|
extend(props) {
|
|
1219
|
-
const newBuilder = new
|
|
939
|
+
const newBuilder = new JObjectInfer();
|
|
1220
940
|
_objectAssign(newBuilder.schema, deepCopyPreservingFunctions(this.schema));
|
|
1221
|
-
const incomingSchemaBuilder = new
|
|
941
|
+
const incomingSchemaBuilder = new JObjectInfer(props);
|
|
1222
942
|
mergeJsonSchemaObjects(newBuilder.schema, incomingSchemaBuilder.schema);
|
|
1223
943
|
// This extend function is not type-safe as it is inferring,
|
|
1224
944
|
// so even if the base schema was already type-checked,
|
|
@@ -1238,7 +958,7 @@ export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
|
|
|
1238
958
|
});
|
|
1239
959
|
}
|
|
1240
960
|
}
|
|
1241
|
-
export class
|
|
961
|
+
export class JArray extends JBuilder {
|
|
1242
962
|
constructor(itemsSchema) {
|
|
1243
963
|
super({
|
|
1244
964
|
type: 'array',
|
|
@@ -1262,7 +982,7 @@ export class JsonSchemaArrayBuilder extends JsonSchemaAnyBuilder {
|
|
|
1262
982
|
return this.cloneAndUpdateSchema({ uniqueItems: true });
|
|
1263
983
|
}
|
|
1264
984
|
}
|
|
1265
|
-
|
|
985
|
+
class JSet2Builder extends JBuilder {
|
|
1266
986
|
constructor(itemsSchema) {
|
|
1267
987
|
super({
|
|
1268
988
|
type: ['array', 'object'],
|
|
@@ -1276,14 +996,7 @@ export class JsonSchemaSet2Builder extends JsonSchemaAnyBuilder {
|
|
|
1276
996
|
return this.cloneAndUpdateSchema({ maxItems });
|
|
1277
997
|
}
|
|
1278
998
|
}
|
|
1279
|
-
export class
|
|
1280
|
-
constructor() {
|
|
1281
|
-
super({
|
|
1282
|
-
Buffer: true,
|
|
1283
|
-
});
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
export class JsonSchemaEnumBuilder extends JsonSchemaAnyBuilder {
|
|
999
|
+
export class JEnum extends JBuilder {
|
|
1287
1000
|
constructor(enumValues, baseType, opt) {
|
|
1288
1001
|
const jsonSchema = { enum: enumValues };
|
|
1289
1002
|
// Specifying the base type helps in cases when we ask Ajv to coerce the types.
|
|
@@ -1302,7 +1015,7 @@ export class JsonSchemaEnumBuilder extends JsonSchemaAnyBuilder {
|
|
|
1302
1015
|
return this;
|
|
1303
1016
|
}
|
|
1304
1017
|
}
|
|
1305
|
-
export class
|
|
1018
|
+
export class JTuple extends JBuilder {
|
|
1306
1019
|
constructor(items) {
|
|
1307
1020
|
super({
|
|
1308
1021
|
type: 'array',
|
|
@@ -1312,43 +1025,11 @@ export class JsonSchemaTupleBuilder extends JsonSchemaAnyBuilder {
|
|
|
1312
1025
|
});
|
|
1313
1026
|
}
|
|
1314
1027
|
}
|
|
1315
|
-
export class JsonSchemaAnyOfByBuilder extends JsonSchemaAnyBuilder {
|
|
1316
|
-
constructor(propertyName, schemaDictionary) {
|
|
1317
|
-
const builtSchemaDictionary = {};
|
|
1318
|
-
for (const [key, schema] of Object.entries(schemaDictionary)) {
|
|
1319
|
-
builtSchemaDictionary[key] = schema.build();
|
|
1320
|
-
}
|
|
1321
|
-
super({
|
|
1322
|
-
type: 'object',
|
|
1323
|
-
hasIsOfTypeCheck: true,
|
|
1324
|
-
anyOfBy: {
|
|
1325
|
-
propertyName,
|
|
1326
|
-
schemaDictionary: builtSchemaDictionary,
|
|
1327
|
-
},
|
|
1328
|
-
});
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
|
-
export class JsonSchemaAnyOfTheseBuilder extends JsonSchemaAnyBuilder {
|
|
1332
|
-
constructor(propertyName, schemaDictionary) {
|
|
1333
|
-
const builtSchemaDictionary = {};
|
|
1334
|
-
for (const [key, schema] of Object.entries(schemaDictionary)) {
|
|
1335
|
-
builtSchemaDictionary[key] = schema.build();
|
|
1336
|
-
}
|
|
1337
|
-
super({
|
|
1338
|
-
type: 'object',
|
|
1339
|
-
hasIsOfTypeCheck: true,
|
|
1340
|
-
anyOfBy: {
|
|
1341
|
-
propertyName,
|
|
1342
|
-
schemaDictionary: builtSchemaDictionary,
|
|
1343
|
-
},
|
|
1344
|
-
});
|
|
1345
|
-
}
|
|
1346
|
-
}
|
|
1347
1028
|
function object(props) {
|
|
1348
|
-
return new
|
|
1029
|
+
return new JObject(props);
|
|
1349
1030
|
}
|
|
1350
1031
|
function objectInfer(props) {
|
|
1351
|
-
return new
|
|
1032
|
+
return new JObjectInfer(props);
|
|
1352
1033
|
}
|
|
1353
1034
|
function objectDbEntity(props) {
|
|
1354
1035
|
return j.object({
|
|
@@ -1367,7 +1048,7 @@ function record(keySchema, valueSchema) {
|
|
|
1367
1048
|
const finalValueSchema = isValueOptional
|
|
1368
1049
|
? { anyOf: [{ isUndefined: true }, valueJsonSchema] }
|
|
1369
1050
|
: valueJsonSchema;
|
|
1370
|
-
return new
|
|
1051
|
+
return new JObject([], {
|
|
1371
1052
|
hasIsOfTypeCheck: false,
|
|
1372
1053
|
keySchema: keyJsonSchema,
|
|
1373
1054
|
patternProperties: {
|
|
@@ -1381,7 +1062,7 @@ function withRegexKeys(keyRegex, schema) {
|
|
|
1381
1062
|
}
|
|
1382
1063
|
const pattern = keyRegex instanceof RegExp ? keyRegex.source : keyRegex;
|
|
1383
1064
|
const jsonSchema = schema.build();
|
|
1384
|
-
return new
|
|
1065
|
+
return new JObject([], {
|
|
1385
1066
|
hasIsOfTypeCheck: false,
|
|
1386
1067
|
patternProperties: {
|
|
1387
1068
|
[pattern]: jsonSchema,
|
|
@@ -1410,11 +1091,324 @@ function withEnumKeys(keys, schema) {
|
|
|
1410
1091
|
_assert(enumValues, 'The key list should be an array of values, NumberEnum or a StringEnum');
|
|
1411
1092
|
const typedValues = enumValues;
|
|
1412
1093
|
const props = Object.fromEntries(typedValues.map(key => [key, schema]));
|
|
1413
|
-
return new
|
|
1094
|
+
return new JObject(props, { hasIsOfTypeCheck: false });
|
|
1095
|
+
}
|
|
1096
|
+
// ==== AjvSchema compat wrapper ====
|
|
1097
|
+
/**
|
|
1098
|
+
* On creation - compiles ajv validation function.
|
|
1099
|
+
* Provides convenient methods, error reporting, etc.
|
|
1100
|
+
*/
|
|
1101
|
+
export class AjvSchema {
|
|
1102
|
+
schema;
|
|
1103
|
+
constructor(schema, cfg = {}, preCompiledFn) {
|
|
1104
|
+
this.schema = schema;
|
|
1105
|
+
this.cfg = {
|
|
1106
|
+
lazy: false,
|
|
1107
|
+
...cfg,
|
|
1108
|
+
ajv: cfg.ajv || getAjv(),
|
|
1109
|
+
// Auto-detecting "InputName" from $id of the schema (e.g "Address.schema.json")
|
|
1110
|
+
inputName: cfg.inputName || (schema.$id ? _substringBefore(schema.$id, '.') : undefined),
|
|
1111
|
+
};
|
|
1112
|
+
if (preCompiledFn) {
|
|
1113
|
+
this._compiledFn = preCompiledFn;
|
|
1114
|
+
}
|
|
1115
|
+
else if (!cfg.lazy) {
|
|
1116
|
+
this._getValidateFn(); // compile eagerly
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Shortcut for AjvSchema.create(schema, { lazy: true })
|
|
1121
|
+
*/
|
|
1122
|
+
static createLazy(schema, cfg) {
|
|
1123
|
+
return AjvSchema.create(schema, {
|
|
1124
|
+
lazy: true,
|
|
1125
|
+
...cfg,
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Conveniently allows to pass either JsonSchema or JSchema builder, or existing AjvSchema.
|
|
1130
|
+
* If it's already an AjvSchema - it'll just return it without any processing.
|
|
1131
|
+
* If it's a Builder - will call `build` before proceeding.
|
|
1132
|
+
* Otherwise - will construct AjvSchema instance ready to be used.
|
|
1133
|
+
*/
|
|
1134
|
+
static create(schema, cfg) {
|
|
1135
|
+
if (schema instanceof AjvSchema)
|
|
1136
|
+
return schema;
|
|
1137
|
+
if (AjvSchema.isSchemaWithCachedAjvSchema(schema)) {
|
|
1138
|
+
return AjvSchema.requireCachedAjvSchema(schema);
|
|
1139
|
+
}
|
|
1140
|
+
let jsonSchema;
|
|
1141
|
+
if (schema instanceof JSchema) {
|
|
1142
|
+
// oxlint-disable typescript-eslint(no-unnecessary-type-assertion)
|
|
1143
|
+
jsonSchema = schema.build();
|
|
1144
|
+
AjvSchema.requireValidJsonSchema(jsonSchema);
|
|
1145
|
+
}
|
|
1146
|
+
else {
|
|
1147
|
+
jsonSchema = schema;
|
|
1148
|
+
}
|
|
1149
|
+
// This is our own helper which marks a schema as optional
|
|
1150
|
+
// in case it is going to be used in an object schema,
|
|
1151
|
+
// where we need to mark the given property as not-required.
|
|
1152
|
+
// But once all compilation is done, the presence of this field
|
|
1153
|
+
// really upsets Ajv.
|
|
1154
|
+
delete jsonSchema.optionalField;
|
|
1155
|
+
const ajvSchema = new AjvSchema(jsonSchema, cfg);
|
|
1156
|
+
AjvSchema.cacheAjvSchema(schema, ajvSchema);
|
|
1157
|
+
return ajvSchema;
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Creates a minimal AjvSchema wrapper from a pre-compiled validate function.
|
|
1161
|
+
* Used internally by JSchema to cache a compatible AjvSchema instance.
|
|
1162
|
+
*/
|
|
1163
|
+
static _wrap(schema, compiledFn) {
|
|
1164
|
+
return new AjvSchema(schema, {}, compiledFn);
|
|
1165
|
+
}
|
|
1166
|
+
static isSchemaWithCachedAjvSchema(schema) {
|
|
1167
|
+
return !!schema?.[HIDDEN_AJV_SCHEMA];
|
|
1168
|
+
}
|
|
1169
|
+
static cacheAjvSchema(schema, ajvSchema) {
|
|
1170
|
+
return Object.assign(schema, { [HIDDEN_AJV_SCHEMA]: ajvSchema });
|
|
1171
|
+
}
|
|
1172
|
+
static requireCachedAjvSchema(schema) {
|
|
1173
|
+
return schema[HIDDEN_AJV_SCHEMA];
|
|
1174
|
+
}
|
|
1175
|
+
cfg;
|
|
1176
|
+
_compiledFn;
|
|
1177
|
+
_getValidateFn() {
|
|
1178
|
+
if (!this._compiledFn) {
|
|
1179
|
+
this._compiledFn = this.cfg.ajv.compile(this.schema);
|
|
1180
|
+
}
|
|
1181
|
+
return this._compiledFn;
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* It returns the original object just for convenience.
|
|
1185
|
+
*/
|
|
1186
|
+
validate(input, opt = {}) {
|
|
1187
|
+
const [err, output] = this.getValidationResult(input, opt);
|
|
1188
|
+
if (err)
|
|
1189
|
+
throw err;
|
|
1190
|
+
return output;
|
|
1191
|
+
}
|
|
1192
|
+
isValid(input, opt) {
|
|
1193
|
+
const [err] = this.getValidationResult(input, opt);
|
|
1194
|
+
return !err;
|
|
1195
|
+
}
|
|
1196
|
+
getValidationResult(input, opt = {}) {
|
|
1197
|
+
const fn = this._getValidateFn();
|
|
1198
|
+
return executeValidation(fn, this.schema, input, opt, this.cfg.inputName);
|
|
1199
|
+
}
|
|
1200
|
+
getValidationFunction() {
|
|
1201
|
+
return (input, opt) => {
|
|
1202
|
+
return this.getValidationResult(input, {
|
|
1203
|
+
mutateInput: opt?.mutateInput,
|
|
1204
|
+
inputName: opt?.inputName,
|
|
1205
|
+
inputId: opt?.inputId,
|
|
1206
|
+
});
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
static requireValidJsonSchema(schema) {
|
|
1210
|
+
// For object schemas we require that it is type checked against an external type, e.g.:
|
|
1211
|
+
// interface Foo { name: string }
|
|
1212
|
+
// const schema = j.object({ name: j.string() }).ofType<Foo>()
|
|
1213
|
+
_assert(schema.type !== 'object' || schema.hasIsOfTypeCheck, 'The schema must be type checked against a type or interface, using the `.isOfType()` helper in `j`.');
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
// ==== Shared validation logic ====
|
|
1217
|
+
const separator = '\n';
|
|
1218
|
+
function executeValidation(fn, builtSchema, input, opt = {}, defaultInputName) {
|
|
1219
|
+
const item = opt.mutateInput !== false || typeof input !== 'object'
|
|
1220
|
+
? input // mutate
|
|
1221
|
+
: _deepCopy(input); // not mutate
|
|
1222
|
+
let valid = fn(item); // mutates item, but not input
|
|
1223
|
+
_typeCast(item);
|
|
1224
|
+
let output = item;
|
|
1225
|
+
if (valid && builtSchema.postValidation) {
|
|
1226
|
+
const [err, result] = _try(() => builtSchema.postValidation(output));
|
|
1227
|
+
if (err) {
|
|
1228
|
+
valid = false;
|
|
1229
|
+
fn.errors = [
|
|
1230
|
+
{
|
|
1231
|
+
instancePath: '',
|
|
1232
|
+
message: err.message,
|
|
1233
|
+
},
|
|
1234
|
+
];
|
|
1235
|
+
}
|
|
1236
|
+
else {
|
|
1237
|
+
output = result;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
if (valid)
|
|
1241
|
+
return [null, output];
|
|
1242
|
+
const errors = fn.errors;
|
|
1243
|
+
const { inputId = _isObject(input) ? input['id'] : undefined, inputName = defaultInputName || 'Object', } = opt;
|
|
1244
|
+
const dataVar = [inputName, inputId].filter(Boolean).join('.');
|
|
1245
|
+
applyImprovementsOnErrorMessages(errors, builtSchema);
|
|
1246
|
+
let message = getAjv().errorsText(errors, {
|
|
1247
|
+
dataVar,
|
|
1248
|
+
separator,
|
|
1249
|
+
});
|
|
1250
|
+
// Note: if we mutated the input already, e.g stripped unknown properties,
|
|
1251
|
+
// the error message Input would contain already mutated object print, such as Input: {}
|
|
1252
|
+
// Unless `getOriginalInput` function is provided - then it will be used to preserve the Input pureness.
|
|
1253
|
+
const inputStringified = _inspect(opt.getOriginalInput?.() || input, { maxLen: 4000 });
|
|
1254
|
+
message = [message, 'Input: ' + inputStringified].join(separator);
|
|
1255
|
+
const err = new AjvValidationError(message, _filterNullishValues({
|
|
1256
|
+
errors,
|
|
1257
|
+
inputName,
|
|
1258
|
+
inputId,
|
|
1259
|
+
}));
|
|
1260
|
+
return [err, output];
|
|
1261
|
+
}
|
|
1262
|
+
// ==== Error formatting helpers ====
|
|
1263
|
+
function applyImprovementsOnErrorMessages(errors, schema) {
|
|
1264
|
+
if (!errors)
|
|
1265
|
+
return;
|
|
1266
|
+
filterNullableAnyOfErrors(errors, schema);
|
|
1267
|
+
const { errorMessages } = schema;
|
|
1268
|
+
for (const error of errors) {
|
|
1269
|
+
const errorMessage = getErrorMessageForInstancePath(schema, error.instancePath, error.keyword);
|
|
1270
|
+
if (errorMessage) {
|
|
1271
|
+
error.message = errorMessage;
|
|
1272
|
+
}
|
|
1273
|
+
else if (errorMessages?.[error.keyword]) {
|
|
1274
|
+
error.message = errorMessages[error.keyword];
|
|
1275
|
+
}
|
|
1276
|
+
else {
|
|
1277
|
+
const unwrapped = unwrapNullableAnyOf(schema);
|
|
1278
|
+
if (unwrapped?.errorMessages?.[error.keyword]) {
|
|
1279
|
+
error.message = unwrapped.errorMessages[error.keyword];
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
error.instancePath = error.instancePath.replaceAll(/\/(\d+)/g, `[$1]`).replaceAll('/', '.');
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Filters out noisy errors produced by nullable anyOf patterns.
|
|
1287
|
+
* When `nullable()` wraps a schema in `anyOf: [realSchema, { type: 'null' }]`,
|
|
1288
|
+
* AJV produces "must be null" and "must match a schema in anyOf" errors
|
|
1289
|
+
* that are confusing. This method splices them out, keeping only the real errors.
|
|
1290
|
+
*/
|
|
1291
|
+
function filterNullableAnyOfErrors(errors, schema) {
|
|
1292
|
+
// Collect exact schemaPaths to remove (anyOf aggregates) and prefixes (null branches)
|
|
1293
|
+
const exactPaths = [];
|
|
1294
|
+
const nullBranchPrefixes = [];
|
|
1295
|
+
for (const error of errors) {
|
|
1296
|
+
if (error.keyword !== 'anyOf')
|
|
1297
|
+
continue;
|
|
1298
|
+
const parentSchema = resolveSchemaPath(schema, error.schemaPath);
|
|
1299
|
+
if (!parentSchema)
|
|
1300
|
+
continue;
|
|
1301
|
+
const nullIndex = unwrapNullableAnyOfIndex(parentSchema);
|
|
1302
|
+
if (nullIndex === -1)
|
|
1303
|
+
continue;
|
|
1304
|
+
exactPaths.push(error.schemaPath); // e.g. "#/anyOf"
|
|
1305
|
+
const anyOfBase = error.schemaPath.slice(0, -'anyOf'.length);
|
|
1306
|
+
nullBranchPrefixes.push(`${anyOfBase}anyOf/${nullIndex}/`); // e.g. "#/anyOf/1/"
|
|
1307
|
+
}
|
|
1308
|
+
if (!exactPaths.length)
|
|
1309
|
+
return;
|
|
1310
|
+
for (let i = errors.length - 1; i >= 0; i--) {
|
|
1311
|
+
const sp = errors[i].schemaPath;
|
|
1312
|
+
if (exactPaths.includes(sp) || nullBranchPrefixes.some(p => sp.startsWith(p))) {
|
|
1313
|
+
errors.splice(i, 1);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Navigates the schema tree using an AJV schemaPath (e.g. "#/properties/foo/anyOf")
|
|
1319
|
+
* and returns the parent schema containing the last keyword.
|
|
1320
|
+
*/
|
|
1321
|
+
function resolveSchemaPath(schema, schemaPath) {
|
|
1322
|
+
// schemaPath looks like "#/properties/foo/anyOf" or "#/anyOf"
|
|
1323
|
+
// We want the schema that contains the final keyword (e.g. "anyOf")
|
|
1324
|
+
const segments = schemaPath.replace(/^#\//, '').split('/');
|
|
1325
|
+
// Remove the last segment (the keyword itself, e.g. "anyOf")
|
|
1326
|
+
segments.pop();
|
|
1327
|
+
let current = schema;
|
|
1328
|
+
for (const segment of segments) {
|
|
1329
|
+
if (!current || typeof current !== 'object')
|
|
1330
|
+
return undefined;
|
|
1331
|
+
current = current[segment];
|
|
1332
|
+
}
|
|
1333
|
+
return current;
|
|
1334
|
+
}
|
|
1335
|
+
function getErrorMessageForInstancePath(schema, instancePath, keyword) {
|
|
1336
|
+
if (!schema || !instancePath)
|
|
1337
|
+
return undefined;
|
|
1338
|
+
const segments = instancePath.split('/').filter(Boolean);
|
|
1339
|
+
return traverseSchemaPath(schema, segments, keyword);
|
|
1340
|
+
}
|
|
1341
|
+
function traverseSchemaPath(schema, segments, keyword) {
|
|
1342
|
+
if (!segments.length)
|
|
1343
|
+
return undefined;
|
|
1344
|
+
const [currentSegment, ...remainingSegments] = segments;
|
|
1345
|
+
const nextSchema = getChildSchema(schema, currentSegment);
|
|
1346
|
+
if (!nextSchema)
|
|
1347
|
+
return undefined;
|
|
1348
|
+
if (nextSchema.errorMessages?.[keyword]) {
|
|
1349
|
+
return nextSchema.errorMessages[keyword];
|
|
1350
|
+
}
|
|
1351
|
+
// Check through nullable wrapper
|
|
1352
|
+
const unwrapped = unwrapNullableAnyOf(nextSchema);
|
|
1353
|
+
if (unwrapped?.errorMessages?.[keyword]) {
|
|
1354
|
+
return unwrapped.errorMessages[keyword];
|
|
1355
|
+
}
|
|
1356
|
+
if (remainingSegments.length) {
|
|
1357
|
+
return traverseSchemaPath(nextSchema, remainingSegments, keyword);
|
|
1358
|
+
}
|
|
1359
|
+
return undefined;
|
|
1360
|
+
}
|
|
1361
|
+
function getChildSchema(schema, segment) {
|
|
1362
|
+
if (!segment)
|
|
1363
|
+
return undefined;
|
|
1364
|
+
// Unwrap nullable anyOf to find properties/items through nullable wrappers
|
|
1365
|
+
const effectiveSchema = unwrapNullableAnyOf(schema) ?? schema;
|
|
1366
|
+
if (/^\d+$/.test(segment) && effectiveSchema.items) {
|
|
1367
|
+
return getArrayItemSchema(effectiveSchema, segment);
|
|
1368
|
+
}
|
|
1369
|
+
return getObjectPropertySchema(effectiveSchema, segment);
|
|
1370
|
+
}
|
|
1371
|
+
function getArrayItemSchema(schema, indexSegment) {
|
|
1372
|
+
if (!schema.items)
|
|
1373
|
+
return undefined;
|
|
1374
|
+
if (Array.isArray(schema.items)) {
|
|
1375
|
+
return schema.items[Number(indexSegment)];
|
|
1376
|
+
}
|
|
1377
|
+
return schema.items;
|
|
1378
|
+
}
|
|
1379
|
+
function getObjectPropertySchema(schema, segment) {
|
|
1380
|
+
return schema.properties?.[segment];
|
|
1381
|
+
}
|
|
1382
|
+
function unwrapNullableAnyOf(schema) {
|
|
1383
|
+
const nullIndex = unwrapNullableAnyOfIndex(schema);
|
|
1384
|
+
if (nullIndex === -1)
|
|
1385
|
+
return undefined;
|
|
1386
|
+
return schema.anyOf[1 - nullIndex];
|
|
1387
|
+
}
|
|
1388
|
+
function unwrapNullableAnyOfIndex(schema) {
|
|
1389
|
+
if (schema.anyOf?.length !== 2)
|
|
1390
|
+
return -1;
|
|
1391
|
+
const nullIndex = schema.anyOf.findIndex(s => s.type === 'null');
|
|
1392
|
+
return nullIndex;
|
|
1393
|
+
}
|
|
1394
|
+
// ==== Utility helpers ====
|
|
1395
|
+
function addPropertiesToSchema(schema, props) {
|
|
1396
|
+
const properties = {};
|
|
1397
|
+
const required = [];
|
|
1398
|
+
for (const [key, builder] of Object.entries(props)) {
|
|
1399
|
+
const isOptional = builder.getSchema().optionalField;
|
|
1400
|
+
if (!isOptional) {
|
|
1401
|
+
required.push(key);
|
|
1402
|
+
}
|
|
1403
|
+
const builtSchema = builder.build();
|
|
1404
|
+
properties[key] = builtSchema;
|
|
1405
|
+
}
|
|
1406
|
+
schema.properties = properties;
|
|
1407
|
+
schema.required = _uniq(required).sort();
|
|
1414
1408
|
}
|
|
1415
1409
|
function hasNoObjectSchemas(schema) {
|
|
1416
1410
|
if (Array.isArray(schema.type)) {
|
|
1417
|
-
schema.type.every(type => ['string', 'number', 'integer', 'boolean', 'null'].includes(type));
|
|
1411
|
+
return schema.type.every(type => ['string', 'number', 'integer', 'boolean', 'null'].includes(type));
|
|
1418
1412
|
}
|
|
1419
1413
|
else if (schema.anyOf) {
|
|
1420
1414
|
return schema.anyOf.every(hasNoObjectSchemas);
|