@osqd/jql 0.1.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 +220 -0
- package/LICENSE +102 -0
- package/README.md +84 -0
- package/bin/jql.mjs +5 -0
- package/conformance/cases.json +290 -0
- package/dist/array.d.ts +11 -0
- package/dist/async.d.ts +44 -0
- package/dist/canonical.d.ts +18 -0
- package/dist/cjs/array.d.ts +11 -0
- package/dist/cjs/async.d.ts +44 -0
- package/dist/cjs/canonical.d.ts +18 -0
- package/dist/cjs/cli.d.ts +15 -0
- package/dist/cjs/collections.d.ts +34 -0
- package/dist/cjs/core.d.ts +117 -0
- package/dist/cjs/errors.d.ts +17 -0
- package/dist/cjs/explain.d.ts +30 -0
- package/dist/cjs/global.d.ts +90 -0
- package/dist/cjs/group.d.ts +47 -0
- package/dist/cjs/index.d.ts +25 -0
- package/dist/cjs/internal/closest.d.ts +9 -0
- package/dist/cjs/internal/duration.d.ts +15 -0
- package/dist/cjs/internal/equal.d.ts +34 -0
- package/dist/cjs/internal/glob.d.ts +30 -0
- package/dist/cjs/internal/order.d.ts +24 -0
- package/dist/cjs/internal/path.d.ts +109 -0
- package/dist/cjs/internal/record.d.ts +18 -0
- package/dist/cjs/internal/values.d.ts +41 -0
- package/dist/cjs/limits.d.ts +64 -0
- package/dist/cjs/operators.d.ts +81 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/plan.d.ts +79 -0
- package/dist/cjs/search.d.ts +42 -0
- package/dist/cjs/targets/mongo.d.ts +55 -0
- package/dist/cjs/text/index.d.ts +12 -0
- package/dist/cjs/text/parse.d.ts +91 -0
- package/dist/cjs/text/suggest.d.ts +16 -0
- package/dist/cjs/text/write.d.ts +34 -0
- package/dist/cjs/types.d.ts +236 -0
- package/dist/cjs/vocabulary.d.ts +106 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +2729 -0
- package/dist/cli.js.map +1 -0
- package/dist/collections.d.ts +34 -0
- package/dist/core.d.ts +117 -0
- package/dist/errors.d.ts +17 -0
- package/dist/explain.d.ts +30 -0
- package/dist/global.cjs +1953 -0
- package/dist/global.cjs.map +1 -0
- package/dist/global.d.ts +90 -0
- package/dist/global.js +1950 -0
- package/dist/global.js.map +1 -0
- package/dist/group.d.ts +47 -0
- package/dist/index.cjs +2529 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +2495 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/closest.d.ts +9 -0
- package/dist/internal/duration.d.ts +15 -0
- package/dist/internal/equal.d.ts +34 -0
- package/dist/internal/glob.d.ts +30 -0
- package/dist/internal/order.d.ts +24 -0
- package/dist/internal/path.d.ts +109 -0
- package/dist/internal/record.d.ts +18 -0
- package/dist/internal/values.d.ts +41 -0
- package/dist/limits.d.ts +64 -0
- package/dist/mongo.cjs +357 -0
- package/dist/mongo.cjs.map +1 -0
- package/dist/mongo.js +354 -0
- package/dist/mongo.js.map +1 -0
- package/dist/operators.d.ts +81 -0
- package/dist/plan.d.ts +79 -0
- package/dist/search.d.ts +42 -0
- package/dist/targets/mongo.d.ts +55 -0
- package/dist/text/index.d.ts +12 -0
- package/dist/text/parse.d.ts +91 -0
- package/dist/text/suggest.d.ts +16 -0
- package/dist/text/write.d.ts +34 -0
- package/dist/text.cjs +674 -0
- package/dist/text.cjs.map +1 -0
- package/dist/text.js +667 -0
- package/dist/text.js.map +1 -0
- package/dist/types.d.ts +236 -0
- package/dist/vocabulary.d.ts +106 -0
- package/docs/course/01-first-query.md +217 -0
- package/docs/course/02-operators.md +285 -0
- package/docs/course/03-arrays-and-paths.md +239 -0
- package/docs/course/04-combining.md +221 -0
- package/docs/course/05-dates.md +214 -0
- package/docs/course/06-typed-queries.md +240 -0
- package/docs/course/07-requests.md +261 -0
- package/docs/course/08-grouping.md +210 -0
- package/docs/course/09-explaining.md +171 -0
- package/docs/course/10-vocabulary.md +276 -0
- package/docs/course/11-the-search-box.md +349 -0
- package/docs/course/12-untrusted.md +257 -0
- package/docs/course/13-saved-filters.md +199 -0
- package/docs/course/14-streams-and-cli.md +276 -0
- package/docs/course/15-pushdown.md +240 -0
- package/docs/course/16-extending.md +199 -0
- package/docs/course/index.md +185 -0
- package/docs/design/decisions.md +198 -0
- package/docs/design/performance.md +102 -0
- package/docs/guides/adopting.md +81 -0
- package/docs/guides/pushdown.md +147 -0
- package/docs/guides/typescript.md +115 -0
- package/docs/guides/untrusted-input.md +86 -0
- package/docs/index.md +102 -0
- package/docs/reference/api.md +266 -0
- package/docs/reference/cli.md +103 -0
- package/docs/reference/index.md +12 -0
- package/docs/reference/specification.md +549 -0
- package/docs/reference/text-syntax.md +152 -0
- package/docs/start/quick-start.md +84 -0
- package/package.json +136 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **The JSON Query Language, version 1.1.** A specification in `docs/reference/specification.md`
|
|
8
|
+
and a conformance suite in `conformance/cases.json` of 156 matching cases and 27 request
|
|
9
|
+
cases that any implementation, in any language, must pass. The operator names are the
|
|
10
|
+
conventional ones, and the array rules are written out in full rather than left to a
|
|
11
|
+
reader's memory of another system. 1.1 is 1.0 plus `$glob`, `$length`, `{ "$field": … }`, relative dates and `omit`;
|
|
12
|
+
since nothing has been released yet, both arrive together here.
|
|
13
|
+
- **An engine that compiles each query once into closures.** Within about 1.1–1.5× of a
|
|
14
|
+
hand-written loop for most queries, with no code generation, so it runs under any
|
|
15
|
+
Content-Security-Policy.
|
|
16
|
+
- **Collection helpers** — `find`, `filter`, `count`, `some`, `every`, `partition`, `findIndex`,
|
|
17
|
+
`filterMap`, `filterRecord`, `findEntry` — over arrays, array-likes, iterables, `Map`s by value
|
|
18
|
+
and plain objects.
|
|
19
|
+
- **`search`**, which runs a whole request — `where`, `sort`, `skip`, `limit`, `fields` — with a
|
|
20
|
+
bounded heap for top-k and an early exit for unsorted pages.
|
|
21
|
+
- **Typed queries.** `Query<T>` checks paths five levels deep, through arrays, and offers each
|
|
22
|
+
operator only where it fits.
|
|
23
|
+
- **`validate` returns the predicate it compiled**, so a service checks and runs one query
|
|
24
|
+
rather than compiling twice — and cannot check against the untrusted limits and then run
|
|
25
|
+
without them.
|
|
26
|
+
- **`@osqd/jql/global`**, which adds `jqlSearch`, `jqlFilter` and the rest to `Array`, `Map` and
|
|
27
|
+
`Set`, with their types, only when imported.
|
|
28
|
+
- **Vocabularies** of named, aliased and computed fields, shared by the engine and the text
|
|
29
|
+
syntax.
|
|
30
|
+
- **`@osqd/jql/text`**, BotHandler's feed-filter syntax, compiling into JQL documents, with
|
|
31
|
+
numeric ranges, `>=` and `<=`, date comparisons, boolean fields, `has:field` and signed
|
|
32
|
+
durations (`at:>-1h`) added.
|
|
33
|
+
- **Comparing one field with another**, written `{ "bytesOut": { "$gt": { "$field": "bytesIn" } } }`.
|
|
34
|
+
A reference is a path, not an expression, so a query holding one still stores, travels and
|
|
35
|
+
validates like any other — and unlike `$expr`, it brings no expression language with it.
|
|
36
|
+
- **`$glob`** — `*`, `?` and `\` — for the pattern people mean when they reach for one. It
|
|
37
|
+
needs no regular-expression engine, cannot backtrack exponentially, and stays available
|
|
38
|
+
when `$regex` is turned off for untrusted callers.
|
|
39
|
+
- **`$length`**, for the length of a string (UTF-16 code units) or an array, so a cap on an
|
|
40
|
+
incoming value no longer needs a pattern to express.
|
|
41
|
+
- **Relative dates.** `{ "$date": { "$ago": "1h" } }`, `{ "$ahead": … }` and `"now"`, resolved
|
|
42
|
+
once when the query compiles against a clock the caller may supply. A saved filter that
|
|
43
|
+
means "the last hour" goes on meaning it.
|
|
44
|
+
- **`omit` in a request**, applied after `fields`, so a server's redaction is data like the
|
|
45
|
+
rest of the request. Results are copies; the items are never touched.
|
|
46
|
+
- **`group`**, which counts the matches by a field — what every dashboard in this family was
|
|
47
|
+
writing for itself, with the three awkward questions (an item with two values, items with
|
|
48
|
+
none, and the order of the groups) answered once.
|
|
49
|
+
- **`explain`**, which says why one item does or does not match, clause by clause, with the
|
|
50
|
+
values it read. It runs the real engine on each clause, so an explanation cannot disagree
|
|
51
|
+
with the filter it explains.
|
|
52
|
+
- **`canonical` and `fingerprint`**, one shape and one short name per meaning, for storing
|
|
53
|
+
saved filters without duplicates and for cache keys.
|
|
54
|
+
- **Asynchronous sources** — `findAsync`, `filterAsync`, `countAsync`, `someAsync`,
|
|
55
|
+
`everyAsync`, `filterStream` and `searchAsync` — for a log read line by line or a cursor
|
|
56
|
+
over a store. They stop reading as soon as the answer cannot change, so an endless source
|
|
57
|
+
still answers "the first twenty matches".
|
|
58
|
+
- **Operators a project can add**, named `$x…` so a query that needs more than a standard
|
|
59
|
+
engine says so, and refused by name when the engine was not given them.
|
|
60
|
+
- **An operator allowlist.** `limits.allowOperators` names what a query may use, so an
|
|
61
|
+
endpoint that has no use for `$text` or `$elemMatch` says so once rather than finding out
|
|
62
|
+
later which of them somebody used. A name in the list that is not an operator is refused
|
|
63
|
+
too: an allowlist with a typo in it would allow nothing while looking like it allowed
|
|
64
|
+
something.
|
|
65
|
+
- **`toText`**, the reverse of the search-box syntax, for a dashboard that stores filters as
|
|
66
|
+
JSON and has to put one back in the box. Deliberately not total: it returns the text *and*
|
|
67
|
+
what it could not say, because showing only the sayable half would show a filter wider than
|
|
68
|
+
the one running.
|
|
69
|
+
- **`plan(query, capabilities)`**, which splits a query into the part a store can answer and
|
|
70
|
+
the part that stays here, with `pushed ∧ remaining ≡ query` as the contract, and
|
|
71
|
+
**`@osqd/jql/mongo`** as the first target. Operators MongoDB cannot answer exactly are left
|
|
72
|
+
out of its capabilities, so they are never approximated.
|
|
73
|
+
- **`jql`, a command**, for filtering JSON lines from a shell: `jql --text 'status:open'
|
|
74
|
+
app.jsonl`. stdout is the artifact and stderr is the conversation, `--limit all` is an
|
|
75
|
+
error rather than a limit that stops limiting, and it is built from the package's own
|
|
76
|
+
exports.
|
|
77
|
+
- **A course**: sixteen lessons in `docs/course/`, building one order desk from a first query
|
|
78
|
+
to a filter behind a public endpoint — the language, typed queries, requests, grouping,
|
|
79
|
+
explanations, vocabularies, the search box both ways, untrusted input, saved filters,
|
|
80
|
+
streams and the command line, store pushdown, and operators of your own. Every checkpoint in
|
|
81
|
+
it was produced by running the code, and `check:package` runs all fifteen runnable lessons
|
|
82
|
+
against the packed tarball so they cannot drift from what the library does. Writing it found
|
|
83
|
+
two defects, both listed below.
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
|
|
87
|
+
- **A path longer than 64 segments matched but was left out of the result.** The engine
|
|
88
|
+
followed a path to any depth while `fields` walked its own tree and stopped at sixty-four,
|
|
89
|
+
silently, so a request could match an item on `a.b.c…` and hand back a copy without it —
|
|
90
|
+
one name meaning two things inside one request. Matching, sorting, projection and redaction
|
|
91
|
+
now share one limit of 512 segments (specification §3), and a longer path is refused rather
|
|
92
|
+
than half-applied. A field name is also the one part of a query the operator count does not
|
|
93
|
+
bound, so the cap bounds it.
|
|
94
|
+
- **A refusal quoted the whole field name back.** A location is built from the query's own
|
|
95
|
+
names, so a long one buried the sentence explaining it; the message now shortens it, while
|
|
96
|
+
`JqlError.at` still carries the whole path for a program to read.
|
|
97
|
+
- **`toText` turned the narrowest query in the language into the widest.** `{ "$not": {} }`
|
|
98
|
+
matches nothing, the empty query matches everything, and a negation holding nothing sayable
|
|
99
|
+
was written as no text at all — which reads back as the empty query — while reporting
|
|
100
|
+
`complete: true`. It is reported now, like the `$or` of nothing it mirrors.
|
|
101
|
+
|
|
102
|
+
- **`--group` accepted five options and then dropped them.** `--sort`, `--skip`, `--fields`,
|
|
103
|
+
`--omit` and `--count` were read and never looked at again, because the grouping path
|
|
104
|
+
builds its own answer: `jql --group status --fields id` printed exactly what
|
|
105
|
+
`jql --group status` printed, with nothing to say the option had gone nowhere. They are
|
|
106
|
+
refused now, and `--limit` — the one that has an obvious meaning beside groups — says how
|
|
107
|
+
many groups to print.
|
|
108
|
+
- **A vocabulary that was not one crashed with a `TypeError` from inside the engine.**
|
|
109
|
+
`defineVocabulary` is curried, so the empty parentheses are easy to forget, and a
|
|
110
|
+
definition looks enough like a vocabulary to pass by eye; either arrived as "Cannot read
|
|
111
|
+
properties of undefined (reading 'get')". It is refused by name now, which also means
|
|
112
|
+
`validate` answers with a verdict rather than re-throwing, and `parseText` refuses the
|
|
113
|
+
mistake in the code rather than failing later on the text.
|
|
114
|
+
- **The MongoDB target dropped a pattern flag it could not use and pushed `$type: "bigint"`.**
|
|
115
|
+
`keep` filtered out the flags MongoDB does not take while calling itself the lock that
|
|
116
|
+
stops them, and a dropped flag is a different pattern; `bigint` names a run-time type JSON
|
|
117
|
+
cannot carry, so the store answered with rows the engine says are not bigints while
|
|
118
|
+
`complete` said no second pass was needed.
|
|
119
|
+
- **A text search checked its `$fields` only when the phrase was not empty.** So
|
|
120
|
+
`{ "$search": "", "$fields": 42 }` compiled, and a dashboard that validated its saved
|
|
121
|
+
filter while the search box was empty was told it was a good one — until somebody typed a
|
|
122
|
+
letter and the same filter was refused. Whether a query is valid cannot depend on how much
|
|
123
|
+
of it has been typed (specification §6.3).
|
|
124
|
+
- **The MongoDB target was the one rewriting checked by its shape rather than by its
|
|
125
|
+
answers.** Its tests asserted what the filter looked like, which cannot catch a filter that
|
|
126
|
+
is *nearly* right — the failure the module exists to avoid. It is now run against a second
|
|
127
|
+
reading of MongoDB's matching rules, written from that database's documented behaviour, over
|
|
128
|
+
generated queries and documents. The first thing that found: **an array held directly inside
|
|
129
|
+
another array** is traversed by JQL at every level and by MongoDB only one level down, so
|
|
130
|
+
the two answer differently in either direction and no capability can say so, because the
|
|
131
|
+
shape is in the data rather than in the query. Written down in specification §11, in the
|
|
132
|
+
pushdown guide and beside the code.
|
|
133
|
+
- **The same filter had two names depending on who wrote it.** `canonical` promised that
|
|
134
|
+
`{ "$and": [x, y] }` and `{ …x, …y }` are one filter and did not deliver it, so a search
|
|
135
|
+
typed into the box — which compiles to an `$and` for every multi-term query — and the same
|
|
136
|
+
filter written as JSON by hand were saved as two, with two fingerprints, which is the
|
|
137
|
+
duplicate the whole module exists to prevent. A part of an `$and` is now lifted beside its
|
|
138
|
+
siblings when every key it holds is free; one whose field is already spoken for stays put,
|
|
139
|
+
because merging two conditions on one field would change which operators an `$options`
|
|
140
|
+
beside them reaches. **This changes the fingerprint of any filter written with a top-level
|
|
141
|
+
`$and`** — stored keys of that shape are invalidated, and the rule is in specification §14.
|
|
142
|
+
Found while writing the course.
|
|
143
|
+
- **`group` left a row out of the tally when its value was an empty list.** Every other value
|
|
144
|
+
that cannot name a group — a missing field, a `null`, an object — already shared `null`, and
|
|
145
|
+
an empty array fell through both: counting by a tag quietly omitted every untagged row, so
|
|
146
|
+
the numbers above a table did not add up to the number of rows in it and nothing said which
|
|
147
|
+
had gone. It was also inconsistent with the same shape met *on the way* down a path, which
|
|
148
|
+
had always counted as `null`. Found while writing the course.
|
|
149
|
+
- **A timestamp with no offset meant a different instant on every machine.** JavaScript reads
|
|
150
|
+
a date-only string as midnight UTC and `"2026-01-01T12:00:00"` as *local* time, so the same
|
|
151
|
+
query over the same document answered one way in Tokyo and another in London — the failure
|
|
152
|
+
the language already rules out for `"12/31/2020"`, reached by a shape that looks entirely
|
|
153
|
+
ordinary. A missing offset is UTC now, on both sides of a comparison, which is the rule the
|
|
154
|
+
date-only form already followed (specification §7). Three conformance cases pin it: they
|
|
155
|
+
pass in every zone with the fix and fail in Tokyo without it.
|
|
156
|
+
- **TypeScript told every CommonJS consumer the package could not be required.** It ships a
|
|
157
|
+
`.cjs` build for each entry that has one, and `require("@osqd/jql")` works — but a single
|
|
158
|
+
`types` condition pointed at declarations that `"type": "module"` marks as an ES module, so
|
|
159
|
+
under `node16` resolution a CommonJS project got `TS1479` on every import of a package it
|
|
160
|
+
could actually load. The declarations are now published twice, the second copy under
|
|
161
|
+
`dist/cjs` with a `package.json` saying what it is, and each `exports` entry carries the
|
|
162
|
+
`types` for its own condition. `check:package` type-checks a consumer both ways round, which
|
|
163
|
+
is what would have caught it.
|
|
164
|
+
- **The subpath entries could not be found under the legacy TypeScript resolution.**
|
|
165
|
+
`moduleResolution: "node"` ignores the `exports` map, so `@osqd/jql/text`, `/global`,
|
|
166
|
+
`/mongo` and `/cli` had no declarations for a project still on that setting, although Node
|
|
167
|
+
itself loads them either way. A `typesVersions` map points each one at its declarations;
|
|
168
|
+
`check:package` now type-checks a consumer under all four resolutions a real project uses —
|
|
169
|
+
`node16` as an ES module and as CommonJS, a bundler's, and the legacy one.
|
|
170
|
+
- **A projected request was typed as whole items.** `search(rows, { fields: ["id"] })` returns
|
|
171
|
+
new objects holding only those paths, and said it returned `Row[]`: the overload's
|
|
172
|
+
intersection asked for `readonly string[]` where `Request` says `readonly FieldName<T>[]`,
|
|
173
|
+
and with `T` inferred from the source the two could not agree, so the candidate was dropped
|
|
174
|
+
and the plain overload answered. The same for `omit`, on `search`, `searchAsync` and the
|
|
175
|
+
`jqlQuery` methods — where it hid for longer, because `Row[]` *is* assignable to
|
|
176
|
+
`Partial<Row>[]`, so the obvious assertion passed either way.
|
|
177
|
+
- **`node dist/cli.js` did nothing while carrying a shebang that said it would.** The file is
|
|
178
|
+
the module behind `@osqd/jql/cli`; `bin/jql.mjs` is the executable.
|
|
179
|
+
- **`jql` rewrote the lines it was asked to filter.** Each match was re-serialised rather
|
|
180
|
+
than written back out, so `jql '{}' file.jsonl > copy.jsonl` did not reproduce the file:
|
|
181
|
+
`1e400` came back as `null` and a thirty-digit identifier as `1.2345678901234568e+29`, both
|
|
182
|
+
silently. A matching line is now written byte for byte as it arrived; asking for it to be
|
|
183
|
+
reshaped or laid out again is asking for something other than the line, and that is still
|
|
184
|
+
written as JSON.
|
|
185
|
+
- **A leading byte-order mark cost a file its first record.** It is what a Windows editor and
|
|
186
|
+
a PowerShell redirect put at the front of a UTF-8 file; it made the first line "not JSON,
|
|
187
|
+
skipped" while every other line came through. It is dropped now.
|
|
188
|
+
- **The conformance suite had no case for `omit`.** The suite is the language as data — what
|
|
189
|
+
another implementation checks itself against — and every part of a request had cases except
|
|
190
|
+
the one that drops fields, which is the part a server redacts with. Six cases now cover the
|
|
191
|
+
rules in §8.2: what it drops, through an array, that it applies after `fields`, that a path
|
|
192
|
+
the item does not have drops nothing, and the refusal.
|
|
193
|
+
- **`$in` held its values in one set with both kinds of number in it**, which measured 1.17×
|
|
194
|
+
the cost of a set of plain numbers — paid by every `$in` ever written, to serve a kind of
|
|
195
|
+
value almost no document holds. The big integers have a set of their own now, consulted
|
|
196
|
+
only after the first has said no.
|
|
197
|
+
- **A big integer was equal to no number a query could write.** Ordering put `10n` and `10`
|
|
198
|
+
in the same place and `$type` called both numbers, while `$eq` compared the two kinds with
|
|
199
|
+
`===` and said no — so one document was at once `$gte: 10`, `$lte: 10` and `$ne: 10`, which
|
|
200
|
+
nothing can be, and the types pointed straight at it by offering `number` as the literal
|
|
201
|
+
for such a field. Equality, `$in`, `$nin`, `$all` and `{ "$field": … }` now treat a big
|
|
202
|
+
integer as the number it is (specification §5.1.1). JSON has no big integer, so no portable
|
|
203
|
+
query changes meaning.
|
|
204
|
+
- **`$mod` matched nothing against a big integer.** The specification defines it over "a
|
|
205
|
+
reached number", and a big integer is one there — `%` refuses to mix the two kinds in
|
|
206
|
+
JavaScript, so the operator quietly answered false for every such value. The types offered
|
|
207
|
+
it on those fields too, and now do.
|
|
208
|
+
- **`jqlQuery` typed a request with `omit` as whole items.** `search` has always said
|
|
209
|
+
`Partial<T>` for one; the methods on `Array` carried only the `fields` overload, so the
|
|
210
|
+
compiler promised fields the result no longer had.
|
|
211
|
+
- **An error quoted a whole function back.** A function where a value belongs is refused, and
|
|
212
|
+
`String(fn)` is its entire source, so the sentence refusing it was a listing.
|
|
213
|
+
|
|
214
|
+
- **The text parser could produce a query that would not run.** `score:1e309` became
|
|
215
|
+
`{ "$eq": Infinity }`, which the engine refuses and `JSON.stringify` turns into `null`, so
|
|
216
|
+
a search box could hand somebody a filter that could neither run nor be saved; the same for
|
|
217
|
+
a duration too large to size (`at:>-99999999999999999999d`), and `-0` did not survive being
|
|
218
|
+
written out. All three are now values that match nothing, which is what an unreadable value
|
|
219
|
+
means everywhere else in the box. Found by the parser fuzzer, which is now part of the
|
|
220
|
+
suite.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
OSQD Non-Resale License, Version 1.0
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michał Płatosz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person or organization
|
|
6
|
+
obtaining a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to use, copy, modify, merge, publish, and distribute the Software
|
|
8
|
+
and to create and distribute Derivative Works, subject to the conditions below.
|
|
9
|
+
|
|
10
|
+
"Derivative Work" means any work based on the Software, including a modified
|
|
11
|
+
copy, a translation, or a work that incorporates any substantial portion of it.
|
|
12
|
+
|
|
13
|
+
"You" means the person or organization exercising the permissions granted here.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
1. NOTICE
|
|
17
|
+
|
|
18
|
+
You must include the above copyright notice and this entire License in all
|
|
19
|
+
copies or substantial portions of the Software and in every Derivative Work
|
|
20
|
+
you distribute, whether or not you have modified it.
|
|
21
|
+
|
|
22
|
+
You must not remove, obscure, or alter any copyright, attribution, or license
|
|
23
|
+
notice contained in the Software.
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
2. NO SALE
|
|
27
|
+
|
|
28
|
+
You must not Sell the Software.
|
|
29
|
+
|
|
30
|
+
"Sell" means charging any fee, of any kind, where the Software or a Derivative
|
|
31
|
+
Work is what is being paid for. This includes, without limitation:
|
|
32
|
+
|
|
33
|
+
(a) distributing copies of the Software or a Derivative Work for a fee;
|
|
34
|
+
|
|
35
|
+
(b) offering the Software or a Derivative Work to third parties as a hosted,
|
|
36
|
+
managed, or software-as-a-service product for a fee;
|
|
37
|
+
|
|
38
|
+
(c) including the Software or a Derivative Work in any product or service you
|
|
39
|
+
charge for, where the Software forms a substantial part of the value you
|
|
40
|
+
are offering; or
|
|
41
|
+
|
|
42
|
+
(d) sublicensing or relicensing the Software for a fee.
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
3. WHAT IS EXPLICITLY PERMITTED
|
|
46
|
+
|
|
47
|
+
For the avoidance of doubt, none of the following is Selling, and all of it is
|
|
48
|
+
permitted, including by a commercial business and in production:
|
|
49
|
+
|
|
50
|
+
(a) using the Software, or a Derivative Work of it, to operate, monitor, or
|
|
51
|
+
protect your own systems — including the systems of the business you work
|
|
52
|
+
for, and including systems you charge your own customers to access, so
|
|
53
|
+
long as the Software itself is not part of what they are paying for;
|
|
54
|
+
|
|
55
|
+
(b) charging for your own professional services — consulting, integration,
|
|
56
|
+
deployment, operation, or support — provided the fee is for your services
|
|
57
|
+
and not for the Software, and you supply any copy of the Software to the
|
|
58
|
+
customer under this License at no charge;
|
|
59
|
+
|
|
60
|
+
(c) modifying the Software for any purpose, internal or otherwise, and keeping
|
|
61
|
+
those modifications private;
|
|
62
|
+
|
|
63
|
+
(d) distributing the Software or a Derivative Work at no charge; and
|
|
64
|
+
|
|
65
|
+
(e) using the Software for personal, educational, academic, research, or
|
|
66
|
+
non-profit purposes of any kind.
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
4. SEPARATE COMMERCIAL TERMS
|
|
70
|
+
|
|
71
|
+
If you want to do something this License does not permit, the copyright holder
|
|
72
|
+
can grant you a separate license on different terms. Ask.
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
5. TERMINATION
|
|
76
|
+
|
|
77
|
+
If you breach this License, the permissions granted to you terminate
|
|
78
|
+
automatically. If the breach is curable and you cure it within 30 days of
|
|
79
|
+
becoming aware of it, your permissions are reinstated retroactively. Any
|
|
80
|
+
permissions granted to those who received the Software from you in compliance
|
|
81
|
+
with this License are unaffected by your termination.
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
6. THIRD-PARTY COMPONENTS
|
|
85
|
+
|
|
86
|
+
This License covers only the Software itself. Dependencies distributed
|
|
87
|
+
alongside or installed with it remain under their own licenses.
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
7. NO WARRANTY
|
|
91
|
+
|
|
92
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
93
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
94
|
+
FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
8. LIMITATION OF LIABILITY
|
|
98
|
+
|
|
99
|
+
IN NO EVENT SHALL THE AUTHOR OR COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM,
|
|
100
|
+
DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR
|
|
101
|
+
OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
|
102
|
+
OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# JQL — JSON Query Language
|
|
2
|
+
|
|
3
|
+
One query language for every place the OSQD projects filter data, and a zero-dependency
|
|
4
|
+
TypeScript engine that runs it.
|
|
5
|
+
|
|
6
|
+
> A query is a JSON document that means the same thing everywhere it is sent — and a query
|
|
7
|
+
> that is not valid JQL is refused with the reason, never run as something that looks like it
|
|
8
|
+
> worked.
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm install @osqd/jql
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Node 20 or newer, and TypeScript 5.4 or newer if you use the types: the published
|
|
15
|
+
declarations use `NoInfer`, which TypeScript 5.4 added. Nothing else is required — the
|
|
16
|
+
package has no runtime dependencies.
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import "@osqd/jql/global";
|
|
20
|
+
|
|
21
|
+
const arr = [{ id: 1, tags: ["a"] }, { id: 2, tags: ["b"] }];
|
|
22
|
+
arr.jqlSearch({ id: 2 }); // { id: 2, tags: ["b"] }
|
|
23
|
+
Array.jqlSearch(arr, { tags: { $in: ["a", "c"] } }); // any array, iterable, Set or Map
|
|
24
|
+
arr.jqlQuery({ where: { id: { $gt: 0 } }, sort: { id: -1 }, limit: 1 });
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Contents
|
|
28
|
+
|
|
29
|
+
- [**The course**](docs/course/index.md) — sixteen lessons that build one real integration, with something to run at every step
|
|
30
|
+
- [Quick start](docs/start/quick-start.md) — install it and ask the first question
|
|
31
|
+
- [The specification](docs/reference/specification.md) — the standard: what every query means
|
|
32
|
+
- [Conformance suite](conformance/cases.json) — the standard as data, for any implementation
|
|
33
|
+
- [Text syntax](docs/reference/text-syntax.md) — what people type into a search box, and writing one back out
|
|
34
|
+
- [The command](docs/reference/cli.md) — `jql`, for filtering JSON lines from a shell
|
|
35
|
+
- [Pushing a query into a store](docs/guides/pushdown.md) — letting a backend answer what it can
|
|
36
|
+
- [Library API](docs/reference/api.md) — every export
|
|
37
|
+
- [TypeScript](docs/guides/typescript.md) — typed queries, the array methods, vocabularies
|
|
38
|
+
- [Queries from outside](docs/guides/untrusted-input.md) — accepting a query from a URL or a body
|
|
39
|
+
- [Adopting JQL in a project](docs/guides/adopting.md) — moving a project's filtering onto it
|
|
40
|
+
- [Performance](docs/design/performance.md) and [Decisions](docs/design/decisions.md)
|
|
41
|
+
- [All documentation](docs/index.md)
|
|
42
|
+
|
|
43
|
+
## What it is
|
|
44
|
+
|
|
45
|
+
- **A standard.** [The specification](docs/reference/specification.md) defines the language
|
|
46
|
+
independently of this code, and [`conformance/cases.json`](conformance/cases.json) states it
|
|
47
|
+
as JSON documents, JSON queries and the answers any implementation must give.
|
|
48
|
+
- **A library.** Compile a query once into a plain predicate, or use `find`, `filter`, `count`,
|
|
49
|
+
`search`, `group` and friends over arrays, iterables, `Map`s, `Set`s, plain objects — and,
|
|
50
|
+
with the `*Async` helpers, over a log or a cursor that arrives over time.
|
|
51
|
+
- **Explainable.** `explain(query, item)` says which clause decided it, with the values it
|
|
52
|
+
read; `canonical` and `fingerprint` give a saved filter one shape and one short name.
|
|
53
|
+
- **Typed.** `Query<T>` checks paths, value types and which operators fit each field, so a typo
|
|
54
|
+
is a compile error rather than a filter that matches nothing.
|
|
55
|
+
- **Fast.** Within about 1.1–1.5× of a hand-written loop for most queries, faster than one for
|
|
56
|
+
top-k sorting, and 2–130× faster than the query-document matchers commonly used in
|
|
57
|
+
JavaScript.
|
|
58
|
+
- **A search box, both ways.** `@osqd/jql/text` turns `status:open total:>100 has:rule at:>-1h`
|
|
59
|
+
into a JQL document, and `toText` turns a stored one back into something somebody can edit.
|
|
60
|
+
- **A shell command.** `jql --text 'level:error' app.jsonl` filters JSON lines, with
|
|
61
|
+
`--sort`, `--group`, `--omit` and `--explain`.
|
|
62
|
+
- **Pushable.** `plan(query, capabilities)` splits a query into the part a store can answer
|
|
63
|
+
and the part that stays here, with `@osqd/jql/mongo` as the first target.
|
|
64
|
+
- **Extensible where it has to be.** A project can add operators of its own, named `$x…` so a
|
|
65
|
+
query that needs more than a standard engine says so.
|
|
66
|
+
|
|
67
|
+
## Why this design
|
|
68
|
+
|
|
69
|
+
| Choice | Instead of | Because |
|
|
70
|
+
| --- | --- | --- |
|
|
71
|
+
| Queries are JSON | a text language | data can be stored, sent, generated and validated; text can only be typed |
|
|
72
|
+
| The conventional operator names | a vocabulary of our own | `$gt` and `$elemMatch` carry their meaning with them; where this language differs, the [specification](docs/reference/specification.md) says so where the operator is defined |
|
|
73
|
+
| Refuse anything not understood | ignore it | an ignored `$gtt` is a filter that silently filters nothing |
|
|
74
|
+
| Compile to closures | generate code | generated code is blocked by any real Content-Security-Policy |
|
|
75
|
+
| Array methods behind an import | patch on load | changing built-ins is the application's decision |
|
|
76
|
+
| A conformance suite in JSON | tests only in TypeScript | a standard other languages can check themselves against |
|
|
77
|
+
| `{ "$field": "other" }` | an expression language (`$expr`) | comparing two fields needs a path, not a language |
|
|
78
|
+
| Relative dates in the query | an instant computed before sending | a saved filter that means "the last hour" goes on meaning it |
|
|
79
|
+
|
|
80
|
+
More in [Decisions](docs/design/decisions.md).
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
OSQD Non-Resale License — see [LICENSE](LICENSE).
|