@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/dist/search.d.ts
ADDED
|
@@ -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;
|