@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
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How much of a query the engine will take.
|
|
3
|
+
*
|
|
4
|
+
* Queries arrive from places the caller does not control — a URL, a request body, a
|
|
5
|
+
* saved filter somebody else wrote — and every limit here is a way such a query could
|
|
6
|
+
* otherwise cost more than the data it filters. Each one refuses with a `JqlError` rather
|
|
7
|
+
* than truncating, because a query that was quietly cut short answers a different
|
|
8
|
+
* question from the one that was asked.
|
|
9
|
+
*
|
|
10
|
+
* The defaults are far past anything a person writes and far short of anything that
|
|
11
|
+
* hurts. The caps are exported so a caller can say what it accepts in its own
|
|
12
|
+
* documentation and refuse a query before it is even parsed.
|
|
13
|
+
*/
|
|
14
|
+
export interface Limits {
|
|
15
|
+
/** How deep operators may nest. Default 32: recursion over pasted input must stop well short of the stack. */
|
|
16
|
+
readonly maxDepth: number;
|
|
17
|
+
/** How many operators and fields a query may hold in total. Default 10 000. `$in` lists do not count: they cost one set lookup each. */
|
|
18
|
+
readonly maxNodes: number;
|
|
19
|
+
/** The longest `$regex` pattern. Default 1024. */
|
|
20
|
+
readonly maxPatternLength: number;
|
|
21
|
+
/**
|
|
22
|
+
* The longest `$glob`. Default 1024, and 256 for untrusted input.
|
|
23
|
+
*
|
|
24
|
+
* A glob cannot backtrack exponentially, but matching one holding `?` is the length of the
|
|
25
|
+
* pattern times the length of the value — so a megabyte of `?` against a megabyte of text
|
|
26
|
+
* is hours per document. It has a cap of its own because `$regex` is turned *off* for
|
|
27
|
+
* untrusted callers while `$glob` stays on, which is exactly when the cap has to hold.
|
|
28
|
+
*/
|
|
29
|
+
readonly maxGlobLength: number;
|
|
30
|
+
/** How deep `$text` looks into a document. Default 16: past that a document is a graph, not a record. */
|
|
31
|
+
readonly maxTextDepth: number;
|
|
32
|
+
/**
|
|
33
|
+
* Which operators a query may use. Default `"all"`.
|
|
34
|
+
*
|
|
35
|
+
* An allowlist, because a public endpoint's answer to "what may a caller ask?" should be
|
|
36
|
+
* a list it wrote rather than everything the language grows later. `$text` walks a whole
|
|
37
|
+
* document and `$elemMatch` carries a query of its own; an endpoint that needs neither can
|
|
38
|
+
* say so once, here, instead of discovering later which of them somebody found.
|
|
39
|
+
*
|
|
40
|
+
* Names are checked when the query compiles: an operator outside the list is refused by
|
|
41
|
+
* name, never ignored. `$options` and `$comment` are exempt — they modify and annotate
|
|
42
|
+
* rather than ask anything — and `$field` counts as an operator, because comparing two
|
|
43
|
+
* fields is a capability in its own right.
|
|
44
|
+
*/
|
|
45
|
+
readonly allowOperators: readonly string[] | "all";
|
|
46
|
+
/**
|
|
47
|
+
* Whether `$regex` is accepted at all. Default `true`.
|
|
48
|
+
*
|
|
49
|
+
* Turn it off for queries from untrusted callers. No engine can tell a pattern that
|
|
50
|
+
* backtracks for a minute from one that does not without running it, and the string
|
|
51
|
+
* operators (`$contains`, `$startsWith`, `$endsWith`, `$word`) answer almost every
|
|
52
|
+
* question a pattern would, in linear time.
|
|
53
|
+
*/
|
|
54
|
+
readonly allowRegex: boolean;
|
|
55
|
+
}
|
|
56
|
+
/** The defaults, frozen, so they can be spread and never edited in place. */
|
|
57
|
+
export declare const DEFAULT_LIMITS: Readonly<Limits>;
|
|
58
|
+
/**
|
|
59
|
+
* What an untrusted query gets: no patterns, and a tighter size.
|
|
60
|
+
*
|
|
61
|
+
* Public so a service taking queries from a URL can use the same answer as every other
|
|
62
|
+
* service in the project, rather than each one picking its own numbers.
|
|
63
|
+
*/
|
|
64
|
+
export declare const UNTRUSTED_LIMITS: Readonly<Limits>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What each field operator means, as a test on one value.
|
|
3
|
+
*
|
|
4
|
+
* Every builder here runs once, when a query is compiled, and returns the smallest
|
|
5
|
+
* closure that answers its question: the operand is validated, normalised and, where it
|
|
6
|
+
* helps, turned into a `Set` or a lower-cased copy before any document is seen. The
|
|
7
|
+
* returned test only ever does the part that depends on the document.
|
|
8
|
+
*
|
|
9
|
+
* None of these knows about arrays or paths. Whether a test is also tried against each
|
|
10
|
+
* element of an array, and how a field is reached, is `core.ts`'s business — so each
|
|
11
|
+
* operator's meaning is written exactly once.
|
|
12
|
+
*/
|
|
13
|
+
export type Test = (value: unknown) => boolean;
|
|
14
|
+
export declare const NEVER: Test;
|
|
15
|
+
/**
|
|
16
|
+
* Equality with one literal.
|
|
17
|
+
*
|
|
18
|
+
* `null` also matches a missing field, because "has no value" is what
|
|
19
|
+
* people mean by it far more often than "has the value null". `$exists` is there for
|
|
20
|
+
* the times the difference matters.
|
|
21
|
+
*/
|
|
22
|
+
export declare function equalsTest(literal: unknown, ignoreCase: boolean, at: string, now: number): Test;
|
|
23
|
+
/** Membership. A `Set` for the primitives, and a walk only for the object and array literals. */
|
|
24
|
+
export declare function inTest(list: unknown, ignoreCase: boolean, at: string, now: number): Test;
|
|
25
|
+
type Ordering = "$gt" | "$gte" | "$lt" | "$lte";
|
|
26
|
+
/**
|
|
27
|
+
* An ordering comparison.
|
|
28
|
+
*
|
|
29
|
+
* Only like with like: a number against numbers (and bigints), a string against strings
|
|
30
|
+
* in code-unit order, a `$date` against anything that reads as a date. Nothing is
|
|
31
|
+
* coerced, so `"10"` is not greater than `9` — a comparison that converted would give a
|
|
32
|
+
* different answer depending on which side happened to be a string, and a query that
|
|
33
|
+
* means different things on different documents is not one anybody can reason about.
|
|
34
|
+
*/
|
|
35
|
+
export declare function orderTest(operator: Ordering, bound: unknown, at: string, now: number): Test;
|
|
36
|
+
type StringOperator = "$contains" | "$startsWith" | "$endsWith" | "$word" | "$glob";
|
|
37
|
+
/**
|
|
38
|
+
* The string operators, each taking one value or a list meaning "any of these".
|
|
39
|
+
*
|
|
40
|
+
* A list is there because "the path starts with any of `/health`, `/metrics`" is a single
|
|
41
|
+
* thought, and spelling it as an `$or` of three conditions makes it three.
|
|
42
|
+
*/
|
|
43
|
+
export declare function stringTest(operator: StringOperator, operand: unknown, ignoreCase: boolean, at: string, maxGlobLength?: number): Test;
|
|
44
|
+
/**
|
|
45
|
+
* The value as a whole component of the text: bounded on each side by the start or end of
|
|
46
|
+
* the text, or by a character that is not a letter or digit.
|
|
47
|
+
*
|
|
48
|
+
* Ported from BotHandler's actor matching, where it replaced a substring test that let
|
|
49
|
+
* `1.2.3.4` match `1.2.3.45` and `11.2.3.4` — so excluding one address excluded its
|
|
50
|
+
* neighbours too. A side of the value that is itself a separator has chosen its own
|
|
51
|
+
* boundary: `203.0.113.` finds the whole network.
|
|
52
|
+
*/
|
|
53
|
+
export declare function containsWord(actual: string, wanted: string): boolean;
|
|
54
|
+
export declare function parseOptions(options: unknown, at: string): string;
|
|
55
|
+
export declare function regexTest(pattern: unknown, flags: string, limits: {
|
|
56
|
+
allowRegex: boolean;
|
|
57
|
+
maxPatternLength: number;
|
|
58
|
+
}, at: string): Test;
|
|
59
|
+
export declare function typeTest(operand: unknown, at: string): Test;
|
|
60
|
+
export declare function modTest(operand: unknown, at: string): Test;
|
|
61
|
+
/** `$all`: every literal is equal to the value or to one of its elements. An empty list matches nothing. */
|
|
62
|
+
export declare function allTest(list: unknown, ignoreCase: boolean, at: string, now: number): Test;
|
|
63
|
+
/**
|
|
64
|
+
* The order of two values that both came out of a document, or `undefined` when they
|
|
65
|
+
* cannot be compared.
|
|
66
|
+
*
|
|
67
|
+
* Only like with like, as everywhere else in the language: numbers with numbers, strings
|
|
68
|
+
* with strings, dates with dates. Nothing is read as a date here, because neither side is
|
|
69
|
+
* a query saying it is one — `{ "$field": … }` compares what the item holds.
|
|
70
|
+
*/
|
|
71
|
+
export declare function orderOf(a: unknown, b: unknown): number | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* Refuses a literal JSON cannot carry.
|
|
74
|
+
*
|
|
75
|
+
* A `RegExp`, a `Date`, a function or `undefined` in a query would work in the process
|
|
76
|
+
* that wrote it and mean something else — or nothing — once the query was serialised.
|
|
77
|
+
* The language is the JSON, so the engine holds to it even when handed an object that
|
|
78
|
+
* could have been more.
|
|
79
|
+
*/
|
|
80
|
+
export declare function checkLiteral(value: unknown, at: string, depth?: number): void;
|
|
81
|
+
export {};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { type CompileOptions } from "./core.js";
|
|
2
|
+
import type { QueryLike, UntypedQuery } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Splitting a query between a store and this engine.
|
|
5
|
+
*
|
|
6
|
+
* A backend can usually answer part of a question — an index on a field, an operator it has
|
|
7
|
+
* natively — and nothing of the rest. The shape that works is the one HackerPot's stores
|
|
8
|
+
* already use: push what the store can do, keep what it cannot, and make sure the two
|
|
9
|
+
* together are exactly the question that was asked.
|
|
10
|
+
*
|
|
11
|
+
* That last part is the whole contract, and it is why this splits only on **conjunctions**.
|
|
12
|
+
* A query's keys all have to hold, and so do the parts of an `$and`, so each one can go to
|
|
13
|
+
* either side independently. Anything else — a branch of an `$or`, an operator inside a
|
|
14
|
+
* condition — cannot be split without widening or narrowing what is asked, so a conjunct is
|
|
15
|
+
* pushed whole or not at all:
|
|
16
|
+
*
|
|
17
|
+
* pushed ∧ remaining ≡ query
|
|
18
|
+
*
|
|
19
|
+
* The store filters with `pushed` and this engine filters what comes back with `remaining`.
|
|
20
|
+
* Neither side is ever wider than the query, so a caller that runs only `pushed` gets too
|
|
21
|
+
* many items rather than too few — but `complete` says when that is safe, and it is the only
|
|
22
|
+
* thing worth checking before skipping the second pass.
|
|
23
|
+
*/
|
|
24
|
+
export interface Capabilities {
|
|
25
|
+
/**
|
|
26
|
+
* The fields the store can filter on, as the store names them — a path, after any
|
|
27
|
+
* vocabulary alias is resolved. `"all"` for a store that can filter on anything.
|
|
28
|
+
*
|
|
29
|
+
* A vocabulary field that is *computed* is never pushed whatever this says: the store has
|
|
30
|
+
* no way to run a function that lives here.
|
|
31
|
+
*/
|
|
32
|
+
readonly fields?: readonly string[] | "all" | undefined;
|
|
33
|
+
/** The operators the store can answer, or `"all"`. Names are JQL's, not the store's. */
|
|
34
|
+
readonly operators?: readonly string[] | "all" | undefined;
|
|
35
|
+
/** Whether the store can answer `$or`. Default `false`: plenty of key-value stores cannot. */
|
|
36
|
+
readonly or?: boolean | undefined;
|
|
37
|
+
/** Whether it can answer a negation — `$not`, `$nor`, `$ne`, `$nin`, `$exists: false`. Default `false`. */
|
|
38
|
+
readonly not?: boolean | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* A last word on one field's condition, for a store whose limits depend on the values
|
|
41
|
+
* rather than only on the operator names.
|
|
42
|
+
*
|
|
43
|
+
* MongoDB is the example this exists for: it answers `$eq` exactly, but not `$eq` against
|
|
44
|
+
* an embedded document (its equality is key-order sensitive where JQL's is not), and not
|
|
45
|
+
* the case-insensitive form of one. Without a say on the values, a target has to choose
|
|
46
|
+
* between dropping `$eq` altogether and pushing a clause that comes back with the wrong
|
|
47
|
+
* rows.
|
|
48
|
+
*
|
|
49
|
+
* The condition is given as written, with a bare value shown as `{ $eq: value }`, and the
|
|
50
|
+
* field under the name the store knows. Returning `false` keeps the clause here.
|
|
51
|
+
*/
|
|
52
|
+
readonly accepts?: ((field: string, condition: Readonly<Record<string, unknown>>) => boolean) | undefined;
|
|
53
|
+
}
|
|
54
|
+
export interface Kept {
|
|
55
|
+
/** Where the conjunct is in the query. */
|
|
56
|
+
readonly at: string;
|
|
57
|
+
/** The conjunct itself. */
|
|
58
|
+
readonly clause: unknown;
|
|
59
|
+
/** Why the store cannot answer it. */
|
|
60
|
+
readonly why: string;
|
|
61
|
+
}
|
|
62
|
+
export interface Plan {
|
|
63
|
+
/** What the store should filter by, or `undefined` when it can answer nothing. */
|
|
64
|
+
readonly pushed: UntypedQuery | undefined;
|
|
65
|
+
/** What this engine must still filter, or `undefined` when the store answers everything. */
|
|
66
|
+
readonly remaining: UntypedQuery | undefined;
|
|
67
|
+
/** True when `remaining` is `undefined`: the store's answer needs no second pass. */
|
|
68
|
+
readonly complete: boolean;
|
|
69
|
+
/** One entry per conjunct that stayed here, and why. */
|
|
70
|
+
readonly kept: readonly Kept[];
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Splits a query into the part a store can answer and the part that stays here.
|
|
74
|
+
*
|
|
75
|
+
* Relative dates in the pushed part are resolved to instants, so what the store receives is
|
|
76
|
+
* self-contained: `{ "$ago": "1h" }` means nothing to a backend, and resolving it here keeps
|
|
77
|
+
* both halves of the split judged against the same moment.
|
|
78
|
+
*/
|
|
79
|
+
export declare function plan<T, Extra extends object = {}>(query: QueryLike<T, NoInfer<Extra>>, capabilities: Capabilities, options?: CompileOptions<Extra>): Plan;
|
|
@@ -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;
|