@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,42 @@
1
+ import { type CompileOptions } from "./core.js";
2
+ import type { FieldName, Request, Source } from "./types.js";
3
+ type Items<T> = Source<T> | ReadonlyMap<unknown, T>;
4
+ /**
5
+ * Runs a request. With `fields`, each result is a new object holding only those paths.
6
+ *
7
+ * A `Map` is read by value, like everywhere else in this library, so its overloads come
8
+ * first: a `Map` also satisfies `Iterable<[key, value]>`, and without them the types would
9
+ * describe a request over the pairs while the code ran one over the values.
10
+ */
11
+ export declare function search<K, V, Extra extends object = {}>(source: ReadonlyMap<K, V>, request: Request<V, NoInfer<Extra>> & {
12
+ readonly fields: readonly FieldName<V, NoInfer<Extra>>[];
13
+ }, options?: CompileOptions<Extra>): Record<string, unknown>[];
14
+ export declare function search<K, V, Extra extends object = {}>(source: ReadonlyMap<K, V>, request: Request<V, NoInfer<Extra>> & {
15
+ readonly omit: readonly FieldName<V, NoInfer<Extra>>[];
16
+ }, options?: CompileOptions<Extra>): Partial<V>[];
17
+ export declare function search<K, V, Extra extends object = {}>(source: ReadonlyMap<K, V>, request?: Request<V, NoInfer<Extra>>, options?: CompileOptions<Extra>): V[];
18
+ export declare function search<T, Extra extends object = {}>(source: Items<T>, request: Request<T, NoInfer<Extra>> & {
19
+ readonly fields: readonly FieldName<T, NoInfer<Extra>>[];
20
+ }, options?: CompileOptions<Extra>): Record<string, unknown>[];
21
+ export declare function search<T, Extra extends object = {}>(source: Items<T>, request: Request<T, NoInfer<Extra>> & {
22
+ readonly omit: readonly FieldName<T, NoInfer<Extra>>[];
23
+ }, options?: CompileOptions<Extra>): Partial<T>[];
24
+ export declare function search<T, Extra extends object = {}>(source: Items<T>, request?: Request<T, NoInfer<Extra>>, options?: CompileOptions<Extra>): T[];
25
+ /**
26
+ * Everything about a request that does not depend on where the items come from.
27
+ *
28
+ * Pulled out so the asynchronous helpers run the same paging, the same heap and the same
29
+ * projection as the synchronous ones. Two copies of "the best twenty, skipping forty"
30
+ * would be two chances to page differently.
31
+ */
32
+ export declare function planRequest<T>(request: Request<T, object>, options: CompileOptions<object>): {
33
+ collect: () => Collector<T>;
34
+ test: ((item: T) => boolean) | undefined;
35
+ shape: (items: T[]) => unknown[];
36
+ };
37
+ /** Gathers the page as items arrive. `offer` returns true once nothing further can change the answer. */
38
+ export interface Collector<T> {
39
+ offer(item: T, index: number): boolean;
40
+ finish(): T[];
41
+ }
42
+ export {};
@@ -0,0 +1,55 @@
1
+ import type { Capabilities } from "../plan.js";
2
+ import type { Vocabulary } from "../vocabulary.js";
3
+ /**
4
+ * JQL as a MongoDB filter.
5
+ *
6
+ * The first target, because it is the one where the two languages nearly agree: most of
7
+ * JQL's operators *are* MongoDB's, with the same array semantics, so the translation is
8
+ * mostly a copy and the interesting part is the handful that differ.
9
+ *
10
+ * The translation is checked by *running* it, not by comparing its shape: `tests/helpers/mongo.ts`
11
+ * is a second reading of MongoDB's matching rules, taken from its documented behaviour rather
12
+ * than from this engine, and the split test runs both halves of every generated query through
13
+ * it and through the engine and compares the answers.
14
+ *
15
+ * That check found the one difference no capability can express: **an array held directly
16
+ * inside another array**. A path segment in MongoDB applies to an array's elements but not to
17
+ * the elements of *those* arrays; JQL sees an array through at every level. Which rows come
18
+ * back then differs, in either direction, and nothing in the query says so — the shape is in
19
+ * the data. It is written down in specification §3 and in the pushdown guide, because a
20
+ * caller with such a collection must not read `complete` as permission to skip the second
21
+ * pass.
22
+ *
23
+ * What it will not do is guess. Every operator MongoDB cannot answer exactly —
24
+ * `$word`, `$length`, a `{ "$field": … }` reference, `$text` — is left out of
25
+ * `MONGO_CAPABILITIES`, so `plan()` keeps those clauses here and this function never sees
26
+ * them. A translation that was *nearly* right would be the worst of both: fewer rows than
27
+ * the query asked for, from a store that looked like it had answered.
28
+ */
29
+ /**
30
+ * What a MongoDB `find` filter can answer, in JQL's own names.
31
+ *
32
+ * Give it to `plan()` and it splits a query into the filter to send and the predicate to
33
+ * apply to what comes back.
34
+ */
35
+ export declare const MONGO_CAPABILITIES: Capabilities;
36
+ export interface MongoOptions {
37
+ /**
38
+ * Resolves a vocabulary's names and aliases to the paths the collection stores.
39
+ *
40
+ * Only needed for a query that did not come from `plan()`: what `plan` pushes already
41
+ * carries the stored names.
42
+ */
43
+ readonly vocabulary?: Vocabulary<never, object> | undefined;
44
+ /** Where "now" comes from, for a relative date. Default the system clock, read once. */
45
+ readonly now?: (() => number) | undefined;
46
+ }
47
+ /** A MongoDB filter document. Values may be `Date` and `RegExp` objects, which a driver expects. */
48
+ export type MongoFilter = Record<string, unknown>;
49
+ /**
50
+ * Translates a JQL query into a MongoDB filter.
51
+ *
52
+ * Give it only what `plan(query, MONGO_CAPABILITIES)` pushed. Anything else is refused by
53
+ * name rather than approximated.
54
+ */
55
+ export declare function toMongoFilter(query: unknown, options?: MongoOptions): MongoFilter;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `@osqd/jql/text` — the search-box syntax, compiled into JQL.
3
+ *
4
+ * A separate entry because a service that only ever receives JSON queries has no reason
5
+ * to load a tokenizer.
6
+ */
7
+ export { parseText, MAX_TEXT_CHARS, MAX_TEXT_DEPTH, TEXT_OPERATORS } from "./parse.js";
8
+ export { suggest } from "./suggest.js";
9
+ export { toText } from "./write.js";
10
+ export type { TextOptions } from "./parse.js";
11
+ export type { Suggestions } from "./suggest.js";
12
+ export type { TextForm, Unexpressed } from "./write.js";
@@ -0,0 +1,91 @@
1
+ import type { UntypedQuery } from "../types.js";
2
+ import { type Vocabulary } from "../vocabulary.js";
3
+ /**
4
+ * The search-box syntax, compiled into a JQL document.
5
+ *
6
+ * Ported from BotHandler's feed filter, where it was its own language with its own
7
+ * matcher. Here it has no matcher at all: it produces the JSON query that means the same
8
+ * thing, and the engine runs that. So a filter typed into a box and the JSON a service
9
+ * receives cannot drift apart — there is only one set of semantics, and the text is a way
10
+ * of writing it.
11
+ *
12
+ * ```text
13
+ * actor:203.0.113.4 -path:/health that address, except its health checks
14
+ * verdict:human $or verdict:unknown either verdict
15
+ * $not path:/health the same as -path:/health, spelled out
16
+ * action:$in(block, drop) one of a set
17
+ * action:$notin(allow, tag) none of a set
18
+ * score:>70 score:>=70 score:10..20 numeric comparisons and a range
19
+ * at:>2026-09-01 a date comparison
20
+ * at:>-1h the last hour, and it stays the last hour
21
+ * has:rule -has:rule the field is set, or is not
22
+ * (verdict:human $or score:<20) $and $not path:/health
23
+ * "GET /api/v2/orders" a phrase, spaces and all
24
+ * ```
25
+ *
26
+ * **Adjacent terms mean AND**, which is what narrowing means. `$or` binds more loosely than
27
+ * `$and`, and `$not` more tightly than either — the conventional precedence. Operators
28
+ * carry a `$` because a bare `or` is a word that appears in User-Agents and paths, and a
29
+ * language where an ordinary search word silently becomes an operator is a language that
30
+ * lies about what it matched.
31
+ *
32
+ * **Nothing here throws for anything anybody types.** It backs live search boxes, so
33
+ * half-typed input is the normal state rather than an error: an unclosed bracket, a dangling
34
+ * `$or`, a `$in(` with nothing after it all parse to the best reading available. A
35
+ * *vocabulary* that is not one is the exception — that is a mistake in the calling code
36
+ * rather than in the box, and it is refused by name instead of failing later, deeper, as a
37
+ * `TypeError` nobody can place. That is the opposite of the JSON
38
+ * engine's rule, and on purpose: JSON is written by a program that can be told it is wrong,
39
+ * a search box by a person who has not finished typing.
40
+ */
41
+ export interface TextOptions {
42
+ /** The field names the text may use, and how each one reads a value. */
43
+ readonly vocabulary?: Vocabulary<never, object> | undefined;
44
+ /**
45
+ * What a name before a colon may be.
46
+ *
47
+ * - `"vocabulary"` — only a vocabulary field; anything else is searched for as text,
48
+ * because a path or a User-Agent can contain a colon. The default when a vocabulary is given.
49
+ * - `"any"` — any name that looks like a path (`address.city:London`), read as text.
50
+ * The default without a vocabulary, since there is nothing else to go on. A URL then
51
+ * has to be quoted: `"http://example.com"`.
52
+ */
53
+ readonly fields?: "vocabulary" | "any" | undefined;
54
+ }
55
+ /**
56
+ * The longest input this will read.
57
+ *
58
+ * Parsing is linear, so length is not a cliff the way nesting is, but this runs on every
59
+ * keystroke and on whatever arrives in a URL. Eight kilobytes is past any query a person
60
+ * writes. Longer input is cut, not refused, because refusing would be the one way this
61
+ * function could fail.
62
+ */
63
+ export declare const MAX_TEXT_CHARS = 8192;
64
+ /**
65
+ * How deep brackets may nest.
66
+ *
67
+ * Past anything anyone types, and deliberately no deeper than the tightest limit the engine
68
+ * is asked to compile under (`UNTRUSTED_LIMITS` allows sixteen levels): a box that produced
69
+ * a query the engine would refuse would be a box that lies about what it accepts.
70
+ */
71
+ export declare const MAX_TEXT_DEPTH = 16;
72
+ /** The name that asks whether a field is set, rather than naming one. */
73
+ export declare const EXISTS_TERM = "has";
74
+ /** The operator words the text syntax understands. */
75
+ export declare const TEXT_OPERATORS: readonly string[];
76
+ /**
77
+ * Parses search-box text into a JQL query. Never throws; an empty input is `{}`, which
78
+ * matches everything. The result is an `UntypedQuery`, so it can be handed to a typed
79
+ * collection: what the text names is only known once somebody has typed it.
80
+ */
81
+ export declare function parseText(input: string, options?: TextOptions): UntypedQuery;
82
+ export declare const SET_TERM: RegExp;
83
+ /**
84
+ * A comparison written out in full, on a field nothing is known about.
85
+ *
86
+ * Without a vocabulary every field reads as text, and `score:>70` would search for the
87
+ * characters ">70" — which nobody typing it means. An operator or a range with a number
88
+ * on it is unambiguous, so it compares. A bare `70` stays text, because on an unknown
89
+ * field it is as likely to be part of an identifier as a quantity.
90
+ */
91
+ export declare const EXPLICIT_COMPARISON: RegExp;
@@ -0,0 +1,16 @@
1
+ import { type Vocabulary } from "../vocabulary.js";
2
+ /**
3
+ * What to offer for the token the caret is in.
4
+ *
5
+ * Returns the completions and the span they replace, so a caller can put one in without
6
+ * disturbing the rest of the input. A token that already names a field with a closed set
7
+ * of values completes the value; anything else completes the field name. Every name
8
+ * offered comes from the vocabulary the parser reads, so nothing offered is something the
9
+ * language would not accept.
10
+ */
11
+ export interface Suggestions {
12
+ readonly options: readonly string[];
13
+ readonly from: number;
14
+ readonly to: number;
15
+ }
16
+ export declare function suggest(input: string, caret: number, vocabulary?: Vocabulary<never, object>): Suggestions;
@@ -0,0 +1,34 @@
1
+ import { type TextOptions } from "./parse.js";
2
+ /**
3
+ * A query, written back as something somebody can type.
4
+ *
5
+ * A dashboard that stores filters as JSON still has to put one in a search box when
6
+ * somebody opens it to edit. That is this, and it is deliberately **not** total: the text
7
+ * syntax has no spelling for `$elemMatch`, `$size`, `$glob` or a `{ "$field": … }`
8
+ * reference, and inventing one per operator would turn a box people can type into a
9
+ * language they have to learn.
10
+ *
11
+ * So the honest answer is two things: the text, and what was left out of it. **The text is
12
+ * exact for what it says** — parsed back with the same vocabulary it means precisely the
13
+ * clauses it carries, which the tests hold it to — but a caller that ignores `unexpressed`
14
+ * is showing somebody a filter wider than the one that is running. `complete` is there so
15
+ * that check is one line.
16
+ */
17
+ export interface Unexpressed {
18
+ /** Where the clause is in the query: `$or[1].tags`. */
19
+ readonly at: string;
20
+ /** The clause itself, as it was written. */
21
+ readonly clause: unknown;
22
+ /** Why the text cannot say it. */
23
+ readonly why: string;
24
+ }
25
+ export interface TextForm {
26
+ /** The query as text. Parsing it back gives exactly the clauses it carries. */
27
+ readonly text: string;
28
+ /** True when the text says everything the query does. */
29
+ readonly complete: boolean;
30
+ /** The clauses with no spelling in the text syntax, in the order they were met. */
31
+ readonly unexpressed: readonly Unexpressed[];
32
+ }
33
+ /** Writes a query as search-box text, and says what it could not write. */
34
+ export declare function toText(query: unknown, options?: TextOptions): TextForm;