@naturalcycles/nodejs-lib 15.92.0 → 15.92.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -44,6 +44,18 @@ export declare class AjvSchema<OUT> {
|
|
|
44
44
|
private getAJVValidateFunction;
|
|
45
45
|
private static requireValidJsonSchema;
|
|
46
46
|
private applyImprovementsOnErrorMessages;
|
|
47
|
+
/**
|
|
48
|
+
* Filters out noisy errors produced by nullable anyOf patterns.
|
|
49
|
+
* When `nullable()` wraps a schema in `anyOf: [realSchema, { type: 'null' }]`,
|
|
50
|
+
* AJV produces "must be null" and "must match a schema in anyOf" errors
|
|
51
|
+
* that are confusing. This method splices them out, keeping only the real errors.
|
|
52
|
+
*/
|
|
53
|
+
private filterNullableAnyOfErrors;
|
|
54
|
+
/**
|
|
55
|
+
* Navigates the schema tree using an AJV schemaPath (e.g. "#/properties/foo/anyOf")
|
|
56
|
+
* and returns the parent schema containing the last keyword.
|
|
57
|
+
*/
|
|
58
|
+
private resolveSchemaPath;
|
|
47
59
|
private getErrorMessageForInstancePath;
|
|
48
60
|
private traverseSchemaPath;
|
|
49
61
|
private getChildSchema;
|
|
@@ -172,6 +172,7 @@ export class AjvSchema {
|
|
|
172
172
|
applyImprovementsOnErrorMessages(errors) {
|
|
173
173
|
if (!errors)
|
|
174
174
|
return;
|
|
175
|
+
this.filterNullableAnyOfErrors(errors);
|
|
175
176
|
const { errorMessages } = this.schema;
|
|
176
177
|
for (const error of errors) {
|
|
177
178
|
const errorMessage = this.getErrorMessageForInstancePath(this.schema, error.instancePath, error.keyword);
|
|
@@ -181,9 +182,65 @@ export class AjvSchema {
|
|
|
181
182
|
else if (errorMessages?.[error.keyword]) {
|
|
182
183
|
error.message = errorMessages[error.keyword];
|
|
183
184
|
}
|
|
185
|
+
else {
|
|
186
|
+
const unwrapped = unwrapNullableAnyOf(this.schema);
|
|
187
|
+
if (unwrapped?.errorMessages?.[error.keyword]) {
|
|
188
|
+
error.message = unwrapped.errorMessages[error.keyword];
|
|
189
|
+
}
|
|
190
|
+
}
|
|
184
191
|
error.instancePath = error.instancePath.replaceAll(/\/(\d+)/g, `[$1]`).replaceAll('/', '.');
|
|
185
192
|
}
|
|
186
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
|
+
}
|
|
187
244
|
getErrorMessageForInstancePath(schema, instancePath, keyword) {
|
|
188
245
|
if (!schema || !instancePath)
|
|
189
246
|
return undefined;
|
|
@@ -200,6 +257,11 @@ export class AjvSchema {
|
|
|
200
257
|
if (nextSchema.errorMessages?.[keyword]) {
|
|
201
258
|
return nextSchema.errorMessages[keyword];
|
|
202
259
|
}
|
|
260
|
+
// Check through nullable wrapper
|
|
261
|
+
const unwrapped = unwrapNullableAnyOf(nextSchema);
|
|
262
|
+
if (unwrapped?.errorMessages?.[keyword]) {
|
|
263
|
+
return unwrapped.errorMessages[keyword];
|
|
264
|
+
}
|
|
203
265
|
if (remainingSegments.length) {
|
|
204
266
|
return this.traverseSchemaPath(nextSchema, remainingSegments, keyword);
|
|
205
267
|
}
|
|
@@ -208,10 +270,12 @@ export class AjvSchema {
|
|
|
208
270
|
getChildSchema(schema, segment) {
|
|
209
271
|
if (!segment)
|
|
210
272
|
return undefined;
|
|
211
|
-
|
|
212
|
-
|
|
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);
|
|
213
277
|
}
|
|
214
|
-
return this.getObjectPropertySchema(
|
|
278
|
+
return this.getObjectPropertySchema(effectiveSchema, segment);
|
|
215
279
|
}
|
|
216
280
|
getArrayItemSchema(schema, indexSegment) {
|
|
217
281
|
if (!schema.items)
|
|
@@ -225,6 +289,18 @@ export class AjvSchema {
|
|
|
225
289
|
return schema.properties?.[segment];
|
|
226
290
|
}
|
|
227
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
|
+
}
|
|
228
304
|
const separator = '\n';
|
|
229
305
|
export const HIDDEN_AJV_SCHEMA = Symbol('HIDDEN_AJV_SCHEMA');
|
|
230
306
|
// ===== JsonSchemaBuilders ===== //
|
package/package.json
CHANGED
|
@@ -266,6 +266,8 @@ export class AjvSchema<OUT> {
|
|
|
266
266
|
): void {
|
|
267
267
|
if (!errors) return
|
|
268
268
|
|
|
269
|
+
this.filterNullableAnyOfErrors(errors)
|
|
270
|
+
|
|
269
271
|
const { errorMessages } = this.schema
|
|
270
272
|
|
|
271
273
|
for (const error of errors) {
|
|
@@ -279,12 +281,73 @@ export class AjvSchema<OUT> {
|
|
|
279
281
|
error.message = errorMessage
|
|
280
282
|
} else if (errorMessages?.[error.keyword]) {
|
|
281
283
|
error.message = errorMessages[error.keyword]
|
|
284
|
+
} else {
|
|
285
|
+
const unwrapped = unwrapNullableAnyOf(this.schema)
|
|
286
|
+
if (unwrapped?.errorMessages?.[error.keyword]) {
|
|
287
|
+
error.message = unwrapped.errorMessages[error.keyword]
|
|
288
|
+
}
|
|
282
289
|
}
|
|
283
290
|
|
|
284
291
|
error.instancePath = error.instancePath.replaceAll(/\/(\d+)/g, `[$1]`).replaceAll('/', '.')
|
|
285
292
|
}
|
|
286
293
|
}
|
|
287
294
|
|
|
295
|
+
/**
|
|
296
|
+
* Filters out noisy errors produced by nullable anyOf patterns.
|
|
297
|
+
* When `nullable()` wraps a schema in `anyOf: [realSchema, { type: 'null' }]`,
|
|
298
|
+
* AJV produces "must be null" and "must match a schema in anyOf" errors
|
|
299
|
+
* that are confusing. This method splices them out, keeping only the real errors.
|
|
300
|
+
*/
|
|
301
|
+
private filterNullableAnyOfErrors(
|
|
302
|
+
errors: ErrorObject<string, Record<string, any>, unknown>[],
|
|
303
|
+
): void {
|
|
304
|
+
// Collect exact schemaPaths to remove (anyOf aggregates) and prefixes (null branches)
|
|
305
|
+
const exactPaths: string[] = []
|
|
306
|
+
const nullBranchPrefixes: string[] = []
|
|
307
|
+
|
|
308
|
+
for (const error of errors) {
|
|
309
|
+
if (error.keyword !== 'anyOf') continue
|
|
310
|
+
|
|
311
|
+
const parentSchema = this.resolveSchemaPath(error.schemaPath)
|
|
312
|
+
if (!parentSchema) continue
|
|
313
|
+
|
|
314
|
+
const nullIndex = unwrapNullableAnyOfIndex(parentSchema)
|
|
315
|
+
if (nullIndex === -1) continue
|
|
316
|
+
|
|
317
|
+
exactPaths.push(error.schemaPath) // e.g. "#/anyOf"
|
|
318
|
+
const anyOfBase = error.schemaPath.slice(0, -'anyOf'.length)
|
|
319
|
+
nullBranchPrefixes.push(`${anyOfBase}anyOf/${nullIndex}/`) // e.g. "#/anyOf/1/"
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (!exactPaths.length) return
|
|
323
|
+
|
|
324
|
+
for (let i = errors.length - 1; i >= 0; i--) {
|
|
325
|
+
const sp = errors[i]!.schemaPath
|
|
326
|
+
if (exactPaths.includes(sp) || nullBranchPrefixes.some(p => sp.startsWith(p))) {
|
|
327
|
+
errors.splice(i, 1)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Navigates the schema tree using an AJV schemaPath (e.g. "#/properties/foo/anyOf")
|
|
334
|
+
* and returns the parent schema containing the last keyword.
|
|
335
|
+
*/
|
|
336
|
+
private resolveSchemaPath(schemaPath: string): JsonSchema | undefined {
|
|
337
|
+
// schemaPath looks like "#/properties/foo/anyOf" or "#/anyOf"
|
|
338
|
+
// We want the schema that contains the final keyword (e.g. "anyOf")
|
|
339
|
+
const segments = schemaPath.replace(/^#\//, '').split('/')
|
|
340
|
+
// Remove the last segment (the keyword itself, e.g. "anyOf")
|
|
341
|
+
segments.pop()
|
|
342
|
+
|
|
343
|
+
let current: any = this.schema
|
|
344
|
+
for (const segment of segments) {
|
|
345
|
+
if (!current || typeof current !== 'object') return undefined
|
|
346
|
+
current = current[segment]
|
|
347
|
+
}
|
|
348
|
+
return current as JsonSchema | undefined
|
|
349
|
+
}
|
|
350
|
+
|
|
288
351
|
private getErrorMessageForInstancePath(
|
|
289
352
|
schema: JsonSchema<OUT> | undefined,
|
|
290
353
|
instancePath: string,
|
|
@@ -312,6 +375,12 @@ export class AjvSchema<OUT> {
|
|
|
312
375
|
return nextSchema.errorMessages[keyword]
|
|
313
376
|
}
|
|
314
377
|
|
|
378
|
+
// Check through nullable wrapper
|
|
379
|
+
const unwrapped = unwrapNullableAnyOf(nextSchema)
|
|
380
|
+
if (unwrapped?.errorMessages?.[keyword]) {
|
|
381
|
+
return unwrapped.errorMessages[keyword]
|
|
382
|
+
}
|
|
383
|
+
|
|
315
384
|
if (remainingSegments.length) {
|
|
316
385
|
return this.traverseSchemaPath(nextSchema, remainingSegments, keyword)
|
|
317
386
|
}
|
|
@@ -321,11 +390,15 @@ export class AjvSchema<OUT> {
|
|
|
321
390
|
|
|
322
391
|
private getChildSchema(schema: JsonSchema, segment: string | undefined): JsonSchema | undefined {
|
|
323
392
|
if (!segment) return undefined
|
|
324
|
-
|
|
325
|
-
|
|
393
|
+
|
|
394
|
+
// Unwrap nullable anyOf to find properties/items through nullable wrappers
|
|
395
|
+
const effectiveSchema = unwrapNullableAnyOf(schema) ?? schema
|
|
396
|
+
|
|
397
|
+
if (/^\d+$/.test(segment) && effectiveSchema.items) {
|
|
398
|
+
return this.getArrayItemSchema(effectiveSchema, segment)
|
|
326
399
|
}
|
|
327
400
|
|
|
328
|
-
return this.getObjectPropertySchema(
|
|
401
|
+
return this.getObjectPropertySchema(effectiveSchema, segment)
|
|
329
402
|
}
|
|
330
403
|
|
|
331
404
|
private getArrayItemSchema(schema: JsonSchema, indexSegment: string): JsonSchema | undefined {
|
|
@@ -343,6 +416,18 @@ export class AjvSchema<OUT> {
|
|
|
343
416
|
}
|
|
344
417
|
}
|
|
345
418
|
|
|
419
|
+
function unwrapNullableAnyOf(schema: JsonSchema): JsonSchema | undefined {
|
|
420
|
+
const nullIndex = unwrapNullableAnyOfIndex(schema)
|
|
421
|
+
if (nullIndex === -1) return undefined
|
|
422
|
+
return schema.anyOf![1 - nullIndex]!
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function unwrapNullableAnyOfIndex(schema: JsonSchema): number {
|
|
426
|
+
if (schema.anyOf?.length !== 2) return -1
|
|
427
|
+
const nullIndex = schema.anyOf.findIndex(s => s.type === 'null')
|
|
428
|
+
return nullIndex
|
|
429
|
+
}
|
|
430
|
+
|
|
346
431
|
const separator = '\n'
|
|
347
432
|
|
|
348
433
|
export const HIDDEN_AJV_SCHEMA = Symbol('HIDDEN_AJV_SCHEMA')
|