ata-validator 0.15.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/index.js +84 -11
- package/package.json +1 -1
- package/scripts/check-doc-coverage.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to ata-validator are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/), and this project adheres to semantic versioning.
|
|
4
4
|
|
|
5
|
+
## 0.15.1 - 2026-05-23
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Coercion, defaults, and `removeAdditional` now follow a cross-schema `$ref` to the referenced shape. A whole-schema reference like `{ $ref: 'shared#' }` (used for shared route schemas) or a property reference like `{ id: { $ref: 'shared#/properties/id' } }` is preprocessed instead of skipped.
|
|
10
|
+
- The compile cache now keys on referenced schema content, not just the `$id`. Two validators that share a root schema string and an `$id` pointing at different schemas no longer reuse the wrong compiled function.
|
|
11
|
+
|
|
5
12
|
## 0.15.0 - 2026-05-18
|
|
6
13
|
|
|
7
14
|
### Added
|
package/index.js
CHANGED
|
@@ -363,6 +363,19 @@ function buildSchemaMap(schemas) {
|
|
|
363
363
|
return map
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
+
// Compile-cache key for a root schema plus its external schemas. Must include
|
|
367
|
+
// the external schema CONTENT, not just their $ids: two validators can share a
|
|
368
|
+
// root schema string and the same $id while pointing that $id at different
|
|
369
|
+
// schemas (separate app instances, test suites, multi-tenant). Keying on $id
|
|
370
|
+
// alone reuses the wrong compiled validator and silently mis-validates.
|
|
371
|
+
function compileCacheKey(schemaStr, schemaMap) {
|
|
372
|
+
if (!schemaMap || schemaMap.size === 0) return schemaStr
|
|
373
|
+
const parts = []
|
|
374
|
+
for (const [id, s] of schemaMap) parts.push(id + '=' + JSON.stringify(s))
|
|
375
|
+
parts.sort()
|
|
376
|
+
return schemaStr + '\0' + parts.join('\0')
|
|
377
|
+
}
|
|
378
|
+
|
|
366
379
|
// Resolve a relative URI ref against a base URI
|
|
367
380
|
function resolveRelativeRef(ref, baseId) {
|
|
368
381
|
if (!baseId || ref.includes('://') || ref.startsWith('#')) return ref
|
|
@@ -371,6 +384,66 @@ function resolveRelativeRef(ref, baseId) {
|
|
|
371
384
|
return baseId.substring(0, lastSlash + 1) + ref
|
|
372
385
|
}
|
|
373
386
|
|
|
387
|
+
// Resolve a cross-schema $ref to its target schema for preprocessing purposes.
|
|
388
|
+
// Handles whole-schema refs (`shared#`), relative-id matching, and JSON pointer
|
|
389
|
+
// fragments (`shared#/properties/id`). Returns null for local-only refs or when
|
|
390
|
+
// the target cannot be found. Used only to read `type`/`properties` for
|
|
391
|
+
// coercion/defaults/removeAdditional, never for validation.
|
|
392
|
+
function resolveRefForPreprocess(ref, schemaMap) {
|
|
393
|
+
if (!schemaMap || schemaMap.size === 0 || typeof ref !== 'string') return null
|
|
394
|
+
const hashIdx = ref.indexOf('#')
|
|
395
|
+
const baseId = hashIdx >= 0 ? ref.slice(0, hashIdx) : ref
|
|
396
|
+
const fragment = hashIdx >= 0 ? ref.slice(hashIdx + 1) : ''
|
|
397
|
+
if (!baseId) return null
|
|
398
|
+
let base = null
|
|
399
|
+
if (schemaMap.has(baseId)) base = schemaMap.get(baseId)
|
|
400
|
+
else if (!ref.includes('://')) {
|
|
401
|
+
for (const [id, s] of schemaMap) {
|
|
402
|
+
if (id.endsWith('/' + baseId)) { base = s; break }
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (!base) return null
|
|
406
|
+
if (!fragment) return base
|
|
407
|
+
let target = base
|
|
408
|
+
for (const part of fragment.split('/')) {
|
|
409
|
+
if (part === '') continue
|
|
410
|
+
if (target == null || typeof target !== 'object') return null
|
|
411
|
+
target = target[part.replace(/~1/g, '/').replace(/~0/g, '~')]
|
|
412
|
+
}
|
|
413
|
+
return target == null ? null : target
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Preprocessing (coerce/defaults/removeAdditional) reads `schema.properties` and
|
|
417
|
+
// each property's `type`. When the data shape lives behind a cross-schema $ref
|
|
418
|
+
// (a whole-schema ref like Fastify's `params: { $ref: 'shared#' }`, or a
|
|
419
|
+
// property ref like `{ id: { $ref: 'shared#/properties/id' } }`), follow the
|
|
420
|
+
// ref so the preprocessor can see the referenced shape. Returns the schema with
|
|
421
|
+
// such refs resolved, cloning only when a substitution is made.
|
|
422
|
+
function resolveSchemaForPreprocess(schema, schemaMap) {
|
|
423
|
+
if (!schema || typeof schema !== 'object' || !schemaMap || schemaMap.size === 0) return schema
|
|
424
|
+
let s = schema
|
|
425
|
+
// Whole-schema ref (only when it has no own properties, to avoid dropping
|
|
426
|
+
// sibling keywords on schemas that mix $ref with properties).
|
|
427
|
+
if (s.$ref && !s.properties) {
|
|
428
|
+
const t = resolveRefForPreprocess(s.$ref, schemaMap)
|
|
429
|
+
if (t && typeof t === 'object') s = t
|
|
430
|
+
}
|
|
431
|
+
if (!s.properties) return s
|
|
432
|
+
// Property-level refs: substitute the resolved target so coercion sees `type`.
|
|
433
|
+
let cloned = null
|
|
434
|
+
for (const key of Object.keys(s.properties)) {
|
|
435
|
+
const p = s.properties[key]
|
|
436
|
+
if (p && typeof p === 'object' && p.$ref && !p.type) {
|
|
437
|
+
const t = resolveRefForPreprocess(p.$ref, schemaMap)
|
|
438
|
+
if (t && typeof t === 'object') {
|
|
439
|
+
if (!cloned) { cloned = Object.assign({}, s); cloned.properties = Object.assign({}, s.properties) }
|
|
440
|
+
cloned.properties[key] = t
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return cloned || s
|
|
445
|
+
}
|
|
446
|
+
|
|
374
447
|
class Validator {
|
|
375
448
|
constructor(schema, opts) {
|
|
376
449
|
const options = opts || {};
|
|
@@ -531,9 +604,7 @@ class Validator {
|
|
|
531
604
|
|
|
532
605
|
// Check cache first -- reuse compiled functions for same schema
|
|
533
606
|
const sm = this._schemaMap.size > 0 ? this._schemaMap : null;
|
|
534
|
-
const mapKey = this._schemaMap
|
|
535
|
-
? this._schemaStr + '\0' + [...this._schemaMap.keys()].sort().join('\0')
|
|
536
|
-
: this._schemaStr;
|
|
607
|
+
const mapKey = compileCacheKey(this._schemaStr, this._schemaMap);
|
|
537
608
|
// Custom formats are JS functions: bypass the compile cache since they can
|
|
538
609
|
// differ between validators that share the same schema string.
|
|
539
610
|
const cached = this._userFormats ? null : _compileCache.get(mapKey);
|
|
@@ -559,13 +630,17 @@ class Validator {
|
|
|
559
630
|
}
|
|
560
631
|
this._jsFn = jsFn;
|
|
561
632
|
|
|
562
|
-
// Data mutators -- try codegen first (12x faster), fallback to closure arrays
|
|
563
|
-
|
|
633
|
+
// Data mutators -- try codegen first (12x faster), fallback to closure arrays.
|
|
634
|
+
// Follow cross-refs so coercion/defaults/removeAdditional see the referenced
|
|
635
|
+
// shape (e.g. Fastify `params: { $ref: 'shared#' }` or property refs like
|
|
636
|
+
// `{ id: { $ref: 'shared#/properties/id' } }`).
|
|
637
|
+
const preprocessSchema = resolveSchemaForPreprocess(schemaObj, this._schemaMap);
|
|
638
|
+
let preprocess = buildPreprocessCodegen(preprocessSchema, options);
|
|
564
639
|
if (!preprocess) {
|
|
565
|
-
const applyDefaults = buildDefaultsApplier(
|
|
566
|
-
const applyCoerce = options.coerceTypes ? buildCoercer(
|
|
640
|
+
const applyDefaults = buildDefaultsApplier(preprocessSchema);
|
|
641
|
+
const applyCoerce = options.coerceTypes ? buildCoercer(preprocessSchema) : null;
|
|
567
642
|
const applyRemove = options.removeAdditional
|
|
568
|
-
? buildRemover(
|
|
643
|
+
? buildRemover(preprocessSchema)
|
|
569
644
|
: null;
|
|
570
645
|
const mutators = [applyRemove, applyCoerce, applyDefaults].filter(Boolean);
|
|
571
646
|
preprocess =
|
|
@@ -1007,9 +1082,7 @@ class Validator {
|
|
|
1007
1082
|
if (typeof process !== 'undefined' && process.env && process.env.ATA_FORCE_NAPI) return;
|
|
1008
1083
|
if (!this._schemaStr) this._schemaStr = JSON.stringify(this._schemaObj);
|
|
1009
1084
|
const sm = this._schemaMap.size > 0 ? this._schemaMap : null;
|
|
1010
|
-
const mapKey = this._schemaMap
|
|
1011
|
-
? this._schemaStr + '\0' + [...this._schemaMap.keys()].sort().join('\0')
|
|
1012
|
-
: this._schemaStr;
|
|
1085
|
+
const mapKey = compileCacheKey(this._schemaStr, this._schemaMap);
|
|
1013
1086
|
// Custom formats are JS functions: skip the shared cache so different
|
|
1014
1087
|
// validators with the same schema string but different formats don't collide.
|
|
1015
1088
|
const cached = this._userFormats ? null : _compileCache.get(mapKey);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"description": "JSON Schema validation with first-class TypeScript and zero runtime cost. AOT compile to per-schema ESM modules with zero validator dependency. Generic Validator<T> for TypeBox/Zod/Valibot composition. Optional runtime API. Standard Schema V1 compatible.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"module": "index.mjs",
|
|
@@ -10,7 +10,7 @@ const doc = fs.readFileSync(path.join(__dirname, '..', 'docs', 'error-codes.md')
|
|
|
10
10
|
const missing = [];
|
|
11
11
|
const placeholder = [];
|
|
12
12
|
for (const code of all()) {
|
|
13
|
-
const headingRe = new RegExp(`^### ${code}
|
|
13
|
+
const headingRe = new RegExp(`^### ${code}\\b`, 'm');
|
|
14
14
|
if (!headingRe.test(doc)) {
|
|
15
15
|
missing.push(code);
|
|
16
16
|
continue;
|