@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,217 @@
|
|
|
1
|
+
# Lesson 1 — Your first query
|
|
2
|
+
|
|
3
|
+
**Goal:** ask a collection a question with a JSON document, and see what happens when the
|
|
4
|
+
document is wrong.
|
|
5
|
+
|
|
6
|
+
← [Course](index.md) · Next: [Asking precisely](02-operators.md)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## A query is data
|
|
11
|
+
|
|
12
|
+
Most filters are code. You write a function, it takes a row, it returns true or false. That
|
|
13
|
+
works right up to the moment you want to *do* something with the filter itself — store it,
|
|
14
|
+
put it in a URL, send it to another service, show it to somebody, check it before running it.
|
|
15
|
+
A function can only be called.
|
|
16
|
+
|
|
17
|
+
A JQL query is **a JSON document**:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{ "status": "open" }
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
That is a whole query. It says: the field `status` equals the string `"open"`. It is also a
|
|
24
|
+
value you could have read from a file or taken out of a request body — and everything else in
|
|
25
|
+
this course follows from that one decision.
|
|
26
|
+
|
|
27
|
+
> Every example below is a complete program. Put it in a file next to `orders.mjs` and run it
|
|
28
|
+
> with `node`.
|
|
29
|
+
|
|
30
|
+
## Asking the question
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
import { filter } from "@osqd/jql";
|
|
34
|
+
import { orders } from "./orders.mjs";
|
|
35
|
+
|
|
36
|
+
const open = filter(orders, { status: "open" });
|
|
37
|
+
|
|
38
|
+
for (const order of open) console.log(order.id, order.customer.name);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
o-1002 Grace Hopper
|
|
43
|
+
o-1005 Tim Berners-Lee
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`filter` takes the collection and the query, and gives back every order that matches. There is
|
|
47
|
+
no callback anywhere — the shape of the question is entirely in that little object.
|
|
48
|
+
|
|
49
|
+
## The query really is just data
|
|
50
|
+
|
|
51
|
+
Watch it survive a round trip through text, which is what happens when a filter is saved in a
|
|
52
|
+
database or arrives in a request:
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
import { count } from "@osqd/jql";
|
|
56
|
+
import { orders } from "./orders.mjs";
|
|
57
|
+
|
|
58
|
+
const query = { status: "open" };
|
|
59
|
+
const sent = JSON.stringify(query);
|
|
60
|
+
const arrived = JSON.parse(sent);
|
|
61
|
+
|
|
62
|
+
console.log("sent over the wire:", sent);
|
|
63
|
+
console.log("open orders, here :", count(orders, query));
|
|
64
|
+
console.log("open orders, there:", count(orders, arrived));
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
sent over the wire: {"status":"open"}
|
|
69
|
+
open orders, here : 2
|
|
70
|
+
open orders, there: 2
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Nothing was lost, because there was nothing to lose. This is the property the rest of the
|
|
74
|
+
library is built on: a saved filter, a URL parameter and a line of code are the same thing.
|
|
75
|
+
|
|
76
|
+
## Two keys mean "and"
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
import { filter } from "@osqd/jql";
|
|
80
|
+
import { orders } from "./orders.mjs";
|
|
81
|
+
|
|
82
|
+
const britishAndPaid = filter(orders, {
|
|
83
|
+
status: "paid",
|
|
84
|
+
"customer.country": "GB",
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
console.log(britishAndPaid.map((order) => order.id));
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
[ 'o-1001', 'o-1003' ]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Two things to notice.
|
|
95
|
+
|
|
96
|
+
**Both keys have to hold.** Listing them side by side means *and*. There is an explicit `$and`
|
|
97
|
+
for the times you need it, and lesson 4 covers when that is.
|
|
98
|
+
|
|
99
|
+
**`customer.country` is a path.** A dot reaches inside a nested object, so you can ask about
|
|
100
|
+
`customer.country` without pulling the customer out first. Lesson 3 is about what a path does
|
|
101
|
+
when it meets a *list*, which is the part worth slowing down for.
|
|
102
|
+
|
|
103
|
+
## Four ways to ask
|
|
104
|
+
|
|
105
|
+
`filter` is one of a family. They all take the same query and differ only in what they hand
|
|
106
|
+
back:
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
import { count, find, filter, some } from "@osqd/jql";
|
|
110
|
+
import { orders } from "./orders.mjs";
|
|
111
|
+
|
|
112
|
+
const unpaid = { paid: 0 };
|
|
113
|
+
|
|
114
|
+
console.log("the first one :", find(orders, unpaid)?.id);
|
|
115
|
+
console.log("all of them :", filter(orders, unpaid).map((order) => order.id));
|
|
116
|
+
console.log("how many :", count(orders, unpaid));
|
|
117
|
+
console.log("are there any :", some(orders, unpaid));
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
the first one : o-1002
|
|
122
|
+
all of them : [ 'o-1002', 'o-1004', 'o-1005', 'o-1007' ]
|
|
123
|
+
how many : 4
|
|
124
|
+
are there any : true
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
| | |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| `find` | the first match, or `undefined`. Stops looking at it |
|
|
130
|
+
| `filter` | every match, as a new array |
|
|
131
|
+
| `count` | how many, without building the list |
|
|
132
|
+
| `some` / `every` | whether any / all match. Both stop early |
|
|
133
|
+
|
|
134
|
+
There are more — `findIndex`, `partition`, `filterMap`, `filterRecord`, `findEntry` — and they
|
|
135
|
+
work over arrays, `Set`s, `Map`s, iterables and plain objects. The query never changes; only
|
|
136
|
+
the question about the collection does.
|
|
137
|
+
|
|
138
|
+
## Compiling, when you ask the same thing often
|
|
139
|
+
|
|
140
|
+
Every helper turns the query into a predicate before it starts. If you are asking the same
|
|
141
|
+
question many times, do that once yourself:
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
import { compile } from "@osqd/jql";
|
|
145
|
+
import { orders } from "./orders.mjs";
|
|
146
|
+
|
|
147
|
+
const isPaid = compile({ status: "paid" });
|
|
148
|
+
|
|
149
|
+
console.log("the predicate is just a function:", typeof isPaid);
|
|
150
|
+
console.log("so anything that takes one works:", orders.filter(isPaid).length);
|
|
151
|
+
console.log("and it answers one item too :", isPaid(orders[0]));
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
the predicate is just a function: function
|
|
156
|
+
so anything that takes one works: 4
|
|
157
|
+
and it answers one item too : true
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`compile` is not something you need before you need it — a small query costs about as much to
|
|
161
|
+
compile as testing a handful of rows. It is for a hot loop, or for handing the predicate to
|
|
162
|
+
something that expects one.
|
|
163
|
+
|
|
164
|
+
## A wrong query is refused
|
|
165
|
+
|
|
166
|
+
This is the one behaviour to take away from lesson 1:
|
|
167
|
+
|
|
168
|
+
```js
|
|
169
|
+
import { filter, JqlError } from "@osqd/jql";
|
|
170
|
+
import { orders } from "./orders.mjs";
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
filter(orders, { total: { $gtt: 100 } });
|
|
174
|
+
} catch (error) {
|
|
175
|
+
console.log("refused:", error instanceof JqlError);
|
|
176
|
+
console.log(error.message);
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
refused: true
|
|
182
|
+
at total.$gtt: "$gtt" is not an operator; did you mean "$gt"?
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`$gtt` is a typo. The query is **refused** — it does not quietly match nothing.
|
|
186
|
+
|
|
187
|
+
That matters more than it looks. A filter that silently matches nothing still returns a
|
|
188
|
+
result, the page still renders, and the number on it is wrong in a way no test notices.
|
|
189
|
+
Everything in this language that seems strict is protecting you from that one outcome, and
|
|
190
|
+
the error always says *where* in the query the problem is.
|
|
191
|
+
|
|
192
|
+
## Exercise
|
|
193
|
+
|
|
194
|
+
Find the orders placed through the app that nobody has paid for. Two keys, no operators.
|
|
195
|
+
|
|
196
|
+
<details>
|
|
197
|
+
<summary>Answer</summary>
|
|
198
|
+
|
|
199
|
+
```js
|
|
200
|
+
import { filter } from "@osqd/jql";
|
|
201
|
+
import { orders } from "./orders.mjs";
|
|
202
|
+
|
|
203
|
+
console.log(filter(orders, { channel: "app", paid: 0 }).map((order) => order.id));
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
[ 'o-1005' ]
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
`o-1006` is on the app too, but it has been paid, so `paid: 0` leaves it out.
|
|
211
|
+
</details>
|
|
212
|
+
|
|
213
|
+
## Related
|
|
214
|
+
|
|
215
|
+
- [Quick start](../start/quick-start.md) — the same ground in five minutes
|
|
216
|
+
- [Library API](../reference/api.md) — every helper, and what each takes
|
|
217
|
+
- [Specification §2](../reference/specification.md#2-queries) — what a query document is
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# Lesson 2 — Asking precisely
|
|
2
|
+
|
|
3
|
+
**Goal:** the field operators, and which one to reach for.
|
|
4
|
+
|
|
5
|
+
← [Course](index.md) · Previous: [Your first query](01-first-query.md) · Next: [Arrays and paths](03-arrays-and-paths.md)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Beyond equality
|
|
10
|
+
|
|
11
|
+
`{ status: "open" }` asks whether a value *equals* something. For everything else, put an
|
|
12
|
+
object in place of the value and fill it with **operators**:
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{ "total": { "$gte": 30 } }
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
An operator is one question about the value the path reached. Several operators in one object
|
|
19
|
+
all have to hold, which is how you write a range:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{ "total": { "$gte": 30, "$lt": 140 } }
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The names are the conventional ones — `$gte`, `$in`, `$exists` — so most of this lesson is
|
|
26
|
+
recognition rather than learning. Read it for the three or four places where the answer is
|
|
27
|
+
not the one you expected.
|
|
28
|
+
|
|
29
|
+
> As in lesson 1, every example is a complete program to run next to `orders.mjs`.
|
|
30
|
+
|
|
31
|
+
## Comparing
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
import { filter } from "@osqd/jql";
|
|
35
|
+
import { orders } from "./orders.mjs";
|
|
36
|
+
|
|
37
|
+
const ids = (query) => filter(orders, query).map((order) => order.id);
|
|
38
|
+
|
|
39
|
+
console.log("at least 30 ", ids({ total: { $gte: 30 } }));
|
|
40
|
+
console.log("30 up to 140 ", ids({ total: { $gte: 30, $lt: 140 } }));
|
|
41
|
+
console.log("under 20 ", ids({ total: { $lt: 20 } }));
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
at least 30 [ 'o-1002', 'o-1004', 'o-1005', 'o-1006', 'o-1007' ]
|
|
46
|
+
30 up to 140 [ 'o-1002', 'o-1004', 'o-1006' ]
|
|
47
|
+
under 20 [ 'o-1003', 'o-1008' ]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`$gt`, `$gte`, `$lt` and `$lte` compare **like with like only**. A number is compared with
|
|
51
|
+
numbers and a string with strings; `"10"` is never greater than `9`, and asking is not an
|
|
52
|
+
error, it simply does not match. A comparison that quietly converted would give a different
|
|
53
|
+
answer depending on which side happened to be text, and a filter that means different things
|
|
54
|
+
on different rows is one nobody can reason about.
|
|
55
|
+
|
|
56
|
+
## Membership
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
import { filter } from "@osqd/jql";
|
|
60
|
+
import { orders } from "./orders.mjs";
|
|
61
|
+
|
|
62
|
+
const ids = (query) => filter(orders, query).map((order) => order.id);
|
|
63
|
+
|
|
64
|
+
console.log("open or cancelled", ids({ status: { $in: ["open", "cancelled"] } }));
|
|
65
|
+
console.log("anything but paid", ids({ status: { $nin: ["paid"] } }));
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
open or cancelled [ 'o-1002', 'o-1005', 'o-1007' ]
|
|
70
|
+
anything but paid [ 'o-1002', 'o-1004', 'o-1005', 'o-1007' ]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`$in` is *any of these*, `$nin` is *none of these*. Both take a plain list, so a set of
|
|
74
|
+
checkboxes in a user interface turns into one operator rather than a chain of `$or`.
|
|
75
|
+
|
|
76
|
+
## Missing, null, and neither
|
|
77
|
+
|
|
78
|
+
This is the part of the lesson worth slowing down for. Three of the eight orders have no
|
|
79
|
+
`note` field at all; `o-1007` has one, and its value is `null`.
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
import { filter } from "@osqd/jql";
|
|
83
|
+
import { orders } from "./orders.mjs";
|
|
84
|
+
|
|
85
|
+
const ids = (query) => filter(orders, query).map((order) => order.id);
|
|
86
|
+
|
|
87
|
+
console.log("the field is there ", ids({ note: { $exists: true } }));
|
|
88
|
+
console.log("the field is absent", ids({ note: { $exists: false } }));
|
|
89
|
+
console.log("note is null ", ids({ note: null }));
|
|
90
|
+
console.log("note is text ", ids({ note: { $type: "string" } }));
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
the field is there [ 'o-1001', 'o-1003', 'o-1004', 'o-1006', 'o-1007' ]
|
|
95
|
+
the field is absent [ 'o-1002', 'o-1005', 'o-1008' ]
|
|
96
|
+
note is null [ 'o-1002', 'o-1005', 'o-1007', 'o-1008' ]
|
|
97
|
+
note is text [ 'o-1001', 'o-1003', 'o-1004', 'o-1006' ]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Compare the second and third lines carefully:
|
|
101
|
+
|
|
102
|
+
| The query | What it asks |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| `{ note: { $exists: true } }` | the field is present — **including `o-1007`, whose note is `null`** |
|
|
105
|
+
| `{ note: { $exists: false } }` | the field is not present at all |
|
|
106
|
+
| `{ note: null }` | the value is `null` **or the field is missing** — both |
|
|
107
|
+
| `{ note: { $type: "string" } }` | present, and text |
|
|
108
|
+
|
|
109
|
+
`{ field: null }` deliberately covers both cases, because "has no value" is what people mean
|
|
110
|
+
by it far more often than "holds the value null". When the difference does matter — an
|
|
111
|
+
optional field that was explicitly cleared versus one never set — `$exists` is the operator
|
|
112
|
+
that can tell them apart.
|
|
113
|
+
|
|
114
|
+
## Text, without a pattern
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
import { filter } from "@osqd/jql";
|
|
118
|
+
import { orders } from "./orders.mjs";
|
|
119
|
+
|
|
120
|
+
const ids = (query) => filter(orders, query).map((order) => order.id);
|
|
121
|
+
|
|
122
|
+
console.log("name contains Ada ", ids({ "customer.name": { $contains: "Ada" } }));
|
|
123
|
+
console.log("email starts ada ", ids({ "customer.email": { $startsWith: "ada" } }));
|
|
124
|
+
console.log("id ends 1 or 8 ", ids({ id: { $endsWith: ["1", "8"] } }));
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
name contains Ada [ 'o-1001', 'o-1008' ]
|
|
129
|
+
email starts ada [ 'o-1001', 'o-1008' ]
|
|
130
|
+
id ends 1 or 8 [ 'o-1001', 'o-1008' ]
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
These four — `$contains`, `$startsWith`, `$endsWith` and `$word` — need no pattern and run in
|
|
134
|
+
linear time, which is why they stay available when `$regex` is turned off for queries arriving
|
|
135
|
+
from outside (lesson 12).
|
|
136
|
+
|
|
137
|
+
Each of them takes **one string or a list meaning *any of these***. `$endsWith: ["1", "8"]`
|
|
138
|
+
is one thought, not three ORed together.
|
|
139
|
+
|
|
140
|
+
### Ignoring case
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
import { filter } from "@osqd/jql";
|
|
144
|
+
import { orders } from "./orders.mjs";
|
|
145
|
+
|
|
146
|
+
const ids = (query) => filter(orders, query).map((order) => order.id);
|
|
147
|
+
|
|
148
|
+
console.log("exactly as typed", ids({ "customer.name": { $contains: "ada" } }));
|
|
149
|
+
console.log("ignoring case ", ids({ "customer.name": { $contains: "ada", $options: "i" } }));
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
exactly as typed []
|
|
154
|
+
ignoring case [ 'o-1001', 'o-1008' ]
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`$options: "i"` applies to **every string comparison in that condition**, not only to a
|
|
158
|
+
pattern. Put it beside `$contains`, `$eq`, `$in` — anywhere a condition compares text — and
|
|
159
|
+
that whole condition stops caring about case.
|
|
160
|
+
|
|
161
|
+
### Why `$word` exists
|
|
162
|
+
|
|
163
|
+
`$contains` is the wrong tool for anything with separators in it, and the way it is wrong is
|
|
164
|
+
quiet:
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
import { filter } from "@osqd/jql";
|
|
168
|
+
|
|
169
|
+
const hits = [{ actor: "203.0.113.4" }, { actor: "203.0.113.45" }, { actor: "10.203.0.113" }];
|
|
170
|
+
const seen = (query) => filter(hits, query).map((hit) => hit.actor);
|
|
171
|
+
|
|
172
|
+
console.log("$contains the address", seen({ actor: { $contains: "203.0.113.4" } }));
|
|
173
|
+
console.log("$word the address ", seen({ actor: { $word: "203.0.113.4" } }));
|
|
174
|
+
console.log("$word a whole network", seen({ actor: { $word: "203.0.113." } }));
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
$contains the address [ '203.0.113.4', '203.0.113.45' ]
|
|
179
|
+
$word the address [ '203.0.113.4' ]
|
|
180
|
+
$word a whole network [ '203.0.113.4', '203.0.113.45' ]
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
A block rule written with `$contains` for one address caught its neighbour as well.
|
|
184
|
+
|
|
185
|
+
`$word` matches the value as a **whole component**: bounded by the start or end of the text,
|
|
186
|
+
or by any character that is not a letter or a digit. So `203.0.113.4` stops finding
|
|
187
|
+
`203.0.113.45`. A value that itself ends in a separator has chosen its own boundary, which is
|
|
188
|
+
why `203.0.113.` still finds the whole network — on purpose.
|
|
189
|
+
|
|
190
|
+
## Patterns
|
|
191
|
+
|
|
192
|
+
```js
|
|
193
|
+
import { filter } from "@osqd/jql";
|
|
194
|
+
import { orders } from "./orders.mjs";
|
|
195
|
+
|
|
196
|
+
const ids = (query) => filter(orders, query).map((order) => order.id);
|
|
197
|
+
|
|
198
|
+
console.log("glob, any ink ", ids({ "lines.sku": { $glob: "ink-*" } }));
|
|
199
|
+
console.log("glob, one char ", ids({ "lines.sku": { $glob: "pad-a?" } }));
|
|
200
|
+
console.log("a real regex ", ids({ id: { $regex: "0{2}[13]$" } }));
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
glob, any ink [ 'o-1001', 'o-1004', 'o-1008' ]
|
|
205
|
+
glob, one char [ 'o-1002', 'o-1005', 'o-1006', 'o-1008' ]
|
|
206
|
+
a real regex [ 'o-1001', 'o-1003' ]
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`lines.sku` is a path through a *list* of order lines, and it matched an order when any
|
|
210
|
+
of its lines did. That is lesson 3's subject, and it is the one rule here worth meeting
|
|
211
|
+
slowly.
|
|
212
|
+
|
|
213
|
+
`$glob` is the pattern people usually mean: `*` for any run of characters, `?` for exactly
|
|
214
|
+
one, `\` to escape either. It has no engine behind it and cannot backtrack, so it is safe to
|
|
215
|
+
accept from a stranger. `$regex` is a full ECMAScript pattern and is the one operator you will
|
|
216
|
+
want to switch off at the edge of your system.
|
|
217
|
+
|
|
218
|
+
## Numbers and sizes
|
|
219
|
+
|
|
220
|
+
```js
|
|
221
|
+
import { filter } from "@osqd/jql";
|
|
222
|
+
import { orders } from "./orders.mjs";
|
|
223
|
+
|
|
224
|
+
const ids = (query) => filter(orders, query).map((order) => order.id);
|
|
225
|
+
|
|
226
|
+
console.log("even totals ", ids({ total: { $mod: [2, 0] } }));
|
|
227
|
+
console.log("a longish note ", ids({ note: { $length: { $gt: 18 } } }));
|
|
228
|
+
console.log("two order lines", ids({ lines: { $size: 2 } }));
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
even totals [ 'o-1002', 'o-1004', 'o-1005', 'o-1007', 'o-1008' ]
|
|
233
|
+
a longish note [ 'o-1001', 'o-1006' ]
|
|
234
|
+
two order lines [ 'o-1001', 'o-1004', 'o-1005', 'o-1006', 'o-1008' ]
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
`$mod` takes `[divisor, remainder]`. `$size` counts the elements of an array. `$length`
|
|
238
|
+
measures an array **or a string**, and takes a nested condition, so "longer than 18
|
|
239
|
+
characters" is `{ $length: { $gt: 18 } }` rather than a second query.
|
|
240
|
+
|
|
241
|
+
## The operators, in full
|
|
242
|
+
|
|
243
|
+
| | |
|
|
244
|
+
| --- | --- |
|
|
245
|
+
| `$eq` `$ne` | equal, and not equal to any reached value |
|
|
246
|
+
| `$gt` `$gte` `$lt` `$lte` | ordering, like with like only |
|
|
247
|
+
| `$in` `$nin` | membership of a list |
|
|
248
|
+
| `$exists` `$type` | presence, and what kind |
|
|
249
|
+
| `$contains` `$startsWith` `$endsWith` `$word` | text, without a pattern |
|
|
250
|
+
| `$glob` | `*`, `?`, `\` — a pattern with no engine behind it |
|
|
251
|
+
| `$regex` `$options` | an ECMAScript pattern, and its flags |
|
|
252
|
+
| `$mod` | `[divisor, remainder]` |
|
|
253
|
+
| `$size` `$length` | elements of an array; length of an array **or a string** |
|
|
254
|
+
| `$all` `$elemMatch` | lesson 3 |
|
|
255
|
+
| `$not` | lesson 4 |
|
|
256
|
+
|
|
257
|
+
## Exercise
|
|
258
|
+
|
|
259
|
+
Find the orders whose note mentions a gift — but not the ones merely *tagged* `gift` —
|
|
260
|
+
without caring about case.
|
|
261
|
+
|
|
262
|
+
<details>
|
|
263
|
+
<summary>Answer</summary>
|
|
264
|
+
|
|
265
|
+
```js
|
|
266
|
+
import { filter } from "@osqd/jql";
|
|
267
|
+
import { orders } from "./orders.mjs";
|
|
268
|
+
|
|
269
|
+
const found = filter(orders, { note: { $contains: "gift", $options: "i" } });
|
|
270
|
+
|
|
271
|
+
console.log(found.map((order) => `${order.id}: ${order.note}`));
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
```
|
|
275
|
+
[ 'o-1006: gift wrap, no receipt' ]
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
`o-1001` is tagged `gift`, but its note says "leave with the neighbour" — and a condition on
|
|
279
|
+
`note` only ever reads `note`.
|
|
280
|
+
</details>
|
|
281
|
+
|
|
282
|
+
## Related
|
|
283
|
+
|
|
284
|
+
- [Specification §5](../reference/specification.md#5-field-operators) — every operator, precisely
|
|
285
|
+
- [Queries from outside](../guides/untrusted-input.md) — why `$regex` is the one that gets turned off
|