@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.
Files changed (115) hide show
  1. package/CHANGELOG.md +220 -0
  2. package/LICENSE +102 -0
  3. package/README.md +84 -0
  4. package/bin/jql.mjs +5 -0
  5. package/conformance/cases.json +290 -0
  6. package/dist/array.d.ts +11 -0
  7. package/dist/async.d.ts +44 -0
  8. package/dist/canonical.d.ts +18 -0
  9. package/dist/cjs/array.d.ts +11 -0
  10. package/dist/cjs/async.d.ts +44 -0
  11. package/dist/cjs/canonical.d.ts +18 -0
  12. package/dist/cjs/cli.d.ts +15 -0
  13. package/dist/cjs/collections.d.ts +34 -0
  14. package/dist/cjs/core.d.ts +117 -0
  15. package/dist/cjs/errors.d.ts +17 -0
  16. package/dist/cjs/explain.d.ts +30 -0
  17. package/dist/cjs/global.d.ts +90 -0
  18. package/dist/cjs/group.d.ts +47 -0
  19. package/dist/cjs/index.d.ts +25 -0
  20. package/dist/cjs/internal/closest.d.ts +9 -0
  21. package/dist/cjs/internal/duration.d.ts +15 -0
  22. package/dist/cjs/internal/equal.d.ts +34 -0
  23. package/dist/cjs/internal/glob.d.ts +30 -0
  24. package/dist/cjs/internal/order.d.ts +24 -0
  25. package/dist/cjs/internal/path.d.ts +109 -0
  26. package/dist/cjs/internal/record.d.ts +18 -0
  27. package/dist/cjs/internal/values.d.ts +41 -0
  28. package/dist/cjs/limits.d.ts +64 -0
  29. package/dist/cjs/operators.d.ts +81 -0
  30. package/dist/cjs/package.json +3 -0
  31. package/dist/cjs/plan.d.ts +79 -0
  32. package/dist/cjs/search.d.ts +42 -0
  33. package/dist/cjs/targets/mongo.d.ts +55 -0
  34. package/dist/cjs/text/index.d.ts +12 -0
  35. package/dist/cjs/text/parse.d.ts +91 -0
  36. package/dist/cjs/text/suggest.d.ts +16 -0
  37. package/dist/cjs/text/write.d.ts +34 -0
  38. package/dist/cjs/types.d.ts +236 -0
  39. package/dist/cjs/vocabulary.d.ts +106 -0
  40. package/dist/cli.d.ts +15 -0
  41. package/dist/cli.js +2729 -0
  42. package/dist/cli.js.map +1 -0
  43. package/dist/collections.d.ts +34 -0
  44. package/dist/core.d.ts +117 -0
  45. package/dist/errors.d.ts +17 -0
  46. package/dist/explain.d.ts +30 -0
  47. package/dist/global.cjs +1953 -0
  48. package/dist/global.cjs.map +1 -0
  49. package/dist/global.d.ts +90 -0
  50. package/dist/global.js +1950 -0
  51. package/dist/global.js.map +1 -0
  52. package/dist/group.d.ts +47 -0
  53. package/dist/index.cjs +2529 -0
  54. package/dist/index.cjs.map +1 -0
  55. package/dist/index.d.ts +25 -0
  56. package/dist/index.js +2495 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/internal/closest.d.ts +9 -0
  59. package/dist/internal/duration.d.ts +15 -0
  60. package/dist/internal/equal.d.ts +34 -0
  61. package/dist/internal/glob.d.ts +30 -0
  62. package/dist/internal/order.d.ts +24 -0
  63. package/dist/internal/path.d.ts +109 -0
  64. package/dist/internal/record.d.ts +18 -0
  65. package/dist/internal/values.d.ts +41 -0
  66. package/dist/limits.d.ts +64 -0
  67. package/dist/mongo.cjs +357 -0
  68. package/dist/mongo.cjs.map +1 -0
  69. package/dist/mongo.js +354 -0
  70. package/dist/mongo.js.map +1 -0
  71. package/dist/operators.d.ts +81 -0
  72. package/dist/plan.d.ts +79 -0
  73. package/dist/search.d.ts +42 -0
  74. package/dist/targets/mongo.d.ts +55 -0
  75. package/dist/text/index.d.ts +12 -0
  76. package/dist/text/parse.d.ts +91 -0
  77. package/dist/text/suggest.d.ts +16 -0
  78. package/dist/text/write.d.ts +34 -0
  79. package/dist/text.cjs +674 -0
  80. package/dist/text.cjs.map +1 -0
  81. package/dist/text.js +667 -0
  82. package/dist/text.js.map +1 -0
  83. package/dist/types.d.ts +236 -0
  84. package/dist/vocabulary.d.ts +106 -0
  85. package/docs/course/01-first-query.md +217 -0
  86. package/docs/course/02-operators.md +285 -0
  87. package/docs/course/03-arrays-and-paths.md +239 -0
  88. package/docs/course/04-combining.md +221 -0
  89. package/docs/course/05-dates.md +214 -0
  90. package/docs/course/06-typed-queries.md +240 -0
  91. package/docs/course/07-requests.md +261 -0
  92. package/docs/course/08-grouping.md +210 -0
  93. package/docs/course/09-explaining.md +171 -0
  94. package/docs/course/10-vocabulary.md +276 -0
  95. package/docs/course/11-the-search-box.md +349 -0
  96. package/docs/course/12-untrusted.md +257 -0
  97. package/docs/course/13-saved-filters.md +199 -0
  98. package/docs/course/14-streams-and-cli.md +276 -0
  99. package/docs/course/15-pushdown.md +240 -0
  100. package/docs/course/16-extending.md +199 -0
  101. package/docs/course/index.md +185 -0
  102. package/docs/design/decisions.md +198 -0
  103. package/docs/design/performance.md +102 -0
  104. package/docs/guides/adopting.md +81 -0
  105. package/docs/guides/pushdown.md +147 -0
  106. package/docs/guides/typescript.md +115 -0
  107. package/docs/guides/untrusted-input.md +86 -0
  108. package/docs/index.md +102 -0
  109. package/docs/reference/api.md +266 -0
  110. package/docs/reference/cli.md +103 -0
  111. package/docs/reference/index.md +12 -0
  112. package/docs/reference/specification.md +549 -0
  113. package/docs/reference/text-syntax.md +152 -0
  114. package/docs/start/quick-start.md +84 -0
  115. 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).
package/bin/jql.mjs ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ // Three lines over the same implementation the package exports, because a command that
3
+ // reimplements the library is a command that drifts from it.
4
+ import { run } from "../dist/cli.js";
5
+ await run();