ata-validator 0.17.3 → 0.17.5
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 +14 -0
- package/bin/ata.js +5 -0
- package/index.js +27 -2
- package/lib/js-compiler.js +29 -28
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
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.17.5 - 2026-05-25
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Compiled validators resolve draft-07 plain-name anchors. A `$defs`/`definitions` entry that declares an anchor with `$id: "#name"` and is referenced by `$ref: "#name"` now compiles through the codegen on every path (boolean, error, combined) instead of bailing. The bail forced a fallback that could not resolve sibling cross-schema refs, which surfaced as `cannot resolve $ref`. This is how shared schemas are referenced under Fastify.
|
|
10
|
+
|
|
11
|
+
## 0.17.4 - 2026-05-25
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Standalone output now embeds user-supplied format functions. `toStandaloneModule` and `bundleCompact` referenced the `_uf_<name>` format helpers without declaring them, so the generated module threw `_uf_<name> is not defined` on the first validation, and their error path skipped the custom format entirely (so `validate` disagreed with `isValid`). Both now serialize the format functions via `Function#toString` and run them on the error path too, matching `bundleStandalone`.
|
|
16
|
+
- Compiled validators report per-property errors for schema-valued `additionalProperties`. The AOT error path previously emitted a single generic `validation failed`; it now validates each undeclared property against the subschema and reports the precise `/<key>` error, matching the runtime validator.
|
|
17
|
+
- `ata --version` (and `-V`) prints the CLI version instead of failing with `unknown command`.
|
|
18
|
+
|
|
5
19
|
## 0.17.3 - 2026-05-25
|
|
6
20
|
|
|
7
21
|
### Security
|
package/bin/ata.js
CHANGED
|
@@ -45,6 +45,7 @@ Build options:
|
|
|
45
45
|
--dual Emit both .compiled.mjs (with source) and .compiled.min.mjs (without)
|
|
46
46
|
|
|
47
47
|
-h, --help Show this message
|
|
48
|
+
-V, --version Print the ata-validator version
|
|
48
49
|
|
|
49
50
|
Examples:
|
|
50
51
|
ata compile schemas/user.json -o src/generated/user.validator.mjs
|
|
@@ -386,6 +387,10 @@ function main() {
|
|
|
386
387
|
|
|
387
388
|
const cmd = argv[0];
|
|
388
389
|
if (cmd === '-h' || cmd === '--help' || cmd === 'help') { usage(); return; }
|
|
390
|
+
if (cmd === '-V' || cmd === '--version' || cmd === 'version') {
|
|
391
|
+
process.stdout.write(require('../package.json').version + '\n');
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
389
394
|
|
|
390
395
|
const rest = argv.slice(1);
|
|
391
396
|
let args;
|
package/index.js
CHANGED
|
@@ -1214,7 +1214,7 @@ module.exports = { boolFn, hybridFactory, errFn };
|
|
|
1214
1214
|
jsErrFn = compileToJSCodegenWithErrors(
|
|
1215
1215
|
typeof this._schemaObj === 'object' ? this._schemaObj : {},
|
|
1216
1216
|
null,
|
|
1217
|
-
|
|
1217
|
+
this._userFormats,
|
|
1218
1218
|
(source && sourceMap && schemaFile) ? { sourceMap, schemaFile } : null,
|
|
1219
1219
|
);
|
|
1220
1220
|
const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
|
|
@@ -1266,6 +1266,16 @@ module.exports = { boolFn, hybridFactory, errFn };
|
|
|
1266
1266
|
// them at module scope too, or _fn references undefined names (e.g. _af1_b0).
|
|
1267
1267
|
const preambleDecls = jsFn._preambleSource ? jsFn._preambleSource + '\n' : '';
|
|
1268
1268
|
|
|
1269
|
+
// User-supplied format functions are referenced as _uf_<name> by both the
|
|
1270
|
+
// boolean (_fn) and error (errFn) bodies. Embed them via Function#toString
|
|
1271
|
+
// so the standalone module stays self-contained.
|
|
1272
|
+
let formatDecls = '';
|
|
1273
|
+
if (jsFn._formatClosures && jsFn._formatClosures.length > 0) {
|
|
1274
|
+
formatDecls = jsFn._formatClosures
|
|
1275
|
+
.map(({ name, fn }) => `const ${name} = ${fn.toString()};`)
|
|
1276
|
+
.join('\n') + '\n';
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1269
1279
|
const validBody = errCore
|
|
1270
1280
|
? 'return _fn(data) ? VALID : { valid: false, errors: errFn(data, true).errors }'
|
|
1271
1281
|
: 'return _fn(data) ? VALID : ABORT';
|
|
@@ -1288,7 +1298,7 @@ const ABORT = Object.freeze({
|
|
|
1288
1298
|
path: '',
|
|
1289
1299
|
})]),
|
|
1290
1300
|
});
|
|
1291
|
-
${closureDecls}${preambleDecls}const _fn = function(d) {
|
|
1301
|
+
${closureDecls}${preambleDecls}${formatDecls}const _fn = function(d) {
|
|
1292
1302
|
${src}
|
|
1293
1303
|
};
|
|
1294
1304
|
${errCore}function isValid(data) { return _fn(data); }
|
|
@@ -1575,6 +1585,7 @@ Validator.bundleCompact = function (schemas, opts) {
|
|
|
1575
1585
|
const jsErrFn = compileToJSCodegenWithErrors(
|
|
1576
1586
|
typeof schema === "string" ? JSON.parse(schema) : schema,
|
|
1577
1587
|
v._schemaMap,
|
|
1588
|
+
v._userFormats,
|
|
1578
1589
|
);
|
|
1579
1590
|
if (jsFn._usesSafeRe || (jsErrFn && jsErrFn._usesSafeRe)) bundleUsesSafeRe = true;
|
|
1580
1591
|
// Hoisted anyOf/oneOf branch helpers (e.g. `_af1_b0`) must travel with the
|
|
@@ -1586,6 +1597,7 @@ Validator.bundleCompact = function (schemas, opts) {
|
|
|
1586
1597
|
return {
|
|
1587
1598
|
hybrid,
|
|
1588
1599
|
err: jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : null,
|
|
1600
|
+
fmt: jsFn._formatClosures || null,
|
|
1589
1601
|
};
|
|
1590
1602
|
});
|
|
1591
1603
|
|
|
@@ -1624,6 +1636,19 @@ Validator.bundleCompact = function (schemas, opts) {
|
|
|
1624
1636
|
const declKW = isEsm ? "const" : "var";
|
|
1625
1637
|
out += `${declKW} R=Object.freeze({valid:true,errors:Object.freeze([])});\n`;
|
|
1626
1638
|
|
|
1639
|
+
// User format functions are referenced as _uf_<name> by the hybrid and error
|
|
1640
|
+
// bodies. Collect them across all schemas (deduped by name) and embed via
|
|
1641
|
+
// Function#toString so the bundle stays self-contained.
|
|
1642
|
+
const fmtSeen = new Set();
|
|
1643
|
+
for (const e of entries) {
|
|
1644
|
+
if (!e || !e.fmt) continue;
|
|
1645
|
+
for (const { name, fn } of e.fmt) {
|
|
1646
|
+
if (fmtSeen.has(name)) continue;
|
|
1647
|
+
fmtSeen.add(name);
|
|
1648
|
+
out += `${declKW} ${name}=${fn.toString()};\n`;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1627
1652
|
// Shared hybrid factories
|
|
1628
1653
|
out += `${declKW} H=[\n`;
|
|
1629
1654
|
out += bodies
|
package/lib/js-compiler.js
CHANGED
|
@@ -699,23 +699,6 @@ function canResolveDynamicRefs(target, callingSchema, schemaMap) {
|
|
|
699
699
|
|
|
700
700
|
// Recursively check if a schema can be safely compiled to JS codegen.
|
|
701
701
|
// Returns false if any sub-schema contains features codegen gets wrong.
|
|
702
|
-
// Does the schema (recursively) contain an anchor-style `$ref` equal to anchorId
|
|
703
|
-
// (e.g. `$ref: '#address'`)? Used to keep codegen away from anchor refs it can't
|
|
704
|
-
// resolve in the combined/error paths.
|
|
705
|
-
function schemaHasAnchorRef(node, anchorId) {
|
|
706
|
-
if (!node || typeof node !== 'object') return false
|
|
707
|
-
if (Array.isArray(node)) {
|
|
708
|
-
for (const x of node) if (schemaHasAnchorRef(x, anchorId)) return true
|
|
709
|
-
return false
|
|
710
|
-
}
|
|
711
|
-
if (node.$ref === anchorId) return true
|
|
712
|
-
for (const k in node) {
|
|
713
|
-
if (k === '$ref') continue
|
|
714
|
-
if (schemaHasAnchorRef(node[k], anchorId)) return true
|
|
715
|
-
}
|
|
716
|
-
return false
|
|
717
|
-
}
|
|
718
|
-
|
|
719
702
|
function codegenSafe(schema, schemaMap) {
|
|
720
703
|
if (typeof schema === 'boolean') return true
|
|
721
704
|
if (typeof schema !== 'object' || schema === null) return true
|
|
@@ -826,14 +809,10 @@ function codegenSafe(schema, schemaMap) {
|
|
|
826
809
|
if (typeof def === 'boolean') return false
|
|
827
810
|
if (typeof def === 'object' && def !== null) {
|
|
828
811
|
// A non-fragment $id (e.g. 'sub.json', 'http://...') opens a new base URI
|
|
829
|
-
// scope — bail. A fragment-only $id ('#name') is a draft-07 anchor;
|
|
830
|
-
//
|
|
831
|
-
//
|
|
832
|
-
|
|
833
|
-
if (def.$id) {
|
|
834
|
-
if (!def.$id.startsWith('#')) return false
|
|
835
|
-
if (schemaHasAnchorRef(schema, def.$id)) return false
|
|
836
|
-
}
|
|
812
|
+
// scope — bail. A fragment-only $id ('#name') is a draft-07 anchor; the
|
|
813
|
+
// codegen anchor maps register it (see "Build anchors map"), so anchor
|
|
814
|
+
// refs (`$ref: '#name'`) resolve to it on all paths.
|
|
815
|
+
if (def.$id && !def.$id.startsWith('#')) return false
|
|
837
816
|
if (def.$ref) return false // nested ref chain — bail
|
|
838
817
|
if (!codegenSafe(def, schemaMap)) return false
|
|
839
818
|
}
|
|
@@ -933,12 +912,15 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
|
|
|
933
912
|
// Root schema's own $dynamicAnchor / $anchor
|
|
934
913
|
if (schema.$dynamicAnchor) anchors['#' + schema.$dynamicAnchor] = schema
|
|
935
914
|
if (schema.$anchor) anchors['#' + schema.$anchor] = schema
|
|
915
|
+
// Draft-07 plain-name anchor: declared as `$id: "#name"`.
|
|
916
|
+
if (typeof schema.$id === 'string' && schema.$id.startsWith('#')) anchors[schema.$id] = schema
|
|
936
917
|
// Anchors from $defs
|
|
937
918
|
if (rootDefs) {
|
|
938
919
|
for (const def of Object.values(rootDefs)) {
|
|
939
920
|
if (def && typeof def === 'object') {
|
|
940
921
|
if (def.$dynamicAnchor) anchors['#' + def.$dynamicAnchor] = def
|
|
941
922
|
if (def.$anchor) anchors['#' + def.$anchor] = def
|
|
923
|
+
if (typeof def.$id === 'string' && def.$id.startsWith('#')) anchors[def.$id] = def
|
|
942
924
|
}
|
|
943
925
|
}
|
|
944
926
|
}
|
|
@@ -948,6 +930,7 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
|
|
|
948
930
|
if (ext && typeof ext === 'object') {
|
|
949
931
|
if (ext.$dynamicAnchor && !anchors['#' + ext.$dynamicAnchor]) anchors['#' + ext.$dynamicAnchor] = ext
|
|
950
932
|
if (ext.$anchor && !anchors['#' + ext.$anchor]) anchors['#' + ext.$anchor] = ext
|
|
933
|
+
if (typeof ext.$id === 'string' && ext.$id.startsWith('#') && !anchors[ext.$id]) anchors[ext.$id] = ext
|
|
951
934
|
}
|
|
952
935
|
}
|
|
953
936
|
}
|
|
@@ -2558,9 +2541,6 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
|
|
|
2558
2541
|
if (s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) return null
|
|
2559
2542
|
// Bail on self-referencing schemas — error codegen doesn't support recursion
|
|
2560
2543
|
if (s.includes('"$ref":"#"')) return null
|
|
2561
|
-
// Bail on additionalProperties as a schema — genCodeE does not emit the
|
|
2562
|
-
// per-key sub-schema loop. Let the closure path handle detailed errors.
|
|
2563
|
-
if (hasAdditionalPropertiesSchema(schema)) return null
|
|
2564
2544
|
}
|
|
2565
2545
|
if (typeof schema === 'boolean') {
|
|
2566
2546
|
return schema
|
|
@@ -2595,11 +2575,13 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
|
|
|
2595
2575
|
const eAnchors = {}
|
|
2596
2576
|
if (schema.$dynamicAnchor) eAnchors['#' + schema.$dynamicAnchor] = schema
|
|
2597
2577
|
if (schema.$anchor) eAnchors['#' + schema.$anchor] = schema
|
|
2578
|
+
if (typeof schema.$id === 'string' && schema.$id.startsWith('#')) eAnchors[schema.$id] = schema
|
|
2598
2579
|
if (eRootDefs) {
|
|
2599
2580
|
for (const def of Object.values(eRootDefs)) {
|
|
2600
2581
|
if (def && typeof def === 'object') {
|
|
2601
2582
|
if (def.$dynamicAnchor) eAnchors['#' + def.$dynamicAnchor] = def
|
|
2602
2583
|
if (def.$anchor) eAnchors['#' + def.$anchor] = def
|
|
2584
|
+
if (typeof def.$id === 'string' && def.$id.startsWith('#')) eAnchors[def.$id] = def
|
|
2603
2585
|
}
|
|
2604
2586
|
}
|
|
2605
2587
|
}
|
|
@@ -2608,6 +2590,7 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
|
|
|
2608
2590
|
if (ext && typeof ext === 'object') {
|
|
2609
2591
|
if (ext.$dynamicAnchor && !eAnchors['#' + ext.$dynamicAnchor]) eAnchors['#' + ext.$dynamicAnchor] = ext
|
|
2610
2592
|
if (ext.$anchor && !eAnchors['#' + ext.$anchor]) eAnchors['#' + ext.$anchor] = ext
|
|
2593
|
+
if (typeof ext.$id === 'string' && ext.$id.startsWith('#') && !eAnchors[ext.$id]) eAnchors[ext.$id] = ext
|
|
2611
2594
|
}
|
|
2612
2595
|
}
|
|
2613
2596
|
}
|
|
@@ -3012,6 +2995,21 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3012
2995
|
}
|
|
3013
2996
|
}
|
|
3014
2997
|
|
|
2998
|
+
// additionalProperties as a schema — validate each property not declared in
|
|
2999
|
+
// `properties` against the subschema, with a dynamic /<key> path. codegenSafe
|
|
3000
|
+
// guarantees no patternProperties or composition here, so "additional" is
|
|
3001
|
+
// simply "not declared".
|
|
3002
|
+
if (typeof schema.additionalProperties === 'object' && schema.additionalProperties !== null) {
|
|
3003
|
+
const ki = ctx.varCounter++
|
|
3004
|
+
const known = Object.keys(schema.properties || {})
|
|
3005
|
+
const guard = known.length ? `const _ak${ki}=new Set([${known.map((k) => JSON.stringify(k)).join(',')}]);` : ''
|
|
3006
|
+
const keep = known.length ? `if(!_ak${ki}.has(_k${ki})){` : `{`
|
|
3007
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${guard}for(const _k${ki} in ${v}){${keep}`)
|
|
3008
|
+
const p = pathExpr ? `${pathExpr}+'/'+_k${ki}` : `'/'+_k${ki}`
|
|
3009
|
+
genCodeE(schema.additionalProperties, `${v}[_k${ki}]`, p, lines, ctx, schemaPrefix + '/additionalProperties')
|
|
3010
|
+
lines.push(`}}}`)
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3015
3013
|
// dependentSchemas
|
|
3016
3014
|
if (schema.dependentSchemas) {
|
|
3017
3015
|
for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
|
|
@@ -3198,11 +3196,13 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
|
|
|
3198
3196
|
const cAnchors = {}
|
|
3199
3197
|
if (schema.$dynamicAnchor) cAnchors['#' + schema.$dynamicAnchor] = schema
|
|
3200
3198
|
if (schema.$anchor) cAnchors['#' + schema.$anchor] = schema
|
|
3199
|
+
if (typeof schema.$id === 'string' && schema.$id.startsWith('#')) cAnchors[schema.$id] = schema
|
|
3201
3200
|
if (cRootDefs) {
|
|
3202
3201
|
for (const def of Object.values(cRootDefs)) {
|
|
3203
3202
|
if (def && typeof def === 'object') {
|
|
3204
3203
|
if (def.$dynamicAnchor) cAnchors['#' + def.$dynamicAnchor] = def
|
|
3205
3204
|
if (def.$anchor) cAnchors['#' + def.$anchor] = def
|
|
3205
|
+
if (typeof def.$id === 'string' && def.$id.startsWith('#')) cAnchors[def.$id] = def
|
|
3206
3206
|
}
|
|
3207
3207
|
}
|
|
3208
3208
|
}
|
|
@@ -3211,6 +3211,7 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
|
|
|
3211
3211
|
if (ext && typeof ext === 'object') {
|
|
3212
3212
|
if (ext.$dynamicAnchor && !cAnchors['#' + ext.$dynamicAnchor]) cAnchors['#' + ext.$dynamicAnchor] = ext
|
|
3213
3213
|
if (ext.$anchor && !cAnchors['#' + ext.$anchor]) cAnchors['#' + ext.$anchor] = ext
|
|
3214
|
+
if (typeof ext.$id === 'string' && ext.$id.startsWith('#') && !cAnchors[ext.$id]) cAnchors[ext.$id] = ext
|
|
3214
3215
|
}
|
|
3215
3216
|
}
|
|
3216
3217
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.5",
|
|
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",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"rebuild": "cmake-js rebuild --target ata",
|
|
43
43
|
"prebuild": "pkg-prebuilds-copy --baseDir build/Release --source ata.node --name=ata --strip --napi_version=10",
|
|
44
44
|
"prebuild-all": "npm run prebuild -- --arch x64 && npm run prebuild -- --arch arm64",
|
|
45
|
-
"test": "node test.js && node tests/test_no_native.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_rich_errors_optout.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node benchmark/bench_aot_size.mjs",
|
|
45
|
+
"test": "node test.js && node tests/test_no_native.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_rich_errors_optout.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node benchmark/bench_aot_size.mjs",
|
|
46
46
|
"bench:size": "node benchmark/bench_aot_size.mjs",
|
|
47
47
|
"test:suite": "node tests/run_suite.js",
|
|
48
48
|
"test:compat": "node tests/test_compat.js",
|