liwi-mongo 13.4.0 → 14.1.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.
Files changed (106) hide show
  1. package/README.md +143 -2
  2. package/dist/definitions/MongoConnection.d.ts +6 -1
  3. package/dist/definitions/MongoConnection.d.ts.map +1 -1
  4. package/dist/definitions/MongoCursor.d.ts.map +1 -1
  5. package/dist/definitions/MongoQuerySingleItem.d.ts +1 -0
  6. package/dist/definitions/MongoQuerySingleItem.d.ts.map +1 -1
  7. package/dist/definitions/MongoQuerySingleItem.test.d.ts +2 -0
  8. package/dist/definitions/MongoQuerySingleItem.test.d.ts.map +1 -0
  9. package/dist/definitions/MongoStore.d.ts +18 -4
  10. package/dist/definitions/MongoStore.d.ts.map +1 -1
  11. package/dist/definitions/SubscribeStore.integration.test.d.ts +2 -0
  12. package/dist/definitions/SubscribeStore.integration.test.d.ts.map +1 -0
  13. package/dist/definitions/index.d.ts +1 -1
  14. package/dist/definitions/index.d.ts.map +1 -1
  15. package/dist/definitions/memory/applyUpdate.d.ts +23 -0
  16. package/dist/definitions/memory/applyUpdate.d.ts.map +1 -0
  17. package/dist/definitions/memory/cloneAsStored.d.ts +10 -0
  18. package/dist/definitions/memory/cloneAsStored.d.ts.map +1 -0
  19. package/dist/definitions/memory/cloneAsStored.test.d.ts +2 -0
  20. package/dist/definitions/memory/cloneAsStored.test.d.ts.map +1 -0
  21. package/dist/definitions/memory/contract.test.d.ts +2 -0
  22. package/dist/definitions/memory/contract.test.d.ts.map +1 -0
  23. package/dist/definitions/memory/createMemoryCollection.d.ts +19 -0
  24. package/dist/definitions/memory/createMemoryCollection.d.ts.map +1 -0
  25. package/dist/definitions/memory/createMemoryCursor.d.ts +29 -0
  26. package/dist/definitions/memory/createMemoryCursor.d.ts.map +1 -0
  27. package/dist/definitions/memory/createMemoryCursor.test.d.ts +2 -0
  28. package/dist/definitions/memory/createMemoryCursor.test.d.ts.map +1 -0
  29. package/dist/definitions/memory/createMemoryDb.d.ts +7 -0
  30. package/dist/definitions/memory/createMemoryDb.d.ts.map +1 -0
  31. package/dist/definitions/memory/createMemoryMongoClient.d.ts +13 -0
  32. package/dist/definitions/memory/createMemoryMongoClient.d.ts.map +1 -0
  33. package/dist/definitions/memory/createMemoryMongoClient.test.d.ts +2 -0
  34. package/dist/definitions/memory/createMemoryMongoClient.test.d.ts.map +1 -0
  35. package/dist/definitions/memory/errors.d.ts +20 -0
  36. package/dist/definitions/memory/errors.d.ts.map +1 -0
  37. package/dist/definitions/memory/guardUnsupported.d.ts +3 -0
  38. package/dist/definitions/memory/guardUnsupported.d.ts.map +1 -0
  39. package/dist/definitions/memory/indexes/createIndexInfo.d.ts +5 -0
  40. package/dist/definitions/memory/indexes/createIndexInfo.d.ts.map +1 -0
  41. package/dist/definitions/memory/indexes/createIndexInfo.test.d.ts +2 -0
  42. package/dist/definitions/memory/indexes/createIndexInfo.test.d.ts.map +1 -0
  43. package/dist/definitions/memory/indexes/createMemoryIndexes.d.ts +19 -0
  44. package/dist/definitions/memory/indexes/createMemoryIndexes.d.ts.map +1 -0
  45. package/dist/definitions/memory/indexes/createUniqueConstraint.d.ts +16 -0
  46. package/dist/definitions/memory/indexes/createUniqueConstraint.d.ts.map +1 -0
  47. package/dist/definitions/memory/normalizeSort.d.ts +4 -0
  48. package/dist/definitions/memory/normalizeSort.d.ts.map +1 -0
  49. package/dist/definitions/memory/normalizeSort.test.d.ts +2 -0
  50. package/dist/definitions/memory/normalizeSort.test.d.ts.map +1 -0
  51. package/dist/definitions/memory/queryOptions.d.ts +3 -0
  52. package/dist/definitions/memory/queryOptions.d.ts.map +1 -0
  53. package/dist/definitions/memory/serializeKey.d.ts +6 -0
  54. package/dist/definitions/memory/serializeKey.d.ts.map +1 -0
  55. package/dist/definitions/memory/settle.d.ts +3 -0
  56. package/dist/definitions/memory/settle.d.ts.map +1 -0
  57. package/dist/definitions/memory.d.ts +2 -0
  58. package/dist/definitions/memory.d.ts.map +1 -0
  59. package/dist/definitions/writes/createReplacePipeline.d.ts +10 -0
  60. package/dist/definitions/writes/createReplacePipeline.d.ts.map +1 -0
  61. package/dist/definitions/writes/stampPartialUpdate.d.ts +9 -0
  62. package/dist/definitions/writes/stampPartialUpdate.d.ts.map +1 -0
  63. package/dist/definitions/writes/stampPartialUpdate.test.d.ts +2 -0
  64. package/dist/definitions/writes/stampPartialUpdate.test.d.ts.map +1 -0
  65. package/dist/index-node.mjs +235 -105
  66. package/dist/index-node.mjs.map +1 -1
  67. package/dist/memory-node.mjs +849 -0
  68. package/dist/memory-node.mjs.map +1 -0
  69. package/package.json +15 -8
  70. package/src/MongoConnection.test.ts +24 -2
  71. package/src/MongoConnection.ts +44 -28
  72. package/src/MongoCursor.ts +2 -4
  73. package/src/MongoQuerySingleItem.test.ts +162 -0
  74. package/src/MongoQuerySingleItem.ts +56 -50
  75. package/src/MongoStore.test.ts +250 -0
  76. package/src/MongoStore.ts +122 -26
  77. package/src/SubscribeStore.integration.test.ts +414 -0
  78. package/src/index.ts +1 -0
  79. package/src/indexes/diffIndexes.test.ts +2 -2
  80. package/src/indexes/normalizeIndex.test.ts +2 -2
  81. package/src/indexes/normalizeIndex.ts +2 -2
  82. package/src/memory/applyUpdate.ts +146 -0
  83. package/src/memory/cloneAsStored.test.ts +55 -0
  84. package/src/memory/cloneAsStored.ts +34 -0
  85. package/src/memory/contract.test.ts +967 -0
  86. package/src/memory/createMemoryCollection.ts +405 -0
  87. package/src/memory/createMemoryCursor.test.ts +78 -0
  88. package/src/memory/createMemoryCursor.ts +90 -0
  89. package/src/memory/createMemoryDb.ts +66 -0
  90. package/src/memory/createMemoryMongoClient.test.ts +85 -0
  91. package/src/memory/createMemoryMongoClient.ts +47 -0
  92. package/src/memory/errors.ts +85 -0
  93. package/src/memory/guardUnsupported.ts +25 -0
  94. package/src/memory/indexes/createIndexInfo.test.ts +36 -0
  95. package/src/memory/indexes/createIndexInfo.ts +82 -0
  96. package/src/memory/indexes/createMemoryIndexes.ts +178 -0
  97. package/src/memory/indexes/createUniqueConstraint.ts +151 -0
  98. package/src/memory/normalizeSort.test.ts +37 -0
  99. package/src/memory/normalizeSort.ts +56 -0
  100. package/src/memory/queryOptions.ts +3 -0
  101. package/src/memory/serializeKey.ts +10 -0
  102. package/src/memory/settle.ts +5 -0
  103. package/src/memory.ts +5 -0
  104. package/src/writes/createReplacePipeline.ts +23 -0
  105. package/src/writes/stampPartialUpdate.test.ts +93 -0
  106. package/src/writes/stampPartialUpdate.ts +84 -0
