@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
@@ -0,0 +1,152 @@
1
+ # Text syntax
2
+
3
+ What people can type into a search box, and the JQL document each piece becomes.
4
+
5
+ ← [Documentation](../index.md) · [Reference](index.md)
6
+
7
+ ---
8
+
9
+ `parseText` from `@osqd/jql/text` turns a line of text into a JQL query. It has no matcher of
10
+ its own: the text *is* a way of writing JSON, and the engine runs the JSON. So a filter typed
11
+ into a dashboard, saved, and later run by a service that only understands JSON finds exactly
12
+ the same things.
13
+
14
+ The syntax is BotHandler's feed filter, moved here so every project shares one.
15
+
16
+ ```ts
17
+ import { parseText } from "@osqd/jql/text";
18
+
19
+ parseText("actor:203.0.113.4 -path:/health score:>70", { vocabulary });
20
+ // { $and: [
21
+ // { actor: { $word: "203.0.113.4", $options: "i" } },
22
+ // { $not: { path: { $contains: "/health", $options: "i" } } },
23
+ // { score: { $gt: 70 } } ] }
24
+ ```
25
+
26
+ ## Terms
27
+
28
+ | You type | Means | Becomes |
29
+ | --- | --- | --- |
30
+ | `checkout` | the text appears anywhere | `{ "$text": "checkout" }` |
31
+ | `"GET /api/v2"` | a phrase, spaces and all | `{ "$text": "GET /api/v2" }` |
32
+ | `path:/api` | the field matches the value (how depends on the field's kind, below) | `{ "path": { "$contains": "/api", "$options": "i" } }` |
33
+ | `-path:/health` or `!path:/health` | not | `{ "$not": { … } }` |
34
+ | `path:$in(/a, /b)` | any of these | `{ "path": { "$contains": ["/a", "/b"], "$options": "i" } }` |
35
+ | `path:$notin(/a, /b)` | none of these | `{ "$not": { "path": … } }` |
36
+ | `has:rule` | the field has a value | `{ "rule": { "$exists": true } }` |
37
+ | `-has:rule` | the field has none | `{ "$not": { "rule": { "$exists": true } } }` |
38
+
39
+ Any quote works — `"…"`, `'…'`, and the curly `“…”` and `‘…’` that smart punctuation produces
40
+ when a filter is pasted from chat or documentation. A single quote counts as a quote only
41
+ where a value begins, so the apostrophe in `don't` is still an apostrophe. Inside a set, a
42
+ comma separates values only outside quotes: `$in("a,b", c)` is two values.
43
+
44
+ ## Operators
45
+
46
+ | You type | Means |
47
+ | --- | --- |
48
+ | `a b` or `a $and b` | both |
49
+ | `a $or b` | either |
50
+ | `$not a` | not — the same as `-a` |
51
+ | `(a $or b) $and c` | grouping |
52
+
53
+ `$not` binds tightest, then `$and`, then `$or`, so `a $or b $and c` reads as `a $or (b $and c)`.
54
+
55
+ **Operators carry a `$`.** A bare `or` is a word that appears in paths and User-Agents, and a
56
+ language where an ordinary search word silently becomes an operator lies about what it
57
+ matched. Typing `or` searches for the text `or`.
58
+
59
+ ## Field kinds
60
+
61
+ How `field:value` reads the value is the field's `kind` in the [vocabulary](../guides/typescript.md#vocabularies):
62
+
63
+ | Kind | `field:value` becomes | For |
64
+ | --- | --- | --- |
65
+ | `text` (default) | `$contains`, ignoring case | prose, paths, User-Agents |
66
+ | `word` | `$word`, ignoring case — `1.2.3.4` does not find `1.2.3.45` | addresses, identifiers |
67
+ | `exact` | `$eq` (or `$in` for a set), ignoring case | closed sets: verdicts, methods, statuses |
68
+ | `number` | a comparison: `>70`, `>=70`, `<5`, `<=5`, `=42`, `42`, and ranges `10..20`, `10..`, `..20` | scores, sizes, durations |
69
+ | `date` | the same comparisons on `{ "$date": … }`: `>2026-09-01`, `2026-09-01..2026-09-30`, and the relative `>-1h`, `<+30m`, `-1h..now` | timestamps |
70
+ | `boolean` | `true`/`false`, `yes`/`no`, `1`/`0`, `on`/`off` | flags |
71
+
72
+ **A relative instant carries its sign**: `-1h` is an hour ago, `+30m` is half an hour from
73
+ now, `now` is now. The sign is what says a duration was meant — a bare `1h` is as likely to be
74
+ part of something somebody is searching for, and reading it as a time would be reading
75
+ something into what was typed. A filter written this way goes on meaning "the last hour"
76
+ whenever it is run, because the query it becomes holds `{ "$ago": "1h" }` rather than an
77
+ instant.
78
+
79
+ `has:` asks about the field rather than its value, so its value is a field name. A name the
80
+ vocabulary does not know falls through and is searched for as text, like any other.
81
+
82
+ A value that does not read as its kind — `score:high`, `certain:maybe` — matches nothing, as
83
+ does an empty set. A search box that widened while somebody was still typing would be lying
84
+ about what it found.
85
+
86
+ ## Which names are fields
87
+
88
+ With a vocabulary, only its names and aliases are fields. Anything else before a colon is
89
+ searched as text, because a path, a URL or a User-Agent can contain a colon: `foo:bar` looks
90
+ for the text `foo:bar`.
91
+
92
+ Without a vocabulary there is nothing to check a name against, so any name that looks like a
93
+ path (`address.city:London`) is a field of kind `text` — except that a value written as an
94
+ explicit comparison (`age:>=30`, `age:30..40`) compares as a number, since nobody typing `>30`
95
+ means the characters. A bare `age:30` stays text. A URL then has to be quoted:
96
+ `"http://example.com"`. Pass `fields: "vocabulary"` or `fields: "any"` to choose either
97
+ behaviour explicitly.
98
+
99
+ A bare word searches the vocabulary's `text` fields when it names some, and the whole item
100
+ otherwise.
101
+
102
+ ## Nothing typed is an error
103
+
104
+ `parseText` never throws. Half-written input is the normal state of a search box: an unclosed
105
+ bracket, a dangling `$or`, a `$in(` with nothing after it all parse to the best reading of what
106
+ is there. Input past 8 KB is cut, and brackets nested past 16 levels are ignored, so a hostile
107
+ link cannot overflow the stack of whoever opens it.
108
+
109
+ That is the opposite of the JSON engine, which refuses anything it does not understand — on
110
+ purpose. JSON is written by a program that can be told it is wrong; a search box is written by
111
+ a person who has not finished typing.
112
+
113
+ ## Completion
114
+
115
+ `suggest(input, caret, vocabulary)` returns the completions for the token under the caret and
116
+ the span they replace. Field names come from the vocabulary; a field with a closed set of
117
+ `values` completes its values and offers `$in(` and `$notin(`; a token starting with `$`
118
+ completes to an operator. A field that takes anything gets no suggestions, because guessing
119
+ there would be inventing options rather than completing them.
120
+
121
+ ## Writing a query back out
122
+
123
+ `toText` is the other direction, for a dashboard that stores filters as JSON and has to put
124
+ one back in the box when somebody opens it to edit:
125
+
126
+ ```ts
127
+ const { text, complete, unexpressed } = toText(saved, { vocabulary });
128
+ box.value = text;
129
+ if (!complete) warn(`${unexpressed.length} part(s) of this filter cannot be shown here`);
130
+ ```
131
+
132
+ The text is **exact for what it says** — parsed back with the same vocabulary it means
133
+ precisely the clauses it carries — but it is not total, and the parts it cannot say are
134
+ returned rather than dropped quietly. Showing only the sayable half would be showing a filter
135
+ wider than the one that is running.
136
+
137
+ A few things have no spelling even though they look as if they should: a bare word cannot
138
+ begin with `-` or be an operator's name, because the parser strips quotes before it decides
139
+ what a word is; and a condition that cares about case cannot be written, because the box
140
+ never does.
141
+
142
+ ## What the box cannot say
143
+
144
+ `$elemMatch`, `$size`, `$length`, `$glob` and `{ "$field": … }` have no spelling here. Each
145
+ would need a syntax of its own, and a search box that grew one per operator would stop being
146
+ something people can type without a manual. They are all reachable in the JSON, which is what
147
+ a saved filter stores anyway.
148
+
149
+ ## Related
150
+
151
+ - [The specification](specification.md) — what the JSON each term becomes means
152
+ - [TypeScript](../guides/typescript.md#vocabularies) — defining a vocabulary
@@ -0,0 +1,84 @@
1
+ # Quick start
2
+
3
+ Install it, ask a question, and know where to go next.
4
+
5
+ ← [Documentation](../index.md)
6
+
7
+ ---
8
+
9
+ ```sh
10
+ npm install @osqd/jql
11
+ ```
12
+
13
+ ## A query is JSON
14
+
15
+ ```ts
16
+ import { filter, find } from "@osqd/jql";
17
+
18
+ const users = [
19
+ { id: 1, name: "Ada", roles: ["admin"], address: { city: "London" } },
20
+ { id: 2, name: "Grace", roles: ["editor"], address: { city: "New York" } },
21
+ ];
22
+
23
+ find(users, { id: 2 }); // Grace
24
+ filter(users, { roles: "admin" }); // [Ada] — a value matches any element
25
+ filter(users, { "address.city": { $startsWith: "New" } }); // [Grace]
26
+ filter(users, { $or: [{ id: 1 }, { name: { $contains: "gra", $options: "i" } }] });
27
+ ```
28
+
29
+ Because the query is plain data, the same object can be stored, put in a URL, sent to
30
+ another service, and it finds the same thing there. The [specification](../reference/specification.md)
31
+ says exactly what each operator means.
32
+
33
+ ## Or a method on the array
34
+
35
+ The methods are opt-in, because patching a built-in is the application's decision:
36
+
37
+ ```ts
38
+ import "@osqd/jql/global";
39
+
40
+ const arr = [{ id: 1 }, { id: 2 }];
41
+ const object2 = arr.jqlSearch({ id: 2 });
42
+ const same = Array.jqlSearch(arr, { id: 2 }); // any array, iterable, Set or Map
43
+ new Map([["a", { n: 1 }]]).jqlFilter({ n: 1 }); // a Map in, a Map out
44
+ [3, 8, 1].jqlFilter({ $gt: 2 }); // [3, 8] — a condition on the item itself
45
+ ```
46
+
47
+ The element type is inferred, so a field that does not exist is a compile error rather than a
48
+ filter that quietly matches nothing. See [TypeScript](../guides/typescript.md).
49
+
50
+ ## Compile once in a loop
51
+
52
+ ```ts
53
+ import { compile } from "@osqd/jql";
54
+
55
+ const isOpen = compile<Order>({ status: "open", total: { $gte: 100 } });
56
+ orders.filter(isOpen);
57
+ ```
58
+
59
+ Every helper compiles the query it is given; `compile` lets you do that once and keep the
60
+ predicate. See [Performance](../design/performance.md).
61
+
62
+ ## Sort, page and pick fields
63
+
64
+ ```ts
65
+ import { search } from "@osqd/jql";
66
+
67
+ search(orders, { where: { status: "open" }, sort: { total: -1 }, skip: 20, limit: 20, fields: ["id", "total"] });
68
+ ```
69
+
70
+ ## Let people type
71
+
72
+ ```ts
73
+ import { parseText } from "@osqd/jql/text";
74
+
75
+ orders.jqlFilter(parseText('status:open total:>100 -"test account"'));
76
+ ```
77
+
78
+ The text becomes a JQL document; nothing about it is matched separately. See
79
+ [Text syntax](../reference/text-syntax.md).
80
+
81
+ ## Related
82
+
83
+ - [The specification](../reference/specification.md) — every operator, precisely
84
+ - [Queries from outside](../guides/untrusted-input.md) — before you accept one from a URL
package/package.json ADDED
@@ -0,0 +1,136 @@
1
+ {
2
+ "name": "@osqd/jql",
3
+ "version": "0.1.1",
4
+ "description": "JSON Query Language — one MongoDB-style query document for every place the OSQD projects filter data, with a zero-dependency TypeScript engine that compiles each query once into a plain predicate.",
5
+ "keywords": [
6
+ "query",
7
+ "json",
8
+ "filter",
9
+ "mongodb",
10
+ "search",
11
+ "predicate",
12
+ "typescript"
13
+ ],
14
+ "author": "Michał Płatosz <platosz.michal@gmail.com>",
15
+ "license": "SEE LICENSE IN LICENSE",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/OSQD-ts/JQL.git"
19
+ },
20
+ "homepage": "https://github.com/OSQD-ts/JQL#readme",
21
+ "bugs": {
22
+ "url": "https://github.com/OSQD-ts/JQL/issues"
23
+ },
24
+ "type": "module",
25
+ "engines": {
26
+ "node": ">=20"
27
+ },
28
+ "main": "./dist/index.cjs",
29
+ "module": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "typesVersions": {
32
+ "*": {
33
+ "text": [
34
+ "./dist/text/index.d.ts"
35
+ ],
36
+ "global": [
37
+ "./dist/global.d.ts"
38
+ ],
39
+ "mongo": [
40
+ "./dist/targets/mongo.d.ts"
41
+ ],
42
+ "cli": [
43
+ "./dist/cli.d.ts"
44
+ ]
45
+ }
46
+ },
47
+ "exports": {
48
+ ".": {
49
+ "import": {
50
+ "types": "./dist/index.d.ts",
51
+ "default": "./dist/index.js"
52
+ },
53
+ "require": {
54
+ "types": "./dist/cjs/index.d.ts",
55
+ "default": "./dist/index.cjs"
56
+ }
57
+ },
58
+ "./text": {
59
+ "import": {
60
+ "types": "./dist/text/index.d.ts",
61
+ "default": "./dist/text.js"
62
+ },
63
+ "require": {
64
+ "types": "./dist/cjs/text/index.d.ts",
65
+ "default": "./dist/text.cjs"
66
+ }
67
+ },
68
+ "./global": {
69
+ "import": {
70
+ "types": "./dist/global.d.ts",
71
+ "default": "./dist/global.js"
72
+ },
73
+ "require": {
74
+ "types": "./dist/cjs/global.d.ts",
75
+ "default": "./dist/global.cjs"
76
+ }
77
+ },
78
+ "./package.json": "./package.json",
79
+ "./mongo": {
80
+ "import": {
81
+ "types": "./dist/targets/mongo.d.ts",
82
+ "default": "./dist/mongo.js"
83
+ },
84
+ "require": {
85
+ "types": "./dist/cjs/targets/mongo.d.ts",
86
+ "default": "./dist/mongo.cjs"
87
+ }
88
+ },
89
+ "./cli": {
90
+ "types": "./dist/cli.d.ts",
91
+ "import": "./dist/cli.js"
92
+ }
93
+ },
94
+ "sideEffects": [
95
+ "./dist/global.js",
96
+ "./dist/global.cjs"
97
+ ],
98
+ "files": [
99
+ "dist",
100
+ "docs",
101
+ "conformance",
102
+ "bin",
103
+ "README.md",
104
+ "CHANGELOG.md",
105
+ "LICENSE"
106
+ ],
107
+ "scripts": {
108
+ "check": "npm run typecheck && npm run lint && npm test && npm run docs:check",
109
+ "typecheck": "tsc -p tsconfig.typecheck.json",
110
+ "lint": "biome check .",
111
+ "format": "biome check --write .",
112
+ "test": "vitest run",
113
+ "test:watch": "vitest",
114
+ "docs:check": "node scripts/check-links.mjs",
115
+ "build": "tsup && tsc --emitDeclarationOnly && tsc -p tsconfig.cli.json && node scripts/cjs-types.mjs",
116
+ "build:watch": "tsup --watch",
117
+ "check:package": "npm run build && tsx scripts/check-package.ts",
118
+ "bench": "tsx --expose-gc scripts/bench.ts",
119
+ "bench:guard": "tsx scripts/bench-guard.ts",
120
+ "example": "tsx --tsconfig tsconfig.typecheck.json examples/basic.ts",
121
+ "prepack": "npm run build",
122
+ "prepublishOnly": "npm run build",
123
+ "cli": "tsx src/cli-run.ts"
124
+ },
125
+ "devDependencies": {
126
+ "@biomejs/biome": "^2.4.0",
127
+ "@types/node": "^26.3.0",
128
+ "tsup": "^8.5.1",
129
+ "tsx": "^4.23.12",
130
+ "typescript": "^5.9.3",
131
+ "vitest": "^4.1.11"
132
+ },
133
+ "bin": {
134
+ "jql": "./bin/jql.mjs"
135
+ }
136
+ }