@naturalcycles/nodejs-lib 15.92.1 → 15.94.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} +232 -271
- package/dist/validation/ajv/{ajvSchema.js → jSchema.js} +453 -460
- package/dist/validation/ajv/jsonSchemaBuilder.util.d.ts +1 -1
- package/dist/zip/zip.util.d.ts +3 -0
- package/dist/zip/zip.util.js +10 -0
- package/package.json +2 -2
- 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} +1447 -1471
- package/src/validation/ajv/jsonSchemaBuilder.util.ts +1 -1
- package/src/zip/zip.util.ts +16 -0
|
@@ -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,28 @@ 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
313
|
getValidationFunction() {
|
|
553
|
-
return
|
|
314
|
+
return (input, opt) => {
|
|
315
|
+
return this.getValidationResult(input, {
|
|
316
|
+
mutateInput: opt?.mutateInput,
|
|
317
|
+
inputName: opt?.inputName,
|
|
318
|
+
inputId: opt?.inputId,
|
|
319
|
+
});
|
|
320
|
+
};
|
|
554
321
|
}
|
|
555
322
|
/**
|
|
556
323
|
* Specify a function to be called after the normal validation is finished.
|
|
@@ -573,7 +340,8 @@ export class JsonSchemaTerminal {
|
|
|
573
340
|
out;
|
|
574
341
|
opt;
|
|
575
342
|
}
|
|
576
|
-
|
|
343
|
+
// ==== JBuilder (chainable base) ====
|
|
344
|
+
export class JBuilder extends JSchema {
|
|
577
345
|
setErrorMessage(ruleName, errorMessage) {
|
|
578
346
|
if (_isUndefined(errorMessage))
|
|
579
347
|
return;
|
|
@@ -585,15 +353,6 @@ export class JsonSchemaAnyBuilder extends JsonSchemaTerminal {
|
|
|
585
353
|
*
|
|
586
354
|
* When the type inferred from the schema differs from the passed-in type,
|
|
587
355
|
* 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
356
|
*/
|
|
598
357
|
isOfType() {
|
|
599
358
|
return this.cloneAndUpdateSchema({ hasIsOfTypeCheck: true });
|
|
@@ -630,7 +389,7 @@ export class JsonSchemaAnyBuilder extends JsonSchemaTerminal {
|
|
|
630
389
|
return clone;
|
|
631
390
|
}
|
|
632
391
|
nullable() {
|
|
633
|
-
return new
|
|
392
|
+
return new JBuilder({
|
|
634
393
|
anyOf: [this.build(), { type: 'null' }],
|
|
635
394
|
});
|
|
636
395
|
}
|
|
@@ -645,7 +404,7 @@ export class JsonSchemaAnyBuilder extends JsonSchemaTerminal {
|
|
|
645
404
|
* Locks the given schema chain and no other modification can be done to it.
|
|
646
405
|
*/
|
|
647
406
|
final() {
|
|
648
|
-
return new
|
|
407
|
+
return new JSchema(this.schema);
|
|
649
408
|
}
|
|
650
409
|
/**
|
|
651
410
|
*
|
|
@@ -676,7 +435,13 @@ export class JsonSchemaAnyBuilder extends JsonSchemaTerminal {
|
|
|
676
435
|
});
|
|
677
436
|
}
|
|
678
437
|
}
|
|
679
|
-
|
|
438
|
+
// ==== Consts
|
|
439
|
+
const TS_2500 = 16725225600; // 2500-01-01
|
|
440
|
+
const TS_2500_MILLIS = TS_2500 * 1000;
|
|
441
|
+
const TS_2000 = 946684800; // 2000-01-01
|
|
442
|
+
const TS_2000_MILLIS = TS_2000 * 1000;
|
|
443
|
+
// ==== Type-specific builders ====
|
|
444
|
+
export class JString extends JBuilder {
|
|
680
445
|
constructor() {
|
|
681
446
|
super({
|
|
682
447
|
type: 'string',
|
|
@@ -690,7 +455,7 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
|
|
|
690
455
|
*
|
|
691
456
|
* Make sure this `optional()` call is at the end of your call chain.
|
|
692
457
|
*
|
|
693
|
-
* When `null` is included in optionalValues, the return type becomes `
|
|
458
|
+
* When `null` is included in optionalValues, the return type becomes `JSchema`
|
|
694
459
|
* (no further chaining allowed) because the schema is wrapped in an anyOf structure.
|
|
695
460
|
*/
|
|
696
461
|
optional(optionalValues) {
|
|
@@ -698,7 +463,7 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
|
|
|
698
463
|
return super.optional();
|
|
699
464
|
}
|
|
700
465
|
_typeCast(optionalValues);
|
|
701
|
-
let newBuilder = new
|
|
466
|
+
let newBuilder = new JString().optional();
|
|
702
467
|
const alternativesSchema = j.enum(optionalValues);
|
|
703
468
|
Object.assign(newBuilder.getSchema(), {
|
|
704
469
|
anyOf: [this.build(), alternativesSchema.build()],
|
|
@@ -709,7 +474,7 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
|
|
|
709
474
|
// but the typing should not reflect that.
|
|
710
475
|
// We also cannot accept more rules attached, since we're not building a StringSchema anymore.
|
|
711
476
|
if (optionalValues.includes(null)) {
|
|
712
|
-
newBuilder = new
|
|
477
|
+
newBuilder = new JSchema({
|
|
713
478
|
anyOf: [{ type: 'null', optionalValues }, newBuilder.build()],
|
|
714
479
|
optionalField: true,
|
|
715
480
|
});
|
|
@@ -772,13 +537,16 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
|
|
|
772
537
|
* because this call effectively starts a new schema chain.
|
|
773
538
|
*/
|
|
774
539
|
isoDate() {
|
|
775
|
-
return new
|
|
540
|
+
return new JIsoDate();
|
|
776
541
|
}
|
|
777
542
|
isoDateTime() {
|
|
778
543
|
return this.cloneAndUpdateSchema({ IsoDateTime: true }).branded();
|
|
779
544
|
}
|
|
780
545
|
isoMonth() {
|
|
781
|
-
return new
|
|
546
|
+
return new JBuilder({
|
|
547
|
+
type: 'string',
|
|
548
|
+
IsoMonth: {},
|
|
549
|
+
});
|
|
782
550
|
}
|
|
783
551
|
/**
|
|
784
552
|
* Validates the string format to be JWT.
|
|
@@ -830,7 +598,7 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
|
|
|
830
598
|
return this.regex(UUID_REGEX, { msg: 'is an invalid UUID' });
|
|
831
599
|
}
|
|
832
600
|
}
|
|
833
|
-
export class
|
|
601
|
+
export class JIsoDate extends JBuilder {
|
|
834
602
|
constructor() {
|
|
835
603
|
super({
|
|
836
604
|
type: 'string',
|
|
@@ -843,7 +611,7 @@ export class JsonSchemaIsoDateBuilder extends JsonSchemaAnyBuilder {
|
|
|
843
611
|
* This `null` feature only works when the current schema is nested in an object or array schema,
|
|
844
612
|
* due to how mutability works in Ajv.
|
|
845
613
|
*
|
|
846
|
-
* When `null` is passed, the return type becomes `
|
|
614
|
+
* When `null` is passed, the return type becomes `JSchema`
|
|
847
615
|
* (no further chaining allowed) because the schema is wrapped in an anyOf structure.
|
|
848
616
|
*/
|
|
849
617
|
optional(nullValue) {
|
|
@@ -854,7 +622,7 @@ export class JsonSchemaIsoDateBuilder extends JsonSchemaAnyBuilder {
|
|
|
854
622
|
// so we must allow `null` values to be parsed by Ajv,
|
|
855
623
|
// but the typing should not reflect that.
|
|
856
624
|
// We also cannot accept more rules attached, since we're not building an ObjectSchema anymore.
|
|
857
|
-
return new
|
|
625
|
+
return new JSchema({
|
|
858
626
|
anyOf: [{ type: 'null', optionalValues: [null] }, this.build()],
|
|
859
627
|
optionalField: true,
|
|
860
628
|
});
|
|
@@ -882,15 +650,7 @@ export class JsonSchemaIsoDateBuilder extends JsonSchemaAnyBuilder {
|
|
|
882
650
|
return this.cloneAndUpdateSchema(schemaPatch);
|
|
883
651
|
}
|
|
884
652
|
}
|
|
885
|
-
export class
|
|
886
|
-
constructor() {
|
|
887
|
-
super({
|
|
888
|
-
type: 'string',
|
|
889
|
-
IsoMonth: {},
|
|
890
|
-
});
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
export class JsonSchemaNumberBuilder extends JsonSchemaAnyBuilder {
|
|
653
|
+
export class JNumber extends JBuilder {
|
|
894
654
|
constructor() {
|
|
895
655
|
super({
|
|
896
656
|
type: 'number',
|
|
@@ -904,7 +664,7 @@ export class JsonSchemaNumberBuilder extends JsonSchemaAnyBuilder {
|
|
|
904
664
|
*
|
|
905
665
|
* Make sure this `optional()` call is at the end of your call chain.
|
|
906
666
|
*
|
|
907
|
-
* When `null` is included in optionalValues, the return type becomes `
|
|
667
|
+
* When `null` is included in optionalValues, the return type becomes `JSchema`
|
|
908
668
|
* (no further chaining allowed) because the schema is wrapped in an anyOf structure.
|
|
909
669
|
*/
|
|
910
670
|
optional(optionalValues) {
|
|
@@ -912,7 +672,7 @@ export class JsonSchemaNumberBuilder extends JsonSchemaAnyBuilder {
|
|
|
912
672
|
return super.optional();
|
|
913
673
|
}
|
|
914
674
|
_typeCast(optionalValues);
|
|
915
|
-
let newBuilder = new
|
|
675
|
+
let newBuilder = new JNumber().optional();
|
|
916
676
|
const alternativesSchema = j.enum(optionalValues);
|
|
917
677
|
Object.assign(newBuilder.getSchema(), {
|
|
918
678
|
anyOf: [this.build(), alternativesSchema.build()],
|
|
@@ -923,7 +683,7 @@ export class JsonSchemaNumberBuilder extends JsonSchemaAnyBuilder {
|
|
|
923
683
|
// but the typing should not reflect that.
|
|
924
684
|
// We also cannot accept more rules attached, since we're not building a NumberSchema anymore.
|
|
925
685
|
if (optionalValues.includes(null)) {
|
|
926
|
-
newBuilder = new
|
|
686
|
+
newBuilder = new JSchema({
|
|
927
687
|
anyOf: [{ type: 'null', optionalValues }, newBuilder.build()],
|
|
928
688
|
optionalField: true,
|
|
929
689
|
});
|
|
@@ -1024,7 +784,7 @@ export class JsonSchemaNumberBuilder extends JsonSchemaAnyBuilder {
|
|
|
1024
784
|
return this.cloneAndUpdateSchema({ precision: numberOfDigits });
|
|
1025
785
|
}
|
|
1026
786
|
}
|
|
1027
|
-
export class
|
|
787
|
+
export class JBoolean extends JBuilder {
|
|
1028
788
|
constructor() {
|
|
1029
789
|
super({
|
|
1030
790
|
type: 'boolean',
|
|
@@ -1040,7 +800,7 @@ export class JsonSchemaBooleanBuilder extends JsonSchemaAnyBuilder {
|
|
|
1040
800
|
if (typeof optionalValue === 'undefined') {
|
|
1041
801
|
return super.optional();
|
|
1042
802
|
}
|
|
1043
|
-
const newBuilder = new
|
|
803
|
+
const newBuilder = new JBoolean().optional();
|
|
1044
804
|
const alternativesSchema = j.enum([optionalValue]);
|
|
1045
805
|
Object.assign(newBuilder.getSchema(), {
|
|
1046
806
|
anyOf: [this.build(), alternativesSchema.build()],
|
|
@@ -1049,7 +809,7 @@ export class JsonSchemaBooleanBuilder extends JsonSchemaAnyBuilder {
|
|
|
1049
809
|
return newBuilder;
|
|
1050
810
|
}
|
|
1051
811
|
}
|
|
1052
|
-
export class
|
|
812
|
+
export class JObject extends JBuilder {
|
|
1053
813
|
constructor(props, opt) {
|
|
1054
814
|
super({
|
|
1055
815
|
type: 'object',
|
|
@@ -1061,22 +821,7 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
|
|
|
1061
821
|
keySchema: opt?.keySchema ?? undefined,
|
|
1062
822
|
});
|
|
1063
823
|
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;
|
|
824
|
+
addPropertiesToSchema(this.schema, props);
|
|
1080
825
|
}
|
|
1081
826
|
/**
|
|
1082
827
|
* @param nullValue Pass `null` to have `null` values be considered/converted as `undefined`.
|
|
@@ -1084,7 +829,7 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
|
|
|
1084
829
|
* This `null` feature only works when the current schema is nested in an object or array schema,
|
|
1085
830
|
* due to how mutability works in Ajv.
|
|
1086
831
|
*
|
|
1087
|
-
* When `null` is passed, the return type becomes `
|
|
832
|
+
* When `null` is passed, the return type becomes `JSchema`
|
|
1088
833
|
* (no further chaining allowed) because the schema is wrapped in an anyOf structure.
|
|
1089
834
|
*/
|
|
1090
835
|
optional(nullValue) {
|
|
@@ -1095,7 +840,7 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
|
|
|
1095
840
|
// so we must allow `null` values to be parsed by Ajv,
|
|
1096
841
|
// but the typing should not reflect that.
|
|
1097
842
|
// We also cannot accept more rules attached, since we're not building an ObjectSchema anymore.
|
|
1098
|
-
return new
|
|
843
|
+
return new JSchema({
|
|
1099
844
|
anyOf: [{ type: 'null', optionalValues: [null] }, this.build()],
|
|
1100
845
|
optionalField: true,
|
|
1101
846
|
});
|
|
@@ -1107,9 +852,9 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
|
|
|
1107
852
|
return this.cloneAndUpdateSchema({ additionalProperties: true });
|
|
1108
853
|
}
|
|
1109
854
|
extend(props) {
|
|
1110
|
-
const newBuilder = new
|
|
855
|
+
const newBuilder = new JObject();
|
|
1111
856
|
_objectAssign(newBuilder.schema, deepCopyPreservingFunctions(this.schema));
|
|
1112
|
-
const incomingSchemaBuilder = new
|
|
857
|
+
const incomingSchemaBuilder = new JObject(props);
|
|
1113
858
|
mergeJsonSchemaObjects(newBuilder.schema, incomingSchemaBuilder.schema);
|
|
1114
859
|
_objectAssign(newBuilder.schema, { hasIsOfTypeCheck: false });
|
|
1115
860
|
return newBuilder;
|
|
@@ -1120,17 +865,6 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
|
|
|
1120
865
|
* It expects you to use `isOfType<T>()` in the chain,
|
|
1121
866
|
* otherwise the validation will throw. This is to ensure
|
|
1122
867
|
* 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
868
|
*/
|
|
1135
869
|
concat(other) {
|
|
1136
870
|
const clone = this.clone();
|
|
@@ -1160,7 +894,7 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
|
|
|
1160
894
|
return this.cloneAndUpdateSchema({ exclusiveProperties: [...exclusiveProperties, propNames] });
|
|
1161
895
|
}
|
|
1162
896
|
}
|
|
1163
|
-
export class
|
|
897
|
+
export class JObjectInfer extends JBuilder {
|
|
1164
898
|
constructor(props) {
|
|
1165
899
|
super({
|
|
1166
900
|
type: 'object',
|
|
@@ -1169,22 +903,7 @@ export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
|
|
|
1169
903
|
additionalProperties: false,
|
|
1170
904
|
});
|
|
1171
905
|
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;
|
|
906
|
+
addPropertiesToSchema(this.schema, props);
|
|
1188
907
|
}
|
|
1189
908
|
/**
|
|
1190
909
|
* @param nullValue Pass `null` to have `null` values be considered/converted as `undefined`.
|
|
@@ -1192,7 +911,7 @@ export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
|
|
|
1192
911
|
* This `null` feature only works when the current schema is nested in an object or array schema,
|
|
1193
912
|
* due to how mutability works in Ajv.
|
|
1194
913
|
*
|
|
1195
|
-
* When `null` is passed, the return type becomes `
|
|
914
|
+
* When `null` is passed, the return type becomes `JSchema`
|
|
1196
915
|
* (no further chaining allowed) because the schema is wrapped in an anyOf structure.
|
|
1197
916
|
*/
|
|
1198
917
|
// @ts-expect-error override adds optional parameter which is compatible but TS can't verify complex mapped types
|
|
@@ -1204,7 +923,7 @@ export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
|
|
|
1204
923
|
// so we must allow `null` values to be parsed by Ajv,
|
|
1205
924
|
// but the typing should not reflect that.
|
|
1206
925
|
// We also cannot accept more rules attached, since we're not building an ObjectSchema anymore.
|
|
1207
|
-
return new
|
|
926
|
+
return new JSchema({
|
|
1208
927
|
anyOf: [{ type: 'null', optionalValues: [null] }, this.build()],
|
|
1209
928
|
optionalField: true,
|
|
1210
929
|
});
|
|
@@ -1216,9 +935,9 @@ export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
|
|
|
1216
935
|
return this.cloneAndUpdateSchema({ additionalProperties: true });
|
|
1217
936
|
}
|
|
1218
937
|
extend(props) {
|
|
1219
|
-
const newBuilder = new
|
|
938
|
+
const newBuilder = new JObjectInfer();
|
|
1220
939
|
_objectAssign(newBuilder.schema, deepCopyPreservingFunctions(this.schema));
|
|
1221
|
-
const incomingSchemaBuilder = new
|
|
940
|
+
const incomingSchemaBuilder = new JObjectInfer(props);
|
|
1222
941
|
mergeJsonSchemaObjects(newBuilder.schema, incomingSchemaBuilder.schema);
|
|
1223
942
|
// This extend function is not type-safe as it is inferring,
|
|
1224
943
|
// so even if the base schema was already type-checked,
|
|
@@ -1238,7 +957,7 @@ export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
|
|
|
1238
957
|
});
|
|
1239
958
|
}
|
|
1240
959
|
}
|
|
1241
|
-
export class
|
|
960
|
+
export class JArray extends JBuilder {
|
|
1242
961
|
constructor(itemsSchema) {
|
|
1243
962
|
super({
|
|
1244
963
|
type: 'array',
|
|
@@ -1262,7 +981,7 @@ export class JsonSchemaArrayBuilder extends JsonSchemaAnyBuilder {
|
|
|
1262
981
|
return this.cloneAndUpdateSchema({ uniqueItems: true });
|
|
1263
982
|
}
|
|
1264
983
|
}
|
|
1265
|
-
|
|
984
|
+
class JSet2Builder extends JBuilder {
|
|
1266
985
|
constructor(itemsSchema) {
|
|
1267
986
|
super({
|
|
1268
987
|
type: ['array', 'object'],
|
|
@@ -1276,14 +995,7 @@ export class JsonSchemaSet2Builder extends JsonSchemaAnyBuilder {
|
|
|
1276
995
|
return this.cloneAndUpdateSchema({ maxItems });
|
|
1277
996
|
}
|
|
1278
997
|
}
|
|
1279
|
-
export class
|
|
1280
|
-
constructor() {
|
|
1281
|
-
super({
|
|
1282
|
-
Buffer: true,
|
|
1283
|
-
});
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
export class JsonSchemaEnumBuilder extends JsonSchemaAnyBuilder {
|
|
998
|
+
export class JEnum extends JBuilder {
|
|
1287
999
|
constructor(enumValues, baseType, opt) {
|
|
1288
1000
|
const jsonSchema = { enum: enumValues };
|
|
1289
1001
|
// Specifying the base type helps in cases when we ask Ajv to coerce the types.
|
|
@@ -1302,7 +1014,7 @@ export class JsonSchemaEnumBuilder extends JsonSchemaAnyBuilder {
|
|
|
1302
1014
|
return this;
|
|
1303
1015
|
}
|
|
1304
1016
|
}
|
|
1305
|
-
export class
|
|
1017
|
+
export class JTuple extends JBuilder {
|
|
1306
1018
|
constructor(items) {
|
|
1307
1019
|
super({
|
|
1308
1020
|
type: 'array',
|
|
@@ -1312,43 +1024,11 @@ export class JsonSchemaTupleBuilder extends JsonSchemaAnyBuilder {
|
|
|
1312
1024
|
});
|
|
1313
1025
|
}
|
|
1314
1026
|
}
|
|
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
1027
|
function object(props) {
|
|
1348
|
-
return new
|
|
1028
|
+
return new JObject(props);
|
|
1349
1029
|
}
|
|
1350
1030
|
function objectInfer(props) {
|
|
1351
|
-
return new
|
|
1031
|
+
return new JObjectInfer(props);
|
|
1352
1032
|
}
|
|
1353
1033
|
function objectDbEntity(props) {
|
|
1354
1034
|
return j.object({
|
|
@@ -1367,7 +1047,7 @@ function record(keySchema, valueSchema) {
|
|
|
1367
1047
|
const finalValueSchema = isValueOptional
|
|
1368
1048
|
? { anyOf: [{ isUndefined: true }, valueJsonSchema] }
|
|
1369
1049
|
: valueJsonSchema;
|
|
1370
|
-
return new
|
|
1050
|
+
return new JObject([], {
|
|
1371
1051
|
hasIsOfTypeCheck: false,
|
|
1372
1052
|
keySchema: keyJsonSchema,
|
|
1373
1053
|
patternProperties: {
|
|
@@ -1381,7 +1061,7 @@ function withRegexKeys(keyRegex, schema) {
|
|
|
1381
1061
|
}
|
|
1382
1062
|
const pattern = keyRegex instanceof RegExp ? keyRegex.source : keyRegex;
|
|
1383
1063
|
const jsonSchema = schema.build();
|
|
1384
|
-
return new
|
|
1064
|
+
return new JObject([], {
|
|
1385
1065
|
hasIsOfTypeCheck: false,
|
|
1386
1066
|
patternProperties: {
|
|
1387
1067
|
[pattern]: jsonSchema,
|
|
@@ -1410,11 +1090,324 @@ function withEnumKeys(keys, schema) {
|
|
|
1410
1090
|
_assert(enumValues, 'The key list should be an array of values, NumberEnum or a StringEnum');
|
|
1411
1091
|
const typedValues = enumValues;
|
|
1412
1092
|
const props = Object.fromEntries(typedValues.map(key => [key, schema]));
|
|
1413
|
-
return new
|
|
1093
|
+
return new JObject(props, { hasIsOfTypeCheck: false });
|
|
1094
|
+
}
|
|
1095
|
+
// ==== AjvSchema compat wrapper ====
|
|
1096
|
+
/**
|
|
1097
|
+
* On creation - compiles ajv validation function.
|
|
1098
|
+
* Provides convenient methods, error reporting, etc.
|
|
1099
|
+
*/
|
|
1100
|
+
export class AjvSchema {
|
|
1101
|
+
schema;
|
|
1102
|
+
constructor(schema, cfg = {}, preCompiledFn) {
|
|
1103
|
+
this.schema = schema;
|
|
1104
|
+
this.cfg = {
|
|
1105
|
+
lazy: false,
|
|
1106
|
+
...cfg,
|
|
1107
|
+
ajv: cfg.ajv || getAjv(),
|
|
1108
|
+
// Auto-detecting "InputName" from $id of the schema (e.g "Address.schema.json")
|
|
1109
|
+
inputName: cfg.inputName || (schema.$id ? _substringBefore(schema.$id, '.') : undefined),
|
|
1110
|
+
};
|
|
1111
|
+
if (preCompiledFn) {
|
|
1112
|
+
this._compiledFn = preCompiledFn;
|
|
1113
|
+
}
|
|
1114
|
+
else if (!cfg.lazy) {
|
|
1115
|
+
this._getValidateFn(); // compile eagerly
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* Shortcut for AjvSchema.create(schema, { lazy: true })
|
|
1120
|
+
*/
|
|
1121
|
+
static createLazy(schema, cfg) {
|
|
1122
|
+
return AjvSchema.create(schema, {
|
|
1123
|
+
lazy: true,
|
|
1124
|
+
...cfg,
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Conveniently allows to pass either JsonSchema or JSchema builder, or existing AjvSchema.
|
|
1129
|
+
* If it's already an AjvSchema - it'll just return it without any processing.
|
|
1130
|
+
* If it's a Builder - will call `build` before proceeding.
|
|
1131
|
+
* Otherwise - will construct AjvSchema instance ready to be used.
|
|
1132
|
+
*/
|
|
1133
|
+
static create(schema, cfg) {
|
|
1134
|
+
if (schema instanceof AjvSchema)
|
|
1135
|
+
return schema;
|
|
1136
|
+
if (AjvSchema.isSchemaWithCachedAjvSchema(schema)) {
|
|
1137
|
+
return AjvSchema.requireCachedAjvSchema(schema);
|
|
1138
|
+
}
|
|
1139
|
+
let jsonSchema;
|
|
1140
|
+
if (schema instanceof JSchema) {
|
|
1141
|
+
// oxlint-disable typescript-eslint(no-unnecessary-type-assertion)
|
|
1142
|
+
jsonSchema = schema.build();
|
|
1143
|
+
AjvSchema.requireValidJsonSchema(jsonSchema);
|
|
1144
|
+
}
|
|
1145
|
+
else {
|
|
1146
|
+
jsonSchema = schema;
|
|
1147
|
+
}
|
|
1148
|
+
// This is our own helper which marks a schema as optional
|
|
1149
|
+
// in case it is going to be used in an object schema,
|
|
1150
|
+
// where we need to mark the given property as not-required.
|
|
1151
|
+
// But once all compilation is done, the presence of this field
|
|
1152
|
+
// really upsets Ajv.
|
|
1153
|
+
delete jsonSchema.optionalField;
|
|
1154
|
+
const ajvSchema = new AjvSchema(jsonSchema, cfg);
|
|
1155
|
+
AjvSchema.cacheAjvSchema(schema, ajvSchema);
|
|
1156
|
+
return ajvSchema;
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Creates a minimal AjvSchema wrapper from a pre-compiled validate function.
|
|
1160
|
+
* Used internally by JSchema to cache a compatible AjvSchema instance.
|
|
1161
|
+
*/
|
|
1162
|
+
static _wrap(schema, compiledFn) {
|
|
1163
|
+
return new AjvSchema(schema, {}, compiledFn);
|
|
1164
|
+
}
|
|
1165
|
+
static isSchemaWithCachedAjvSchema(schema) {
|
|
1166
|
+
return !!schema?.[HIDDEN_AJV_SCHEMA];
|
|
1167
|
+
}
|
|
1168
|
+
static cacheAjvSchema(schema, ajvSchema) {
|
|
1169
|
+
return Object.assign(schema, { [HIDDEN_AJV_SCHEMA]: ajvSchema });
|
|
1170
|
+
}
|
|
1171
|
+
static requireCachedAjvSchema(schema) {
|
|
1172
|
+
return schema[HIDDEN_AJV_SCHEMA];
|
|
1173
|
+
}
|
|
1174
|
+
cfg;
|
|
1175
|
+
_compiledFn;
|
|
1176
|
+
_getValidateFn() {
|
|
1177
|
+
if (!this._compiledFn) {
|
|
1178
|
+
this._compiledFn = this.cfg.ajv.compile(this.schema);
|
|
1179
|
+
}
|
|
1180
|
+
return this._compiledFn;
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* It returns the original object just for convenience.
|
|
1184
|
+
*/
|
|
1185
|
+
validate(input, opt = {}) {
|
|
1186
|
+
const [err, output] = this.getValidationResult(input, opt);
|
|
1187
|
+
if (err)
|
|
1188
|
+
throw err;
|
|
1189
|
+
return output;
|
|
1190
|
+
}
|
|
1191
|
+
isValid(input, opt) {
|
|
1192
|
+
const [err] = this.getValidationResult(input, opt);
|
|
1193
|
+
return !err;
|
|
1194
|
+
}
|
|
1195
|
+
getValidationResult(input, opt = {}) {
|
|
1196
|
+
const fn = this._getValidateFn();
|
|
1197
|
+
return executeValidation(fn, this.schema, input, opt, this.cfg.inputName);
|
|
1198
|
+
}
|
|
1199
|
+
getValidationFunction() {
|
|
1200
|
+
return (input, opt) => {
|
|
1201
|
+
return this.getValidationResult(input, {
|
|
1202
|
+
mutateInput: opt?.mutateInput,
|
|
1203
|
+
inputName: opt?.inputName,
|
|
1204
|
+
inputId: opt?.inputId,
|
|
1205
|
+
});
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
static requireValidJsonSchema(schema) {
|
|
1209
|
+
// For object schemas we require that it is type checked against an external type, e.g.:
|
|
1210
|
+
// interface Foo { name: string }
|
|
1211
|
+
// const schema = j.object({ name: j.string() }).ofType<Foo>()
|
|
1212
|
+
_assert(schema.type !== 'object' || schema.hasIsOfTypeCheck, 'The schema must be type checked against a type or interface, using the `.isOfType()` helper in `j`.');
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
// ==== Shared validation logic ====
|
|
1216
|
+
const separator = '\n';
|
|
1217
|
+
function executeValidation(fn, builtSchema, input, opt = {}, defaultInputName) {
|
|
1218
|
+
const item = opt.mutateInput !== false || typeof input !== 'object'
|
|
1219
|
+
? input // mutate
|
|
1220
|
+
: _deepCopy(input); // not mutate
|
|
1221
|
+
let valid = fn(item); // mutates item, but not input
|
|
1222
|
+
_typeCast(item);
|
|
1223
|
+
let output = item;
|
|
1224
|
+
if (valid && builtSchema.postValidation) {
|
|
1225
|
+
const [err, result] = _try(() => builtSchema.postValidation(output));
|
|
1226
|
+
if (err) {
|
|
1227
|
+
valid = false;
|
|
1228
|
+
fn.errors = [
|
|
1229
|
+
{
|
|
1230
|
+
instancePath: '',
|
|
1231
|
+
message: err.message,
|
|
1232
|
+
},
|
|
1233
|
+
];
|
|
1234
|
+
}
|
|
1235
|
+
else {
|
|
1236
|
+
output = result;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
if (valid)
|
|
1240
|
+
return [null, output];
|
|
1241
|
+
const errors = fn.errors;
|
|
1242
|
+
const { inputId = _isObject(input) ? input['id'] : undefined, inputName = defaultInputName || 'Object', } = opt;
|
|
1243
|
+
const dataVar = [inputName, inputId].filter(Boolean).join('.');
|
|
1244
|
+
applyImprovementsOnErrorMessages(errors, builtSchema);
|
|
1245
|
+
let message = getAjv().errorsText(errors, {
|
|
1246
|
+
dataVar,
|
|
1247
|
+
separator,
|
|
1248
|
+
});
|
|
1249
|
+
// Note: if we mutated the input already, e.g stripped unknown properties,
|
|
1250
|
+
// the error message Input would contain already mutated object print, such as Input: {}
|
|
1251
|
+
// Unless `getOriginalInput` function is provided - then it will be used to preserve the Input pureness.
|
|
1252
|
+
const inputStringified = _inspect(opt.getOriginalInput?.() || input, { maxLen: 4000 });
|
|
1253
|
+
message = [message, 'Input: ' + inputStringified].join(separator);
|
|
1254
|
+
const err = new AjvValidationError(message, _filterNullishValues({
|
|
1255
|
+
errors,
|
|
1256
|
+
inputName,
|
|
1257
|
+
inputId,
|
|
1258
|
+
}));
|
|
1259
|
+
return [err, output];
|
|
1260
|
+
}
|
|
1261
|
+
// ==== Error formatting helpers ====
|
|
1262
|
+
function applyImprovementsOnErrorMessages(errors, schema) {
|
|
1263
|
+
if (!errors)
|
|
1264
|
+
return;
|
|
1265
|
+
filterNullableAnyOfErrors(errors, schema);
|
|
1266
|
+
const { errorMessages } = schema;
|
|
1267
|
+
for (const error of errors) {
|
|
1268
|
+
const errorMessage = getErrorMessageForInstancePath(schema, error.instancePath, error.keyword);
|
|
1269
|
+
if (errorMessage) {
|
|
1270
|
+
error.message = errorMessage;
|
|
1271
|
+
}
|
|
1272
|
+
else if (errorMessages?.[error.keyword]) {
|
|
1273
|
+
error.message = errorMessages[error.keyword];
|
|
1274
|
+
}
|
|
1275
|
+
else {
|
|
1276
|
+
const unwrapped = unwrapNullableAnyOf(schema);
|
|
1277
|
+
if (unwrapped?.errorMessages?.[error.keyword]) {
|
|
1278
|
+
error.message = unwrapped.errorMessages[error.keyword];
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
error.instancePath = error.instancePath.replaceAll(/\/(\d+)/g, `[$1]`).replaceAll('/', '.');
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Filters out noisy errors produced by nullable anyOf patterns.
|
|
1286
|
+
* When `nullable()` wraps a schema in `anyOf: [realSchema, { type: 'null' }]`,
|
|
1287
|
+
* AJV produces "must be null" and "must match a schema in anyOf" errors
|
|
1288
|
+
* that are confusing. This method splices them out, keeping only the real errors.
|
|
1289
|
+
*/
|
|
1290
|
+
function filterNullableAnyOfErrors(errors, schema) {
|
|
1291
|
+
// Collect exact schemaPaths to remove (anyOf aggregates) and prefixes (null branches)
|
|
1292
|
+
const exactPaths = [];
|
|
1293
|
+
const nullBranchPrefixes = [];
|
|
1294
|
+
for (const error of errors) {
|
|
1295
|
+
if (error.keyword !== 'anyOf')
|
|
1296
|
+
continue;
|
|
1297
|
+
const parentSchema = resolveSchemaPath(schema, error.schemaPath);
|
|
1298
|
+
if (!parentSchema)
|
|
1299
|
+
continue;
|
|
1300
|
+
const nullIndex = unwrapNullableAnyOfIndex(parentSchema);
|
|
1301
|
+
if (nullIndex === -1)
|
|
1302
|
+
continue;
|
|
1303
|
+
exactPaths.push(error.schemaPath); // e.g. "#/anyOf"
|
|
1304
|
+
const anyOfBase = error.schemaPath.slice(0, -'anyOf'.length);
|
|
1305
|
+
nullBranchPrefixes.push(`${anyOfBase}anyOf/${nullIndex}/`); // e.g. "#/anyOf/1/"
|
|
1306
|
+
}
|
|
1307
|
+
if (!exactPaths.length)
|
|
1308
|
+
return;
|
|
1309
|
+
for (let i = errors.length - 1; i >= 0; i--) {
|
|
1310
|
+
const sp = errors[i].schemaPath;
|
|
1311
|
+
if (exactPaths.includes(sp) || nullBranchPrefixes.some(p => sp.startsWith(p))) {
|
|
1312
|
+
errors.splice(i, 1);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Navigates the schema tree using an AJV schemaPath (e.g. "#/properties/foo/anyOf")
|
|
1318
|
+
* and returns the parent schema containing the last keyword.
|
|
1319
|
+
*/
|
|
1320
|
+
function resolveSchemaPath(schema, schemaPath) {
|
|
1321
|
+
// schemaPath looks like "#/properties/foo/anyOf" or "#/anyOf"
|
|
1322
|
+
// We want the schema that contains the final keyword (e.g. "anyOf")
|
|
1323
|
+
const segments = schemaPath.replace(/^#\//, '').split('/');
|
|
1324
|
+
// Remove the last segment (the keyword itself, e.g. "anyOf")
|
|
1325
|
+
segments.pop();
|
|
1326
|
+
let current = schema;
|
|
1327
|
+
for (const segment of segments) {
|
|
1328
|
+
if (!current || typeof current !== 'object')
|
|
1329
|
+
return undefined;
|
|
1330
|
+
current = current[segment];
|
|
1331
|
+
}
|
|
1332
|
+
return current;
|
|
1333
|
+
}
|
|
1334
|
+
function getErrorMessageForInstancePath(schema, instancePath, keyword) {
|
|
1335
|
+
if (!schema || !instancePath)
|
|
1336
|
+
return undefined;
|
|
1337
|
+
const segments = instancePath.split('/').filter(Boolean);
|
|
1338
|
+
return traverseSchemaPath(schema, segments, keyword);
|
|
1339
|
+
}
|
|
1340
|
+
function traverseSchemaPath(schema, segments, keyword) {
|
|
1341
|
+
if (!segments.length)
|
|
1342
|
+
return undefined;
|
|
1343
|
+
const [currentSegment, ...remainingSegments] = segments;
|
|
1344
|
+
const nextSchema = getChildSchema(schema, currentSegment);
|
|
1345
|
+
if (!nextSchema)
|
|
1346
|
+
return undefined;
|
|
1347
|
+
if (nextSchema.errorMessages?.[keyword]) {
|
|
1348
|
+
return nextSchema.errorMessages[keyword];
|
|
1349
|
+
}
|
|
1350
|
+
// Check through nullable wrapper
|
|
1351
|
+
const unwrapped = unwrapNullableAnyOf(nextSchema);
|
|
1352
|
+
if (unwrapped?.errorMessages?.[keyword]) {
|
|
1353
|
+
return unwrapped.errorMessages[keyword];
|
|
1354
|
+
}
|
|
1355
|
+
if (remainingSegments.length) {
|
|
1356
|
+
return traverseSchemaPath(nextSchema, remainingSegments, keyword);
|
|
1357
|
+
}
|
|
1358
|
+
return undefined;
|
|
1359
|
+
}
|
|
1360
|
+
function getChildSchema(schema, segment) {
|
|
1361
|
+
if (!segment)
|
|
1362
|
+
return undefined;
|
|
1363
|
+
// Unwrap nullable anyOf to find properties/items through nullable wrappers
|
|
1364
|
+
const effectiveSchema = unwrapNullableAnyOf(schema) ?? schema;
|
|
1365
|
+
if (/^\d+$/.test(segment) && effectiveSchema.items) {
|
|
1366
|
+
return getArrayItemSchema(effectiveSchema, segment);
|
|
1367
|
+
}
|
|
1368
|
+
return getObjectPropertySchema(effectiveSchema, segment);
|
|
1369
|
+
}
|
|
1370
|
+
function getArrayItemSchema(schema, indexSegment) {
|
|
1371
|
+
if (!schema.items)
|
|
1372
|
+
return undefined;
|
|
1373
|
+
if (Array.isArray(schema.items)) {
|
|
1374
|
+
return schema.items[Number(indexSegment)];
|
|
1375
|
+
}
|
|
1376
|
+
return schema.items;
|
|
1377
|
+
}
|
|
1378
|
+
function getObjectPropertySchema(schema, segment) {
|
|
1379
|
+
return schema.properties?.[segment];
|
|
1380
|
+
}
|
|
1381
|
+
function unwrapNullableAnyOf(schema) {
|
|
1382
|
+
const nullIndex = unwrapNullableAnyOfIndex(schema);
|
|
1383
|
+
if (nullIndex === -1)
|
|
1384
|
+
return undefined;
|
|
1385
|
+
return schema.anyOf[1 - nullIndex];
|
|
1386
|
+
}
|
|
1387
|
+
function unwrapNullableAnyOfIndex(schema) {
|
|
1388
|
+
if (schema.anyOf?.length !== 2)
|
|
1389
|
+
return -1;
|
|
1390
|
+
const nullIndex = schema.anyOf.findIndex(s => s.type === 'null');
|
|
1391
|
+
return nullIndex;
|
|
1392
|
+
}
|
|
1393
|
+
// ==== Utility helpers ====
|
|
1394
|
+
function addPropertiesToSchema(schema, props) {
|
|
1395
|
+
const properties = {};
|
|
1396
|
+
const required = [];
|
|
1397
|
+
for (const [key, builder] of Object.entries(props)) {
|
|
1398
|
+
const isOptional = builder.getSchema().optionalField;
|
|
1399
|
+
if (!isOptional) {
|
|
1400
|
+
required.push(key);
|
|
1401
|
+
}
|
|
1402
|
+
const builtSchema = builder.build();
|
|
1403
|
+
properties[key] = builtSchema;
|
|
1404
|
+
}
|
|
1405
|
+
schema.properties = properties;
|
|
1406
|
+
schema.required = _uniq(required).sort();
|
|
1414
1407
|
}
|
|
1415
1408
|
function hasNoObjectSchemas(schema) {
|
|
1416
1409
|
if (Array.isArray(schema.type)) {
|
|
1417
|
-
schema.type.every(type => ['string', 'number', 'integer', 'boolean', 'null'].includes(type));
|
|
1410
|
+
return schema.type.every(type => ['string', 'number', 'integer', 'boolean', 'null'].includes(type));
|
|
1418
1411
|
}
|
|
1419
1412
|
else if (schema.anyOf) {
|
|
1420
1413
|
return schema.anyOf.every(hasNoObjectSchemas);
|