@procedures/mongo 0.33.0 → 0.35.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 +59 -0
- package/README.md +34 -5
- package/dist/.tsbuildinfo +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/procedures/collections.create.d.ts.map +1 -1
- package/dist/procedures/collections.create.js +5 -5
- package/dist/procedures/collections.create.js.map +1 -1
- package/dist/procedures/collections.drop.d.ts.map +1 -1
- package/dist/procedures/collections.drop.js +5 -5
- package/dist/procedures/collections.drop.js.map +1 -1
- package/dist/procedures/collections.list.d.ts.map +1 -1
- package/dist/procedures/collections.list.js +4 -4
- package/dist/procedures/collections.list.js.map +1 -1
- package/dist/procedures/collections.stats.d.ts.map +1 -1
- package/dist/procedures/collections.stats.js +4 -4
- package/dist/procedures/collections.stats.js.map +1 -1
- package/dist/procedures/database.info.d.ts.map +1 -1
- package/dist/procedures/database.info.js +4 -4
- package/dist/procedures/database.info.js.map +1 -1
- package/dist/procedures/database.ping.d.ts.map +1 -1
- package/dist/procedures/database.ping.js +4 -4
- package/dist/procedures/database.ping.js.map +1 -1
- package/dist/procedures/documents.aggregate.d.ts.map +1 -1
- package/dist/procedures/documents.aggregate.js +5 -5
- package/dist/procedures/documents.aggregate.js.map +1 -1
- package/dist/procedures/documents.bulk.d.ts.map +1 -1
- package/dist/procedures/documents.bulk.js +3 -3
- package/dist/procedures/documents.bulk.js.map +1 -1
- package/dist/procedures/documents.count.d.ts.map +1 -1
- package/dist/procedures/documents.count.js +5 -5
- package/dist/procedures/documents.count.js.map +1 -1
- package/dist/procedures/documents.delete.d.ts.map +1 -1
- package/dist/procedures/documents.delete.js +3 -3
- package/dist/procedures/documents.delete.js.map +1 -1
- package/dist/procedures/documents.distinct.d.ts.map +1 -1
- package/dist/procedures/documents.distinct.js +5 -5
- package/dist/procedures/documents.distinct.js.map +1 -1
- package/dist/procedures/documents.explain.d.ts.map +1 -1
- package/dist/procedures/documents.explain.js +3 -3
- package/dist/procedures/documents.explain.js.map +1 -1
- package/dist/procedures/documents.find.d.ts.map +1 -1
- package/dist/procedures/documents.find.js +3 -3
- package/dist/procedures/documents.find.js.map +1 -1
- package/dist/procedures/documents.get.d.ts.map +1 -1
- package/dist/procedures/documents.get.js +3 -3
- package/dist/procedures/documents.get.js.map +1 -1
- package/dist/procedures/documents.insert.d.ts.map +1 -1
- package/dist/procedures/documents.insert.js +5 -5
- package/dist/procedures/documents.insert.js.map +1 -1
- package/dist/procedures/documents.update.d.ts.map +1 -1
- package/dist/procedures/documents.update.js +3 -3
- package/dist/procedures/documents.update.js.map +1 -1
- package/dist/procedures/indexes.create.d.ts.map +1 -1
- package/dist/procedures/indexes.create.js +5 -5
- package/dist/procedures/indexes.create.js.map +1 -1
- package/dist/procedures/indexes.drop.d.ts.map +1 -1
- package/dist/procedures/indexes.drop.js +5 -5
- package/dist/procedures/indexes.drop.js.map +1 -1
- package/dist/procedures/indexes.list.d.ts.map +1 -1
- package/dist/procedures/indexes.list.js +4 -4
- package/dist/procedures/indexes.list.js.map +1 -1
- package/dist/procedures/watch.d.ts.map +1 -1
- package/dist/procedures/watch.js +3 -3
- package/dist/procedures/watch.js.map +1 -1
- package/dist/storage/document-storage.d.ts +177 -8
- package/dist/storage/document-storage.d.ts.map +1 -1
- package/dist/storage/document-storage.js +383 -72
- package/dist/storage/document-storage.js.map +1 -1
- package/dist/storage/index.d.ts +3 -3
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +2 -1
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/materializer.d.ts +27 -12
- package/dist/storage/materializer.d.ts.map +1 -1
- package/dist/storage/materializer.js +37 -15
- package/dist/storage/materializer.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +11 -3
- package/src/procedures/collections.create.ts +5 -9
- package/src/procedures/collections.drop.ts +5 -9
- package/src/procedures/collections.list.ts +4 -8
- package/src/procedures/collections.stats.ts +4 -8
- package/src/procedures/database.info.ts +4 -8
- package/src/procedures/database.ping.ts +4 -8
- package/src/procedures/documents.aggregate.ts +5 -9
- package/src/procedures/documents.bulk.ts +3 -5
- package/src/procedures/documents.count.ts +5 -9
- package/src/procedures/documents.delete.ts +3 -5
- package/src/procedures/documents.distinct.ts +5 -9
- package/src/procedures/documents.explain.ts +3 -5
- package/src/procedures/documents.find.ts +2 -5
- package/src/procedures/documents.get.ts +2 -5
- package/src/procedures/documents.insert.ts +5 -9
- package/src/procedures/documents.update.ts +3 -5
- package/src/procedures/indexes.create.ts +5 -9
- package/src/procedures/indexes.drop.ts +5 -9
- package/src/procedures/indexes.list.ts +4 -8
- package/src/procedures/watch.ts +3 -5
- package/src/storage/document-storage.ts +441 -71
- package/src/storage/index.ts +11 -3
- package/src/storage/materializer.ts +54 -22
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
* `{ _id, data }` documents.
|
|
5
5
|
*
|
|
6
6
|
* The documents:
|
|
7
|
-
* - An item that is a plain object
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* - An item that is a plain object is stored flat: its fields beside the fields of the storage. A
|
|
8
|
+
* key with `$` or a dot below the top level, and a top-level key with a dot, stay as they are
|
|
9
|
+
* (MongoDB 5.0 or later keeps them). Another item is stored as JSON text in `_json`: null, a
|
|
10
|
+
* primitive, an array, an object with a top-level key of the storage or a top-level key that
|
|
11
|
+
* starts with `$`, a key `__proto__`, or a class instance other than `Date`. Both ways give the
|
|
12
|
+
* item back as it was. An object with a text `$ref` and an `$id` is refused (`TypeError`):
|
|
13
|
+
* MongoDB gives it back as a `DBRef`.
|
|
11
14
|
* - An item whose `_id` is its id keeps it: the storage gives it back.
|
|
12
15
|
* - The fields of the storage: `_key` (the id), `_part` (the partition), `_seq` (the sequence
|
|
13
16
|
* number of the last write), `_ord` (the number of the first write: the order of the entries, as
|
|
@@ -16,6 +19,40 @@
|
|
|
16
19
|
* character U+001F. Each partition reads and writes only its documents: each write filter has the
|
|
17
20
|
* partition too, so a write of a stored `_id` of another partition fails and changes nothing.
|
|
18
21
|
*
|
|
22
|
+
* The stored form (roadmap CLI-76): an app with its own form of documents keeps it with the option
|
|
23
|
+
* `form`, one `StoredForm` object. `DEFAULT_STORED_FORM` gives the form above, byte for byte.
|
|
24
|
+
* - `storedId(partition, id)` gives the stored `_id`, and `idOf(storedId, partition)` is its inverse.
|
|
25
|
+
* The stored `_id` can be any value but `undefined` and `null`, for example `tenant:key`.
|
|
26
|
+
* - `partitionFields(partition)` gives the fields that name the partition (default `{ _part }`).
|
|
27
|
+
* Each document of the partition has them, and they are the filter of the partition. A field with
|
|
28
|
+
* the value `undefined` is absent: the document does not have it, and the filter is
|
|
29
|
+
* `{ $exists: false }`. An item can have a partition field (one that is not a field of the
|
|
30
|
+
* storage) with the value of its partition: the write leaves it out and notes it in `_kpf`, and
|
|
31
|
+
* the read gives it back. The same field with another value is refused (`TypeError`).
|
|
32
|
+
* - `partitionOf(doc)` gives the partition of a stored document, for `ViewMaterializer`. A delete
|
|
33
|
+
* event has only the `documentKey`: so the `_id` must tell the partition, or the collection needs
|
|
34
|
+
* `changeStreamPreAndPostImages`.
|
|
35
|
+
* - The storage adds its fields (`_key`, `_seq`, `_ord`, `_tomb`, and `_kid` or `_json` as needed)
|
|
36
|
+
* in each form. A reader of the raw documents can leave them out (`DOCUMENT_STORAGE_FIELDS`), and
|
|
37
|
+
* must leave out the tombstones (`TOMBSTONE_FIELD` is `true`). `decodeStoredDocument(doc)` does
|
|
38
|
+
* this, as the storage reads a document.
|
|
39
|
+
* - A document without the fields of the storage (for example one written before the app used the
|
|
40
|
+
* storage) is an item too. Its id comes from `idOf`, else from `_key`, and its item gets that id
|
|
41
|
+
* as `_id`. Its partition fields follow the rule of a written document without `_kpf`: a field
|
|
42
|
+
* with the value of the partition stays out, and a field with another value stays in. So the
|
|
43
|
+
* item has the same form before and after its first write. A document whose id neither
|
|
44
|
+
* tells (for example an `ObjectId` that `idOf` does not name) is not an item: the reads and
|
|
45
|
+
* `size()` leave it out. Its version is `0`, and it comes before the other items in the order of
|
|
46
|
+
* the entries (then by `_id`). A full result of the change log has it, and a delta has it after
|
|
47
|
+
* its first write through the storage. That write gives it its fields and a version, and keeps
|
|
48
|
+
* its place.
|
|
49
|
+
* - Write through the storage. A raw write (for example `findOneAndReplace` or a script) takes no
|
|
50
|
+
* number of the sequence, so a delta of the change log does not give it, and a raw replace drops
|
|
51
|
+
* `_seq`.
|
|
52
|
+
* - A raw writer must not hard-delete an item while the storage writes it. The write of a document
|
|
53
|
+
* without `_seq` is an upsert with `{ _seq: { $exists: false } }`: after a hard delete between
|
|
54
|
+
* the read and the write, the upsert inserts the document again.
|
|
55
|
+
*
|
|
19
56
|
* The change log:
|
|
20
57
|
* - Each write gets the next number of the sequence of the collection. `_seq` is also the version
|
|
21
58
|
* of the item (`versionOf`).
|
|
@@ -31,6 +68,9 @@
|
|
|
31
68
|
* its lease moves its documents to new numbers, so a reader that passed the old numbers gets
|
|
32
69
|
* it. One case stays on a single server: a process that stops between such a late write and the
|
|
33
70
|
* move of its documents. Then a reader that passed the old numbers does not get that change.
|
|
71
|
+
* - The storage uses a transaction only with a client: the option `client`, or the default
|
|
72
|
+
* connection when `db` is absent. With `db` and no `client`, each write uses the watermark, also
|
|
73
|
+
* on a replica set.
|
|
34
74
|
*
|
|
35
75
|
* The queries: `query()` sends the conditions that MongoDB tests in the same way (the equality of
|
|
36
76
|
* a string, a number or a boolean, and `$in` of them) as a filter. `queryEntries()` of the core
|
|
@@ -45,8 +85,13 @@
|
|
|
45
85
|
* precondition has the `_seq` that the test read in its filter (a compare-and-set). Another writer
|
|
46
86
|
* between the test and the write makes the write fail with `VersionConflictError`. Then a batch of
|
|
47
87
|
* more than one item can keep a part of its writes: only a transaction makes a batch all or nothing.
|
|
88
|
+
* One case stays on a single server: a writer outside the storage that writes a document without
|
|
89
|
+
* `_seq` between the test and the write. The guard of such a document is `{ _seq: { $exists:
|
|
90
|
+
* false } }`, so it does not see that writer. On a replica set, the test and the write are one
|
|
91
|
+
* transaction, and this case does not occur.
|
|
48
92
|
*/
|
|
49
93
|
import { randomUUID } from "node:crypto";
|
|
94
|
+
import { isDeepStrictEqual } from "node:util";
|
|
50
95
|
import { VersionConflictError, applyPatch, preconditionNotSupported, queryEntries, versionConflict, } from "@procedures/collections";
|
|
51
96
|
import { optionalMs, toMs } from "@procedures/client";
|
|
52
97
|
import { ensureConnection } from "../connection.js";
|
|
@@ -61,9 +106,47 @@ const TOMB = "_tomb";
|
|
|
61
106
|
const JSON_FIELD = "_json";
|
|
62
107
|
/** True in a stored document whose item had `_id` equal to its id. */
|
|
63
108
|
const KEEP_ID = "_kid";
|
|
64
|
-
|
|
109
|
+
/** The partition fields that the item had with the value of its partition (roadmap CLI-76). The write leaves them out of the fields. */
|
|
110
|
+
const KEEP_PART = "_kpf";
|
|
111
|
+
const STORAGE_FIELDS = new Set(["_id", KEY, PART, SEQ, ORD, TOMB, JSON_FIELD, KEEP_ID, KEEP_PART]);
|
|
65
112
|
/** The separator of the partition and the id in a stored `_id`. */
|
|
66
113
|
export const SEPARATOR = "\u001f";
|
|
114
|
+
/**
|
|
115
|
+
* The fields that a `MongoDocumentStorage` can add to a stored document, in each stored form
|
|
116
|
+
* (roadmap CLI-76). A reader of the raw documents can leave them out. The partition fields are
|
|
117
|
+
* not in the list.
|
|
118
|
+
*/
|
|
119
|
+
export const DOCUMENT_STORAGE_FIELDS = Object.freeze([KEY, PART, SEQ, ORD, TOMB, JSON_FIELD, KEEP_ID, KEEP_PART]);
|
|
120
|
+
/** The field that is `true` in a tombstone: the document of a deleted item. A reader of the raw documents leaves them out. */
|
|
121
|
+
export const TOMBSTONE_FIELD = TOMB;
|
|
122
|
+
/**
|
|
123
|
+
* The default stored form, the form of 0.34.0. The stored `_id` is the id, or `<partition>` +
|
|
124
|
+
* U+001F + `<id>` with a partition. The partition field is `_part` (absent without a partition).
|
|
125
|
+
* `partitionOf` reads `_part`, else the part of `_id` before U+001F.
|
|
126
|
+
*/
|
|
127
|
+
export const DEFAULT_STORED_FORM = Object.freeze({
|
|
128
|
+
storedId(partition, id) {
|
|
129
|
+
return partition === undefined ? id : `${partition}${SEPARATOR}${id}`;
|
|
130
|
+
},
|
|
131
|
+
idOf(storedId, partition) {
|
|
132
|
+
if (typeof storedId !== "string")
|
|
133
|
+
return undefined;
|
|
134
|
+
if (partition === undefined)
|
|
135
|
+
return storedId.includes(SEPARATOR) ? undefined : storedId;
|
|
136
|
+
const prefix = `${partition}${SEPARATOR}`;
|
|
137
|
+
return storedId.startsWith(prefix) ? storedId.slice(prefix.length) : undefined;
|
|
138
|
+
},
|
|
139
|
+
partitionFields(partition) {
|
|
140
|
+
return { [PART]: partition };
|
|
141
|
+
},
|
|
142
|
+
partitionOf(doc) {
|
|
143
|
+
if (typeof doc[PART] === "string")
|
|
144
|
+
return doc[PART];
|
|
145
|
+
const id = doc["_id"];
|
|
146
|
+
const at = typeof id === "string" ? id.indexOf(SEPARATOR) : -1;
|
|
147
|
+
return at > 0 ? id.slice(0, at) : undefined;
|
|
148
|
+
},
|
|
149
|
+
});
|
|
67
150
|
/** True for the error of a duplicate key: an insert of an upsert whose filter found no document. */
|
|
68
151
|
function isDuplicateKey(error) {
|
|
69
152
|
// A bulk write error has the code of its first write error, and the list of the write errors
|
|
@@ -102,25 +185,91 @@ function enqueue(db, collection, task) {
|
|
|
102
185
|
});
|
|
103
186
|
return run;
|
|
104
187
|
}
|
|
105
|
-
/**
|
|
106
|
-
|
|
188
|
+
/**
|
|
189
|
+
* True for a value of a field that BSON gives back as it is: a key with `$` or a dot stays as it is
|
|
190
|
+
* (MongoDB 5.0 or later). False when only JSON text keeps it: a class instance other than `Date`,
|
|
191
|
+
* or a key `__proto__`. An object that BSON gives back as a `DBRef` adds a problem.
|
|
192
|
+
*/
|
|
193
|
+
function isNativeValue(value, path, problems) {
|
|
107
194
|
if (Array.isArray(value))
|
|
108
|
-
return value.every((element) =>
|
|
195
|
+
return value.every((element, index) => isNativeValue(element, `${path}.${index}`, problems));
|
|
109
196
|
if (typeof value !== "object" || value === null)
|
|
110
197
|
return true;
|
|
111
198
|
const proto = Object.getPrototypeOf(value);
|
|
112
199
|
if (proto !== Object.prototype && proto !== null)
|
|
113
200
|
return value instanceof Date;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
201
|
+
const object = value;
|
|
202
|
+
// The test of the BSON reader: a text `$ref`, an `$id`, and no `$db` or a text `$db`
|
|
203
|
+
if (typeof object["$ref"] === "string" && object["$id"] !== undefined && object["$id"] !== null && (object["$db"] === undefined || typeof object["$db"] === "string")) {
|
|
204
|
+
problems.push(`the field ${path} has $ref and $id, and MongoDB gives it back as a DBRef`);
|
|
205
|
+
}
|
|
206
|
+
for (const [key, field] of Object.entries(object)) {
|
|
207
|
+
if (key === "__proto__")
|
|
118
208
|
return false;
|
|
119
|
-
if (!
|
|
209
|
+
if (!isNativeValue(field, `${path}.${key}`, problems))
|
|
120
210
|
return false;
|
|
121
211
|
}
|
|
122
212
|
return true;
|
|
123
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* The item of a stored document (not a tombstone) whose id is known. `_json` gives the item as it
|
|
216
|
+
* was. Else the fields of the storage stay out, the item gets `_id` when it had it (`_kid`), when
|
|
217
|
+
* the document is from outside the storage (no `_key`) or with `addId`, and it gets back the
|
|
218
|
+
* partition fields of `_kpf`. A partition field with the value of the partition stays out (for a
|
|
219
|
+
* document from outside the storage too); a field with another value stays in.
|
|
220
|
+
*/
|
|
221
|
+
function itemOf(doc, id, partitionEntries, addId) {
|
|
222
|
+
if (typeof doc[JSON_FIELD] === "string") {
|
|
223
|
+
const item = JSON.parse(doc[JSON_FIELD]);
|
|
224
|
+
if (addId && isPlainObject(item) && !("_id" in item))
|
|
225
|
+
return { _id: id, ...item };
|
|
226
|
+
return item;
|
|
227
|
+
}
|
|
228
|
+
const outside = doc[KEY] === undefined;
|
|
229
|
+
const value = {};
|
|
230
|
+
if (addId || outside || doc[KEEP_ID] === true)
|
|
231
|
+
value["_id"] = id;
|
|
232
|
+
const partitionValues = new Map(partitionEntries);
|
|
233
|
+
for (const [key, field] of Object.entries(doc)) {
|
|
234
|
+
if (STORAGE_FIELDS.has(key))
|
|
235
|
+
continue;
|
|
236
|
+
// The value of the partition comes back only through `_kpf`, so the item has one form at each age of its document
|
|
237
|
+
if (partitionValues.has(key) && isDeepStrictEqual(field, partitionValues.get(key)))
|
|
238
|
+
continue;
|
|
239
|
+
put(value, key, field);
|
|
240
|
+
}
|
|
241
|
+
const kept = doc[KEEP_PART];
|
|
242
|
+
if (Array.isArray(kept)) {
|
|
243
|
+
for (const [name, field] of partitionEntries)
|
|
244
|
+
if (kept.includes(name) && !STORAGE_FIELDS.has(name))
|
|
245
|
+
put(value, name, field);
|
|
246
|
+
}
|
|
247
|
+
return value;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* This function reads a raw document of a `MongoDocumentStorage` collection as the storage reads
|
|
251
|
+
* it (roadmap CLI-76), for an app that reads the collection directly (a query, a view, an
|
|
252
|
+
* aggregation). It gives `undefined` for a tombstone and for a document whose id neither `idOf`
|
|
253
|
+
* nor `_key` tells. Else it parses `_json`, leaves out the fields of the storage
|
|
254
|
+
* (`DOCUMENT_STORAGE_FIELDS`) and gives the item its id as `_id` (see `addId`).
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* ```typescript
|
|
258
|
+
* const items = docs.flatMap((doc) => decodeStoredDocument<Note>(doc, { form: notes.form })?.item ?? []);
|
|
259
|
+
* ```
|
|
260
|
+
*/
|
|
261
|
+
export function decodeStoredDocument(doc, options = {}) {
|
|
262
|
+
if (!isPlainObject(doc) || doc[TOMB] === true)
|
|
263
|
+
return undefined;
|
|
264
|
+
const form = options.form ?? DEFAULT_STORED_FORM;
|
|
265
|
+
const partition = options.partition ?? form.partitionOf(doc);
|
|
266
|
+
const fromId = form.idOf(doc["_id"], partition);
|
|
267
|
+
const id = typeof fromId === "string" ? fromId : typeof doc[KEY] === "string" ? doc[KEY] : undefined;
|
|
268
|
+
if (id === undefined)
|
|
269
|
+
return undefined;
|
|
270
|
+
const entries = Object.entries(form.partitionFields(partition));
|
|
271
|
+
return { id, partition, item: itemOf(doc, id, entries, options.addId ?? true) };
|
|
272
|
+
}
|
|
124
273
|
/** True for a plain object. */
|
|
125
274
|
function isPlainObject(value) {
|
|
126
275
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
@@ -137,17 +286,47 @@ function isScalar(value) {
|
|
|
137
286
|
return typeof value === "string" || typeof value === "boolean" || (typeof value === "number" && Number.isFinite(value));
|
|
138
287
|
}
|
|
139
288
|
/** The field of a stored document for a path of a selector, or undefined when the filter cannot test it. */
|
|
140
|
-
function storedPath(path) {
|
|
289
|
+
function storedPath(path, reserved) {
|
|
141
290
|
if (path === "_id")
|
|
142
291
|
return KEY;
|
|
143
|
-
const
|
|
144
|
-
|
|
292
|
+
const parts = path.split(".");
|
|
293
|
+
const first = parts[0];
|
|
294
|
+
if (first === "_id" || STORAGE_FIELDS.has(first) || reserved.has(first))
|
|
295
|
+
return undefined;
|
|
296
|
+
// A part with `$` is a field name of an item, but an operator for MongoDB
|
|
297
|
+
if (parts.some((part) => part.startsWith("$")))
|
|
145
298
|
return undefined;
|
|
146
299
|
return path;
|
|
147
300
|
}
|
|
301
|
+
/**
|
|
302
|
+
* The filter of a condition on `_id` against the stored `_id`, or undefined. A flat item with an
|
|
303
|
+
* `_id` has its id as `_id`, and an item stored as JSON passes the filter of `query()` anyway.
|
|
304
|
+
*/
|
|
305
|
+
function idConditionFilter(condition, storedId) {
|
|
306
|
+
try {
|
|
307
|
+
if (typeof condition === "string")
|
|
308
|
+
return { _id: storedId(condition) };
|
|
309
|
+
if (!isPlainObject(condition))
|
|
310
|
+
return undefined;
|
|
311
|
+
const operators = {};
|
|
312
|
+
const eq = condition["$eq"];
|
|
313
|
+
if (typeof eq === "string")
|
|
314
|
+
operators["$eq"] = storedId(eq);
|
|
315
|
+
const among = condition["$in"];
|
|
316
|
+
if (Array.isArray(among) && among.length > 0 && among.every((value) => typeof value === "string"))
|
|
317
|
+
operators["$in"] = among.map((value) => storedId(value));
|
|
318
|
+
return Object.keys(operators).length > 0 ? { _id: operators } : undefined;
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
// An id that the storage refuses: no condition is a correct filter (it can pass more)
|
|
322
|
+
return undefined;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
148
325
|
/** The filter of one condition, or undefined when the filter cannot test it in the same way. */
|
|
149
|
-
function conditionFilter(path, condition) {
|
|
150
|
-
|
|
326
|
+
function conditionFilter(path, condition, options, reserved) {
|
|
327
|
+
if (path === "_id" && options.storedId)
|
|
328
|
+
return idConditionFilter(condition, options.storedId);
|
|
329
|
+
const field = storedPath(path, reserved);
|
|
151
330
|
if (field === undefined)
|
|
152
331
|
return undefined;
|
|
153
332
|
if (isScalar(condition))
|
|
@@ -167,26 +346,29 @@ function conditionFilter(path, condition) {
|
|
|
167
346
|
* This function gives a filter that each item of the selector passes (a filter can pass more),
|
|
168
347
|
* or undefined for no condition. Only conditions that MongoDB tests in the same way go in it.
|
|
169
348
|
*/
|
|
170
|
-
export function selectorPrefilter(selector) {
|
|
349
|
+
export function selectorPrefilter(selector, options = {}) {
|
|
350
|
+
return prefilter(selector, options, new Set(options.storageFields ?? []));
|
|
351
|
+
}
|
|
352
|
+
function prefilter(selector, options, reserved) {
|
|
171
353
|
const parts = [];
|
|
172
354
|
for (const [key, condition] of Object.entries(selector)) {
|
|
173
355
|
if (condition === undefined)
|
|
174
356
|
continue;
|
|
175
357
|
if (key === "$and" && Array.isArray(condition)) {
|
|
176
358
|
for (const branch of condition) {
|
|
177
|
-
const filter = isPlainObject(branch) ?
|
|
359
|
+
const filter = isPlainObject(branch) ? prefilter(branch, options, reserved) : undefined;
|
|
178
360
|
if (filter)
|
|
179
361
|
parts.push(filter);
|
|
180
362
|
}
|
|
181
363
|
}
|
|
182
364
|
else if (key === "$or" && Array.isArray(condition) && condition.length > 0) {
|
|
183
|
-
const branches = condition.map((branch) => (isPlainObject(branch) ?
|
|
365
|
+
const branches = condition.map((branch) => (isPlainObject(branch) ? prefilter(branch, options, reserved) : undefined));
|
|
184
366
|
// A branch without a condition passes each item: then the $or tests nothing
|
|
185
367
|
if (branches.every((branch) => branch !== undefined))
|
|
186
368
|
parts.push({ $or: branches });
|
|
187
369
|
}
|
|
188
370
|
else if (!key.startsWith("$")) {
|
|
189
|
-
const filter = conditionFilter(key, condition);
|
|
371
|
+
const filter = conditionFilter(key, condition, options, reserved);
|
|
190
372
|
if (filter)
|
|
191
373
|
parts.push(filter);
|
|
192
374
|
}
|
|
@@ -214,12 +396,37 @@ export class MongoDocumentStorage {
|
|
|
214
396
|
writeLeaseMs;
|
|
215
397
|
/** The storages of the partitions, for `forPartition`: the same storage for the same partition. */
|
|
216
398
|
partitions;
|
|
399
|
+
/** The stored form of the documents (roadmap CLI-76). A `ViewMaterializer` of the collection takes the same form. */
|
|
400
|
+
form;
|
|
401
|
+
/** The fields that name the partition, in their order. An `undefined` value is an absent field. */
|
|
402
|
+
partitionEntries;
|
|
403
|
+
/** The values of the partition fields that are not fields of the storage, by name. */
|
|
404
|
+
partitionValues;
|
|
405
|
+
/** The fields that only the storage writes: its fields and the partition fields. */
|
|
406
|
+
reserved;
|
|
217
407
|
indexes;
|
|
218
408
|
closed = false;
|
|
219
409
|
constructor(options, partitions) {
|
|
220
410
|
if (options.partition !== undefined && (options.partition === "" || options.partition.includes(SEPARATOR))) {
|
|
221
411
|
throw new Error("A partition must be a text without the character U+001F");
|
|
222
412
|
}
|
|
413
|
+
const form = options.form ?? DEFAULT_STORED_FORM;
|
|
414
|
+
for (const name of ["storedId", "idOf", "partitionFields", "partitionOf"]) {
|
|
415
|
+
if (typeof form[name] !== "function")
|
|
416
|
+
throw new TypeError(`The stored form must have the function ${name}`);
|
|
417
|
+
}
|
|
418
|
+
this.form = form;
|
|
419
|
+
const fields = form.partitionFields(options.partition);
|
|
420
|
+
if (!isPlainObject(fields) || Object.keys(fields).length === 0)
|
|
421
|
+
throw new TypeError("partitionFields must give an object with at least one field");
|
|
422
|
+
for (const name of Object.keys(fields)) {
|
|
423
|
+
if (name === "_id" || name.startsWith("$") || name.includes(".") || name === "__proto__" || (STORAGE_FIELDS.has(name) && name !== PART)) {
|
|
424
|
+
throw new TypeError(`partitionFields cannot have the field ${JSON.stringify(name)}`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
this.partitionEntries = Object.entries(fields);
|
|
428
|
+
this.partitionValues = new Map(this.partitionEntries.filter(([name]) => !STORAGE_FIELDS.has(name)));
|
|
429
|
+
this.reserved = new Set([...STORAGE_FIELDS, ...Object.keys(fields)]);
|
|
223
430
|
this.options = options;
|
|
224
431
|
this.collectionName = options.collection;
|
|
225
432
|
this.partition = options.partition;
|
|
@@ -251,8 +458,9 @@ export class MongoDocumentStorage {
|
|
|
251
458
|
async collection() {
|
|
252
459
|
const db = await this.database();
|
|
253
460
|
const items = db.collection(this.collectionName);
|
|
461
|
+
const prefix = Object.fromEntries(this.partitionEntries.map(([name]) => [name, 1]));
|
|
254
462
|
this.indexes ??= items
|
|
255
|
-
.createIndexes([{ key: {
|
|
463
|
+
.createIndexes([{ key: { ...prefix, [SEQ]: 1 } }, { key: { ...prefix, [ORD]: 1 } }, { key: { ...prefix, [TOMB]: 1 } }])
|
|
256
464
|
.then(() => undefined)
|
|
257
465
|
.catch((error) => {
|
|
258
466
|
this.indexes = undefined;
|
|
@@ -263,7 +471,15 @@ export class MongoDocumentStorage {
|
|
|
263
471
|
}
|
|
264
472
|
/** The filter of the documents of the partition. */
|
|
265
473
|
partitionFilter() {
|
|
266
|
-
|
|
474
|
+
// An object value is an equality too, never a set of operators
|
|
475
|
+
const condition = (value) => (value === undefined ? { $exists: false } : typeof value === "object" && value !== null ? { $eq: value } : value);
|
|
476
|
+
return Object.fromEntries(this.partitionEntries.map(([name, value]) => [name, condition(value)]));
|
|
477
|
+
}
|
|
478
|
+
/** This method puts the partition fields on a stored document, in their order. An absent field stays absent. */
|
|
479
|
+
putPartition(doc) {
|
|
480
|
+
for (const [name, value] of this.partitionEntries)
|
|
481
|
+
if (value !== undefined)
|
|
482
|
+
put(doc, name, value);
|
|
267
483
|
}
|
|
268
484
|
/** The filter of the items (not the tombstones) of the partition. */
|
|
269
485
|
liveFilter(extra) {
|
|
@@ -273,7 +489,23 @@ export class MongoDocumentStorage {
|
|
|
273
489
|
storedId(id) {
|
|
274
490
|
if (typeof id !== "string" || id.includes(SEPARATOR))
|
|
275
491
|
throw new Error("An id must be a text without the character U+001F");
|
|
276
|
-
|
|
492
|
+
const stored = this.form.storedId(this.partition, id);
|
|
493
|
+
if (stored === undefined || stored === null)
|
|
494
|
+
throw new TypeError(`storedId gave ${String(stored)} for the id ${JSON.stringify(id)}`);
|
|
495
|
+
return stored;
|
|
496
|
+
}
|
|
497
|
+
/** The id of a stored document: the inverse of its stored `_id`, else `_key`, or undefined when neither tells it. */
|
|
498
|
+
idOfDoc(doc) {
|
|
499
|
+
const id = this.form.idOf(doc["_id"], this.partition);
|
|
500
|
+
if (typeof id === "string")
|
|
501
|
+
return id;
|
|
502
|
+
const key = doc[KEY];
|
|
503
|
+
return typeof key === "string" ? key : undefined;
|
|
504
|
+
}
|
|
505
|
+
/** The place of a stored document in the order of the entries. A document without `_ord` comes first. */
|
|
506
|
+
placeOf(doc) {
|
|
507
|
+
const ord = doc[ORD];
|
|
508
|
+
return typeof ord === "number" ? ord : 0;
|
|
277
509
|
}
|
|
278
510
|
/** The filter of a write of one stored document: its `_id` and the partition. */
|
|
279
511
|
writeFilter(id) {
|
|
@@ -282,33 +514,72 @@ export class MongoDocumentStorage {
|
|
|
282
514
|
/** The stored document of an item, with the number of this write and the number of its place. */
|
|
283
515
|
encode(id, value, seq, ord) {
|
|
284
516
|
const doc = { _id: this.storedId(id), [KEY]: id, [SEQ]: seq, [ORD]: ord };
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (keepsId)
|
|
517
|
+
this.putPartition(doc);
|
|
518
|
+
const flat = this.flatOf(id, value);
|
|
519
|
+
if (flat) {
|
|
520
|
+
if (flat.keepsId)
|
|
290
521
|
doc[KEEP_ID] = true;
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
522
|
+
if (flat.kept.length > 0)
|
|
523
|
+
doc[KEEP_PART] = flat.kept;
|
|
524
|
+
for (const [key, field] of flat.fields)
|
|
525
|
+
put(doc, key, field);
|
|
294
526
|
}
|
|
295
527
|
else {
|
|
296
528
|
doc[JSON_FIELD] = JSON.stringify(value);
|
|
297
529
|
}
|
|
298
530
|
return doc;
|
|
299
531
|
}
|
|
300
|
-
/**
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
532
|
+
/**
|
|
533
|
+
* The fields of the flat document of an item, or undefined when only JSON text keeps it: an item
|
|
534
|
+
* that is not a plain object, an `_id` other than its id, a top-level key of the storage or with
|
|
535
|
+
* `$`, a key `__proto__`, a class instance other than `Date`. It throws a `TypeError` for a flat
|
|
536
|
+
* item that a document cannot keep: a partition field with another value than the partition, or
|
|
537
|
+
* an object that MongoDB gives back as a `DBRef`.
|
|
538
|
+
*/
|
|
539
|
+
flatOf(id, value) {
|
|
540
|
+
if (!isPlainObject(value))
|
|
541
|
+
return undefined;
|
|
542
|
+
if ("_id" in value && value["_id"] !== id)
|
|
543
|
+
return undefined;
|
|
544
|
+
const flat = { keepsId: "_id" in value, kept: [], fields: [] };
|
|
545
|
+
const problems = [];
|
|
546
|
+
for (const [key, field] of Object.entries(value)) {
|
|
547
|
+
if (key === "_id")
|
|
548
|
+
continue;
|
|
549
|
+
if (key.startsWith("$") || key === "__proto__" || STORAGE_FIELDS.has(key))
|
|
550
|
+
return undefined;
|
|
551
|
+
if (this.partitionValues.has(key)) {
|
|
552
|
+
// The partition field restores it on read; another value would put the document in another partition
|
|
553
|
+
if (isDeepStrictEqual(field, this.partitionValues.get(key)))
|
|
554
|
+
flat.kept.push(key);
|
|
555
|
+
else
|
|
556
|
+
problems.push(`the field ${key} names the partition and has another value than the partition`);
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (!isNativeValue(field, key, problems))
|
|
560
|
+
return undefined;
|
|
561
|
+
flat.fields.push([key, field]);
|
|
562
|
+
}
|
|
563
|
+
if (problems.length > 0)
|
|
564
|
+
throw new TypeError(`The storage cannot keep the item ${JSON.stringify(id)}: ${problems.join("; ")}`);
|
|
565
|
+
return flat;
|
|
566
|
+
}
|
|
567
|
+
/** The id and the item of a stored document, or undefined when the document does not tell its id. */
|
|
568
|
+
decode(doc, known) {
|
|
569
|
+
const id = this.idOfDoc(doc) ?? known;
|
|
570
|
+
if (id === undefined)
|
|
571
|
+
return undefined;
|
|
572
|
+
return [id, itemOf(doc, id, this.partitionEntries, false)];
|
|
573
|
+
}
|
|
574
|
+
/** The entries of stored documents, in their order. A document that does not tell its id is left out. */
|
|
575
|
+
decodeAll(docs) {
|
|
576
|
+
const entries = [];
|
|
577
|
+
for (const doc of docs) {
|
|
578
|
+
const entry = this.decode(doc);
|
|
579
|
+
if (entry)
|
|
580
|
+
entries.push(entry);
|
|
581
|
+
}
|
|
582
|
+
return entries;
|
|
312
583
|
}
|
|
313
584
|
/**
|
|
314
585
|
* The next `count` numbers of the sequence of the collection. They stay pending in the counter
|
|
@@ -468,17 +739,33 @@ export class MongoDocumentStorage {
|
|
|
468
739
|
const floor = Math.max(...oldest.map((doc) => Number(doc[SEQ])));
|
|
469
740
|
await db.collection(this.countersName).updateOne({ _id: this.floorId() }, { $max: { floor } }, { upsert: true });
|
|
470
741
|
}
|
|
742
|
+
/** The ids of stored documents, in their order. A document that does not tell its id is left out. */
|
|
743
|
+
idsOf(docs) {
|
|
744
|
+
const ids = [];
|
|
745
|
+
for (const doc of docs) {
|
|
746
|
+
const id = this.idOfDoc(doc);
|
|
747
|
+
if (id !== undefined)
|
|
748
|
+
ids.push(id);
|
|
749
|
+
}
|
|
750
|
+
return ids;
|
|
751
|
+
}
|
|
471
752
|
/** The items of the partition, in the order of the entries. */
|
|
472
753
|
async liveDocs(extra) {
|
|
473
754
|
const { items } = await this.collection();
|
|
474
|
-
return items.find(this.liveFilter(extra)).sort({ [ORD]: 1 }).toArray();
|
|
755
|
+
return items.find(this.liveFilter(extra)).sort({ [ORD]: 1, _id: 1 }).toArray();
|
|
475
756
|
}
|
|
476
757
|
/** The places (`_ord`) of the items of some ids that the partition has. */
|
|
477
758
|
async placesOf(collection, ids) {
|
|
478
759
|
const docs = await collection
|
|
479
760
|
.find(this.liveFilter({ _id: { $in: [...new Set(ids)].map((id) => this.storedId(id)) } }), { projection: { [KEY]: 1, [ORD]: 1 } })
|
|
480
761
|
.toArray();
|
|
481
|
-
|
|
762
|
+
const places = new Map();
|
|
763
|
+
for (const doc of docs) {
|
|
764
|
+
const id = this.idOfDoc(doc);
|
|
765
|
+
if (id !== undefined)
|
|
766
|
+
places.set(id, this.placeOf(doc));
|
|
767
|
+
}
|
|
768
|
+
return places;
|
|
482
769
|
}
|
|
483
770
|
// ===========================================================================
|
|
484
771
|
// Reads
|
|
@@ -486,18 +773,18 @@ export class MongoDocumentStorage {
|
|
|
486
773
|
async get(id) {
|
|
487
774
|
const { items } = await this.collection();
|
|
488
775
|
const doc = await items.findOne(this.liveFilter({ _id: this.storedId(id) }));
|
|
489
|
-
return doc ? this.decode(doc)[1] : undefined;
|
|
776
|
+
return doc ? this.decode(doc, id)?.[1] : undefined;
|
|
490
777
|
}
|
|
491
778
|
async getAll() {
|
|
492
|
-
return (await this.liveDocs()).map((
|
|
779
|
+
return this.decodeAll(await this.liveDocs()).map(([, value]) => value);
|
|
493
780
|
}
|
|
494
781
|
async getEntries() {
|
|
495
|
-
return (await this.liveDocs())
|
|
782
|
+
return this.decodeAll(await this.liveDocs());
|
|
496
783
|
}
|
|
497
784
|
async getIds() {
|
|
498
785
|
const { items } = await this.collection();
|
|
499
|
-
const docs = await items.find(this.liveFilter(), { projection: { [KEY]: 1 } }).sort({ [ORD]: 1 }).toArray();
|
|
500
|
-
return
|
|
786
|
+
const docs = await items.find(this.liveFilter(), { projection: { _id: 1, [KEY]: 1 } }).sort({ [ORD]: 1, _id: 1 }).toArray();
|
|
787
|
+
return this.idsOf(docs);
|
|
501
788
|
}
|
|
502
789
|
async find(predicate) {
|
|
503
790
|
return (await this.getAll()).filter(predicate);
|
|
@@ -506,9 +793,9 @@ export class MongoDocumentStorage {
|
|
|
506
793
|
const rule = policy ?? this.options.queryPolicy;
|
|
507
794
|
// The reference checks the selector and the options first, without a read of the database
|
|
508
795
|
queryEntries([], selector, options, rule);
|
|
509
|
-
const filter = selectorPrefilter(selector);
|
|
796
|
+
const filter = selectorPrefilter(selector, { storedId: (id) => this.storedId(id), storageFields: this.reserved });
|
|
510
797
|
const docs = await this.liveDocs(filter ? { $or: [{ [JSON_FIELD]: { $exists: true } }, filter] } : undefined);
|
|
511
|
-
return queryEntries(
|
|
798
|
+
return queryEntries(this.decodeAll(docs), selector, options, rule);
|
|
512
799
|
}
|
|
513
800
|
async has(id) {
|
|
514
801
|
const { items } = await this.collection();
|
|
@@ -516,7 +803,12 @@ export class MongoDocumentStorage {
|
|
|
516
803
|
}
|
|
517
804
|
async size() {
|
|
518
805
|
const { items } = await this.collection();
|
|
519
|
-
|
|
806
|
+
// A document with `_key` tells its id. A document from outside the storage counts only when `idOf` names it, as in getAll
|
|
807
|
+
const [all, outside] = await Promise.all([
|
|
808
|
+
items.countDocuments(this.liveFilter()),
|
|
809
|
+
items.find(this.liveFilter({ [KEY]: { $exists: false } }), { projection: { _id: 1 } }).toArray(),
|
|
810
|
+
]);
|
|
811
|
+
return all - (outside.length - this.idsOf(outside).length);
|
|
520
812
|
}
|
|
521
813
|
async getBatch(ids) {
|
|
522
814
|
const result = new Map();
|
|
@@ -526,7 +818,7 @@ export class MongoDocumentStorage {
|
|
|
526
818
|
}
|
|
527
819
|
const { items } = await this.collection();
|
|
528
820
|
const docs = await items.find(this.liveFilter({ _id: { $in: ids.map((id) => this.storedId(id)) } })).toArray();
|
|
529
|
-
const byId = new Map(
|
|
821
|
+
const byId = new Map(this.decodeAll(docs));
|
|
530
822
|
// The order of the ids of the call
|
|
531
823
|
for (const id of ids)
|
|
532
824
|
if (byId.has(id))
|
|
@@ -554,11 +846,16 @@ export class MongoDocumentStorage {
|
|
|
554
846
|
const results = await items.aggregate(bounded.pipeline, { maxTimeMS }).toArray();
|
|
555
847
|
return cutResults(results, maxResults, bounded.limited);
|
|
556
848
|
}
|
|
557
|
-
/**
|
|
849
|
+
/**
|
|
850
|
+
* The version of an item: the sequence number of its last write, or undefined for a missing id.
|
|
851
|
+
* An item without a write through the storage (no `_seq`) has the version `0`.
|
|
852
|
+
*/
|
|
558
853
|
async versionOf(id) {
|
|
559
854
|
const { items } = await this.collection();
|
|
560
855
|
const doc = await items.findOne(this.liveFilter({ _id: this.storedId(id) }), { projection: { [SEQ]: 1 } });
|
|
561
|
-
|
|
856
|
+
if (!doc)
|
|
857
|
+
return undefined;
|
|
858
|
+
return typeof doc[SEQ] === "number" ? String(doc[SEQ]) : "0";
|
|
562
859
|
}
|
|
563
860
|
async getChangesSince(since) {
|
|
564
861
|
const { db, items } = await this.collection();
|
|
@@ -568,7 +865,7 @@ export class MongoDocumentStorage {
|
|
|
568
865
|
const full = async () => ({
|
|
569
866
|
version,
|
|
570
867
|
full: true,
|
|
571
|
-
changed: (await items.find(this.liveFilter()).sort({ [ORD]: 1 }).toArray())
|
|
868
|
+
changed: this.decodeAll(await items.find(this.liveFilter()).sort({ [ORD]: 1, _id: 1 }).toArray()),
|
|
572
869
|
deleted: [],
|
|
573
870
|
});
|
|
574
871
|
const match = since === undefined ? null : /^([^.]+)\.(\d+)$/.exec(since);
|
|
@@ -587,10 +884,13 @@ export class MongoDocumentStorage {
|
|
|
587
884
|
const changed = [];
|
|
588
885
|
const deleted = [];
|
|
589
886
|
for (const doc of docs) {
|
|
887
|
+
const id = this.idOfDoc(doc);
|
|
888
|
+
if (id === undefined)
|
|
889
|
+
continue;
|
|
590
890
|
if (doc[TOMB] === true)
|
|
591
|
-
deleted.push(
|
|
891
|
+
deleted.push(id);
|
|
592
892
|
else
|
|
593
|
-
changed.push(this.decode(doc));
|
|
893
|
+
changed.push(this.decode(doc, id));
|
|
594
894
|
}
|
|
595
895
|
return { version, full: false, changed, deleted };
|
|
596
896
|
});
|
|
@@ -618,6 +918,7 @@ export class MongoDocumentStorage {
|
|
|
618
918
|
// The ids and the encoding are checked before a number of the sequence goes
|
|
619
919
|
for (const [id, value] of items) {
|
|
620
920
|
this.storedId(id);
|
|
921
|
+
this.flatOf(id, value);
|
|
621
922
|
JSON.stringify(value);
|
|
622
923
|
}
|
|
623
924
|
await this.write(async (db, collection) => {
|
|
@@ -651,8 +952,10 @@ export class MongoDocumentStorage {
|
|
|
651
952
|
const doc = await collection.findOne(this.liveFilter({ _id: this.storedId(id) }));
|
|
652
953
|
if (!doc)
|
|
653
954
|
return undefined;
|
|
654
|
-
const next = applyPatch(this.decode(doc)[1], fields);
|
|
655
|
-
|
|
955
|
+
const next = applyPatch(this.decode(doc, id)[1], fields);
|
|
956
|
+
// The encoding is checked before a number of the sequence goes
|
|
957
|
+
this.flatOf(id, next);
|
|
958
|
+
await this.withNumbers(db, collection, 1, (first, session) => collection.replaceOne(this.writeFilter(id), this.encode(id, next, first, this.placeOf(doc)), { upsert: true, ...(session ? { session } : {}) }));
|
|
656
959
|
return next;
|
|
657
960
|
});
|
|
658
961
|
}
|
|
@@ -687,7 +990,7 @@ export class MongoDocumentStorage {
|
|
|
687
990
|
.toArray();
|
|
688
991
|
if (live.length === 0)
|
|
689
992
|
return 0;
|
|
690
|
-
const liveIds = new Set(
|
|
993
|
+
const liveIds = new Set(this.idsOf(live));
|
|
691
994
|
const deleted = unique.filter((id) => liveIds.has(id));
|
|
692
995
|
await this.tombstone(db, collection, deleted);
|
|
693
996
|
return deleted.length;
|
|
@@ -695,10 +998,11 @@ export class MongoDocumentStorage {
|
|
|
695
998
|
}
|
|
696
999
|
async clear(_options) {
|
|
697
1000
|
await this.write(async (db, collection) => {
|
|
698
|
-
const live = await collection.find(this.liveFilter(), { projection: { [KEY]: 1 } }).sort({ [ORD]: 1 }).toArray();
|
|
699
|
-
|
|
1001
|
+
const live = await collection.find(this.liveFilter(), { projection: { _id: 1, [KEY]: 1 } }).sort({ [ORD]: 1, _id: 1 }).toArray();
|
|
1002
|
+
const ids = this.idsOf(live);
|
|
1003
|
+
if (ids.length === 0)
|
|
700
1004
|
return;
|
|
701
|
-
await this.tombstone(db, collection,
|
|
1005
|
+
await this.tombstone(db, collection, ids);
|
|
702
1006
|
});
|
|
703
1007
|
}
|
|
704
1008
|
/**
|
|
@@ -710,8 +1014,7 @@ export class MongoDocumentStorage {
|
|
|
710
1014
|
const guards = await this.checkVersions(collection, expected, session);
|
|
711
1015
|
const operations = ids.map((id, index) => {
|
|
712
1016
|
const tomb = { _id: this.storedId(id), [KEY]: id, [SEQ]: first + index, [TOMB]: true };
|
|
713
|
-
|
|
714
|
-
tomb[PART] = this.partition;
|
|
1017
|
+
this.putPartition(tomb);
|
|
715
1018
|
return { replaceOne: { filter: { ...this.writeFilter(id), ...guards.get(id) }, replacement: tomb } };
|
|
716
1019
|
});
|
|
717
1020
|
const result = await collection.bulkWrite(operations, { ordered: true, ...(session ? { session } : {}) });
|
|
@@ -739,11 +1042,18 @@ export class MongoDocumentStorage {
|
|
|
739
1042
|
const docs = await collection
|
|
740
1043
|
.find({ ...this.partitionFilter(), _id: { $in: [...new Set(ids)].map((id) => this.storedId(id)) } }, session ? { session } : {})
|
|
741
1044
|
.toArray();
|
|
742
|
-
|
|
1045
|
+
const byId = new Map();
|
|
1046
|
+
for (const doc of docs) {
|
|
1047
|
+
const id = this.idOfDoc(doc);
|
|
1048
|
+
if (id !== undefined)
|
|
1049
|
+
byId.set(id, doc);
|
|
1050
|
+
}
|
|
1051
|
+
return byId;
|
|
743
1052
|
}
|
|
744
1053
|
/** The current item of a stored document: no item for a tombstone or a missing document. */
|
|
745
1054
|
currentOf(doc) {
|
|
746
|
-
|
|
1055
|
+
const entry = doc && doc[TOMB] !== true ? this.decode(doc) : undefined;
|
|
1056
|
+
return entry ? { found: true, value: entry[1] } : { found: false };
|
|
747
1057
|
}
|
|
748
1058
|
/**
|
|
749
1059
|
* This method tests the expected versions (roadmap CLI-16). A conflict throws
|
|
@@ -768,7 +1078,8 @@ export class MongoDocumentStorage {
|
|
|
768
1078
|
return guards;
|
|
769
1079
|
for (const [id] of expected) {
|
|
770
1080
|
const doc = docs.get(id);
|
|
771
|
-
|
|
1081
|
+
// A document without `_seq` (not written through the storage yet) keeps that state until the write
|
|
1082
|
+
guards.set(id, doc && typeof doc[SEQ] === "number" ? { [SEQ]: doc[SEQ] } : { [SEQ]: { $exists: false } });
|
|
772
1083
|
}
|
|
773
1084
|
return guards;
|
|
774
1085
|
}
|