@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/text.js
ADDED
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
// src/internal/duration.ts
|
|
2
|
+
var PART = /(\d+)(ms|s|m|h|d|w)/g;
|
|
3
|
+
var SHAPE = /^(?:\d+(?:ms|s|m|h|d|w))+$/;
|
|
4
|
+
var UNITS = {
|
|
5
|
+
ms: 1,
|
|
6
|
+
s: 1e3,
|
|
7
|
+
m: 6e4,
|
|
8
|
+
h: 36e5,
|
|
9
|
+
d: 864e5,
|
|
10
|
+
w: 6048e5
|
|
11
|
+
};
|
|
12
|
+
function parseDuration(text) {
|
|
13
|
+
if (!SHAPE.test(text)) return void 0;
|
|
14
|
+
let total = 0;
|
|
15
|
+
PART.lastIndex = 0;
|
|
16
|
+
for (; ; ) {
|
|
17
|
+
const part = PART.exec(text);
|
|
18
|
+
if (part === null) break;
|
|
19
|
+
const count = Number(part[1]);
|
|
20
|
+
if (!Number.isSafeInteger(count)) return void 0;
|
|
21
|
+
total += count * UNITS[part[2]];
|
|
22
|
+
}
|
|
23
|
+
return Number.isSafeInteger(total) ? total : void 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/errors.ts
|
|
27
|
+
var JqlError = class extends Error {
|
|
28
|
+
/**
|
|
29
|
+
* Where in the query the problem is, written the way you would reach it in code:
|
|
30
|
+
* `$or[1].age.$gt`. Empty for the query as a whole.
|
|
31
|
+
*/
|
|
32
|
+
at;
|
|
33
|
+
constructor(message, at = "") {
|
|
34
|
+
super(at === "" ? message : `at ${show(at)}: ${message}`);
|
|
35
|
+
this.name = "JqlError";
|
|
36
|
+
this.at = at;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
function show(at) {
|
|
40
|
+
return at.length > 80 ? `${at.slice(0, 80)}\u2026` : at;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/internal/values.ts
|
|
44
|
+
function isPlainObject(value) {
|
|
45
|
+
if (value === null || typeof value !== "object") return false;
|
|
46
|
+
const prototype = Object.getPrototypeOf(value);
|
|
47
|
+
return prototype === Object.prototype || prototype === null;
|
|
48
|
+
}
|
|
49
|
+
function describe(value) {
|
|
50
|
+
if (value === null) return "null";
|
|
51
|
+
if (typeof value === "function") return "a function";
|
|
52
|
+
if (Array.isArray(value)) return "an array";
|
|
53
|
+
if (value instanceof RegExp) return "a RegExp object";
|
|
54
|
+
if (value instanceof Date) return "a Date object";
|
|
55
|
+
if (value instanceof Map) return "a Map";
|
|
56
|
+
if (value instanceof Set) return "a Set";
|
|
57
|
+
if (typeof value === "object") {
|
|
58
|
+
if (isPlainObject(value)) return "an object";
|
|
59
|
+
const name = nameOf(value);
|
|
60
|
+
return name === void 0 ? "an object" : `an instance of ${name}`;
|
|
61
|
+
}
|
|
62
|
+
if (typeof value === "string") return `the string ${JSON.stringify(value.length > 40 ? `${value.slice(0, 40)}\u2026` : value)}`;
|
|
63
|
+
return `${typeof value} ${String(value)}`;
|
|
64
|
+
}
|
|
65
|
+
function nameOf(value) {
|
|
66
|
+
const prototype = Object.getPrototypeOf(value);
|
|
67
|
+
const name = prototype?.constructor?.name;
|
|
68
|
+
return typeof name === "string" && name !== "" && name !== "Object" ? name : void 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/vocabulary.ts
|
|
72
|
+
function checkVocabulary(value, at = "vocabulary") {
|
|
73
|
+
if (value === void 0 || value !== null && typeof value === "object" && value.lookup instanceof Map) return value;
|
|
74
|
+
throw new JqlError(
|
|
75
|
+
`is not a vocabulary, but ${describe(value)}: build one with defineVocabulary()({ fields: \u2026 }) \u2014 the empty parentheses first \u2014 and pass what that returns`,
|
|
76
|
+
at
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/text/parse.ts
|
|
81
|
+
var MAX_TEXT_CHARS = 8192;
|
|
82
|
+
var MAX_TEXT_DEPTH = 16;
|
|
83
|
+
var EXISTS_TERM = "has";
|
|
84
|
+
var TEXT_OPERATORS = ["$and", "$or", "$not", "$in", "$notin"];
|
|
85
|
+
function parseText(input, options = {}) {
|
|
86
|
+
checkVocabulary(options.vocabulary);
|
|
87
|
+
const trimmed = plainQuotes(String(input).trim());
|
|
88
|
+
const tokens = tokenize(trimmed.length > MAX_TEXT_CHARS ? trimmed.slice(0, MAX_TEXT_CHARS) : trimmed);
|
|
89
|
+
const mode = options.fields ?? (options.vocabulary === void 0 ? "any" : "vocabulary");
|
|
90
|
+
return toQuery(parseTokens(tokens), options.vocabulary, mode);
|
|
91
|
+
}
|
|
92
|
+
var DOUBLE_QUOTES = "\u201C\u201D\u201E\u201F\u2033\u2036";
|
|
93
|
+
var TYPOGRAPHIC_QUOTES = /[\u201c\u201d\u201e\u201f\u2033\u2036\u2018\u2019\u201a\u201b\u2032\u2035]/g;
|
|
94
|
+
function plainQuotes(input) {
|
|
95
|
+
return input.replace(TYPOGRAPHIC_QUOTES, (quote2) => DOUBLE_QUOTES.includes(quote2) ? '"' : "'");
|
|
96
|
+
}
|
|
97
|
+
var SET_OPENER = /\$(in|notin)$/i;
|
|
98
|
+
var SPACE = /\s/;
|
|
99
|
+
function tokenize(input) {
|
|
100
|
+
const tokens = [];
|
|
101
|
+
let current = "";
|
|
102
|
+
let quote2;
|
|
103
|
+
let setQuote;
|
|
104
|
+
let depth = 0;
|
|
105
|
+
const flush = () => {
|
|
106
|
+
if (current === "") return;
|
|
107
|
+
const lower = current.toLowerCase();
|
|
108
|
+
if (lower === "$and" || lower === "$or" || lower === "$not") tokens.push({ kind: "op", op: lower.slice(1) });
|
|
109
|
+
else tokens.push({ kind: "word", text: current });
|
|
110
|
+
current = "";
|
|
111
|
+
};
|
|
112
|
+
for (let i = 0; i < input.length; i++) {
|
|
113
|
+
const character = input[i];
|
|
114
|
+
if (depth > 0) {
|
|
115
|
+
current += character;
|
|
116
|
+
if (setQuote !== void 0) {
|
|
117
|
+
if (character === setQuote) setQuote = void 0;
|
|
118
|
+
} else if (character === '"' || character === "'") setQuote = character;
|
|
119
|
+
else if (character === "(") depth++;
|
|
120
|
+
else if (character === ")") {
|
|
121
|
+
depth--;
|
|
122
|
+
if (depth === 0) flush();
|
|
123
|
+
}
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (quote2 !== void 0) {
|
|
127
|
+
if (character === quote2) quote2 = void 0;
|
|
128
|
+
else current += character;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (character === '"') {
|
|
132
|
+
quote2 = '"';
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (character === "'" && (current === "" || current === "-" || current === "!" || current.endsWith(":"))) {
|
|
136
|
+
quote2 = "'";
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (character === "(") {
|
|
140
|
+
if (SET_OPENER.test(current)) {
|
|
141
|
+
current += character;
|
|
142
|
+
depth = 1;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
flush();
|
|
146
|
+
tokens.push({ kind: "open" });
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (character === ")") {
|
|
150
|
+
flush();
|
|
151
|
+
tokens.push({ kind: "close" });
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (SPACE.test(character)) {
|
|
155
|
+
flush();
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
current += character;
|
|
159
|
+
}
|
|
160
|
+
flush();
|
|
161
|
+
return tokens;
|
|
162
|
+
}
|
|
163
|
+
function splitSet(inside) {
|
|
164
|
+
const values = [];
|
|
165
|
+
let at = 0;
|
|
166
|
+
while (at < inside.length) {
|
|
167
|
+
while (at < inside.length && SPACE.test(inside[at])) at++;
|
|
168
|
+
if (at >= inside.length) break;
|
|
169
|
+
const open = inside[at];
|
|
170
|
+
let value;
|
|
171
|
+
if (open === '"' || open === "'") {
|
|
172
|
+
const close = inside.indexOf(open, at + 1);
|
|
173
|
+
value = close === -1 ? inside.slice(at + 1) : inside.slice(at + 1, close);
|
|
174
|
+
at = close === -1 ? inside.length : close + 1;
|
|
175
|
+
const comma = inside.indexOf(",", at);
|
|
176
|
+
at = comma === -1 ? inside.length : comma + 1;
|
|
177
|
+
} else {
|
|
178
|
+
const comma = inside.indexOf(",", at);
|
|
179
|
+
const end = comma === -1 ? inside.length : comma;
|
|
180
|
+
value = inside.slice(at, end);
|
|
181
|
+
at = end + 1;
|
|
182
|
+
}
|
|
183
|
+
const cleaned = value.trim();
|
|
184
|
+
if (cleaned !== "") values.push(cleaned);
|
|
185
|
+
}
|
|
186
|
+
return values;
|
|
187
|
+
}
|
|
188
|
+
function unquote(value) {
|
|
189
|
+
const trimmed = value.trim();
|
|
190
|
+
if (trimmed.length < 2) return trimmed;
|
|
191
|
+
const first = trimmed[0];
|
|
192
|
+
return (first === '"' || first === "'") && trimmed.endsWith(first) ? trimmed.slice(1, -1) : trimmed;
|
|
193
|
+
}
|
|
194
|
+
var SET_TERM = /^\$(in|notin)\(([\s\S]*)$/i;
|
|
195
|
+
function toTerm(word) {
|
|
196
|
+
const negated = word.startsWith("-") || word.startsWith("!");
|
|
197
|
+
const body = negated ? word.slice(1) : word;
|
|
198
|
+
if (body === "") return void 0;
|
|
199
|
+
const colon = body.indexOf(":");
|
|
200
|
+
if (colon <= 0) return { name: void 0, value: body, negated };
|
|
201
|
+
const name = body.slice(0, colon);
|
|
202
|
+
const rest = body.slice(colon + 1);
|
|
203
|
+
const set = SET_TERM.exec(rest);
|
|
204
|
+
if (set !== null) {
|
|
205
|
+
const raw = set[2];
|
|
206
|
+
const values = splitSet(raw.endsWith(")") ? raw.slice(0, -1) : raw);
|
|
207
|
+
const inverted = set[1].toLowerCase() === "notin";
|
|
208
|
+
return { name, value: values[0] ?? "", negated: negated !== inverted, values };
|
|
209
|
+
}
|
|
210
|
+
const value = unquote(rest);
|
|
211
|
+
if (value === "") return void 0;
|
|
212
|
+
return { name, value, negated };
|
|
213
|
+
}
|
|
214
|
+
function parseTokens(tokens) {
|
|
215
|
+
let at = 0;
|
|
216
|
+
let depth = 0;
|
|
217
|
+
const parseUnary = () => {
|
|
218
|
+
const token = tokens[at];
|
|
219
|
+
if (token === void 0) return void 0;
|
|
220
|
+
if (token.kind === "op" && token.op === "not") {
|
|
221
|
+
let negations = 0;
|
|
222
|
+
while (tokens[at]?.kind === "op" && tokens[at].op === "not") {
|
|
223
|
+
at++;
|
|
224
|
+
negations++;
|
|
225
|
+
}
|
|
226
|
+
const of = parseUnary();
|
|
227
|
+
if (of === void 0) return void 0;
|
|
228
|
+
return negations % 2 === 0 ? of : { kind: "not", of };
|
|
229
|
+
}
|
|
230
|
+
if (token.kind === "open") {
|
|
231
|
+
at++;
|
|
232
|
+
if (depth >= MAX_TEXT_DEPTH) return void 0;
|
|
233
|
+
depth++;
|
|
234
|
+
const inner = parseOr();
|
|
235
|
+
depth--;
|
|
236
|
+
if (tokens[at]?.kind === "close") at++;
|
|
237
|
+
return inner.kind === "all" ? void 0 : inner;
|
|
238
|
+
}
|
|
239
|
+
if (token.kind === "close") return void 0;
|
|
240
|
+
if (token.kind === "op") {
|
|
241
|
+
at++;
|
|
242
|
+
return parseUnary();
|
|
243
|
+
}
|
|
244
|
+
at++;
|
|
245
|
+
const term = toTerm(token.text);
|
|
246
|
+
return term === void 0 ? void 0 : { kind: "term", term };
|
|
247
|
+
};
|
|
248
|
+
const parseAnd = () => {
|
|
249
|
+
const parts2 = [];
|
|
250
|
+
while (at < tokens.length) {
|
|
251
|
+
const token = tokens[at];
|
|
252
|
+
if (token === void 0 || token.kind === "close") break;
|
|
253
|
+
if (token.kind === "op" && token.op === "or") break;
|
|
254
|
+
if (token.kind === "op" && token.op === "and") {
|
|
255
|
+
at++;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const before = at;
|
|
259
|
+
const part = parseUnary();
|
|
260
|
+
if (part !== void 0) parts2.push(part);
|
|
261
|
+
if (at === before) at++;
|
|
262
|
+
}
|
|
263
|
+
return parts2.length === 0 ? { kind: "all" } : parts2.length === 1 ? parts2[0] : { kind: "and", parts: parts2 };
|
|
264
|
+
};
|
|
265
|
+
const parseOr = () => {
|
|
266
|
+
const parts2 = [parseAnd()];
|
|
267
|
+
while (tokens[at]?.kind === "op" && tokens[at].op === "or") {
|
|
268
|
+
at++;
|
|
269
|
+
parts2.push(parseAnd());
|
|
270
|
+
}
|
|
271
|
+
const real = parts2.filter((part) => part.kind !== "all");
|
|
272
|
+
if (real.length === 0) return { kind: "all" };
|
|
273
|
+
return real.length === 1 ? real[0] : { kind: "or", parts: real };
|
|
274
|
+
};
|
|
275
|
+
const parts = [];
|
|
276
|
+
while (at < tokens.length) {
|
|
277
|
+
const tree = parseOr();
|
|
278
|
+
if (tree.kind !== "all") parts.push(tree);
|
|
279
|
+
if (tokens[at]?.kind === "close") at++;
|
|
280
|
+
}
|
|
281
|
+
if (parts.length === 0) return { kind: "all" };
|
|
282
|
+
return parts.length === 1 ? parts[0] : { kind: "and", parts };
|
|
283
|
+
}
|
|
284
|
+
function nothing(key) {
|
|
285
|
+
return { [key]: { $in: [] } };
|
|
286
|
+
}
|
|
287
|
+
function toQuery(tree, vocabulary, mode) {
|
|
288
|
+
switch (tree.kind) {
|
|
289
|
+
case "all":
|
|
290
|
+
return {};
|
|
291
|
+
case "not":
|
|
292
|
+
return { $not: toQuery(tree.of, vocabulary, mode) };
|
|
293
|
+
case "and":
|
|
294
|
+
return { $and: tree.parts.map((part) => toQuery(part, vocabulary, mode)) };
|
|
295
|
+
case "or":
|
|
296
|
+
return { $or: tree.parts.map((part) => toQuery(part, vocabulary, mode)) };
|
|
297
|
+
case "term": {
|
|
298
|
+
const query = termQuery(tree.term, vocabulary, mode);
|
|
299
|
+
return tree.term.negated ? { $not: query } : query;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
var PATH_NAME = /^[\p{L}_][\p{L}\p{N}_-]*(?:\.[\p{L}\p{N}_-]+)*$/u;
|
|
304
|
+
function termQuery(term, vocabulary, mode) {
|
|
305
|
+
let field;
|
|
306
|
+
let key;
|
|
307
|
+
if (term.name !== void 0 && term.name.toLowerCase() === EXISTS_TERM && term.values === void 0) {
|
|
308
|
+
const named = existence(term.value, vocabulary, mode);
|
|
309
|
+
if (named !== void 0) return named;
|
|
310
|
+
}
|
|
311
|
+
if (term.name !== void 0) {
|
|
312
|
+
field = vocabulary?.lookup.get(term.name.toLowerCase());
|
|
313
|
+
if (field !== void 0) key = field.name;
|
|
314
|
+
else if (mode === "any" && PATH_NAME.test(term.name)) key = term.name;
|
|
315
|
+
}
|
|
316
|
+
if (key === void 0) {
|
|
317
|
+
const text = term.name === void 0 ? term.value : `${term.name}:${term.values === void 0 ? term.value : term.values.join(",")}`;
|
|
318
|
+
return freeText(text, vocabulary);
|
|
319
|
+
}
|
|
320
|
+
const kind = field?.kind ?? (term.values === void 0 && EXPLICIT_COMPARISON.test(term.value) ? "number" : "text");
|
|
321
|
+
const values = term.values ?? [term.value];
|
|
322
|
+
if (values.length === 0) return nothing(key);
|
|
323
|
+
switch (kind) {
|
|
324
|
+
case "text":
|
|
325
|
+
return { [key]: { $contains: single(values), $options: "i" } };
|
|
326
|
+
case "word":
|
|
327
|
+
return { [key]: { $word: single(values), $options: "i" } };
|
|
328
|
+
case "exact":
|
|
329
|
+
return values.length === 1 ? { [key]: { $eq: values[0], $options: "i" } } : { [key]: { $in: values, $options: "i" } };
|
|
330
|
+
case "boolean": {
|
|
331
|
+
const read = values.map(readBoolean).filter((value) => value !== void 0);
|
|
332
|
+
if (read.length === 0) return nothing(key);
|
|
333
|
+
return read.length === 1 ? { [key]: read[0] } : { [key]: { $in: read } };
|
|
334
|
+
}
|
|
335
|
+
case "number":
|
|
336
|
+
case "date": {
|
|
337
|
+
if (term.values !== void 0) {
|
|
338
|
+
const read = values.map((value) => readScalar(value, kind)).filter((value) => value !== void 0);
|
|
339
|
+
return read.length === 0 ? nothing(key) : { [key]: { $in: read } };
|
|
340
|
+
}
|
|
341
|
+
const condition = comparison(term.value, kind);
|
|
342
|
+
return condition === void 0 ? nothing(key) : { [key]: condition };
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
var EXPLICIT_COMPARISON = /^(?:(?:>=|<=|>|<)[+-]?(?:\d+\.?\d*|\.\d+)|[+-]?(?:\d+\.?\d*|\.\d+)?\.\.[+-]?(?:\d+\.?\d*|\.\d+)?)$/;
|
|
347
|
+
function existence(name, vocabulary, mode) {
|
|
348
|
+
const field = vocabulary?.lookup.get(name.toLowerCase());
|
|
349
|
+
if (field !== void 0) return { [field.name]: { $exists: true } };
|
|
350
|
+
if (mode === "any" && PATH_NAME.test(name)) return { [name]: { $exists: true } };
|
|
351
|
+
return void 0;
|
|
352
|
+
}
|
|
353
|
+
function single(values) {
|
|
354
|
+
return values.length === 1 ? values[0] : values;
|
|
355
|
+
}
|
|
356
|
+
function freeText(text, vocabulary) {
|
|
357
|
+
if (vocabulary?.text !== void 0) return { $text: { $search: text, $fields: vocabulary.text } };
|
|
358
|
+
return { $text: text };
|
|
359
|
+
}
|
|
360
|
+
var TRUE_WORDS = /* @__PURE__ */ new Set(["true", "yes", "1", "on"]);
|
|
361
|
+
var FALSE_WORDS = /* @__PURE__ */ new Set(["false", "no", "0", "off"]);
|
|
362
|
+
function readBoolean(value) {
|
|
363
|
+
const lower = value.toLowerCase();
|
|
364
|
+
if (TRUE_WORDS.has(lower)) return true;
|
|
365
|
+
if (FALSE_WORDS.has(lower)) return false;
|
|
366
|
+
return void 0;
|
|
367
|
+
}
|
|
368
|
+
var NUMBER = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
369
|
+
var RELATIVE = /^([+-])(\d+(?:ms|s|m|h|d|w))+$/;
|
|
370
|
+
function readScalar(value, kind) {
|
|
371
|
+
const trimmed = value.trim();
|
|
372
|
+
if (kind === "number") {
|
|
373
|
+
if (!NUMBER.test(trimmed)) return void 0;
|
|
374
|
+
const read = Number(trimmed);
|
|
375
|
+
if (!Number.isFinite(read)) return void 0;
|
|
376
|
+
return read === 0 ? 0 : read;
|
|
377
|
+
}
|
|
378
|
+
if (trimmed.toLowerCase() === "now") return { $date: "now" };
|
|
379
|
+
const relative = RELATIVE.exec(trimmed);
|
|
380
|
+
if (relative !== null) {
|
|
381
|
+
const span = trimmed.slice(1);
|
|
382
|
+
if (parseDuration(span) === void 0) return void 0;
|
|
383
|
+
return { $date: relative[1] === "-" ? { $ago: span } : { $ahead: span } };
|
|
384
|
+
}
|
|
385
|
+
return trimmed !== "" && !Number.isNaN(Date.parse(trimmed)) ? { $date: trimmed } : void 0;
|
|
386
|
+
}
|
|
387
|
+
var COMPARISON = /^(>=|<=|>|<|=)?([\s\S]*)$/;
|
|
388
|
+
function comparison(value, kind) {
|
|
389
|
+
const range = value.indexOf("..");
|
|
390
|
+
if (range !== -1) {
|
|
391
|
+
const low = value.slice(0, range);
|
|
392
|
+
const high = value.slice(range + 2);
|
|
393
|
+
const from = low === "" ? void 0 : readScalar(low, kind);
|
|
394
|
+
const to = high === "" ? void 0 : readScalar(high, kind);
|
|
395
|
+
if (low !== "" && from === void 0 || high !== "" && to === void 0 || from === void 0 && to === void 0) return void 0;
|
|
396
|
+
return { ...from === void 0 ? {} : { $gte: from }, ...to === void 0 ? {} : { $lte: to } };
|
|
397
|
+
}
|
|
398
|
+
const [, operator, rest] = COMPARISON.exec(value);
|
|
399
|
+
const scalar = readScalar(rest, kind);
|
|
400
|
+
if (scalar === void 0) return void 0;
|
|
401
|
+
switch (operator) {
|
|
402
|
+
case ">":
|
|
403
|
+
return { $gt: scalar };
|
|
404
|
+
case ">=":
|
|
405
|
+
return { $gte: scalar };
|
|
406
|
+
case "<":
|
|
407
|
+
return { $lt: scalar };
|
|
408
|
+
case "<=":
|
|
409
|
+
return { $lte: scalar };
|
|
410
|
+
default:
|
|
411
|
+
return { $eq: scalar };
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/text/suggest.ts
|
|
416
|
+
function suggest(input, caret, vocabulary) {
|
|
417
|
+
checkVocabulary(vocabulary);
|
|
418
|
+
const before = input.slice(0, caret);
|
|
419
|
+
const start = Math.max(before.lastIndexOf(" ") + 1, 0);
|
|
420
|
+
const token = before.slice(start);
|
|
421
|
+
const to = caret;
|
|
422
|
+
const negated = token.startsWith("-") || token.startsWith("!");
|
|
423
|
+
const body = negated ? token.slice(1) : token;
|
|
424
|
+
const sign = negated ? token[0] : "";
|
|
425
|
+
const colon = body.indexOf(":");
|
|
426
|
+
if (colon === -1) {
|
|
427
|
+
const partial2 = body.toLowerCase();
|
|
428
|
+
if (partial2.startsWith("$")) return { options: TEXT_OPERATORS.filter((name2) => name2.startsWith(partial2)), from: start, to };
|
|
429
|
+
const names = vocabulary?.names ?? [];
|
|
430
|
+
return { options: names.filter((name2) => name2.startsWith(partial2)).map((name2) => `${sign}${name2}:`), from: start, to };
|
|
431
|
+
}
|
|
432
|
+
const name = body.slice(0, colon).toLowerCase();
|
|
433
|
+
const values = vocabulary?.lookup.get(name)?.values;
|
|
434
|
+
if (values === void 0) return { options: [], from: start, to };
|
|
435
|
+
const partial = body.slice(colon + 1).toLowerCase();
|
|
436
|
+
const prefix = `${sign}${name}:`;
|
|
437
|
+
const forms = partial.startsWith("$") ? ["$in(", "$notin("].filter((form) => form.startsWith(partial)).map((form) => `${prefix}${form}`) : [];
|
|
438
|
+
return { options: [...forms, ...values.filter((value) => value.toLowerCase().startsWith(partial)).map((value) => `${prefix}${value}`)], from: start, to };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// src/text/write.ts
|
|
442
|
+
var SAYABLE = {
|
|
443
|
+
text: ["$contains", "$exists"],
|
|
444
|
+
word: ["$word", "$exists"],
|
|
445
|
+
exact: ["$eq", "$in", "$exists"],
|
|
446
|
+
number: ["$eq", "$gt", "$gte", "$lt", "$lte", "$in", "$exists"],
|
|
447
|
+
date: ["$eq", "$gt", "$gte", "$lt", "$lte", "$in", "$exists"],
|
|
448
|
+
boolean: ["$eq", "$in", "$exists"]
|
|
449
|
+
};
|
|
450
|
+
var PATH_NAME2 = /^[\p{L}_][\p{L}\p{N}_-]*(?:\.[\p{L}\p{N}_-]+)*$/u;
|
|
451
|
+
var INFERRED = ["$gt", "$gte", "$lt", "$lte"];
|
|
452
|
+
function inferKind(condition) {
|
|
453
|
+
const operators = Object.keys(condition).filter((operator) => operator !== "$options");
|
|
454
|
+
const numeric = operators.length > 0 && operators.every((operator) => INFERRED.includes(operator) && typeof condition[operator] === "number" && Number.isFinite(condition[operator]));
|
|
455
|
+
return numeric ? "number" : "text";
|
|
456
|
+
}
|
|
457
|
+
function toText(query, options = {}) {
|
|
458
|
+
const vocabulary = checkVocabulary(options.vocabulary);
|
|
459
|
+
const mode = options.fields ?? (vocabulary === void 0 ? "any" : "vocabulary");
|
|
460
|
+
const left = [];
|
|
461
|
+
const text = writeQuery(query, { vocabulary, mode, left }, "", false);
|
|
462
|
+
return { text, complete: left.length === 0, unexpressed: left };
|
|
463
|
+
}
|
|
464
|
+
function drop(writing, at, clause, why) {
|
|
465
|
+
writing.left.push({ at, clause, why });
|
|
466
|
+
return "";
|
|
467
|
+
}
|
|
468
|
+
function writeQuery(query, writing, at, grouped) {
|
|
469
|
+
if (query === null || typeof query !== "object" || Array.isArray(query)) {
|
|
470
|
+
return drop(writing, at, query, "only an object is a query, and the text syntax writes nothing else");
|
|
471
|
+
}
|
|
472
|
+
const clauses = [];
|
|
473
|
+
for (const [key, value] of Object.entries(query)) {
|
|
474
|
+
const here = at === "" ? key : `${at}.${key}`;
|
|
475
|
+
const written = writeClause(key, value, writing, here, grouped);
|
|
476
|
+
if (written !== "") clauses.push(written);
|
|
477
|
+
}
|
|
478
|
+
if (clauses.length === 0) return "";
|
|
479
|
+
const joined = clauses.join(" ");
|
|
480
|
+
return grouped && clauses.length > 1 ? `(${joined})` : joined;
|
|
481
|
+
}
|
|
482
|
+
function writeClause(key, value, writing, at, grouped) {
|
|
483
|
+
switch (key) {
|
|
484
|
+
case "$comment":
|
|
485
|
+
return "";
|
|
486
|
+
case "$and": {
|
|
487
|
+
if (!Array.isArray(value)) return drop(writing, at, value, "$and takes a list of queries");
|
|
488
|
+
const parts = value.map((part, index) => writeQuery(part, writing, `${at}[${index}]`, true)).filter((part) => part !== "");
|
|
489
|
+
const joined = parts.join(" ");
|
|
490
|
+
return grouped && parts.length > 1 ? `(${joined})` : joined;
|
|
491
|
+
}
|
|
492
|
+
case "$or": {
|
|
493
|
+
if (!Array.isArray(value)) return drop(writing, at, value, "$or takes a list of queries");
|
|
494
|
+
if (value.length === 0) return drop(writing, at, value, "an $or of nothing matches nothing, and no term says that");
|
|
495
|
+
const parts = value.map((part, index) => writeQuery(part, writing, `${at}[${index}]`, true));
|
|
496
|
+
if (parts.some((part) => part === "")) return drop(writing, at, value, "one branch of the $or has no spelling, and writing the others would widen it");
|
|
497
|
+
return parts.length === 1 ? parts[0] : `(${parts.join(" $or ")})`;
|
|
498
|
+
}
|
|
499
|
+
case "$nor": {
|
|
500
|
+
if (!Array.isArray(value) || value.length === 0) return drop(writing, at, value, "$nor takes a list of queries");
|
|
501
|
+
const parts = value.map((part, index) => writeQuery(part, writing, `${at}[${index}]`, true));
|
|
502
|
+
if (parts.some((part) => part === "")) return drop(writing, at, value, "one branch of the $nor has no spelling");
|
|
503
|
+
return `$not (${parts.join(" $or ")})`;
|
|
504
|
+
}
|
|
505
|
+
case "$not":
|
|
506
|
+
return writeNot(value, writing, at);
|
|
507
|
+
case "$text":
|
|
508
|
+
return writeText(value, writing, at);
|
|
509
|
+
default:
|
|
510
|
+
if (key.startsWith("$")) return drop(writing, at, value, `"${key}" is a query operator the text syntax cannot say`);
|
|
511
|
+
return writeField(key, value, writing, at, false);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
function writeNot(value, writing, at) {
|
|
515
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
516
|
+
const keys = Object.keys(value);
|
|
517
|
+
if (keys.length === 1) {
|
|
518
|
+
const key = keys[0];
|
|
519
|
+
if (!key.startsWith("$")) return writeField(key, value[key], writing, `${at}.${key}`, true);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
const reported = writing.left.length;
|
|
523
|
+
const inner = writeQuery(value, writing, at, true);
|
|
524
|
+
if (inner === "") {
|
|
525
|
+
if (writing.left.length === reported) return drop(writing, at, value, "$not of a query that asks nothing matches nothing, and no term says that");
|
|
526
|
+
return "";
|
|
527
|
+
}
|
|
528
|
+
return inner.startsWith("(") ? `$not ${inner}` : `$not (${inner})`;
|
|
529
|
+
}
|
|
530
|
+
function writeText(value, writing, at) {
|
|
531
|
+
const search = typeof value === "string" ? { $search: value } : value;
|
|
532
|
+
if (search === null || typeof search !== "object" || typeof search.$search !== "string") {
|
|
533
|
+
return drop(writing, at, value, "a text search is a phrase or { $search: \u2026 }");
|
|
534
|
+
}
|
|
535
|
+
if (search.$caseSensitive === true) return drop(writing, at, value, "a bare word in the text syntax always ignores case");
|
|
536
|
+
if (search.$fields !== void 0) {
|
|
537
|
+
const fields = search.$fields;
|
|
538
|
+
const same = Array.isArray(fields) && writing.vocabulary?.text !== void 0 && fields.length === writing.vocabulary.text.length && fields.every((name, index) => name === (writing.vocabulary?.text)[index]);
|
|
539
|
+
if (!same) return drop(writing, at, value, "a bare word searches the vocabulary's text fields, not a list of its own");
|
|
540
|
+
}
|
|
541
|
+
if (search.$search === "") return drop(writing, at, value, "an empty search is not a term anybody can type");
|
|
542
|
+
if (/^[-!]/.test(search.$search)) return drop(writing, at, value, "a bare word cannot begin with a dash: quoted or not, the parser reads that as a negation");
|
|
543
|
+
if (/^\$(and|or|not)$/i.test(search.$search)) return drop(writing, at, value, "a bare word cannot be an operator's name: quoted or not, the parser reads it as the operator");
|
|
544
|
+
const quoted = quote(search.$search, false);
|
|
545
|
+
return quoted ?? drop(writing, at, value, "the phrase holds both kinds of quote, and the syntax has no escape for either");
|
|
546
|
+
}
|
|
547
|
+
function writeField(name, value, writing, at, negated) {
|
|
548
|
+
const field = writing.vocabulary?.lookup.get(name.toLowerCase());
|
|
549
|
+
if (field === void 0 && (writing.mode === "vocabulary" || !PATH_NAME2.test(name))) {
|
|
550
|
+
return drop(writing, at, { [name]: value }, writing.mode === "vocabulary" ? `"${name}" is not a field of this vocabulary, so the text would search for it as words` : `"${name}" cannot be written before a colon`);
|
|
551
|
+
}
|
|
552
|
+
const key = field?.name ?? name;
|
|
553
|
+
if (key.toLowerCase() === EXISTS_TERM) {
|
|
554
|
+
return drop(writing, at, { [name]: value }, `"${key}" is how the syntax asks whether a field is set, so a field of that name cannot be written`);
|
|
555
|
+
}
|
|
556
|
+
const condition = value !== null && typeof value === "object" && !Array.isArray(value) ? value : { $eq: value };
|
|
557
|
+
const inferred = field === void 0;
|
|
558
|
+
const kind = field?.kind ?? inferKind(condition);
|
|
559
|
+
const operators = Object.keys(condition).filter((operator) => operator !== "$options");
|
|
560
|
+
if (operators.length === 0 || !operators.every((operator) => operator.startsWith("$"))) {
|
|
561
|
+
return drop(writing, at, { [name]: value }, "the value mixes operators with fields, or holds none");
|
|
562
|
+
}
|
|
563
|
+
const sayable = SAYABLE[kind];
|
|
564
|
+
const range = writeRange(key, kind, condition, operators, negated);
|
|
565
|
+
if (range !== void 0) return range;
|
|
566
|
+
const terms = [];
|
|
567
|
+
for (const operator of operators) {
|
|
568
|
+
const term = writeOperator(key, kind, operator, condition, writing, at, negated, sayable, inferred);
|
|
569
|
+
if (term === "") return "";
|
|
570
|
+
terms.push(term);
|
|
571
|
+
}
|
|
572
|
+
if (terms.length > 1 && negated) return drop(writing, at, { [name]: value }, "a negated field with several operators would have to be written as $not ( \u2026 ), which changes what a set means");
|
|
573
|
+
return terms.join(" ");
|
|
574
|
+
}
|
|
575
|
+
function writeRange(key, kind, condition, operators, negated) {
|
|
576
|
+
if (negated || kind !== "number" && kind !== "date") return void 0;
|
|
577
|
+
if (operators.length !== 2 || !operators.includes("$gte") || !operators.includes("$lte")) return void 0;
|
|
578
|
+
const low = writeValue(condition.$gte, kind, "$eq", false, false);
|
|
579
|
+
const high = writeValue(condition.$lte, kind, "$eq", false, false);
|
|
580
|
+
if (low === void 0 || high === void 0) return void 0;
|
|
581
|
+
return `${key}:${low}..${high}`;
|
|
582
|
+
}
|
|
583
|
+
function writeOperator(key, kind, operator, condition, writing, at, negated, sayable, inferred) {
|
|
584
|
+
const operand = condition[operator];
|
|
585
|
+
const here = `${at}.${operator}`;
|
|
586
|
+
const sign = negated ? "-" : "";
|
|
587
|
+
if (!sayable.includes(operator)) return drop(writing, here, { [operator]: operand }, `"${operator}" has no spelling in the text syntax`);
|
|
588
|
+
if (operator === "$exists") {
|
|
589
|
+
if (operand !== true) return drop(writing, here, { [operator]: operand }, "only $exists: true is written, as has:field; its negation is the leading dash");
|
|
590
|
+
return `${sign}has:${key}`;
|
|
591
|
+
}
|
|
592
|
+
const ignoresCase = condition.$options === "i";
|
|
593
|
+
if ((kind === "text" || kind === "word" || kind === "exact") && !ignoresCase) {
|
|
594
|
+
return drop(writing, here, condition, "the text syntax always ignores case, and this condition does not");
|
|
595
|
+
}
|
|
596
|
+
const set = operator === "$in" || (operator === "$contains" || operator === "$word") && Array.isArray(operand);
|
|
597
|
+
const values = set ? operand : [operand];
|
|
598
|
+
if (!Array.isArray(values)) return drop(writing, here, { [operator]: operand }, "$in takes a list");
|
|
599
|
+
if (values.length === 0) return drop(writing, here, { [operator]: operand }, "an empty set is not something anybody types");
|
|
600
|
+
const written = [];
|
|
601
|
+
for (const value of values) {
|
|
602
|
+
const one = writeValue(value, kind, operator, values.length > 1, inferred);
|
|
603
|
+
if (one === void 0) return drop(writing, here, { [operator]: operand }, `${describeValue(value)} cannot be written after a colon`);
|
|
604
|
+
written.push(one);
|
|
605
|
+
}
|
|
606
|
+
if (written.length > 1) return `${key}:${negated ? "$notin" : "$in"}(${written.join(", ")})`;
|
|
607
|
+
return `${sign}${key}:${written[0]}`;
|
|
608
|
+
}
|
|
609
|
+
function writeValue(value, kind, operator, inSet, inferred) {
|
|
610
|
+
if (kind === "boolean") return value === true ? "true" : value === false ? "false" : void 0;
|
|
611
|
+
if (kind === "number") {
|
|
612
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
|
|
613
|
+
return `${comparison2(operator)}${String(value)}`;
|
|
614
|
+
}
|
|
615
|
+
if (kind === "date") {
|
|
616
|
+
const instant = writeDate(value);
|
|
617
|
+
return instant === void 0 ? void 0 : `${comparison2(operator)}${instant}`;
|
|
618
|
+
}
|
|
619
|
+
if (typeof value !== "string" || value === "") return void 0;
|
|
620
|
+
if (value.trim() !== value) return void 0;
|
|
621
|
+
if (SET_TERM.test(value)) return void 0;
|
|
622
|
+
if (kind === "text" && inferred && EXPLICIT_COMPARISON.test(value)) return void 0;
|
|
623
|
+
return quote(value, inSet);
|
|
624
|
+
}
|
|
625
|
+
function comparison2(operator) {
|
|
626
|
+
switch (operator) {
|
|
627
|
+
case "$gt":
|
|
628
|
+
return ">";
|
|
629
|
+
case "$gte":
|
|
630
|
+
return ">=";
|
|
631
|
+
case "$lt":
|
|
632
|
+
return "<";
|
|
633
|
+
case "$lte":
|
|
634
|
+
return "<=";
|
|
635
|
+
default:
|
|
636
|
+
return "";
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
function writeDate(value) {
|
|
640
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
641
|
+
const date = value.$date;
|
|
642
|
+
if (Object.keys(value).length !== 1) return void 0;
|
|
643
|
+
if (typeof date === "string") return date === "now" ? "now" : /\s/.test(date) ? void 0 : date;
|
|
644
|
+
if (date !== null && typeof date === "object") {
|
|
645
|
+
const relative = date;
|
|
646
|
+
if (typeof relative.$ago === "string") return `-${relative.$ago}`;
|
|
647
|
+
if (typeof relative.$ahead === "string") return `+${relative.$ahead}`;
|
|
648
|
+
}
|
|
649
|
+
return void 0;
|
|
650
|
+
}
|
|
651
|
+
function quote(value, inSet) {
|
|
652
|
+
const plain = !/[\s()"'“”‘’]/.test(value) && !/^[-!$]/.test(value) && !(inSet && value.includes(","));
|
|
653
|
+
if (plain) return value;
|
|
654
|
+
if (!value.includes('"')) return `"${value}"`;
|
|
655
|
+
if (!value.includes("'")) return `'${value}'`;
|
|
656
|
+
return void 0;
|
|
657
|
+
}
|
|
658
|
+
function describeValue(value) {
|
|
659
|
+
if (value === null) return "null";
|
|
660
|
+
if (Array.isArray(value)) return "a list";
|
|
661
|
+
if (typeof value === "object") return "an object";
|
|
662
|
+
return `${typeof value} ${JSON.stringify(value)}`;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
export { MAX_TEXT_CHARS, MAX_TEXT_DEPTH, TEXT_OPERATORS, parseText, suggest, toText };
|
|
666
|
+
//# sourceMappingURL=text.js.map
|
|
667
|
+
//# sourceMappingURL=text.js.map
|