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