@@ -0,0 +1,849 @@
1
+ import mongodb from 'mongodb';
2
+ import { updateOne, Query, find, aggregate, ProcessingMode } from 'mingo';
3
+ import { MingoError } from 'mingo/util';
4
+
5
+ const isBSONValue = (value) => "_bsontype" in value;
6
+ const listEntries = (value) => value instanceof Map ? [...value.entries()] : Object.entries(value);
7
+ const cloneAsStored = (value) => {
8
+ if (value === void 0) return null;
9
+ if (typeof value !== "object" || value === null) return value;
10
+ if (value instanceof Date) return new Date(value);
11
+ if (value instanceof RegExp || isBSONValue(value)) return value;
12
+ if (value instanceof Uint8Array) {
13
+ return new mongodb.Binary(Uint8Array.from(value));
14
+ }
15
+ if (Array.isArray(value)) return value.map(cloneAsStored);
16
+ return Object.fromEntries(
17
+ listEntries(value).filter(([, fieldValue]) => typeof fieldValue !== "function").map(([field, fieldValue]) => [field, cloneAsStored(fieldValue)])
18
+ );
19
+ };
20
+ const cloneDocument = (document) => cloneAsStored(document);
21
+
22
+ const createServerError = (code, codeName, errmsg, details = {}) => new mongodb.MongoServerError({ ok: 0, errmsg, code, codeName, ...details });
23
+ const formatKeyValue = (keyValue) => `{ ${Object.entries(keyValue).map(([field, value]) => `${field}: ${JSON.stringify(value) ?? "null"}`).join(", ")} }`;
24
+ const createDuplicateKeyError = ({
25
+ namespace,
26
+ indexName,
27
+ keyPattern,
28
+ keyValue
29
+ }) => createServerError(
30
+ 11e3,
31
+ "DuplicateKey",
32
+ `E11000 duplicate key error collection: ${namespace} index: ${indexName} dup key: ${formatKeyValue(keyValue)}`,
33
+ { keyPattern, keyValue }
34
+ );
35
+ const createNamespaceNotFoundError = (namespace) => createServerError(26, "NamespaceNotFound", `ns does not exist: ${namespace}`);
36
+ const createIndexNotFoundError = (name) => createServerError(27, "IndexNotFound", `index not found with name [${name}]`);
37
+ const createInvalidOptionsError = (message) => createServerError(72, "InvalidOptions", message);
38
+ const createIndexOptionsConflictError = (message) => createServerError(85, "IndexOptionsConflict", message);
39
+ const createIndexKeySpecsConflictError = (message) => createServerError(86, "IndexKeySpecsConflict", message);
40
+ const createImmutableIdError = () => createServerError(
41
+ 66,
42
+ "ImmutableField",
43
+ "Performing an update on the path '_id' would modify the immutable field '_id'"
44
+ );
45
+ const mingoUpdateErrorCodes = [
46
+ [/immutable field/, 66, "ImmutableField"],
47
+ [/would create a conflict/, 40, "ConflictingUpdateOperators"],
48
+ [/Unknown update operator/, 9, "FailedToParse"]
49
+ ];
50
+ const toServerError = (error) => {
51
+ if (!(error instanceof MingoError)) return error;
52
+ const [, code, codeName] = mingoUpdateErrorCodes.find(
53
+ ([pattern]) => pattern.test(error.message)
54
+ ) ?? [void 0, 2, "BadValue"];
55
+ return createServerError(code, codeName, error.message);
56
+ };
57
+ const createUnsupportedError = (feature) => new TypeError(`liwi-mongo/memory: ${feature} is not supported`);
58
+
59
+ const queryOptions = { idKey: "_id" };
60
+
61
+ const serializeKey = (value) => typeof value === "string" ? `s:${value}` : mongodb.BSON.EJSON.stringify({ value }, { relaxed: false });
62
+
63
+ const isOperatorObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date) && !(value instanceof RegExp) && !("_bsontype" in value) && Object.keys(value).some((key) => key.startsWith("$"));
64
+ const assertUpdate = (update) => {
65
+ if (Array.isArray(update)) return;
66
+ const operators = Object.keys(update);
67
+ if (operators.length === 0 || !operators.every((operator) => operator.startsWith("$"))) {
68
+ throw new mongodb.MongoInvalidArgumentError(
69
+ "Update document requires atomic operators"
70
+ );
71
+ }
72
+ };
73
+ const omitField = (document, omitted) => Object.fromEntries(
74
+ Object.entries(document).filter(([field]) => field !== omitted)
75
+ );
76
+ const prepareModifier = (update, isInsert) => {
77
+ const $setOnInsert = update.$setOnInsert;
78
+ const modifier = omitField(update, "$setOnInsert");
79
+ if (isInsert && $setOnInsert) {
80
+ return {
81
+ ...modifier,
82
+ $set: { ...modifier.$set, ...$setOnInsert }
83
+ };
84
+ }
85
+ return Object.keys(modifier).length === 0 ? void 0 : modifier;
86
+ };
87
+ const omitIdWrite = (modifier, working) => {
88
+ const $set = modifier.$set;
89
+ if (!$set || !("_id" in $set)) return modifier;
90
+ const id = $set._id;
91
+ if (working._id === void 0) {
92
+ working._id = id;
93
+ } else if (serializeKey(id) !== serializeKey(working._id)) {
94
+ return modifier;
95
+ }
96
+ return { ...modifier, $set: omitField($set, "_id") };
97
+ };
98
+ const setPath = (document, path, value) => {
99
+ const fields = path.split(".");
100
+ const lastField = fields.pop();
101
+ let parent = document;
102
+ for (const field of fields) {
103
+ parent[field] ??= {};
104
+ parent = parent[field];
105
+ }
106
+ parent[lastField] = value;
107
+ };
108
+ const createUpsertBase = (filter) => {
109
+ const base = {};
110
+ const collect = (criteria) => {
111
+ Object.entries(criteria).forEach(([field, condition]) => {
112
+ if (field === "$and") {
113
+ condition.forEach(collect);
114
+ return;
115
+ }
116
+ if (field.startsWith("$") || condition instanceof RegExp) return;
117
+ if (!isOperatorObject(condition)) {
118
+ setPath(base, field, condition);
119
+ } else if ("$eq" in condition) {
120
+ setPath(base, field, condition.$eq);
121
+ }
122
+ });
123
+ };
124
+ collect(filter);
125
+ return cloneDocument(base);
126
+ };
127
+ const applyUpdate = (document, update, { filter, arrayFilters, isInsert }) => {
128
+ const storedUpdate = cloneAsStored(update);
129
+ const modifier = Array.isArray(storedUpdate) ? storedUpdate : prepareModifier(storedUpdate, isInsert);
130
+ if (!modifier) return { next: document, isModified: false };
131
+ const working = cloneDocument(document);
132
+ const documents = [working];
133
+ try {
134
+ const { modifiedCount } = updateOne(
135
+ documents,
136
+ isInsert ? {} : cloneDocument(filter),
137
+ Array.isArray(modifier) ? modifier : omitIdWrite(modifier, working),
138
+ { arrayFilters: cloneAsStored(arrayFilters) },
139
+ queryOptions
140
+ );
141
+ return {
142
+ next: cloneDocument(documents[0]),
143
+ isModified: modifiedCount > 0
144
+ };
145
+ } catch (error) {
146
+ throw toServerError(error);
147
+ }
148
+ };
149
+
150
+ const settle = (run) => new Promise((resolve) => {
151
+ resolve(run());
152
+ });
153
+
154
+ const createMemoryCursor = (run) => {
155
+ let pending;
156
+ const take = () => {
157
+ pending ??= run();
158
+ return pending;
159
+ };
160
+ const next = () => settle(() => take().shift() ?? null);
161
+ return {
162
+ isStarted: () => pending !== void 0,
163
+ next,
164
+ tryNext: next,
165
+ hasNext: () => settle(() => take().length > 0),
166
+ toArray: () => settle(() => take().splice(0)),
167
+ forEach: (iterator) => settle(() => {
168
+ for (const document of take().splice(0)) {
169
+ if (iterator(document) === false) return;
170
+ }
171
+ }),
172
+ close: () => settle(() => {
173
+ pending = [];
174
+ }),
175
+ // oxlint-disable-next-line typescript/require-await -- the protocol `for await` reads, nothing to wait for
176
+ async *[Symbol.asyncIterator]() {
177
+ const documents = take();
178
+ while (documents.length > 0) yield documents.shift();
179
+ }
180
+ };
181
+ };
182
+ const createMemoryFindCursor = (run, initialOptions) => {
183
+ const options = { ...initialOptions };
184
+ const { isStarted, ...cursor } = createMemoryCursor(() => run(options));
185
+ const configure = (option, value) => {
186
+ if (isStarted()) throw new mongodb.MongoCursorInUseError();
187
+ Object.assign(options, { [option]: value });
188
+ return findCursor;
189
+ };
190
+ const findCursor = {
191
+ ...cursor,
192
+ sort: (sort) => configure("sort", sort),
193
+ project: (projection) => configure("projection", projection),
194
+ skip: (skip) => configure("skip", skip),
195
+ limit: (limit) => configure("limit", limit)
196
+ };
197
+ return findCursor;
198
+ };
199
+
200
+ const probedProperties = /* @__PURE__ */ new Set([
201
+ "then",
202
+ "toJSON",
203
+ "asymmetricMatch",
204
+ "$$typeof",
205
+ "nodeType"
206
+ ]);
207
+ const guardUnsupported = (target, label) => new Proxy(target, {
208
+ get: (object, property, receiver) => {
209
+ if (typeof property === "symbol" || property in object) {
210
+ return Reflect.get(object, property, receiver);
211
+ }
212
+ if (probedProperties.has(property)) return void 0;
213
+ throw createUnsupportedError(`${label}.${property}`);
214
+ }
215
+ });
216
+
217
+ const buildIndexName = (key) => Object.entries(key).map(([field, direction]) => `${field}_${direction}`).join("_");
218
+
219
+ const textKeyEntries = [
220
+ ["_fts", "text"],
221
+ ["_ftsx", 1]
222
+ ];
223
+ const toKeyRecord = (key) => key instanceof Map ? Object.fromEntries(key) : key;
224
+ const toStoredKey = (key) => {
225
+ const entries = Object.entries(key);
226
+ const firstTextIndex = entries.findIndex(
227
+ ([, direction]) => direction === "text"
228
+ );
229
+ if (firstTextIndex === -1) return key;
230
+ const otherEntries = entries.filter(([, direction]) => direction !== "text");
231
+ const textPosition = entries.slice(0, firstTextIndex).filter(([, direction]) => direction !== "text").length;
232
+ return Object.fromEntries([
233
+ ...otherEntries.slice(0, textPosition),
234
+ ...textKeyEntries,
235
+ ...otherEntries.slice(textPosition)
236
+ ]);
237
+ };
238
+ const buildTextOptions = (key, description) => ({
239
+ weights: {
240
+ ...Object.fromEntries(
241
+ Object.entries(key).filter(([, direction]) => direction === "text").map(([field]) => [field, 1])
242
+ ),
243
+ ...description.weights
244
+ },
245
+ // oxlint-disable-next-line eslint-js/camelcase -- mongo option name
246
+ default_language: description.default_language ?? "english",
247
+ // oxlint-disable-next-line eslint-js/camelcase -- mongo option name
248
+ language_override: description.language_override ?? "language",
249
+ textIndexVersion: 3
250
+ });
251
+ const isText = (key) => Object.values(key).includes("text");
252
+ const createIndexInfo = (description) => {
253
+ const key = toKeyRecord(description.key);
254
+ const { key: _key, name, version: _version, ...options } = description;
255
+ const definedOptions = Object.fromEntries(
256
+ Object.entries(options).filter(([, value]) => value !== void 0)
257
+ );
258
+ return {
259
+ v: 2,
260
+ key: toStoredKey(key),
261
+ name: name ?? buildIndexName(key),
262
+ ...definedOptions,
263
+ ...isText(key) ? buildTextOptions(key, description) : {}
264
+ };
265
+ };
266
+ const idIndexInfo = {
267
+ v: 2,
268
+ key: { _id: 1 },
269
+ name: "_id_"
270
+ };
271
+
272
+ const emptyArray = /* @__PURE__ */ Symbol("empty array");
273
+ const isDocument = (value) => typeof value === "object" && value !== null && !(value instanceof Date) && !(value instanceof RegExp) && !("_bsontype" in value);
274
+ const valuesAtPath = (value, path) => {
275
+ if (path.length === 0) {
276
+ if (!Array.isArray(value)) return [value];
277
+ return value.length === 0 ? [emptyArray] : value;
278
+ }
279
+ if (Array.isArray(value)) {
280
+ return value.flatMap((item) => valuesAtPath(item, path));
281
+ }
282
+ if (!isDocument(value)) return [void 0];
283
+ const [field, ...rest] = path;
284
+ return valuesAtPath(value[field], rest);
285
+ };
286
+ const cartesianProduct = (lists) => {
287
+ let combinations = [[]];
288
+ for (const list of lists) {
289
+ combinations = combinations.flatMap(
290
+ (combination) => list.map((value) => [...combination, value])
291
+ );
292
+ }
293
+ return combinations;
294
+ };
295
+ const createStringFolder = (collation) => {
296
+ const strength = collation?.strength ?? 3;
297
+ if (!collation || strength >= 3) return (value) => value;
298
+ const locale = collation.locale === "simple" ? void 0 : collation.locale;
299
+ if (strength === 2) return (value) => value.toLocaleLowerCase(locale);
300
+ return (value) => value.normalize("NFD").replaceAll(new RegExp("\\p{Diacritic}", "gu"), "").toLocaleLowerCase(locale);
301
+ };
302
+ const createUniqueConstraint = ({
303
+ namespace,
304
+ indexName,
305
+ description
306
+ }) => {
307
+ const keyPattern = Object.fromEntries(
308
+ Object.entries(description.key).filter(
309
+ ([, direction]) => direction !== "text"
310
+ )
311
+ );
312
+ const paths = Object.keys(keyPattern).map((field) => field.split("."));
313
+ const partialFilter = description.partialFilterExpression ? new Query(description.partialFilterExpression, { idKey: "_id" }) : void 0;
314
+ const foldString = createStringFolder(description.collation);
315
+ const owners = /* @__PURE__ */ new Map();
316
+ const serializeValue = (value) => {
317
+ if (value === emptyArray) return "[]";
318
+ if (value === void 0) return serializeKey(null);
319
+ return serializeKey(typeof value === "string" ? foldString(value) : value);
320
+ };
321
+ const listEntries = (document) => {
322
+ if (partialFilter && !partialFilter.test(document)) return [];
323
+ const valuesByField = paths.map((path) => valuesAtPath(document, path));
324
+ const isMissingEveryField = valuesByField.every(
325
+ (values) => values.every((value) => value === void 0)
326
+ );
327
+ if (description.sparse && isMissingEveryField) return [];
328
+ const entries = /* @__PURE__ */ new Map();
329
+ cartesianProduct(valuesByField).forEach((values) => {
330
+ const key = JSON.stringify(values.map(serializeValue));
331
+ entries.set(key, { key, values });
332
+ });
333
+ return [...entries.values()];
334
+ };
335
+ const toKeyValue = (values) => Object.fromEntries(
336
+ Object.keys(keyPattern).map((field, index) => {
337
+ const value = values[index];
338
+ return [field, value === emptyArray ? [] : value ?? null];
339
+ })
340
+ );
341
+ return {
342
+ assertCanWrite: (documentKey, document) => {
343
+ listEntries(document).forEach(({ key, values }) => {
344
+ const owner = owners.get(key);
345
+ if (owner === void 0 || owner === documentKey) return;
346
+ throw createDuplicateKeyError({
347
+ namespace,
348
+ indexName,
349
+ keyPattern,
350
+ keyValue: toKeyValue(values)
351
+ });
352
+ });
353
+ },
354
+ write: (documentKey, previous, next) => {
355
+ if (previous) {
356
+ listEntries(previous).forEach(({ key }) => {
357
+ if (owners.get(key) === documentKey) owners.delete(key);
358
+ });
359
+ }
360
+ if (next) {
361
+ listEntries(next).forEach(({ key }) => owners.set(key, documentKey));
362
+ }
363
+ },
364
+ clear: () => {
365
+ owners.clear();
366
+ }
367
+ };
368
+ };
369
+
370
+ const serializeSpec = ({
371
+ v: _version,
372
+ ...info
373
+ }) => serializeKey(
374
+ Object.fromEntries(
375
+ Object.entries(info).toSorted(
376
+ ([fieldA], [fieldB]) => fieldA.localeCompare(fieldB)
377
+ )
378
+ )
379
+ );
380
+ const isSameKeySpace = (a, b) => serializeKey(a.key) === serializeKey(b.key) && serializeKey(a.partialFilterExpression ?? null) === serializeKey(b.partialFilterExpression ?? null) && serializeKey(a.collation ?? null) === serializeKey(b.collation ?? null);
381
+ const isTextInfo = (info) => "_fts" in info.key;
382
+ const createMemoryIndexes = (namespace) => {
383
+ const indexes = /* @__PURE__ */ new Map();
384
+ const listInfos = () => [
385
+ idIndexInfo,
386
+ ...[...indexes.values()].map(({ info }) => info)
387
+ ];
388
+ const isAlreadyCreated = (info) => {
389
+ const sameName = listInfos().find(({ name }) => name === info.name);
390
+ if (sameName) {
391
+ if (serializeSpec(sameName) === serializeSpec(info)) return true;
392
+ throw createIndexKeySpecsConflictError(
393
+ `An existing index has the same name as the requested index. Requested index: ${JSON.stringify(info)}, existing index: ${JSON.stringify(sameName)}`
394
+ );
395
+ }
396
+ const sameKey = listInfos().find(
397
+ (existing) => isSameKeySpace(existing, info)
398
+ );
399
+ if (sameKey) {
400
+ throw createIndexOptionsConflictError(
401
+ `Index already exists with a different name: ${String(sameKey.name)}`
402
+ );
403
+ }
404
+ const otherText = isTextInfo(info) && listInfos().find(isTextInfo);
405
+ if (otherText) {
406
+ throw createIndexOptionsConflictError(
407
+ `An equivalent index already exists with a different name and options. Requested index: ${JSON.stringify(info)}, existing index: ${JSON.stringify(otherText)}`
408
+ );
409
+ }
410
+ return false;
411
+ };
412
+ const buildIndex = (description, info, documents) => {
413
+ if (!description.unique) return { info };
414
+ const unique = createUniqueConstraint({
415
+ namespace,
416
+ indexName: info.name,
417
+ description
418
+ });
419
+ documents.forEach((document, documentKey) => {
420
+ unique.assertCanWrite(documentKey, document);
421
+ unique.write(documentKey, void 0, document);
422
+ });
423
+ return { info, unique };
424
+ };
425
+ const getIndex = (name) => {
426
+ const index = indexes.get(name);
427
+ if (!index) throw createIndexNotFoundError(name);
428
+ return index;
429
+ };
430
+ const listUniqueConstraints = () => [...indexes.values()].flatMap(({ unique }) => unique ? [unique] : []);
431
+ return {
432
+ list: listInfos,
433
+ create: (descriptions, documents) => {
434
+ const created = descriptions.map((description) => ({
435
+ description,
436
+ info: createIndexInfo(description)
437
+ })).filter(({ info }) => !isAlreadyCreated(info)).map(
438
+ ({ description, info }) => buildIndex(description, info, documents)
439
+ );
440
+ created.forEach((index) => indexes.set(index.info.name, index));
441
+ return descriptions.map(
442
+ (description) => createIndexInfo(description).name
443
+ );
444
+ },
445
+ drop: (name) => {
446
+ if (name === idIndexInfo.name) {
447
+ throw createInvalidOptionsError("cannot drop _id index");
448
+ }
449
+ getIndex(name);
450
+ indexes.delete(name);
451
+ },
452
+ dropAll: () => {
453
+ indexes.clear();
454
+ },
455
+ modify: ({ name, hidden, expireAfterSeconds }) => {
456
+ const { info } = getIndex(name);
457
+ if (hidden === true) info.hidden = true;
458
+ if (hidden === false) delete info.hidden;
459
+ if (expireAfterSeconds !== void 0) {
460
+ info.expireAfterSeconds = expireAfterSeconds;
461
+ }
462
+ },
463
+ assertCanWrite: (documentKey, document) => {
464
+ listUniqueConstraints().forEach((unique) => {
465
+ unique.assertCanWrite(documentKey, document);
466
+ });
467
+ },
468
+ write: (documentKey, previous, next) => {
469
+ listUniqueConstraints().forEach((unique) => {
470
+ unique.write(documentKey, previous, next);
471
+ });
472
+ },
473
+ clearDocuments: () => {
474
+ listUniqueConstraints().forEach((unique) => {
475
+ unique.clear();
476
+ });
477
+ }
478
+ };
479
+ };
480
+
481
+ const toDirection = (direction) => {
482
+ switch (direction) {
483
+ case 1:
484
+ case "asc":
485
+ case "ascending":
486
+ return 1;
487
+ case -1:
488
+ case "desc":
489
+ case "descending":
490
+ return -1;
491
+ default:
492
+ throw createUnsupportedError(`sort ${JSON.stringify(direction)}`);
493
+ }
494
+ };
495
+ const sortDirections = /* @__PURE__ */ new Set([
496
+ 1,
497
+ -1,
498
+ "asc",
499
+ "ascending",
500
+ "desc",
501
+ "descending"
502
+ ]);
503
+ const isSortPair = (sort) => sort.length === 2 && typeof sort[0] === "string" && sortDirections.has(sort[1]);
504
+ const listSortEntries = (sort) => {
505
+ if (typeof sort === "string") return [[sort, 1]];
506
+ if (sort instanceof Map) {
507
+ return [...sort.entries()];
508
+ }
509
+ if (Array.isArray(sort)) {
510
+ if (isSortPair(sort)) return [sort];
511
+ return sort.map(
512
+ (entry) => typeof entry === "string" ? [entry, 1] : entry
513
+ );
514
+ }
515
+ return Object.entries(sort);
516
+ };
517
+ const normalizeSort = (sort) => Object.fromEntries(
518
+ listSortEntries(sort).map(([field, direction]) => [
519
+ field,
520
+ toDirection(direction)
521
+ ])
522
+ );
523
+
524
+ const toStoredDocument = (document) => {
525
+ const stored = cloneDocument(document);
526
+ return Object.fromEntries([
527
+ ["_id", stored._id],
528
+ ...Object.entries(stored).filter(([field]) => field !== "_id")
529
+ ]);
530
+ };
531
+ const createMemoryCollection = ({
532
+ databaseName,
533
+ collectionName,
534
+ resolveCollection
535
+ }) => {
536
+ const namespace = `${databaseName}.${collectionName}`;
537
+ const documents = /* @__PURE__ */ new Map();
538
+ const indexes = createMemoryIndexes(namespace);
539
+ let exists = false;
540
+ const listDocuments = () => [...documents.values()];
541
+ const findStored = (filter = {}, { sort, projection, skip, limit } = {}) => {
542
+ const cursor = find(
543
+ listDocuments(),
544
+ cloneDocument(filter),
545
+ projection,
546
+ queryOptions
547
+ );
548
+ if (sort) cursor.sort(normalizeSort(sort));
549
+ if (skip) cursor.skip(skip);
550
+ if (limit) cursor.limit(Math.abs(limit));
551
+ return cursor.all();
552
+ };
553
+ const findFirstStored = (filter, sort) => findStored(filter, { sort, limit: 1 })[0];
554
+ const project = (document, projection) => cloneDocument(
555
+ projection ? find([document], {}, projection, queryOptions).all()[0] : document
556
+ );
557
+ const insertStored = (document) => {
558
+ const documentKey = serializeKey(document._id);
559
+ if (documents.has(documentKey)) {
560
+ throw createDuplicateKeyError({
561
+ namespace,
562
+ indexName: "_id_",
563
+ keyPattern: { _id: 1 },
564
+ keyValue: { _id: document._id }
565
+ });
566
+ }
567
+ indexes.assertCanWrite(documentKey, document);
568
+ indexes.write(documentKey, void 0, document);
569
+ documents.set(documentKey, document);
570
+ exists = true;
571
+ };
572
+ const replaceStored = (previous, next) => {
573
+ const documentKey = serializeKey(previous._id);
574
+ if (serializeKey(next._id) !== documentKey) throw createImmutableIdError();
575
+ indexes.assertCanWrite(documentKey, next);
576
+ indexes.write(documentKey, previous, next);
577
+ documents.set(documentKey, next);
578
+ };
579
+ const removeStored = (document) => {
580
+ const documentKey = serializeKey(document._id);
581
+ indexes.write(documentKey, document, void 0);
582
+ documents.delete(documentKey);
583
+ };
584
+ const assignId = (document) => {
585
+ if (document._id === void 0) document._id = new mongodb.ObjectId();
586
+ return document;
587
+ };
588
+ const updateStored = (previous, { filter, update, arrayFilters }) => {
589
+ const { next, isModified } = applyUpdate(previous, update, {
590
+ filter,
591
+ arrayFilters,
592
+ isInsert: false
593
+ });
594
+ if (!isModified) return void 0;
595
+ const stored = toStoredDocument(next);
596
+ replaceStored(previous, stored);
597
+ return stored;
598
+ };
599
+ const upsertStored = ({
600
+ filter,
601
+ update,
602
+ arrayFilters
603
+ }) => {
604
+ const { next } = applyUpdate(createUpsertBase(filter), update, {
605
+ filter,
606
+ arrayFilters,
607
+ isInsert: true
608
+ });
609
+ const stored = toStoredDocument(assignId(next));
610
+ insertStored(stored);
611
+ return stored;
612
+ };
613
+ const updateMatching = (write, { upsert, sort }, limit) => {
614
+ assertUpdate(write.update);
615
+ const matched = findStored(write.filter, { sort, limit });
616
+ if (matched.length === 0 && upsert) {
617
+ const inserted = upsertStored(write);
618
+ return {
619
+ acknowledged: true,
620
+ matchedCount: 0,
621
+ modifiedCount: 0,
622
+ upsertedCount: 1,
623
+ upsertedId: inserted._id
624
+ };
625
+ }
626
+ const modifiedCount = matched.filter(
627
+ (previous) => updateStored(previous, write)
628
+ ).length;
629
+ return {
630
+ acknowledged: true,
631
+ matchedCount: matched.length,
632
+ modifiedCount,
633
+ upsertedCount: 0,
634
+ upsertedId: null
635
+ };
636
+ };
637
+ const assertExists = () => {
638
+ if (!exists) throw createNamespaceNotFoundError(namespace);
639
+ };
640
+ const createIndexes = (descriptions) => {
641
+ const names = indexes.create(descriptions, documents);
642
+ exists = true;
643
+ return names;
644
+ };
645
+ const listIndexes = () => {
646
+ assertExists();
647
+ return indexes.list().map(cloneDocument);
648
+ };
649
+ const drop = () => {
650
+ documents.clear();
651
+ indexes.dropAll();
652
+ exists = false;
653
+ };
654
+ const api = {
655
+ collectionName,
656
+ dbName: databaseName,
657
+ namespace,
658
+ insertOne: (document) => settle(() => {
659
+ const stored = toStoredDocument(assignId(document));
660
+ insertStored(stored);
661
+ return { acknowledged: true, insertedId: stored._id };
662
+ }),
663
+ insertMany: (newDocuments) => settle(() => {
664
+ const insertedIds = Object.fromEntries(
665
+ newDocuments.map((document, index) => {
666
+ const stored = toStoredDocument(assignId(document));
667
+ insertStored(stored);
668
+ return [index, stored._id];
669
+ })
670
+ );
671
+ return {
672
+ acknowledged: true,
673
+ insertedCount: newDocuments.length,
674
+ insertedIds
675
+ };
676
+ }),
677
+ findOne: (filter, options = {}) => settle(() => {
678
+ const [document] = findStored(filter, { ...options, limit: 1 });
679
+ return document ? cloneDocument(document) : null;
680
+ }),
681
+ find: (filter, options = {}) => createMemoryFindCursor(
682
+ (findOptions) => findStored(filter, findOptions).map(cloneDocument),
683
+ options
684
+ ),
685
+ countDocuments: (filter, { skip, limit } = {}) => settle(() => findStored(filter, { skip, limit }).length),
686
+ estimatedDocumentCount: () => settle(() => documents.size),
687
+ aggregate: (pipeline = []) => createMemoryCursor(
688
+ () => aggregate(listDocuments(), pipeline, {
689
+ ...queryOptions,
690
+ processingMode: ProcessingMode.CLONE_INPUT,
691
+ collectionResolver: resolveCollection
692
+ }).map(cloneDocument)
693
+ ),
694
+ updateOne: (filter, update, options = {}) => settle(
695
+ () => updateMatching(
696
+ { filter, update, arrayFilters: options.arrayFilters },
697
+ options,
698
+ 1
699
+ )
700
+ ),
701
+ updateMany: (filter, update, options = {}) => settle(
702
+ () => updateMatching(
703
+ { filter, update, arrayFilters: options.arrayFilters },
704
+ options,
705
+ void 0
706
+ )
707
+ ),
708
+ findOneAndUpdate: (filter, update, {
709
+ upsert,
710
+ arrayFilters,
711
+ sort,
712
+ returnDocument = "before",
713
+ projection
714
+ } = {}) => settle(() => {
715
+ assertUpdate(update);
716
+ const write = { filter, update, arrayFilters };
717
+ const previous = findFirstStored(filter, sort);
718
+ if (!previous) {
719
+ if (!upsert) return null;
720
+ const inserted = upsertStored(write);
721
+ return returnDocument === "after" ? project(inserted, projection) : null;
722
+ }
723
+ const next = updateStored(previous, write) ?? previous;
724
+ return project(
725
+ returnDocument === "after" ? next : previous,
726
+ projection
727
+ );
728
+ }),
729
+ deleteOne: (filter) => settle(() => {
730
+ const document = findFirstStored(filter, void 0);
731
+ if (document) removeStored(document);
732
+ return { acknowledged: true, deletedCount: document ? 1 : 0 };
733
+ }),
734
+ deleteMany: (filter) => settle(() => {
735
+ const matched = findStored(filter);
736
+ matched.forEach(removeStored);
737
+ return { acknowledged: true, deletedCount: matched.length };
738
+ }),
739
+ findOneAndDelete: (filter, { sort, projection } = {}) => settle(() => {
740
+ const document = findFirstStored(filter, sort);
741
+ if (!document) return null;
742
+ removeStored(document);
743
+ return project(document, projection);
744
+ }),
745
+ createIndexes: (descriptions) => settle(() => createIndexes(descriptions)),
746
+ createIndex: (key, options = {}) => settle(() => createIndexes([{ ...options, key }])[0]),
747
+ listIndexes: () => createMemoryCursor(listIndexes),
748
+ indexes: () => settle(listIndexes),
749
+ dropIndex: (name) => settle(() => {
750
+ assertExists();
751
+ indexes.drop(name);
752
+ return { ok: 1 };
753
+ }),
754
+ drop: () => settle(() => {
755
+ drop();
756
+ return true;
757
+ })
758
+ };
759
+ return {
760
+ api: guardUnsupported(api, `collection(${collectionName})`),
761
+ listDocuments,
762
+ modifyIndex: (changes) => {
763
+ assertExists();
764
+ indexes.modify(changes);
765
+ },
766
+ clear: () => {
767
+ documents.clear();
768
+ indexes.clearDocuments();
769
+ },
770
+ drop
771
+ };
772
+ };
773
+
774
+ const createMemoryDb = (databaseName) => {
775
+ const collections = /* @__PURE__ */ new Map();
776
+ const getCollection = (collectionName) => {
777
+ const existing = collections.get(collectionName);
778
+ if (existing) return existing;
779
+ const collection = createMemoryCollection({
780
+ databaseName,
781
+ collectionName,
782
+ resolveCollection: (otherName) => getCollection(otherName).listDocuments().map(cloneDocument)
783
+ });
784
+ collections.set(collectionName, collection);
785
+ return collection;
786
+ };
787
+ const runCommand = (command) => {
788
+ if ("ping" in command) return { ok: 1 };
789
+ if ("collMod" in command && "index" in command) {
790
+ getCollection(command.collMod).modifyIndex(
791
+ command.index
792
+ );
793
+ return { ok: 1 };
794
+ }
795
+ throw createUnsupportedError(
796
+ `db.command({ ${Object.keys(command)[0] ?? ""} })`
797
+ );
798
+ };
799
+ const api = {
800
+ databaseName,
801
+ collection: (collectionName) => getCollection(collectionName).api,
802
+ command: (command) => settle(() => runCommand(command)),
803
+ dropDatabase: () => settle(() => {
804
+ collections.forEach((collection) => {
805
+ collection.drop();
806
+ });
807
+ return true;
808
+ })
809
+ };
810
+ return {
811
+ api: guardUnsupported(api, `db(${databaseName})`),
812
+ clear: () => {
813
+ collections.forEach((collection) => {
814
+ collection.clear();
815
+ });
816
+ }
817
+ };
818
+ };
819
+
820
+ const createMemoryMongoClient = ({
821
+ database = "liwi-memory"
822
+ } = {}) => {
823
+ const databases = /* @__PURE__ */ new Map();
824
+ const getDb = (databaseName) => {
825
+ const existing = databases.get(databaseName);
826
+ if (existing) return existing;
827
+ const db = createMemoryDb(databaseName);
828
+ databases.set(databaseName, db);
829
+ return db;
830
+ };
831
+ const target = {
832
+ db: (databaseName = database) => getDb(databaseName).api,
833
+ close: () => Promise.resolve()
834
+ };
835
+ const client = guardUnsupported(target, "client");
836
+ target.connect = () => Promise.resolve(client);
837
+ target.on = () => client;
838
+ return {
839
+ client,
840
+ clear: () => {
841
+ databases.forEach((db) => {
842
+ db.clear();
843
+ });
844
+ }
845
+ };
846
+ };
847
+
848
+ export { createMemoryMongoClient };
849
+ //# sourceMappingURL=memory-node.mjs.map