@jarenjs/json 0.9.2 → 0.34.2
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/ARCHITECTURE.md +86 -13
- package/README.md +248 -23
- package/dist/types/canonical.d.ts +37 -0
- package/dist/types/cow.d.ts +28 -0
- package/dist/types/errors.d.ts +45 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/jslt/errors.d.ts +15 -8
- package/dist/types/jslt/index.d.ts +22 -0
- package/dist/types/jslt/packs/finance.d.ts +119 -0
- package/dist/types/jslt/packs/index.d.ts +310 -0
- package/dist/types/jslt/packs/math.d.ts +159 -0
- package/dist/types/jslt/packs/stats.d.ts +48 -0
- package/dist/types/jslt/registry.d.ts +65 -0
- package/dist/types/jtlt/errors.d.ts +3 -6
- package/dist/types/option-variants.d.ts +29 -0
- package/dist/types/patch.d.ts +214 -0
- package/dist/types/path.d.ts +139 -9
- package/dist/types/pointer.d.ts +100 -9
- package/dist/types/query/compile.d.ts +12 -0
- package/dist/types/query/errors.d.ts +72 -8
- package/dist/types/query/index.d.ts +317 -25
- package/dist/types/query/normalize.d.ts +24 -0
- package/dist/types/query/operators.d.ts +241 -1
- package/dist/types/query/runtime.d.ts +5 -8
- package/dist/types/query/types.d.ts +34 -0
- package/dist/types/segments.d.ts +31 -0
- package/dist/types/write.d.ts +204 -0
- package/dist/types/xquery/parse.d.ts +2 -3
- package/docs/JSLT-FORMAT.md +74 -3
- package/docs/JSLT-PRELUDE.md +1 -1
- package/docs/QUERY-FORMAT.md +695 -33
- package/package.json +18 -4
- package/schemas/geojson.draft-07.schema.json +323 -0
- package/schemas/geojson.jaren.schema.json +863 -0
- package/schemas/geojson.schema.json +172 -0
- package/schemas/jaren-jslt.authoring.schema.json +142 -0
- package/schemas/jaren-jslt.draft-07.schema.json +152 -11
- package/schemas/jaren-jslt.llm-profile.schema.json +782 -0
- package/schemas/jaren-jslt.schema.json +152 -11
- package/schemas/jaren-query.draft-07.schema.json +152 -11
- package/schemas/jaren-query.llm-profile.schema.json +619 -0
- package/schemas/jaren-query.schema.json +82 -15
- package/src/basic.js +1 -1
- package/src/canonical.js +170 -0
- package/src/cow.js +106 -0
- package/src/errors.js +68 -0
- package/src/index.js +3 -0
- package/src/jslt/dispatch.js +178 -28
- package/src/jslt/errors.js +19 -14
- package/src/jslt/index.js +37 -29
- package/src/jslt/packs/finance.js +49 -0
- package/src/jslt/packs/index.js +18 -0
- package/src/jslt/packs/math.js +46 -0
- package/src/jslt/packs/stats.js +65 -0
- package/src/jslt/registry.js +200 -0
- package/src/jslt/stylesheet.js +14 -23
- package/src/jtlt/desugar.js +2 -3
- package/src/jtlt/errors.js +6 -12
- package/src/jtlt/index.js +12 -29
- package/src/jtlt/template.js +9 -18
- package/src/option-variants.js +54 -0
- package/src/patch.js +1052 -0
- package/src/path.js +319 -52
- package/src/pointer.js +225 -44
- package/src/query/compile.js +790 -75
- package/src/query/errors.js +72 -12
- package/src/query/index.js +274 -42
- package/src/query/normalize.js +489 -78
- package/src/query/operators.js +620 -23
- package/src/query/runtime.js +5 -19
- package/src/query/types.js +213 -0
- package/src/segments.js +409 -64
- package/src/write.js +660 -0
- package/src/xquery/parse.js +37 -53
package/src/query/errors.js
CHANGED
|
@@ -4,16 +4,72 @@
|
|
|
4
4
|
// an RFC 6901 JSON Pointer into the *query document* locating the offending
|
|
5
5
|
// construct.
|
|
6
6
|
|
|
7
|
+
import { CodedError } from '@jarenjs/core/errors';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The runtime code table (the `CSV_CODES` shape): one entry per code
|
|
11
|
+
* the engine can raise, proven in sync with QUERY-FORMAT.md §10's
|
|
12
|
+
* normative tables by a test — the table cannot silently drift from
|
|
13
|
+
* the spec.
|
|
14
|
+
*/
|
|
15
|
+
export const QUERY_CODES = Object.freeze({
|
|
16
|
+
JQ0001: 'object mixes $-prefixed and plain keys',
|
|
17
|
+
JQ0002: 'unknown operator or $-key outside the vocabulary',
|
|
18
|
+
JQ0003: 'known phrase with bad arity, value shape, or key combination',
|
|
19
|
+
JQ0004: 'string starting $ is not a valid path or escape',
|
|
20
|
+
JQ0005: 'variable reference neither bound nor a declared external',
|
|
21
|
+
JQ0006: 'version envelope with unknown or non-string $query',
|
|
22
|
+
JQ0007: 'duplicate variable binding within one phrase',
|
|
23
|
+
JQ0008: 'schema operator in a query compiled without a type-test compiler',
|
|
24
|
+
JQ0009: 'schema literal rejected by the type-test compiler',
|
|
25
|
+
JQ0010: '$call/$collation naming no registered function/collation',
|
|
26
|
+
JQ0011: 'expression nesting deeper than limits.depth',
|
|
27
|
+
JQ2001: 'runtime type error',
|
|
28
|
+
JQ2002: '$idiv/$mod by zero',
|
|
29
|
+
JQ2003: 'EBV of a multi-item sequence',
|
|
30
|
+
JQ2004: '$map key expression not a single string',
|
|
31
|
+
JQ2005: 'incomparable $orderby/$sort keys',
|
|
32
|
+
JQ2006: 'reference to an unbound external parameter',
|
|
33
|
+
JQ2007: 'resource guard: an operator result exceeding an implementation limit',
|
|
34
|
+
JQ2008: 'schema assertion failure',
|
|
35
|
+
JQ2009: 'an execution limit exceeded',
|
|
36
|
+
JQ2010: 'a registered $call function threw',
|
|
37
|
+
JQ2011: 'the input document is undefined',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Shared constructor body for the two query error classes: `cause`
|
|
42
|
+
* retains what host code threw, BY VALUE — set via an own property
|
|
43
|
+
* even for `undefined`, so presence is testable (the base's `hasOwn`
|
|
44
|
+
* options form, passed through unchanged).
|
|
45
|
+
*/
|
|
46
|
+
class JsonQueryError extends CodedError {
|
|
47
|
+
/**
|
|
48
|
+
* @param {string} name - The public class name for `error.name`
|
|
49
|
+
* @param {string} code
|
|
50
|
+
* @param {string} reason
|
|
51
|
+
* @param {string} docPath
|
|
52
|
+
* @param {{ cause?: unknown }} [options]
|
|
53
|
+
*/
|
|
54
|
+
constructor(name, code, reason, docPath, options) {
|
|
55
|
+
super(name, code, reason, docPath, options);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
7
59
|
/**
|
|
8
60
|
* Error thrown when a query document is rejected at compile time
|
|
9
61
|
* (`JQ0xxx` codes, QUERY-FORMAT.md section 10.2).
|
|
10
62
|
*/
|
|
11
|
-
export class JsonQueryCompileError extends
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
63
|
+
export class JsonQueryCompileError extends JsonQueryError {
|
|
64
|
+
/**
|
|
65
|
+
* @param {string} code
|
|
66
|
+
* @param {string} reason
|
|
67
|
+
* @param {string} docPath
|
|
68
|
+
* @param {{ cause?: unknown }} [options] - `cause` retains what a
|
|
69
|
+
* host hook (e.g. `compileTypeTest`) threw
|
|
70
|
+
*/
|
|
71
|
+
constructor(code, reason, docPath, options) {
|
|
72
|
+
super('JsonQueryCompileError', code, reason, docPath, options);
|
|
17
73
|
}
|
|
18
74
|
}
|
|
19
75
|
|
|
@@ -21,12 +77,16 @@ export class JsonQueryCompileError extends Error {
|
|
|
21
77
|
* Error thrown when evaluating a compiled query fails
|
|
22
78
|
* (`JQ2xxx` codes, QUERY-FORMAT.md section 10.3).
|
|
23
79
|
*/
|
|
24
|
-
export class JsonQueryRuntimeError extends
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
80
|
+
export class JsonQueryRuntimeError extends JsonQueryError {
|
|
81
|
+
/**
|
|
82
|
+
* @param {string} code
|
|
83
|
+
* @param {string} reason
|
|
84
|
+
* @param {string} docPath
|
|
85
|
+
* @param {{ cause?: unknown }} [options] - `cause` retains what host
|
|
86
|
+
* code threw
|
|
87
|
+
*/
|
|
88
|
+
constructor(code, reason, docPath, options) {
|
|
89
|
+
super('JsonQueryRuntimeError', code, reason, docPath, options);
|
|
30
90
|
}
|
|
31
91
|
}
|
|
32
92
|
|
package/src/query/index.js
CHANGED
|
@@ -13,20 +13,154 @@
|
|
|
13
13
|
// plain JSON out (empty sequence -> undefined, singleton -> the item,
|
|
14
14
|
// longer sequence -> array of items).
|
|
15
15
|
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
16
|
+
import { createBoundedCache, createWeakCache } from '@jarenjs/core/cache';
|
|
17
|
+
import { normalizeQuery, deepFreezeCopy, NODE_KINDS } from './normalize.js';
|
|
18
|
+
import { compileQueryRoot, UNBOUND } from './compile.js';
|
|
18
19
|
import { EMPTY, Seq, ebv } from './runtime.js';
|
|
20
|
+
import { JsonQueryRuntimeError } from './errors.js';
|
|
19
21
|
|
|
20
|
-
export { JsonQueryCompileError, JsonQueryRuntimeError } from './errors.js';
|
|
22
|
+
export { JsonQueryCompileError, JsonQueryRuntimeError, QUERY_CODES } from './errors.js';
|
|
23
|
+
export { NODE_KINDS };
|
|
24
|
+
export { annotateTypes, TYPE_TAGS } from './types.js';
|
|
21
25
|
|
|
22
26
|
const hasOwn = Object.hasOwn;
|
|
23
27
|
|
|
28
|
+
/**
|
|
29
|
+
* The enforced query limits (QUERY-FORMAT.md section 8.12). These are
|
|
30
|
+
* deterministic result/phrase-OUTPUT caps, not general resource
|
|
31
|
+
* budgets: they bound what a phrase or the query hands onward, never
|
|
32
|
+
* the memory, work, fan-out or recursion spent producing it (a group
|
|
33
|
+
* or order barrier may accumulate arbitrarily many items behind a
|
|
34
|
+
* small final result). Untrusted queries need worker isolation, not
|
|
35
|
+
* these limits.
|
|
36
|
+
* @typedef {Object} JsonQueryLimits
|
|
37
|
+
* @property {number} [sequenceItems] - Caps every FLWOR phrase
|
|
38
|
+
* materialization and tightens `$range`'s resource guard; exceeding
|
|
39
|
+
* it is `JQ2009` (`$range` keeps its historical `JQ2007`).
|
|
40
|
+
* @property {number} [resultItems] - Caps the final result at the
|
|
41
|
+
* query boundary (`JQ2009`); checked after evaluation, and
|
|
42
|
+
* deliberately bypassed by `first`/`exists`/`ebv`.
|
|
43
|
+
* @property {number} [steps] - Caps expression-node evaluations
|
|
44
|
+
* (`JQ2009`). This is the one limit that bounds *work* rather than
|
|
45
|
+
* output, so it is also the one that costs: setting it compiles a
|
|
46
|
+
* counter check into every node, roughly halving throughput. A step
|
|
47
|
+
* is one node evaluation, not one primitive operation - a single
|
|
48
|
+
* node that loops internally (`$range` materialization, a general
|
|
49
|
+
* comparison's cross product) counts once.
|
|
50
|
+
* @property {number} [depth] - Caps expression nesting, enforced at
|
|
51
|
+
* compile time (`JQ0011`). The language has no recursion, so the
|
|
52
|
+
* compiled closure tree's evaluation depth IS the document's static
|
|
53
|
+
* nesting: checking it once is exact and costs nothing to evaluate.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Compile options for {@link compileJsonQuery}.
|
|
58
|
+
* @typedef {Object} JsonQueryOptions
|
|
59
|
+
* @property {(schemaJson: any, docPath: string) => ((value: any) => boolean)} [compileTypeTest]
|
|
60
|
+
* Hook compiling a JSON Schema literal into a boolean item
|
|
61
|
+
* predicate, called once per schema literal at query compile time
|
|
62
|
+
* (QUERY-FORMAT.md section 8.11). `@jarenjs/validate/query` exports
|
|
63
|
+
* `createTypeTestCompiler()` producing one; any conforming
|
|
64
|
+
* implementation works - this package never imports the validator.
|
|
65
|
+
* Without a hook, the schema operators `$valid`/`$assert`/`$as` are
|
|
66
|
+
* compile error JQ0008.
|
|
67
|
+
* @property {object} [extensions] - Package-internal operator
|
|
68
|
+
* extension point, the operator analogue of `compileTypeTest` (used
|
|
69
|
+
* by the JSLT layer; not a public contract). A plain object of
|
|
70
|
+
* `name -> entry` following the operator registry contract; see
|
|
71
|
+
* normalizeQuery in normalize.js for the full shape. The published
|
|
72
|
+
* format vocabulary is unchanged: without extensions, documents
|
|
73
|
+
* using such operators fail JQ0002.
|
|
74
|
+
* @property {Record<string, (...args: any[]) => any>} [functions]
|
|
75
|
+
* Registry of named trusted pure host functions for `$call`
|
|
76
|
+
* (QUERY-FORMAT.md section 8.12); an unregistered or empty name is
|
|
77
|
+
* rejected at compile time (JQ0010 / TypeError).
|
|
78
|
+
* @property {Record<string, (a: string, b: string) => number>} [collations]
|
|
79
|
+
* Registry of named pure compare functions for `$orderby`'s
|
|
80
|
+
* `$collation` member (QUERY-FORMAT.md section 6.6).
|
|
81
|
+
* @property {Record<string, import('../path.js').JSONPathFunction>} [pathFunctions]
|
|
82
|
+
* Registry of custom JSONPath function extensions (RFC 9535 section
|
|
83
|
+
* 2.4), available inside the filters of every path string the
|
|
84
|
+
* document contains. Deliberately separate from `functions`: that
|
|
85
|
+
* registry extends the query vocabulary through `$call`, this one
|
|
86
|
+
* extends the RFC 9535 grammar the path strings are written in.
|
|
87
|
+
* @property {JsonQueryLimits} [limits] - Enforced execution limits.
|
|
88
|
+
* @property {readonly string[]} [externals] - Closed-world compilation:
|
|
89
|
+
* the variable names (no `$` sigil) the document may leave free.
|
|
90
|
+
* Every other free variable is compile error `JQ0005` at its own
|
|
91
|
+
* reference site, so a query cannot silently acquire a parameter the
|
|
92
|
+
* host never meant to expose. `[]` declares none. Omitted, the open
|
|
93
|
+
* world of QUERY-FORMAT.md section 9 applies: use is the declaration.
|
|
94
|
+
* @property {boolean} [analysis] - On `compileJsonQuery`: additionally
|
|
95
|
+
* expose the normalized-form record (QUERY-FORMAT.md Appendix C.1) at
|
|
96
|
+
* `query.analysis`, from the same normalization. Compilation itself
|
|
97
|
+
* stays strict — schema hooks remain required.
|
|
98
|
+
*/
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The published normalized-form record (QUERY-FORMAT.md Appendix C.1):
|
|
102
|
+
* what `analyzeQuery` returns and `compileJsonQuery`'s `analysis`
|
|
103
|
+
* option exposes.
|
|
104
|
+
* @typedef {Object} JsonQueryAnalysis
|
|
105
|
+
* @property {number} astVersion - see the compatibility policy (C.7)
|
|
106
|
+
* @property {object} root - the frozen node tree (C.3)
|
|
107
|
+
* @property {readonly { name: string, slot: number }[]} externals
|
|
108
|
+
* @property {number} frameSize
|
|
109
|
+
* @property {Readonly<JsonQueryDependencies>} dependencies
|
|
110
|
+
* @property {object | null} limits
|
|
111
|
+
*/
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The frozen dependency record of a compiled query (saved-rule
|
|
115
|
+
* vetting): the external names it binds, the operators it uses, and
|
|
116
|
+
* the registered functions/collations it resolved.
|
|
117
|
+
* @typedef {Object} JsonQueryDependencies
|
|
118
|
+
* @property {readonly string[]} externals
|
|
119
|
+
* @property {readonly string[]} operators
|
|
120
|
+
* @property {readonly string[]} functions
|
|
121
|
+
* @property {readonly string[]} collations
|
|
122
|
+
*/
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A plain-JSON explanation of a compiled query: its dependencies plus
|
|
126
|
+
* the enforced limits. A fresh value each `explain()` call.
|
|
127
|
+
* @typedef {Object} JsonQueryExplanation
|
|
128
|
+
* @property {string[]} externals
|
|
129
|
+
* @property {string[]} operators
|
|
130
|
+
* @property {string[]} functions
|
|
131
|
+
* @property {string[]} collations
|
|
132
|
+
* @property {{ sequenceItems: number | null, resultItems: number | null, steps: number | null, depth: number | null } | null} limits
|
|
133
|
+
*/
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The compiled query returned by {@link compileJsonQuery}: the query
|
|
137
|
+
* function itself, carrying its helper methods and metadata.
|
|
138
|
+
* @typedef {((data: any, externals?: Record<string, any>) => any) & {
|
|
139
|
+
* first: (data: any, externals?: Record<string, any>) => any,
|
|
140
|
+
* exists: (data: any, externals?: Record<string, any>) => boolean,
|
|
141
|
+
* ebv: (data: any, externals?: Record<string, any>) => boolean,
|
|
142
|
+
* externals: readonly string[],
|
|
143
|
+
* doc: any,
|
|
144
|
+
* dependencies: Readonly<JsonQueryDependencies>,
|
|
145
|
+
* explain: () => JsonQueryExplanation,
|
|
146
|
+
* analysis?: Readonly<JsonQueryAnalysis>,
|
|
147
|
+
* }} CompiledJsonQuery
|
|
148
|
+
*/
|
|
149
|
+
|
|
24
150
|
/**
|
|
25
151
|
* Compile a Jaren JSON Query document into a reusable query function.
|
|
26
152
|
*
|
|
27
153
|
* The returned function applies the query to a JSON value and returns the
|
|
28
154
|
* result as plain JSON: `undefined` for the empty sequence, the item
|
|
29
155
|
* itself for a singleton result, an array of items for a longer sequence.
|
|
156
|
+
*
|
|
157
|
+
* The `data` argument MUST be a JSON value (section 2.1). The engine does
|
|
158
|
+
* not deep-validate it - that would cost a full walk per call - so a
|
|
159
|
+
* non-JSON value inside the document simply flows through as an opaque
|
|
160
|
+
* item. The single exception is `undefined`, rejected with `JQ2011`
|
|
161
|
+
* because this API already spends `undefined` on the empty sequence.
|
|
162
|
+
* An external bound to `undefined` reads as unbound (`JQ2006` on use).
|
|
163
|
+
*
|
|
30
164
|
* It also carries helper methods and metadata:
|
|
31
165
|
*
|
|
32
166
|
* - `query(data, externals?)` - the query result as described above
|
|
@@ -44,26 +178,15 @@ const hasOwn = Object.hasOwn;
|
|
|
44
178
|
* external raises `JQ2006`.
|
|
45
179
|
* - `query.doc` - a deeply frozen copy of the query document (the
|
|
46
180
|
* caller's object is never frozen)
|
|
181
|
+
* - `query.dependencies` - what the query depends on (frozen JSON):
|
|
182
|
+
* external names, operators, registered functions and collations
|
|
183
|
+
* - `query.explain()` - a fresh plain-JSON explanation: the
|
|
184
|
+
* dependencies plus the enforced limits
|
|
47
185
|
*
|
|
48
186
|
* @param {any} doc - the query document (any JSON value; a bare RFC 9535
|
|
49
187
|
* JSONPath string is the degenerate query)
|
|
50
|
-
* @param {
|
|
51
|
-
* @
|
|
52
|
-
* [options.compileTypeTest] - hook compiling a JSON Schema literal into
|
|
53
|
-
* a boolean item predicate, called once per schema literal at query
|
|
54
|
-
* compile time (QUERY-FORMAT.md section 8.11). `@jarenjs/validate/query`
|
|
55
|
-
* exports `createTypeTestCompiler()` producing one; any conforming
|
|
56
|
-
* implementation works - this package never imports the validator.
|
|
57
|
-
* Without a hook, the schema operators `$valid`/`$assert`/`$as` are
|
|
58
|
-
* compile error JQ0008.
|
|
59
|
-
* @param {object} [options.extensions] - package-internal operator
|
|
60
|
-
* extension point, the operator analogue of `compileTypeTest` (used by
|
|
61
|
-
* the JSLT layer; not a public contract). A plain object of
|
|
62
|
-
* `name -> entry` following the operator registry contract; see
|
|
63
|
-
* normalizeQuery in normalize.js for the full shape. The published
|
|
64
|
-
* format vocabulary is unchanged: without extensions, documents using
|
|
65
|
-
* such operators fail JQ0002.
|
|
66
|
-
* @returns {function} the compiled query function
|
|
188
|
+
* @param {JsonQueryOptions} [options] - compile options
|
|
189
|
+
* @returns {CompiledJsonQuery} the compiled query function
|
|
67
190
|
* @throws {JsonQueryCompileError} when the document violates the format
|
|
68
191
|
* @example
|
|
69
192
|
* const q = compileJsonQuery({
|
|
@@ -74,16 +197,40 @@ const hasOwn = Object.hasOwn;
|
|
|
74
197
|
* q(data, { max: 10 }); // { title: 'Sayings of the Century', cheap: true }
|
|
75
198
|
*/
|
|
76
199
|
export function compileJsonQuery(doc, options = {}) {
|
|
77
|
-
|
|
78
|
-
|
|
200
|
+
// `analysis: true` additionally exposes the normalized-form record at
|
|
201
|
+
// `query.analysis` (QUERY-FORMAT.md Appendix C.1) from the SAME
|
|
202
|
+
// normalization — but compilation itself stays strict (the flag is
|
|
203
|
+
// stripped before normalizeQuery, so schema hooks stay required).
|
|
204
|
+
const wantAnalysis = options.analysis === true;
|
|
205
|
+
const normalized =
|
|
206
|
+
normalizeQuery(doc, wantAnalysis ? { ...options, analysis: false } : options);
|
|
207
|
+
const { root, frameSize, externals, limits, stepSlot, usedOps, usedFunctions, usedCollations } =
|
|
208
|
+
normalized;
|
|
209
|
+
const get = compileQueryRoot(root,
|
|
210
|
+
stepSlot < 0 ? null : { slot: stepSlot, limit: limits.steps });
|
|
79
211
|
const extCount = externals.length;
|
|
212
|
+
const resultCap = limits !== null && limits.resultItems !== null ? limits.resultItems : 0;
|
|
80
213
|
|
|
81
214
|
function evaluate(data, ext) {
|
|
215
|
+
// The data model is JSON (section 2.1), and the engine does not
|
|
216
|
+
// deep-validate its input - that would be an O(size) walk on every
|
|
217
|
+
// call. The one violation that must not pass silently is `undefined`,
|
|
218
|
+
// because this API already spends `undefined` on the empty sequence:
|
|
219
|
+
// `query(undefined)` would answer "empty" while `query.exists(...)`
|
|
220
|
+
// answered true. Every other non-JSON value flows through as an
|
|
221
|
+
// opaque item, which is the caller's contract to keep.
|
|
222
|
+
if (data === undefined)
|
|
223
|
+
throw new JsonQueryRuntimeError('JQ2011', 'the input document is undefined, which is not a JSON value', '');
|
|
82
224
|
const frame = new Array(frameSize);
|
|
83
225
|
frame[0] = data;
|
|
226
|
+
if (stepSlot >= 0)
|
|
227
|
+
frame[stepSlot] = 0;
|
|
84
228
|
for (let i = 0; i < extCount; i++) {
|
|
85
229
|
const e = externals[i];
|
|
86
|
-
|
|
230
|
+
// an external explicitly bound to `undefined` reads as unbound, so
|
|
231
|
+
// the reference raises JQ2006 instead of yielding a non-JSON item
|
|
232
|
+
const v = ext != null && hasOwn(ext, e.name) ? ext[e.name] : undefined;
|
|
233
|
+
frame[e.slot] = v === undefined ? UNBOUND : v;
|
|
87
234
|
}
|
|
88
235
|
return get(frame);
|
|
89
236
|
}
|
|
@@ -92,7 +239,13 @@ export function compileJsonQuery(doc, options = {}) {
|
|
|
92
239
|
const v = evaluate(data, ext);
|
|
93
240
|
if (v === EMPTY)
|
|
94
241
|
return undefined;
|
|
95
|
-
|
|
242
|
+
if (v instanceof Seq) {
|
|
243
|
+
if (resultCap > 0 && v.items.length > resultCap)
|
|
244
|
+
throw new JsonQueryRuntimeError('JQ2009',
|
|
245
|
+
`the query result has ${v.items.length} items, more than limits.resultItems (${resultCap})`, '');
|
|
246
|
+
return v.items;
|
|
247
|
+
}
|
|
248
|
+
return v;
|
|
96
249
|
};
|
|
97
250
|
query.first = (data, ext) => {
|
|
98
251
|
const v = evaluate(data, ext);
|
|
@@ -104,18 +257,107 @@ export function compileJsonQuery(doc, options = {}) {
|
|
|
104
257
|
query.ebv = (data, ext) => ebv(evaluate(data, ext), '');
|
|
105
258
|
query.externals = Object.freeze(externals.map((e) => e.name));
|
|
106
259
|
query.doc = deepFreezeCopy(doc);
|
|
260
|
+
/**
|
|
261
|
+
* What the compiled query depends on (saved-rule vetting): the
|
|
262
|
+
* external names it binds, the operators it uses, and the registered
|
|
263
|
+
* functions/collations it resolved. All frozen JSON.
|
|
264
|
+
*/
|
|
265
|
+
query.dependencies = Object.freeze({
|
|
266
|
+
externals: query.externals,
|
|
267
|
+
operators: Object.freeze([...usedOps].sort()),
|
|
268
|
+
functions: Object.freeze([...usedFunctions].sort()),
|
|
269
|
+
collations: Object.freeze([...usedCollations].sort()),
|
|
270
|
+
});
|
|
271
|
+
/**
|
|
272
|
+
* A plain-JSON explanation of the compiled query: its dependencies
|
|
273
|
+
* plus the enforced limits. A fresh value each call.
|
|
274
|
+
* @returns {JsonQueryExplanation}
|
|
275
|
+
*/
|
|
276
|
+
query.explain = () => ({
|
|
277
|
+
externals: [...query.externals],
|
|
278
|
+
operators: [...query.dependencies.operators],
|
|
279
|
+
functions: [...query.dependencies.functions],
|
|
280
|
+
collations: [...query.dependencies.collations],
|
|
281
|
+
limits: limits === null ? null : {
|
|
282
|
+
sequenceItems: limits.sequenceItems,
|
|
283
|
+
resultItems: limits.resultItems,
|
|
284
|
+
steps: limits.steps,
|
|
285
|
+
depth: limits.depth,
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
if (wantAnalysis)
|
|
289
|
+
query.analysis = analysisRecord(normalized);
|
|
107
290
|
return query;
|
|
108
291
|
}
|
|
109
292
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
293
|
+
/**
|
|
294
|
+
* The version of the published normalized form (QUERY-FORMAT.md
|
|
295
|
+
* Appendix C.7): bumped when a node kind is added or removed, a
|
|
296
|
+
* published field is removed or retyped, or an appendix invariant
|
|
297
|
+
* changes. Adding an optional field is NOT a bump.
|
|
298
|
+
*/
|
|
299
|
+
export const AST_VERSION = 1;
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Build the frozen analysis record from a `normalizeQuery` result —
|
|
303
|
+
* shared by `analyzeQuery` and `compileJsonQuery`'s `analysis` option
|
|
304
|
+
* so both expose byte-identical shapes from one normalization.
|
|
305
|
+
* @param {ReturnType<typeof normalizeQuery>} normalized
|
|
306
|
+
*/
|
|
307
|
+
function analysisRecord(normalized) {
|
|
308
|
+
const { root, frameSize, externals, limits, usedOps, usedFunctions, usedCollations } = normalized;
|
|
309
|
+
return Object.freeze({
|
|
310
|
+
astVersion: AST_VERSION,
|
|
311
|
+
root,
|
|
312
|
+
externals,
|
|
313
|
+
frameSize,
|
|
314
|
+
dependencies: Object.freeze({
|
|
315
|
+
externals: Object.freeze(externals.map((e) => e.name)),
|
|
316
|
+
operators: Object.freeze([...usedOps].sort()),
|
|
317
|
+
functions: Object.freeze([...usedFunctions].sort()),
|
|
318
|
+
collations: Object.freeze([...usedCollations].sort()),
|
|
319
|
+
}),
|
|
320
|
+
limits,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Analyse a query document WITHOUT compiling it: the engine's own
|
|
326
|
+
* normalized reading of the document — the frozen node tree, the
|
|
327
|
+
* externals in first-appearance order, the frame size and the
|
|
328
|
+
* dependency sets — published as the versioned contract of
|
|
329
|
+
* QUERY-FORMAT.md Appendix C. Another package walks this instead of
|
|
330
|
+
* re-implementing the grammar, and an unknown `kind` in its dispatch is
|
|
331
|
+
* a loud failure instead of a silent divergence.
|
|
332
|
+
*
|
|
333
|
+
* Analysis applies every JQ0xxx rejection compilation would, with one
|
|
334
|
+
* difference (Appendix C.1): schema literals do not require
|
|
335
|
+
* `options.compileTypeTest` — without the hook they normalize to `raw`
|
|
336
|
+
* nodes whose `test` is `null`, so a document can be analysed by a
|
|
337
|
+
* consumer that could not execute it. With the hook supplied, analysis
|
|
338
|
+
* compiles the predicates exactly as compilation would.
|
|
339
|
+
* @param {any} doc - the query document (any JSON value)
|
|
340
|
+
* @param {JsonQueryOptions} [options] - the same options as
|
|
341
|
+
* `compileJsonQuery`
|
|
342
|
+
* @returns {{ astVersion: number, root: object, externals: readonly
|
|
343
|
+
* { name: string, slot: number }[], frameSize: number,
|
|
344
|
+
* dependencies: Readonly<JsonQueryDependencies>, limits: object | null }}
|
|
345
|
+
* @throws {JsonQueryCompileError} on any JQ0xxx condition (except
|
|
346
|
+
* JQ0008, which analysis does not raise)
|
|
347
|
+
*/
|
|
348
|
+
export function analyzeQuery(doc, options = {}) {
|
|
349
|
+
return analysisRecord(normalizeQuery(doc, { ...options, analysis: true }));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const OBJECT_CACHE = createWeakCache();
|
|
353
|
+
const STRING_CACHE = createBoundedCache(512);
|
|
354
|
+
const compileUncached = (/** @type {any} */ doc) => compileJsonQuery(doc);
|
|
113
355
|
|
|
114
356
|
/**
|
|
115
357
|
* Apply a Jaren JSON Query document to a JSON value in one call.
|
|
116
|
-
* Compiled queries are cached: object documents by identity (
|
|
117
|
-
* string documents (the degenerate JSONPath case) by value (
|
|
118
|
-
* entries
|
|
358
|
+
* Compiled queries are cached: object documents by identity (weak),
|
|
359
|
+
* string documents (the degenerate JSONPath case) by value (bounded
|
|
360
|
+
* LRU, 512 entries — the shared `@jarenjs/core/cache` primitive).
|
|
119
361
|
* @param {any} doc - the query document
|
|
120
362
|
* @param {any} data - the JSON value to query
|
|
121
363
|
* @param {object} [externals] - external parameter bindings (`{ name: value }`)
|
|
@@ -126,20 +368,10 @@ const STRING_CACHE_LIMIT = 512;
|
|
|
126
368
|
export function queryJson(doc, data, externals) {
|
|
127
369
|
let query;
|
|
128
370
|
if (typeof doc === 'string') {
|
|
129
|
-
query = STRING_CACHE.
|
|
130
|
-
if (query === undefined) {
|
|
131
|
-
query = compileJsonQuery(doc);
|
|
132
|
-
if (STRING_CACHE.size >= STRING_CACHE_LIMIT)
|
|
133
|
-
STRING_CACHE.delete(STRING_CACHE.keys().next().value);
|
|
134
|
-
STRING_CACHE.set(doc, query);
|
|
135
|
-
}
|
|
371
|
+
query = STRING_CACHE.getOrCreate(doc, compileUncached);
|
|
136
372
|
}
|
|
137
373
|
else if (typeof doc === 'object' && doc !== null) {
|
|
138
|
-
query = OBJECT_CACHE.
|
|
139
|
-
if (query === undefined) {
|
|
140
|
-
query = compileJsonQuery(doc);
|
|
141
|
-
OBJECT_CACHE.set(doc, query);
|
|
142
|
-
}
|
|
374
|
+
query = OBJECT_CACHE.getOrCreate(doc, compileUncached);
|
|
143
375
|
}
|
|
144
376
|
else { // scalar documents are trivial literals; compiling is cheaper than caching
|
|
145
377
|
query = compileJsonQuery(doc);
|