@procedures/client 0.8.0 → 0.9.0
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 +78 -0
- package/README.md +95 -8
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/http/client/transport.d.ts +4 -0
- package/dist/adapters/http/client/transport.d.ts.map +1 -1
- package/dist/adapters/http/client/transport.js +11 -1
- package/dist/adapters/http/client/transport.js.map +1 -1
- package/dist/adapters/http/client/types.d.ts +19 -0
- package/dist/adapters/http/client/types.d.ts.map +1 -1
- package/dist/adapters/http/server/cors.d.ts +64 -0
- package/dist/adapters/http/server/cors.d.ts.map +1 -0
- package/dist/adapters/http/server/cors.js +113 -0
- package/dist/adapters/http/server/cors.js.map +1 -0
- package/dist/adapters/http/server/handler.d.ts +136 -0
- package/dist/adapters/http/server/handler.d.ts.map +1 -0
- package/dist/adapters/http/server/handler.js +495 -0
- package/dist/adapters/http/server/handler.js.map +1 -0
- package/dist/adapters/http/server/strategies.d.ts +14 -5
- package/dist/adapters/http/server/strategies.d.ts.map +1 -1
- package/dist/adapters/http/server/strategies.js.map +1 -1
- package/dist/adapters/http/server/transport.d.ts +30 -46
- package/dist/adapters/http/server/transport.d.ts.map +1 -1
- package/dist/adapters/http/server/transport.js +48 -432
- package/dist/adapters/http/server/transport.js.map +1 -1
- package/dist/adapters/http/server/types.d.ts +13 -0
- package/dist/adapters/http/server/types.d.ts.map +1 -1
- package/dist/adapters/websocket/client/transport.d.ts.map +1 -1
- package/dist/adapters/websocket/client/transport.js +4 -12
- package/dist/adapters/websocket/client/transport.js.map +1 -1
- package/dist/adapters/websocket/server/transport.d.ts +46 -1
- package/dist/adapters/websocket/server/transport.d.ts.map +1 -1
- package/dist/adapters/websocket/server/transport.js +152 -15
- package/dist/adapters/websocket/server/transport.js.map +1 -1
- package/dist/adapters/websocket/server/types.d.ts +10 -3
- package/dist/adapters/websocket/server/types.d.ts.map +1 -1
- package/dist/adapters/websocket/server/upgrade-router.d.ts +28 -0
- package/dist/adapters/websocket/server/upgrade-router.d.ts.map +1 -0
- package/dist/adapters/websocket/server/upgrade-router.js +62 -0
- package/dist/adapters/websocket/server/upgrade-router.js.map +1 -0
- package/dist/adapters/websocket/server/upgrade.d.ts +68 -0
- package/dist/adapters/websocket/server/upgrade.d.ts.map +1 -0
- package/dist/adapters/websocket/server/upgrade.js +49 -0
- package/dist/adapters/websocket/server/upgrade.js.map +1 -0
- package/dist/adapters/websocket/shared/rpc.d.ts +2 -0
- package/dist/adapters/websocket/shared/rpc.d.ts.map +1 -1
- package/dist/adapters/websocket/shared/rpc.js +8 -2
- package/dist/adapters/websocket/shared/rpc.js.map +1 -1
- package/dist/browser.d.ts +12 -4
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +13 -2
- package/dist/browser.js.map +1 -1
- package/dist/client/client.d.ts +43 -0
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +69 -1
- package/dist/client/client.js.map +1 -1
- package/dist/events/publisher.d.ts +25 -1
- package/dist/events/publisher.d.ts.map +1 -1
- package/dist/events/publisher.js +66 -9
- package/dist/events/publisher.js.map +1 -1
- package/dist/events/rows.d.ts +31 -0
- package/dist/events/rows.d.ts.map +1 -0
- package/dist/events/rows.js +39 -0
- package/dist/events/rows.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +14 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +9 -0
- package/dist/node.js.map +1 -1
- package/dist/procedures/capability.d.ts +6 -0
- package/dist/procedures/capability.d.ts.map +1 -1
- package/dist/procedures/capability.js +25 -0
- package/dist/procedures/capability.js.map +1 -1
- package/dist/procedures/collection/changes.d.ts +94 -0
- package/dist/procedures/collection/changes.d.ts.map +1 -0
- package/dist/procedures/collection/changes.js +182 -0
- package/dist/procedures/collection/changes.js.map +1 -0
- package/dist/procedures/collection/index.d.ts +10 -3
- package/dist/procedures/collection/index.d.ts.map +1 -1
- package/dist/procedures/collection/index.js +8 -1
- package/dist/procedures/collection/index.js.map +1 -1
- package/dist/procedures/collection/partition.d.ts +67 -0
- package/dist/procedures/collection/partition.d.ts.map +1 -0
- package/dist/procedures/collection/partition.js +82 -0
- package/dist/procedures/collection/partition.js.map +1 -0
- package/dist/procedures/collection/procedures.d.ts +39 -2
- package/dist/procedures/collection/procedures.d.ts.map +1 -1
- package/dist/procedures/collection/procedures.js +147 -44
- package/dist/procedures/collection/procedures.js.map +1 -1
- package/dist/procedures/collection/query.d.ts +246 -0
- package/dist/procedures/collection/query.d.ts.map +1 -0
- package/dist/procedures/collection/query.js +854 -0
- package/dist/procedures/collection/query.js.map +1 -0
- package/dist/procedures/collection/schemas.d.ts +50 -0
- package/dist/procedures/collection/schemas.d.ts.map +1 -1
- package/dist/procedures/collection/schemas.js +144 -6
- package/dist/procedures/collection/schemas.js.map +1 -1
- package/dist/procedures/collection/storage.d.ts +58 -6
- package/dist/procedures/collection/storage.d.ts.map +1 -1
- package/dist/procedures/collection/storage.js +23 -0
- package/dist/procedures/collection/storage.js.map +1 -1
- package/dist/procedures/elicit.d.ts +190 -0
- package/dist/procedures/elicit.d.ts.map +1 -0
- package/dist/procedures/elicit.js +127 -0
- package/dist/procedures/elicit.js.map +1 -0
- package/dist/procedures/hints.d.ts +43 -0
- package/dist/procedures/hints.d.ts.map +1 -0
- package/dist/procedures/hints.js +74 -0
- package/dist/procedures/hints.js.map +1 -0
- package/dist/procedures/index.d.ts +9 -2
- package/dist/procedures/index.d.ts.map +1 -1
- package/dist/procedures/index.js +5 -1
- package/dist/procedures/index.js.map +1 -1
- package/dist/procedures/invoke.d.ts +24 -2
- package/dist/procedures/invoke.d.ts.map +1 -1
- package/dist/procedures/invoke.js +14 -1
- package/dist/procedures/invoke.js.map +1 -1
- package/dist/procedures/progress.d.ts +24 -0
- package/dist/procedures/progress.d.ts.map +1 -0
- package/dist/procedures/progress.js +11 -0
- package/dist/procedures/progress.js.map +1 -0
- package/dist/procedures/registry.d.ts.map +1 -1
- package/dist/procedures/registry.js +5 -0
- package/dist/procedures/registry.js.map +1 -1
- package/dist/procedures/types.d.ts +55 -0
- package/dist/procedures/types.d.ts.map +1 -1
- package/dist/procedures/types.js.map +1 -1
- package/dist/procedures/untrusted.d.ts +28 -0
- package/dist/procedures/untrusted.d.ts.map +1 -0
- package/dist/procedures/untrusted.js +32 -0
- package/dist/procedures/untrusted.js.map +1 -0
- package/dist/server/browser-guard.d.ts +12 -2
- package/dist/server/browser-guard.d.ts.map +1 -1
- package/dist/server/browser-guard.js +43 -6
- package/dist/server/browser-guard.js.map +1 -1
- package/dist/server/host-policy.d.ts +10 -1
- package/dist/server/host-policy.d.ts.map +1 -1
- package/dist/server/host-policy.js +5 -0
- package/dist/server/host-policy.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/procedure-server.d.ts +68 -7
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +124 -24
- package/dist/server/procedure-server.js.map +1 -1
- package/dist/server/request-auth.d.ts +51 -4
- package/dist/server/request-auth.d.ts.map +1 -1
- package/dist/server/request-auth.js +116 -5
- package/dist/server/request-auth.js.map +1 -1
- package/dist/server/types.d.ts +73 -5
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js.map +1 -1
- package/package.json +7 -3
- package/src/adapters/http/client/transport.ts +11 -1
- package/src/adapters/http/client/types.ts +22 -0
- package/src/adapters/http/server/cors.ts +144 -0
- package/src/adapters/http/server/handler.ts +624 -0
- package/src/adapters/http/server/strategies.ts +16 -6
- package/src/adapters/http/server/transport.ts +55 -478
- package/src/adapters/http/server/types.ts +15 -0
- package/src/adapters/websocket/client/transport.ts +4 -12
- package/src/adapters/websocket/server/transport.ts +166 -16
- package/src/adapters/websocket/server/types.ts +11 -3
- package/src/adapters/websocket/server/upgrade-router.ts +77 -0
- package/src/adapters/websocket/server/upgrade.ts +88 -0
- package/src/adapters/websocket/shared/rpc.ts +9 -2
- package/src/browser.ts +85 -2
- package/src/client/client.ts +83 -1
- package/src/events/publisher.ts +75 -11
- package/src/events/rows.ts +48 -0
- package/src/index.ts +1 -1
- package/src/node.ts +21 -0
- package/src/procedures/capability.ts +21 -0
- package/src/procedures/collection/changes.ts +245 -0
- package/src/procedures/collection/index.ts +57 -1
- package/src/procedures/collection/partition.ts +123 -0
- package/src/procedures/collection/procedures.ts +206 -51
- package/src/procedures/collection/query.ts +1009 -0
- package/src/procedures/collection/schemas.ts +196 -7
- package/src/procedures/collection/storage.ts +78 -6
- package/src/procedures/elicit.ts +320 -0
- package/src/procedures/hints.ts +91 -0
- package/src/procedures/index.ts +83 -2
- package/src/procedures/invoke.ts +33 -2
- package/src/procedures/progress.ts +25 -0
- package/src/procedures/registry.ts +4 -0
- package/src/procedures/types.ts +65 -0
- package/src/procedures/untrusted.ts +50 -0
- package/src/server/browser-guard.ts +48 -6
- package/src/server/host-policy.ts +13 -1
- package/src/server/index.ts +3 -0
- package/src/server/procedure-server.ts +147 -26
- package/src/server/request-auth.ts +128 -6
- package/src/server/types.ts +82 -5
|
@@ -0,0 +1,854 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The query language of the collection storages: a selector, an order, a skip, a limit, the query
|
|
3
|
+
* policy that limits them, and the two forms of a patch.
|
|
4
|
+
*
|
|
5
|
+
* A selector is JSON: a subset of the query selectors of MongoDB. It goes over the wire as it is,
|
|
6
|
+
* so the procedure `collections.<name>.find` and `query()` of each storage take the same selector.
|
|
7
|
+
* A storage that can run a selector itself (for example a database) gives the same result as
|
|
8
|
+
* `queryEntries()` of this module, which is the reference.
|
|
9
|
+
*
|
|
10
|
+
* This module is the one copy of the language (roadmap CLI-34). The language is a part of the wire
|
|
11
|
+
* contract of the core. `@procedures/collections` and the storage packages import it from
|
|
12
|
+
* `@procedures/client/query`, an entry without other imports.
|
|
13
|
+
*
|
|
14
|
+
* The rules:
|
|
15
|
+
* - A key of a selector is a field path, for example `"meta.kind"`. A number in a path is an
|
|
16
|
+
* index of an array, for example `"tags.0"`.
|
|
17
|
+
* - A path through an array of objects looks into each object: `"items.name"` gives the name of
|
|
18
|
+
* each item.
|
|
19
|
+
* - The value of a field path is a literal or an object of operators. A literal is a test for
|
|
20
|
+
* equality. A field whose value is an array also matches when one element is equal.
|
|
21
|
+
* - The literal `null` also matches a missing field.
|
|
22
|
+
* - The operators are `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$exists`,
|
|
23
|
+
* `$not`, `$regex` with `$options`, `$size`, `$all` and `$elemMatch`. The top of a selector also
|
|
24
|
+
* takes `$and`, `$or` and `$nor`. Another operator is an error, not a condition that is ignored.
|
|
25
|
+
* - `$gt`, `$gte`, `$lt` and `$lte` compare only values of one type: numbers with numbers, strings
|
|
26
|
+
* with strings, and booleans with booleans.
|
|
27
|
+
* - Two objects are equal when they have the same keys with equal values. The order of the keys
|
|
28
|
+
* does not change the result.
|
|
29
|
+
* - A key whose value is `undefined` is not in the selector, as in JSON.
|
|
30
|
+
* - A query policy limits the operators, the size and the depth of a selector, the lists, the
|
|
31
|
+
* regular expressions and the limit. The default policy does not permit `$regex`.
|
|
32
|
+
*/
|
|
33
|
+
/** Each operator of the query language. A policy with these operators permits `$regex`. */
|
|
34
|
+
export const QUERY_OPERATORS = Object.freeze([
|
|
35
|
+
"$eq",
|
|
36
|
+
"$ne",
|
|
37
|
+
"$gt",
|
|
38
|
+
"$gte",
|
|
39
|
+
"$lt",
|
|
40
|
+
"$lte",
|
|
41
|
+
"$in",
|
|
42
|
+
"$nin",
|
|
43
|
+
"$exists",
|
|
44
|
+
"$not",
|
|
45
|
+
"$regex",
|
|
46
|
+
"$size",
|
|
47
|
+
"$all",
|
|
48
|
+
"$elemMatch",
|
|
49
|
+
"$and",
|
|
50
|
+
"$or",
|
|
51
|
+
"$nor",
|
|
52
|
+
]);
|
|
53
|
+
/** The operators of the default policy: each operator except `$regex`. */
|
|
54
|
+
export const DEFAULT_QUERY_OPERATORS = Object.freeze(QUERY_OPERATORS.filter((operator) => operator !== "$regex"));
|
|
55
|
+
/** The limits of a policy that gives no limit of its own. `$regex` is off. */
|
|
56
|
+
export const DEFAULT_QUERY_POLICY = Object.freeze({
|
|
57
|
+
operators: DEFAULT_QUERY_OPERATORS,
|
|
58
|
+
maxRegexLength: 128,
|
|
59
|
+
regexFlags: "imsu",
|
|
60
|
+
maxClauses: 64,
|
|
61
|
+
maxDepth: 6,
|
|
62
|
+
maxListLength: 256,
|
|
63
|
+
});
|
|
64
|
+
/** The limit of the procedure `find` for a call without a limit, when its policy gives no `maxLimit`. */
|
|
65
|
+
export const DEFAULT_FIND_LIMIT = 1000;
|
|
66
|
+
//
|
|
67
|
+
// ═══ The values ═══
|
|
68
|
+
//
|
|
69
|
+
/** True for an object that is not an array and not null. */
|
|
70
|
+
function isObject(value) {
|
|
71
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
72
|
+
}
|
|
73
|
+
/** The keys of an object whose values are not `undefined`, as JSON keeps them. */
|
|
74
|
+
function definedKeys(value) {
|
|
75
|
+
return Object.keys(value).filter((key) => value[key] !== undefined);
|
|
76
|
+
}
|
|
77
|
+
/** The defined entries of an object, as JSON keeps them. */
|
|
78
|
+
function definedEntries(value) {
|
|
79
|
+
return Object.entries(value).filter(([, entry]) => entry !== undefined);
|
|
80
|
+
}
|
|
81
|
+
/** The rank of the type of a value in the order: null, numbers, strings, objects, arrays, booleans. */
|
|
82
|
+
function rankOf(value) {
|
|
83
|
+
if (value === null || value === undefined)
|
|
84
|
+
return 0;
|
|
85
|
+
switch (typeof value) {
|
|
86
|
+
case "number":
|
|
87
|
+
return 1;
|
|
88
|
+
case "string":
|
|
89
|
+
return 2;
|
|
90
|
+
case "object":
|
|
91
|
+
return Array.isArray(value) ? 4 : 3;
|
|
92
|
+
case "boolean":
|
|
93
|
+
return 5;
|
|
94
|
+
default:
|
|
95
|
+
return 6;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** The order of two primitive values of one type. */
|
|
99
|
+
function order(a, b) {
|
|
100
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* This function compares two JSON values in the order of the queries: `null` and a missing value
|
|
104
|
+
* first, then numbers, strings, objects, arrays and booleans. Numbers compare by value, and strings
|
|
105
|
+
* by their UTF-16 code units. Arrays compare element by element. Objects compare by their sorted
|
|
106
|
+
* keys, then by the values of the keys.
|
|
107
|
+
*
|
|
108
|
+
* @returns A negative number when `a` comes first, a positive number when `b` comes first, or 0
|
|
109
|
+
*/
|
|
110
|
+
export function compareStorageValues(a, b) {
|
|
111
|
+
const rankA = rankOf(a);
|
|
112
|
+
const rankB = rankOf(b);
|
|
113
|
+
if (rankA !== rankB)
|
|
114
|
+
return rankA < rankB ? -1 : 1;
|
|
115
|
+
switch (rankA) {
|
|
116
|
+
case 1:
|
|
117
|
+
return order(a, b);
|
|
118
|
+
case 2:
|
|
119
|
+
return order(a, b);
|
|
120
|
+
case 5:
|
|
121
|
+
return order(a, b);
|
|
122
|
+
case 4: {
|
|
123
|
+
const left = a;
|
|
124
|
+
const right = b;
|
|
125
|
+
for (let index = 0; index < Math.min(left.length, right.length); index++) {
|
|
126
|
+
const result = compareStorageValues(left[index], right[index]);
|
|
127
|
+
if (result !== 0)
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
return order(left.length, right.length);
|
|
131
|
+
}
|
|
132
|
+
case 3: {
|
|
133
|
+
const left = a;
|
|
134
|
+
const right = b;
|
|
135
|
+
const leftKeys = definedKeys(left).sort(order);
|
|
136
|
+
const rightKeys = definedKeys(right).sort(order);
|
|
137
|
+
for (let index = 0; index < Math.min(leftKeys.length, rightKeys.length); index++) {
|
|
138
|
+
const leftKey = leftKeys[index];
|
|
139
|
+
const rightKey = rightKeys[index];
|
|
140
|
+
if (leftKey !== rightKey)
|
|
141
|
+
return order(leftKey, rightKey);
|
|
142
|
+
const result = compareStorageValues(left[leftKey], right[rightKey]);
|
|
143
|
+
if (result !== 0)
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
return order(leftKeys.length, rightKeys.length);
|
|
147
|
+
}
|
|
148
|
+
default:
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** True when two JSON values are equal. The order of the keys of an object does not count. */
|
|
153
|
+
function equal(a, b) {
|
|
154
|
+
return compareStorageValues(a, b) === 0;
|
|
155
|
+
}
|
|
156
|
+
const INDEX = /^(?:0|[1-9][0-9]*)$/;
|
|
157
|
+
/** This function finds the values at a path. A path through an array of objects looks into each object. */
|
|
158
|
+
function lookup(item, path) {
|
|
159
|
+
const found = { values: [], missing: false };
|
|
160
|
+
walk(item, path, 0, found);
|
|
161
|
+
return found;
|
|
162
|
+
}
|
|
163
|
+
function walk(value, path, index, found) {
|
|
164
|
+
if (index === path.length) {
|
|
165
|
+
if (value === undefined)
|
|
166
|
+
found.missing = true;
|
|
167
|
+
else
|
|
168
|
+
found.values.push(value);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const key = path[index];
|
|
172
|
+
if (Array.isArray(value)) {
|
|
173
|
+
if (INDEX.test(key)) {
|
|
174
|
+
const position = Number(key);
|
|
175
|
+
if (position < value.length)
|
|
176
|
+
walk(value[position], path, index + 1, found);
|
|
177
|
+
else
|
|
178
|
+
found.missing = true;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
let objects = 0;
|
|
182
|
+
for (const element of value) {
|
|
183
|
+
if (!isObject(element))
|
|
184
|
+
continue;
|
|
185
|
+
objects++;
|
|
186
|
+
walk(element, path, index, found);
|
|
187
|
+
}
|
|
188
|
+
if (objects === 0)
|
|
189
|
+
found.missing = true;
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (isObject(value) && Object.hasOwn(value, key))
|
|
193
|
+
walk(value[key], path, index + 1, found);
|
|
194
|
+
else
|
|
195
|
+
found.missing = true;
|
|
196
|
+
}
|
|
197
|
+
/** The values that a comparison examines: each value, and each element of a value that is an array. */
|
|
198
|
+
function candidates(found) {
|
|
199
|
+
const result = [];
|
|
200
|
+
for (const value of found.values) {
|
|
201
|
+
result.push(value);
|
|
202
|
+
if (Array.isArray(value))
|
|
203
|
+
result.push(...value);
|
|
204
|
+
}
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
/** The segments of a field path, or undefined after a problem. */
|
|
208
|
+
function pathOf(key, at, problems) {
|
|
209
|
+
const segments = key.split(".");
|
|
210
|
+
if (segments.some((segment) => segment === "")) {
|
|
211
|
+
problems.push(`${at}: the field path "${key}" has an empty part`);
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
if (segments.some((segment) => segment.startsWith("$"))) {
|
|
215
|
+
problems.push(`${at}: the field path "${key}" has a part that starts with $`);
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
return segments;
|
|
219
|
+
}
|
|
220
|
+
/** The limits of a policy: each limit that the policy does not give has its default. */
|
|
221
|
+
function limitsOf(policy) {
|
|
222
|
+
return {
|
|
223
|
+
operators: new Set(policy?.operators ?? DEFAULT_QUERY_OPERATORS),
|
|
224
|
+
maxRegexLength: policy?.maxRegexLength ?? 128,
|
|
225
|
+
regexFlags: policy?.regexFlags ?? "imsu",
|
|
226
|
+
maxClauses: policy?.maxClauses ?? 64,
|
|
227
|
+
maxDepth: policy?.maxDepth ?? 6,
|
|
228
|
+
maxListLength: policy?.maxListLength ?? 256,
|
|
229
|
+
maxLimit: policy?.maxLimit ?? Number.POSITIVE_INFINITY,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
const KNOWN = new Set(QUERY_OPERATORS);
|
|
233
|
+
/** False, with a problem, when the language does not know the operator, or the policy does not permit it. */
|
|
234
|
+
function permitted(operator, at, compilation) {
|
|
235
|
+
if (!KNOWN.has(operator)) {
|
|
236
|
+
compilation.problems.push(`${at}: the operator ${operator} is not known`);
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
if (!compilation.limits.operators.has(operator)) {
|
|
240
|
+
const hint = operator === "$regex" ? ". A host turns it on with a queryPolicy whose operators include $regex" : "";
|
|
241
|
+
compilation.problems.push(`${at}: the query policy does not permit the operator ${operator}${hint}`);
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
compilation.clauses++;
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
/** False, with a problem, when the nesting goes deeper than the policy permits. */
|
|
248
|
+
function withinDepth(depth, at, compilation) {
|
|
249
|
+
if (depth <= compilation.limits.maxDepth)
|
|
250
|
+
return true;
|
|
251
|
+
compilation.problems.push(`${at}: the selector is nested deeper than the ${compilation.limits.maxDepth} levels of the query policy`);
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* True when a pattern repeats a group that repeats without a bound, for example `(a+)+` or
|
|
256
|
+
* `(a|b*)*`. Such a pattern can take an exponential time on a long text.
|
|
257
|
+
*/
|
|
258
|
+
function repeatsRepetition(pattern) {
|
|
259
|
+
// For each open group: true when it holds a repetition without a bound
|
|
260
|
+
const groups = [];
|
|
261
|
+
let lastGroupRepeats = false;
|
|
262
|
+
let inClass = false;
|
|
263
|
+
for (let index = 0; index < pattern.length; index++) {
|
|
264
|
+
const char = pattern[index];
|
|
265
|
+
if (char === "\\") {
|
|
266
|
+
index++;
|
|
267
|
+
lastGroupRepeats = false;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (inClass) {
|
|
271
|
+
if (char === "]")
|
|
272
|
+
inClass = false;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (char === "[") {
|
|
276
|
+
inClass = true;
|
|
277
|
+
lastGroupRepeats = false;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (char === "(") {
|
|
281
|
+
groups.push(false);
|
|
282
|
+
lastGroupRepeats = false;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (char === ")") {
|
|
286
|
+
lastGroupRepeats = groups.pop() ?? false;
|
|
287
|
+
// A group that holds a repetition makes each group around it hold one too
|
|
288
|
+
if (lastGroupRepeats && groups.length > 0)
|
|
289
|
+
groups[groups.length - 1] = true;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const unbounded = char === "*" || char === "+" || (char === "{" && /^\{\d*,\}/.test(pattern.slice(index)));
|
|
293
|
+
if (unbounded) {
|
|
294
|
+
if (lastGroupRepeats)
|
|
295
|
+
return true;
|
|
296
|
+
if (groups.length > 0)
|
|
297
|
+
groups[groups.length - 1] = true;
|
|
298
|
+
}
|
|
299
|
+
if (char !== "?")
|
|
300
|
+
lastGroupRepeats = false;
|
|
301
|
+
}
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
const LOGICAL = new Set(["$and", "$or", "$nor"]);
|
|
305
|
+
const REGEX_FLAGS = /^(?!.*(.).*\1)[imsu]*$/;
|
|
306
|
+
const NEVER = () => false;
|
|
307
|
+
const ALWAYS = () => true;
|
|
308
|
+
/** True for an object of operators: each defined key starts with `$`. */
|
|
309
|
+
function isOperatorObject(value) {
|
|
310
|
+
if (!isObject(value))
|
|
311
|
+
return false;
|
|
312
|
+
const keys = definedKeys(value);
|
|
313
|
+
return keys.length > 0 && keys.every((key) => key.startsWith("$"));
|
|
314
|
+
}
|
|
315
|
+
/** The test of equality with a literal. `null` also matches a missing field. */
|
|
316
|
+
function equalTo(literal) {
|
|
317
|
+
return (found) => (literal === null && found.missing) || candidates(found).some((value) => equal(value, literal));
|
|
318
|
+
}
|
|
319
|
+
/** True when two values have one type of the comparisons: number, string or boolean. */
|
|
320
|
+
function comparable(value, operand) {
|
|
321
|
+
const type = typeof operand;
|
|
322
|
+
return typeof value === type && (type === "number" || type === "string" || type === "boolean");
|
|
323
|
+
}
|
|
324
|
+
/** The test of a comparison operator. */
|
|
325
|
+
function compareTo(operator, operand, at, compilation) {
|
|
326
|
+
if (operand === null) {
|
|
327
|
+
// Only null is equal to null: $gte and $lte match null and a missing field
|
|
328
|
+
return operator === "$gte" || operator === "$lte" ? equalTo(null) : NEVER;
|
|
329
|
+
}
|
|
330
|
+
if (typeof operand !== "number" && typeof operand !== "string" && typeof operand !== "boolean") {
|
|
331
|
+
compilation.problems.push(`${at}.${operator} takes a number, a string, a boolean or null`);
|
|
332
|
+
return NEVER;
|
|
333
|
+
}
|
|
334
|
+
const accept = operator === "$gt" ? (result) => result > 0 : operator === "$gte" ? (result) => result >= 0 : operator === "$lt" ? (result) => result < 0 : (result) => result <= 0;
|
|
335
|
+
return (found) => candidates(found).some((value) => comparable(value, operand) && accept(compareStorageValues(value, operand)));
|
|
336
|
+
}
|
|
337
|
+
/** The tests of a list operator: `$in`, `$nin` or `$all`. */
|
|
338
|
+
function listOf(operator, operand, at, compilation) {
|
|
339
|
+
if (!Array.isArray(operand)) {
|
|
340
|
+
compilation.problems.push(`${at}.${operator} takes a list`);
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
if (operand.length > compilation.limits.maxListLength) {
|
|
344
|
+
compilation.problems.push(`${at}.${operator} has ${operand.length} values, more than the ${compilation.limits.maxListLength} of the query policy`);
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
return operand.map((value, index) => {
|
|
348
|
+
if (isOperatorObject(value)) {
|
|
349
|
+
compilation.problems.push(`${at}.${operator}[${index}] is an object of operators: ${operator} takes values`);
|
|
350
|
+
return NEVER;
|
|
351
|
+
}
|
|
352
|
+
return equalTo(value);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
/** The test of an object of operators on one field. */
|
|
356
|
+
function compileOperators(operators, at, compilation, depth) {
|
|
357
|
+
const tests = [];
|
|
358
|
+
const options = operators["$options"];
|
|
359
|
+
if (options !== undefined && operators["$regex"] === undefined)
|
|
360
|
+
compilation.problems.push(`${at}.$options needs $regex`);
|
|
361
|
+
for (const [operator, operand] of definedEntries(operators)) {
|
|
362
|
+
// $options goes with $regex
|
|
363
|
+
if (operator === "$options")
|
|
364
|
+
continue;
|
|
365
|
+
if (!permitted(operator, at, compilation))
|
|
366
|
+
continue;
|
|
367
|
+
switch (operator) {
|
|
368
|
+
case "$eq":
|
|
369
|
+
tests.push(equalTo(operand));
|
|
370
|
+
break;
|
|
371
|
+
case "$ne": {
|
|
372
|
+
const test = equalTo(operand);
|
|
373
|
+
tests.push((found) => !test(found));
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
case "$gt":
|
|
377
|
+
case "$gte":
|
|
378
|
+
case "$lt":
|
|
379
|
+
case "$lte":
|
|
380
|
+
tests.push(compareTo(operator, operand, at, compilation));
|
|
381
|
+
break;
|
|
382
|
+
case "$in":
|
|
383
|
+
case "$nin": {
|
|
384
|
+
const list = listOf(operator, operand, at, compilation);
|
|
385
|
+
if (!list)
|
|
386
|
+
break;
|
|
387
|
+
tests.push(operator === "$in" ? (found) => list.some((test) => test(found)) : (found) => !list.some((test) => test(found)));
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
case "$all": {
|
|
391
|
+
const list = listOf(operator, operand, at, compilation);
|
|
392
|
+
if (!list)
|
|
393
|
+
break;
|
|
394
|
+
// An empty $all matches nothing, as in MongoDB
|
|
395
|
+
tests.push((found) => list.length > 0 && list.every((test) => test(found)));
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
case "$exists":
|
|
399
|
+
if (typeof operand !== "boolean") {
|
|
400
|
+
compilation.problems.push(`${at}.$exists takes true or false`);
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
tests.push((found) => (found.values.length > 0) === operand);
|
|
404
|
+
break;
|
|
405
|
+
case "$size":
|
|
406
|
+
if (typeof operand !== "number" || !Number.isInteger(operand) || operand < 0) {
|
|
407
|
+
compilation.problems.push(`${at}.$size takes a whole number, 0 or more`);
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
tests.push((found) => found.values.some((value) => Array.isArray(value) && value.length === operand));
|
|
411
|
+
break;
|
|
412
|
+
case "$regex": {
|
|
413
|
+
const regex = regexOf(operand, options, at, compilation);
|
|
414
|
+
if (regex)
|
|
415
|
+
tests.push((found) => candidates(found).some((value) => typeof value === "string" && regex.test(value)));
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
case "$not": {
|
|
419
|
+
if (!isOperatorObject(operand)) {
|
|
420
|
+
compilation.problems.push(`${at}.$not takes an object of operators`);
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
if (!withinDepth(depth + 1, `${at}.$not`, compilation))
|
|
424
|
+
break;
|
|
425
|
+
const test = compileOperators(operand, `${at}.$not`, compilation, depth + 1);
|
|
426
|
+
tests.push((found) => !test(found));
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
case "$elemMatch":
|
|
430
|
+
tests.push(elementMatch(operand, at, compilation, depth + 1));
|
|
431
|
+
break;
|
|
432
|
+
default:
|
|
433
|
+
// $and, $or and $nor are operators of a selector, not of a field
|
|
434
|
+
compilation.problems.push(`${at}: the operator ${operator} goes at the top of a selector, not on a field`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return tests.length === 1 ? tests[0] : (found) => tests.every((test) => test(found));
|
|
438
|
+
}
|
|
439
|
+
/** The regular expression of `$regex` and `$options`, or undefined after a problem. */
|
|
440
|
+
function regexOf(pattern, flags, at, compilation) {
|
|
441
|
+
const { limits, problems } = compilation;
|
|
442
|
+
if (typeof pattern !== "string") {
|
|
443
|
+
problems.push(`${at}.$regex takes a string`);
|
|
444
|
+
return undefined;
|
|
445
|
+
}
|
|
446
|
+
if (pattern.length > limits.maxRegexLength) {
|
|
447
|
+
problems.push(`${at}.$regex has ${pattern.length} characters, more than the ${limits.maxRegexLength} of the query policy`);
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
450
|
+
if (flags !== undefined && (typeof flags !== "string" || !REGEX_FLAGS.test(flags))) {
|
|
451
|
+
problems.push(`${at}.$options takes the flags i, m, s and u, each one time`);
|
|
452
|
+
return undefined;
|
|
453
|
+
}
|
|
454
|
+
const refused = typeof flags === "string" ? [...flags].filter((flag) => !limits.regexFlags.includes(flag)) : [];
|
|
455
|
+
if (refused.length > 0) {
|
|
456
|
+
problems.push(`${at}.$options has the flag ${refused.join(", ")}, and the query policy permits only "${limits.regexFlags}"`);
|
|
457
|
+
return undefined;
|
|
458
|
+
}
|
|
459
|
+
if (repeatsRepetition(pattern)) {
|
|
460
|
+
problems.push(`${at}.$regex repeats a group that repeats without a bound: such a pattern can take an exponential time`);
|
|
461
|
+
return undefined;
|
|
462
|
+
}
|
|
463
|
+
try {
|
|
464
|
+
return new RegExp(pattern, typeof flags === "string" ? flags : "");
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
problems.push(`${at}.$regex is not a regular expression: ${error instanceof Error ? error.message : String(error)}`);
|
|
468
|
+
return undefined;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
/** The test of `$elemMatch`: an element of an array matches the operators, or the selector of objects. */
|
|
472
|
+
function elementMatch(operand, at, compilation, depth) {
|
|
473
|
+
if (!isObject(operand)) {
|
|
474
|
+
compilation.problems.push(`${at}.$elemMatch takes an object`);
|
|
475
|
+
return NEVER;
|
|
476
|
+
}
|
|
477
|
+
if (!withinDepth(depth, `${at}.$elemMatch`, compilation))
|
|
478
|
+
return NEVER;
|
|
479
|
+
const keys = definedKeys(operand);
|
|
480
|
+
if (isOperatorObject(operand) && !keys.some((key) => LOGICAL.has(key))) {
|
|
481
|
+
const test = compileOperators(operand, `${at}.$elemMatch`, compilation, depth);
|
|
482
|
+
return (found) => found.values.some((value) => Array.isArray(value) && value.some((element) => element !== undefined && test({ values: [element], missing: false })));
|
|
483
|
+
}
|
|
484
|
+
const matcher = compileInto(operand, `${at}.$elemMatch`, compilation, depth);
|
|
485
|
+
return (found) => found.values.some((value) => Array.isArray(value) && value.some((element) => isObject(element) && matcher(element)));
|
|
486
|
+
}
|
|
487
|
+
/** The test of the condition of one field path: an object of operators, or a literal. */
|
|
488
|
+
function compileCondition(condition, at, compilation, depth) {
|
|
489
|
+
if (isObject(condition)) {
|
|
490
|
+
const keys = definedKeys(condition);
|
|
491
|
+
const operators = keys.filter((key) => key.startsWith("$"));
|
|
492
|
+
if (operators.length > 0 && operators.length < keys.length) {
|
|
493
|
+
compilation.problems.push(`${at} mixes operators and fields: write the fields as a literal, or use only operators`);
|
|
494
|
+
return NEVER;
|
|
495
|
+
}
|
|
496
|
+
if (operators.length > 0)
|
|
497
|
+
return compileOperators(condition, at, compilation, depth);
|
|
498
|
+
}
|
|
499
|
+
compilation.clauses++;
|
|
500
|
+
return equalTo(condition);
|
|
501
|
+
}
|
|
502
|
+
/** The matcher of a selector. Each problem goes to the compilation. */
|
|
503
|
+
function compileInto(selector, at, compilation, depth) {
|
|
504
|
+
if (!isObject(selector)) {
|
|
505
|
+
compilation.problems.push(`${at} must be an object`);
|
|
506
|
+
return () => false;
|
|
507
|
+
}
|
|
508
|
+
const parts = [];
|
|
509
|
+
for (const [key, condition] of definedEntries(selector)) {
|
|
510
|
+
if (key.startsWith("$")) {
|
|
511
|
+
if (!LOGICAL.has(key)) {
|
|
512
|
+
compilation.problems.push(`${at}: the operator ${key} is not known`);
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
if (!permitted(key, at, compilation))
|
|
516
|
+
continue;
|
|
517
|
+
if (!Array.isArray(condition) || condition.length === 0) {
|
|
518
|
+
compilation.problems.push(`${at}.${key} takes a list of one or more selectors`);
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if (!withinDepth(depth + 1, `${at}.${key}`, compilation))
|
|
522
|
+
continue;
|
|
523
|
+
const matchers = condition.map((part, index) => compileInto(part, `${at}.${key}[${index}]`, compilation, depth + 1));
|
|
524
|
+
if (key === "$and")
|
|
525
|
+
parts.push((item) => matchers.every((matcher) => matcher(item)));
|
|
526
|
+
else if (key === "$or")
|
|
527
|
+
parts.push((item) => matchers.some((matcher) => matcher(item)));
|
|
528
|
+
else
|
|
529
|
+
parts.push((item) => !matchers.some((matcher) => matcher(item)));
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
const path = pathOf(key, at, compilation.problems);
|
|
533
|
+
if (!path)
|
|
534
|
+
continue;
|
|
535
|
+
const test = compileCondition(condition, `${at}.${key}`, compilation, depth);
|
|
536
|
+
parts.push((item) => test(lookup(item, path)));
|
|
537
|
+
}
|
|
538
|
+
if (parts.length === 0)
|
|
539
|
+
return ALWAYS;
|
|
540
|
+
if (parts.length === 1)
|
|
541
|
+
return parts[0];
|
|
542
|
+
return (item) => parts.every((part) => part(item));
|
|
543
|
+
}
|
|
544
|
+
/** This function compiles a selector, and gives the matcher and each problem. */
|
|
545
|
+
function compile(selector, policy) {
|
|
546
|
+
const compilation = { limits: limitsOf(policy), problems: [], clauses: 0 };
|
|
547
|
+
const matcher = compileInto(selector, "selector", compilation, 0);
|
|
548
|
+
if (compilation.clauses > compilation.limits.maxClauses) {
|
|
549
|
+
compilation.problems.push(`selector: it has ${compilation.clauses} clauses, more than the ${compilation.limits.maxClauses} of the query policy`);
|
|
550
|
+
}
|
|
551
|
+
return { matcher, problems: compilation.problems };
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* This function gives the problems of a selector: each part that the language does not know, and
|
|
555
|
+
* each part that the policy does not permit. An empty list means that the selector is correct.
|
|
556
|
+
*
|
|
557
|
+
* @param policy - The query policy. The default policy does not permit `$regex`.
|
|
558
|
+
*/
|
|
559
|
+
export function selectorProblems(selector, policy) {
|
|
560
|
+
return compile(selector, policy).problems;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* This function makes a test from a selector. The test gives true for each item that the selector
|
|
564
|
+
* matches. Compile a selector one time for many items.
|
|
565
|
+
*
|
|
566
|
+
* @param policy - The query policy. The default policy does not permit `$regex`.
|
|
567
|
+
* @throws TypeError when the selector has a problem (refer to `selectorProblems`)
|
|
568
|
+
*/
|
|
569
|
+
export function compileSelector(selector, policy) {
|
|
570
|
+
const { matcher, problems } = compile(selector, policy);
|
|
571
|
+
if (problems.length > 0)
|
|
572
|
+
throw new TypeError(`The selector is not correct: ${problems.join(". ")}`);
|
|
573
|
+
return matcher;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* This function tells if a selector matches an item.
|
|
577
|
+
*
|
|
578
|
+
* @throws TypeError when the selector has a problem
|
|
579
|
+
*/
|
|
580
|
+
export function matchesSelector(selector, item, policy) {
|
|
581
|
+
return compileSelector(selector, policy)(item);
|
|
582
|
+
}
|
|
583
|
+
//
|
|
584
|
+
// ═══ The options ═══
|
|
585
|
+
//
|
|
586
|
+
const OPTION_KEYS = new Set(["sort", "skip", "limit"]);
|
|
587
|
+
/**
|
|
588
|
+
* This function gives the problems of the options of a query. An empty list means that the
|
|
589
|
+
* options are correct. A policy with a `maxLimit` refuses a larger limit.
|
|
590
|
+
*/
|
|
591
|
+
export function queryOptionsProblems(options, policy) {
|
|
592
|
+
if (options === undefined)
|
|
593
|
+
return [];
|
|
594
|
+
if (!isObject(options))
|
|
595
|
+
return ["the options of a query must be an object"];
|
|
596
|
+
const problems = [];
|
|
597
|
+
for (const key of definedKeys(options)) {
|
|
598
|
+
if (!OPTION_KEYS.has(key))
|
|
599
|
+
problems.push(`the option ${key} is not known: use sort, skip and limit`);
|
|
600
|
+
}
|
|
601
|
+
const sort = options["sort"];
|
|
602
|
+
const skip = options["skip"];
|
|
603
|
+
const limit = options["limit"];
|
|
604
|
+
if (sort !== undefined) {
|
|
605
|
+
if (!isObject(sort)) {
|
|
606
|
+
problems.push("sort must be an object of field paths with 1 or -1");
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
for (const [key, direction] of definedEntries(sort)) {
|
|
610
|
+
pathOf(key, "sort", problems);
|
|
611
|
+
if (direction !== 1 && direction !== -1)
|
|
612
|
+
problems.push(`sort.${key} must be 1 or -1`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (skip !== undefined && (typeof skip !== "number" || !Number.isSafeInteger(skip) || skip < 0)) {
|
|
617
|
+
problems.push("skip must be a whole number, 0 or more");
|
|
618
|
+
}
|
|
619
|
+
if (limit !== undefined && (typeof limit !== "number" || !Number.isSafeInteger(limit) || limit < 1)) {
|
|
620
|
+
problems.push("limit must be a whole number, 1 or more");
|
|
621
|
+
}
|
|
622
|
+
else if (typeof limit === "number") {
|
|
623
|
+
const maxLimit = limitsOf(policy).maxLimit;
|
|
624
|
+
if (limit > maxLimit)
|
|
625
|
+
problems.push(`limit must be at most ${maxLimit}, the maxLimit of the query policy`);
|
|
626
|
+
}
|
|
627
|
+
return problems;
|
|
628
|
+
}
|
|
629
|
+
/** The key of an item for one sort key: the least value for ascending, the most for descending. */
|
|
630
|
+
function sortKeyOf(item, path, direction) {
|
|
631
|
+
const found = lookup(item, path);
|
|
632
|
+
const values = [];
|
|
633
|
+
for (const value of found.values) {
|
|
634
|
+
if (Array.isArray(value))
|
|
635
|
+
values.push(...value);
|
|
636
|
+
else
|
|
637
|
+
values.push(value);
|
|
638
|
+
}
|
|
639
|
+
if (found.missing)
|
|
640
|
+
values.push(null);
|
|
641
|
+
if (values.length === 0)
|
|
642
|
+
return null;
|
|
643
|
+
let best = values[0];
|
|
644
|
+
for (let index = 1; index < values.length; index++) {
|
|
645
|
+
const result = compareStorageValues(values[index], best);
|
|
646
|
+
if (direction === 1 ? result < 0 : result > 0)
|
|
647
|
+
best = values[index];
|
|
648
|
+
}
|
|
649
|
+
return best;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* This function runs a query on entries, as the reference of each storage: the selector, then the
|
|
653
|
+
* order, then the skip and the limit. The result has the entries themselves, not copies.
|
|
654
|
+
*
|
|
655
|
+
* - Without `sort`, the result has the order of the entries.
|
|
656
|
+
* - A sort key on an array field uses its least element for ascending, and its most element for
|
|
657
|
+
* descending. A missing field sorts as `null`.
|
|
658
|
+
* - A query without a limit gets the `maxLimit` of the policy, when it has one.
|
|
659
|
+
*
|
|
660
|
+
* @param policy - The query policy. The default policy does not permit `$regex`.
|
|
661
|
+
* @throws TypeError when the selector or the options have a problem
|
|
662
|
+
*/
|
|
663
|
+
export function queryEntries(entries, selector = {}, options = {}, policy) {
|
|
664
|
+
const matches = compileSelector(selector, policy);
|
|
665
|
+
const problems = queryOptionsProblems(options, policy);
|
|
666
|
+
if (problems.length > 0)
|
|
667
|
+
throw new TypeError(`The options of the query are not correct: ${problems.join(". ")}`);
|
|
668
|
+
let result = [];
|
|
669
|
+
for (const [id, value] of entries) {
|
|
670
|
+
if (matches(value))
|
|
671
|
+
result.push([id, value]);
|
|
672
|
+
}
|
|
673
|
+
const keys = definedEntries(options.sort ?? {}).map(([key, direction]) => ({
|
|
674
|
+
path: key.split("."),
|
|
675
|
+
direction: direction,
|
|
676
|
+
}));
|
|
677
|
+
if (keys.length > 0) {
|
|
678
|
+
const rows = result.map((entry, index) => ({ entry, index, keys: keys.map((key) => sortKeyOf(entry[1], key.path, key.direction)) }));
|
|
679
|
+
rows.sort((a, b) => {
|
|
680
|
+
for (let index = 0; index < keys.length; index++) {
|
|
681
|
+
const delta = compareStorageValues(a.keys[index], b.keys[index]) * keys[index].direction;
|
|
682
|
+
if (delta !== 0)
|
|
683
|
+
return delta;
|
|
684
|
+
}
|
|
685
|
+
return a.index - b.index;
|
|
686
|
+
});
|
|
687
|
+
result = rows.map((row) => row.entry);
|
|
688
|
+
}
|
|
689
|
+
const skip = options.skip ?? 0;
|
|
690
|
+
const maxLimit = limitsOf(policy).maxLimit;
|
|
691
|
+
const limit = options.limit ?? (Number.isFinite(maxLimit) ? maxLimit : undefined);
|
|
692
|
+
if (skip === 0 && limit === undefined)
|
|
693
|
+
return result;
|
|
694
|
+
return result.slice(skip, limit === undefined ? undefined : skip + limit);
|
|
695
|
+
}
|
|
696
|
+
//
|
|
697
|
+
// ═══ The patch ═══
|
|
698
|
+
//
|
|
699
|
+
const PATCH_OPERATIONS = new Set(["$set", "$unset"]);
|
|
700
|
+
/** True for the operations form of a patch: an object whose defined keys each start with `$`. */
|
|
701
|
+
export function isPatchOperations(fields) {
|
|
702
|
+
return isOperatorObject(fields);
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* This function gives the problems of the fields of a patch. An empty list means that the patch
|
|
706
|
+
* is correct: a JSON Merge Patch, or the operations `$set` and `$unset`. A patch that mixes the two
|
|
707
|
+
* forms is not correct.
|
|
708
|
+
*/
|
|
709
|
+
export function patchProblems(fields) {
|
|
710
|
+
if (!isObject(fields))
|
|
711
|
+
return ["the fields of a patch must be an object"];
|
|
712
|
+
const keys = definedKeys(fields);
|
|
713
|
+
const operations = keys.filter((key) => key.startsWith("$"));
|
|
714
|
+
if (operations.length === 0)
|
|
715
|
+
return [];
|
|
716
|
+
if (operations.length < keys.length) {
|
|
717
|
+
return ["the patch mixes operations and fields: give { $set, $unset }, or only fields (a JSON Merge Patch)"];
|
|
718
|
+
}
|
|
719
|
+
const problems = [];
|
|
720
|
+
const paths = [];
|
|
721
|
+
for (const operation of operations) {
|
|
722
|
+
if (!PATCH_OPERATIONS.has(operation))
|
|
723
|
+
problems.push(`the patch operation ${operation} is not known: use $set and $unset`);
|
|
724
|
+
}
|
|
725
|
+
const set = fields["$set"];
|
|
726
|
+
if (set !== undefined) {
|
|
727
|
+
if (!isObject(set))
|
|
728
|
+
problems.push("$set takes an object of field paths and values");
|
|
729
|
+
else
|
|
730
|
+
for (const key of definedKeys(set))
|
|
731
|
+
if (pathOf(key, "$set", problems))
|
|
732
|
+
paths.push(key);
|
|
733
|
+
}
|
|
734
|
+
const unset = fields["$unset"];
|
|
735
|
+
if (unset !== undefined) {
|
|
736
|
+
if (!Array.isArray(unset) || !unset.every((path) => typeof path === "string"))
|
|
737
|
+
problems.push("$unset takes a list of field paths");
|
|
738
|
+
else
|
|
739
|
+
for (const key of unset)
|
|
740
|
+
if (pathOf(key, "$unset", problems))
|
|
741
|
+
paths.push(key);
|
|
742
|
+
}
|
|
743
|
+
for (let first = 0; first < paths.length; first++) {
|
|
744
|
+
for (let second = first + 1; second < paths.length; second++) {
|
|
745
|
+
const a = paths[first];
|
|
746
|
+
const b = paths[second];
|
|
747
|
+
if (a === b || b.startsWith(`${a}.`) || a.startsWith(`${b}.`))
|
|
748
|
+
problems.push(`the field paths ${a} and ${b} of the patch overlap`);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return problems;
|
|
752
|
+
}
|
|
753
|
+
/** This function sets a key as an own property, also the key `__proto__`. */
|
|
754
|
+
function put(target, key, value) {
|
|
755
|
+
Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
|
|
756
|
+
}
|
|
757
|
+
/** A shallow copy of an object, with its own keys, also `__proto__`. */
|
|
758
|
+
function copyOf(value) {
|
|
759
|
+
const result = {};
|
|
760
|
+
for (const key of Object.keys(value))
|
|
761
|
+
put(result, key, value[key]);
|
|
762
|
+
return result;
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* This function applies a JSON Merge Patch (RFC 7386) to a value, and gives the new value. It does
|
|
766
|
+
* not change the value or the patch, but the new value can share parts with them.
|
|
767
|
+
*
|
|
768
|
+
* - Each field of the patch replaces the field of the value.
|
|
769
|
+
* - An object field of the patch merges into an object field of the value.
|
|
770
|
+
* - A `null` field removes the field.
|
|
771
|
+
* - A value that is not an object becomes an empty object first.
|
|
772
|
+
*/
|
|
773
|
+
export function mergePatch(value, fields) {
|
|
774
|
+
const result = isObject(value) ? copyOf(value) : {};
|
|
775
|
+
for (const key of Object.keys(fields)) {
|
|
776
|
+
const field = fields[key];
|
|
777
|
+
if (field === undefined)
|
|
778
|
+
continue;
|
|
779
|
+
if (field === null)
|
|
780
|
+
delete result[key];
|
|
781
|
+
else
|
|
782
|
+
put(result, key, isObject(field) ? mergePatch(Object.hasOwn(result, key) ? result[key] : undefined, field) : field);
|
|
783
|
+
}
|
|
784
|
+
return result;
|
|
785
|
+
}
|
|
786
|
+
/** The container at a path with a new value at its end: a copy of each object and array on the path. */
|
|
787
|
+
function setIn(container, path, index, value) {
|
|
788
|
+
const key = path[index];
|
|
789
|
+
const last = index === path.length - 1;
|
|
790
|
+
const where = path.slice(0, index).join(".") || "the item";
|
|
791
|
+
if (Array.isArray(container)) {
|
|
792
|
+
if (!INDEX.test(key))
|
|
793
|
+
throw new TypeError(`The patch cannot set ${path.join(".")}: ${where} is an array`);
|
|
794
|
+
const position = Number(key);
|
|
795
|
+
if (position > container.length) {
|
|
796
|
+
throw new TypeError(`The patch cannot set ${path.join(".")}: ${where} has ${container.length} elements`);
|
|
797
|
+
}
|
|
798
|
+
const copy = container.slice();
|
|
799
|
+
copy[position] = last ? value : setIn(copy[position] ?? {}, path, index + 1, value);
|
|
800
|
+
return copy;
|
|
801
|
+
}
|
|
802
|
+
if (!isObject(container))
|
|
803
|
+
throw new TypeError(`The patch cannot set ${path.join(".")}: ${where} is not an object`);
|
|
804
|
+
const copy = copyOf(container);
|
|
805
|
+
const child = Object.hasOwn(copy, key) ? copy[key] : undefined;
|
|
806
|
+
put(copy, key, last ? value : setIn(child ?? {}, path, index + 1, value));
|
|
807
|
+
return copy;
|
|
808
|
+
}
|
|
809
|
+
/** The container without the value at a path: a copy of each object and array on the path. */
|
|
810
|
+
function unsetIn(container, path, index) {
|
|
811
|
+
const key = path[index];
|
|
812
|
+
const last = index === path.length - 1;
|
|
813
|
+
if (Array.isArray(container)) {
|
|
814
|
+
if (!INDEX.test(key) || Number(key) >= container.length)
|
|
815
|
+
return container;
|
|
816
|
+
const copy = container.slice();
|
|
817
|
+
// An element of an array becomes null, as in MongoDB: the other elements keep their places
|
|
818
|
+
copy[Number(key)] = last ? null : unsetIn(copy[Number(key)], path, index + 1);
|
|
819
|
+
return copy;
|
|
820
|
+
}
|
|
821
|
+
if (!isObject(container) || !Object.hasOwn(container, key))
|
|
822
|
+
return container;
|
|
823
|
+
const copy = copyOf(container);
|
|
824
|
+
if (last)
|
|
825
|
+
delete copy[key];
|
|
826
|
+
else
|
|
827
|
+
put(copy, key, unsetIn(copy[key], path, index + 1));
|
|
828
|
+
return copy;
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* This function applies a patch to a value, and gives the new value: a JSON Merge Patch, or the
|
|
832
|
+
* operations `$set` and `$unset`. It does not change the value or the patch.
|
|
833
|
+
*
|
|
834
|
+
* - `$set` sets each field path, also to `null`. A missing or null part of a path becomes an object.
|
|
835
|
+
* - `$unset` removes each field path. An element of an array becomes `null`.
|
|
836
|
+
* - A value that is not an object becomes an empty object first.
|
|
837
|
+
*
|
|
838
|
+
* @throws TypeError when the patch has a problem (refer to `patchProblems`), or when a path of
|
|
839
|
+
* `$set` goes through a value that is not an object, or past the end of an array
|
|
840
|
+
*/
|
|
841
|
+
export function applyPatch(value, fields) {
|
|
842
|
+
const problems = patchProblems(fields);
|
|
843
|
+
if (problems.length > 0)
|
|
844
|
+
throw new TypeError(`The patch is not correct: ${problems.join(". ")}`);
|
|
845
|
+
if (!isPatchOperations(fields))
|
|
846
|
+
return mergePatch(value, fields);
|
|
847
|
+
let result = isObject(value) ? value : {};
|
|
848
|
+
for (const [key, field] of definedEntries(fields.$set ?? {}))
|
|
849
|
+
result = setIn(result, key.split("."), 0, field);
|
|
850
|
+
for (const key of fields.$unset ?? [])
|
|
851
|
+
result = unsetIn(result, key.split("."), 0);
|
|
852
|
+
return result === value ? copyOf(value) : result;
|
|
853
|
+
}
|
|
854
|
+
//# sourceMappingURL=query.js.map
|