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,4 @@
1
+ import type { Sort } from "mongodb";
2
+ /** The driver's sort shapes as one `{ field: 1 | -1 }`, in order. */
3
+ export declare const normalizeSort: (sort: Sort) => Record<string, -1 | 1>;
4
+ //# sourceMappingURL=normalizeSort.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalizeSort.d.ts","sourceRoot":"","sources":["../../../src/memory/normalizeSort.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAiB,MAAM,SAAS,CAAC;AAgDnD,qEAAqE;AACrE,eAAO,MAAM,aAAa,SAAU,IAAI,KAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAM7D,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=normalizeSort.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalizeSort.test.d.ts","sourceRoot":"","sources":["../../../src/memory/normalizeSort.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,3 @@
1
+ import type { Options } from "mingo/types";
2
+ export declare const queryOptions: Partial<Options>;
3
+ //# sourceMappingURL=queryOptions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queryOptions.d.ts","sourceRoot":"","sources":["../../../src/memory/queryOptions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C,eAAO,MAAM,YAAY,EAAE,OAAO,CAAC,OAAO,CAAoB,CAAC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Same string for values mongo considers the same key: canonical extended JSON
3
+ * keeps types apart (`"1"` and `1`, a `Date` and its timestamp).
4
+ */
5
+ export declare const serializeKey: (value: unknown) => string;
6
+ //# sourceMappingURL=serializeKey.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serializeKey.d.ts","sourceRoot":"","sources":["../../../src/memory/serializeKey.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,eAAO,MAAM,YAAY,UAAW,OAAO,KAAG,MAGmB,CAAC"}
@@ -0,0 +1,3 @@
1
+ /** Runs `run` now, its throw as a rejection: driver methods never throw synchronously. */
2
+ export declare const settle: <Result>(run: () => Result) => Promise<Result>;
3
+ //# sourceMappingURL=settle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settle.d.ts","sourceRoot":"","sources":["../../../src/memory/settle.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAC1F,eAAO,MAAM,MAAM,GAAI,MAAM,OAAO,MAAM,MAAM,KAAG,OAAO,CAAC,MAAM,CAG7D,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { createMemoryMongoClient, type MemoryMongoClient, type MemoryMongoClientOptions, } from "./memory/createMemoryMongoClient.ts";
2
+ //# sourceMappingURL=memory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/memory.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,GAC9B,MAAM,qCAAqC,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { Document } from "mongodb";
2
+ import type { MongoBaseModel } from "../MongoBaseModel.ts";
3
+ /**
4
+ * Replaces the stored document with `replacement` while keeping the stored `created`
5
+ * (falling back to the replacement's when the stored document has none).
6
+ * `$literal` keeps string values starting with `$` from being read as field paths.
7
+ * Update pipelines require MongoDB >= 4.2.
8
+ */
9
+ export declare const createReplacePipeline: (replacement: MongoBaseModel<any>) => Document[];
10
+ //# sourceMappingURL=createReplacePipeline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createReplacePipeline.d.ts","sourceRoot":"","sources":["../../../src/writes/createReplacePipeline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,gBACnB,cAAc,CAAC,GAAG,CAAC,KAC/B,QAAQ,EAWV,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { BaseModel, Update } from "liwi-store";
2
+ /**
3
+ * Throws `ProtectedFieldUpdateError` when the update touches `created`, or touches
4
+ * `updated` with an operator other than `$set`, `$currentDate` or `$max`.
5
+ * Otherwise returns the update with `updated` set to `now`, unless the caller sets
6
+ * `updated` itself or `now` is `undefined`. The caller's update is never mutated.
7
+ */
8
+ export declare const stampPartialUpdate: <Model extends BaseModel>(update: Update<Model>, now: Date | undefined) => Update<Model>;
9
+ //# sourceMappingURL=stampPartialUpdate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stampPartialUpdate.d.ts","sourceRoot":"","sources":["../../../src/writes/stampPartialUpdate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAoEpD;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,KAAK,SAAS,SAAS,UAChD,MAAM,CAAC,KAAK,CAAC,OAChB,IAAI,GAAG,SAAS,KACpB,MAAM,CAAC,KAAK,CAKd,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=stampPartialUpdate.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stampPartialUpdate.test.d.ts","sourceRoot":"","sources":["../../../src/writes/stampPartialUpdate.test.ts"],"names":[],"mappings":""}
@@ -1,5 +1,5 @@
1
+ import { AbstractStoreCursor, ProtectedFieldUpdateError, NotFoundError, AbstractConnection } from 'liwi-store';
1
2
  import mongodb from 'mongodb';
2
- import { AbstractStoreCursor, AbstractConnection } from 'liwi-store';
3
3
  import { AbstractSubscribableStoreQuery, SubscribeStore } from 'liwi-subscribe-store';
4
4
  import { Query } from 'mingo';
5
5
  import { Logger } from 'nightingale-logger';
@@ -35,11 +35,10 @@ class MongoCursor extends AbstractStoreCursor {
35
35
  if (!this._result) throw new Error("Cannot call result() before next()");
36
36
  return Promise.resolve(this._result);
37
37
  }
38
- close() {
38
+ async close() {
39
39
  if (this.cursor) {
40
- this.cursor.close();
40
+ await this.cursor.close();
41
41
  }
42
- return Promise.resolve();
43
42
  }
44
43
  toArray() {
45
44
  return this.cursor.toArray();
@@ -215,6 +214,49 @@ class MongoQuerySingleItem extends AbstractSubscribableStoreQuery {
215
214
  });
216
215
  });
217
216
  }
217
+ async computeChanges(action, testCriteria) {
218
+ switch (action.type) {
219
+ case "inserted": {
220
+ const filtered = action.next.filter(testCriteria);
221
+ if (filtered.length === 0) return [];
222
+ return [{ type: "updated", result: this.transformer(filtered[0]) }];
223
+ }
224
+ case "deleted": {
225
+ const filtered = action.prev.filter(testCriteria);
226
+ if (filtered.length === 0) return [];
227
+ return [
228
+ {
229
+ type: "deleted",
230
+ keys: filtered.map((object) => object[this.store.keyPath])
231
+ }
232
+ ];
233
+ }
234
+ case "updated": {
235
+ const filtered = action.changes.filter(
236
+ ([prev, next2]) => testCriteria(prev) || testCriteria(next2)
237
+ );
238
+ if (filtered.length === 0) return [];
239
+ if (this.options.sort) {
240
+ const { result } = await this.fetch((res) => res);
241
+ return [{ type: "updated", result }];
242
+ }
243
+ if (filtered.length > 1) {
244
+ throw new Error(
245
+ "should not match more than 1, use sort if you can have multiple match"
246
+ );
247
+ }
248
+ const [, next] = filtered[0];
249
+ return [
250
+ {
251
+ type: "updated",
252
+ result: testCriteria(next) ? this.transformer(next) : null
253
+ }
254
+ ];
255
+ }
256
+ default:
257
+ throw new Error("Unsupported type");
258
+ }
259
+ }
218
260
  _subscribe(callback, _includeInitial) {
219
261
  const store = super.getSubscribeStore();
220
262
  const testCriteria = this.createMingoTestCriteria();
@@ -229,55 +271,12 @@ class MongoQuerySingleItem extends AbstractSubscribableStoreQuery {
229
271
  ]);
230
272
  }) : Promise.resolve();
231
273
  const unsubscribe = store.subscribe(async (action) => {
232
- const changes = [];
233
- switch (action.type) {
234
- case "inserted": {
235
- const filtered = action.next.filter(testCriteria);
236
- if (filtered.length > 0) {
237
- changes.push({
238
- type: "updated",
239
- result: this.transformer(filtered[0])
240
- });
241
- }
242
- break;
243
- }
244
- case "deleted": {
245
- const filtered = action.prev.filter(testCriteria);
246
- if (filtered.length > 0) {
247
- changes.push({
248
- type: "deleted",
249
- keys: filtered.map((object) => object[this.store.keyPath])
250
- });
251
- }
252
- break;
253
- }
254
- case "updated": {
255
- const filtered = action.changes.filter(
256
- ([prev, next]) => testCriteria(prev)
257
- );
258
- if (filtered.length > 0) {
259
- if (this.options.sort) {
260
- const { result } = await this.fetch((res) => res);
261
- changes.push({
262
- type: "updated",
263
- result
264
- });
265
- } else if (filtered.length !== 1) {
266
- throw new Error(
267
- "should not match more than 1, use sort if you can have multiple match"
268
- );
269
- } else {
270
- const [, next] = filtered[0];
271
- changes.push({
272
- type: "updated",
273
- result: testCriteria(next) ? this.transformer(next) : null
274
- });
275
- }
276
- } else if (filtered.length === 0) ;
277
- break;
278
- }
279
- default:
280
- throw new Error("Unsupported type");
274
+ let changes;
275
+ try {
276
+ changes = await this.computeChanges(action, testCriteria);
277
+ } catch (error) {
278
+ callback(error, []);
279
+ return;
281
280
  }
282
281
  if (changes.length === 0) return;
283
282
  callback(null, changes);
@@ -709,6 +708,72 @@ const diffIndexes = ({
709
708
  };
710
709
  const isMongoIndexPlanEmpty = (plan) => plan.toCreate.length === 0 && plan.toRecreate.length === 0 && plan.toCollMod.length === 0 && plan.toDrop.length === 0;
711
710
 
711
+ const createReplacePipeline = (replacement) => [
712
+ {
713
+ $replaceWith: {
714
+ $mergeObjects: [
715
+ { $literal: replacement },
716
+ {
717
+ created: { $ifNull: ["$created", { $literal: replacement.created }] }
718
+ }
719
+ ]
720
+ }
721
+ }
722
+ ];
723
+
724
+ const protectedFields = ["created", "updated"];
725
+ const updatedOverrideOperators = /* @__PURE__ */ new Set([
726
+ "$set",
727
+ "$currentDate",
728
+ "$max"
729
+ ]);
730
+ const findProtectedField = (path) => protectedFields.find(
731
+ (field) => path === field || path.startsWith(`${field}.`)
732
+ );
733
+ const listTouchedPaths = (operator, fields) => {
734
+ const keys = Object.keys(fields);
735
+ if (operator !== "$rename") return keys;
736
+ const renameTargets = Object.values(fields).filter(
737
+ (target) => typeof target === "string"
738
+ );
739
+ return [...keys, ...renameTargets];
740
+ };
741
+ const describeRejection = (field, operator, path) => field === "created" ? `Cannot use ${operator} on "${path}": created is set on insert and never updated` : `Cannot use ${operator} on "${path}": updated can only be set with $set, $currentDate or $max`;
742
+ const validateProtectedFields = (update) => {
743
+ let isUpdatedSetByCaller = false;
744
+ const operations = Object.entries(
745
+ update
746
+ );
747
+ for (const [operator, fields] of operations) {
748
+ if (!fields) continue;
749
+ for (const path of listTouchedPaths(operator, fields)) {
750
+ const field = findProtectedField(path);
751
+ if (!field) continue;
752
+ if (field === "updated" && path === "updated" && updatedOverrideOperators.has(operator)) {
753
+ isUpdatedSetByCaller = true;
754
+ continue;
755
+ }
756
+ throw new ProtectedFieldUpdateError(
757
+ describeRejection(field, operator, path)
758
+ );
759
+ }
760
+ }
761
+ return { isUpdatedSetByCaller };
762
+ };
763
+ const stampPartialUpdate = (update, now) => {
764
+ const { isUpdatedSetByCaller } = validateProtectedFields(update);
765
+ if (isUpdatedSetByCaller || !now) return update;
766
+ const $set = { ...update.$set, updated: now };
767
+ return { ...update, $set };
768
+ };
769
+
770
+ const resolveWriteTime = (options) => options?.touchUpdated === false ? void 0 : /* @__PURE__ */ new Date();
771
+ const toStoredValues = (values) => Object.fromEntries(
772
+ Object.entries(values).map(([field, value]) => [
773
+ field,
774
+ value === void 0 ? null : value
775
+ ])
776
+ );
712
777
  class MongoStore {
713
778
  keyPath = "_id";
714
779
  connection;
@@ -802,11 +867,37 @@ class MongoStore {
802
867
  }
803
868
  return object;
804
869
  }
870
+ /**
871
+ * Always sets `updated` to the write time and keeps the stored `created`: the
872
+ * object's own `created` and `updated` are ignored. The object is not mutated.
873
+ */
805
874
  async replaceOne(object) {
806
- if (!object.updated) object.updated = /* @__PURE__ */ new Date();
807
875
  const collection = await this.collection;
808
- await collection.replaceOne({ _id: object._id }, object);
809
- return object;
876
+ const next = await collection.findOneAndUpdate(
877
+ { _id: object._id },
878
+ createReplacePipeline({ ...object, updated: /* @__PURE__ */ new Date() }),
879
+ { returnDocument: "after" }
880
+ );
881
+ if (!next) {
882
+ throw new NotFoundError(`Document not found: ${String(object._id)}`);
883
+ }
884
+ return next;
885
+ }
886
+ /** Same rules as `replaceOne`; `next` is derived from the atomic `prev`, no read back. */
887
+ async replaceOneWithInfo(object) {
888
+ const replacement = { ...object, updated: /* @__PURE__ */ new Date() };
889
+ const collection = await this.collection;
890
+ const prev = await collection.findOneAndUpdate(
891
+ { _id: object._id },
892
+ createReplacePipeline(replacement),
893
+ { returnDocument: "before" }
894
+ );
895
+ if (!prev) return void 0;
896
+ const storedCreated = prev.created;
897
+ return {
898
+ prev,
899
+ next: { ...replacement, created: storedCreated ?? replacement.created }
900
+ };
810
901
  }
811
902
  async upsertOne(object, setOnInsertPartialObject) {
812
903
  const result = await this.upsertOneWithInfo(
@@ -816,6 +907,11 @@ class MongoStore {
816
907
  return result.object;
817
908
  }
818
909
  async upsertOneWithInfo(object, setOnInsertPartialObject) {
910
+ if (setOnInsertPartialObject && "updated" in setOnInsertPartialObject) {
911
+ throw new ProtectedFieldUpdateError(
912
+ 'Cannot use $setOnInsert on "updated": upsert always sets it with $set, pass it in the object instead'
913
+ );
914
+ }
819
915
  const $setOnInsert = {
820
916
  // @ts-expect-error -- created is Date as set in BaseModel
821
917
  created: object.created || /* @__PURE__ */ new Date(),
@@ -840,7 +936,7 @@ class MongoStore {
840
936
  object
841
937
  };
842
938
  }
843
- const next = { ...prev, ...$set };
939
+ const next = { ...prev, ...toStoredValues($set) };
844
940
  return {
845
941
  resolvedAs: "updated",
846
942
  inserted: false,
@@ -851,34 +947,62 @@ class MongoStore {
851
947
  replaceSeveral(objects) {
852
948
  return Promise.all(objects.map((object) => this.replaceOne(object)));
853
949
  }
854
- async partialUpdateByKey(key, partialUpdate, criteria) {
950
+ async partialUpdateByKey(key, partialUpdate, criteria, options) {
951
+ const update = stampPartialUpdate(partialUpdate, resolveWriteTime(options));
855
952
  const collection = await this.collection;
856
- const commandResult = await collection.updateOne(
953
+ const next = await collection.findOneAndUpdate(
857
954
  { _id: key, ...criteria },
858
- partialUpdate
955
+ update,
956
+ { returnDocument: "after" }
957
+ );
958
+ if (!next) throw new NotFoundError(`Document not found: ${String(key)}`);
959
+ return next;
960
+ }
961
+ /**
962
+ * `prev` is read atomically with the write. `next` is read right after it,
963
+ * so it may already include a later concurrent write, which emits its own change.
964
+ * Resolves `undefined` when no document matches.
965
+ */
966
+ async partialUpdateByKeyWithInfo(key, partialUpdate, criteria, options) {
967
+ const update = stampPartialUpdate(partialUpdate, resolveWriteTime(options));
968
+ const collection = await this.collection;
969
+ const prev = await collection.findOneAndUpdate(
970
+ { _id: key, ...criteria },
971
+ update,
972
+ { returnDocument: "before" }
973
+ );
974
+ if (!prev) return void 0;
975
+ return { prev, next: await this.findByKey(key) };
976
+ }
977
+ partialUpdateOne(object, partialUpdate, options) {
978
+ return this.partialUpdateByKey(
979
+ object._id,
980
+ partialUpdate,
981
+ void 0,
982
+ options
859
983
  );
860
- if (!commandResult.acknowledged) {
861
- console.error(commandResult);
862
- throw new Error("Update failed");
863
- }
864
- const object = await this.findByKey(key);
865
- return object;
866
984
  }
867
- partialUpdateOne(object, partialUpdate) {
868
- return this.partialUpdateByKey(object._id, partialUpdate);
985
+ /** One `updateMany` command; every document gets the same `updated`. */
986
+ async partialUpdateMany(criteria, partialUpdate, options) {
987
+ const update = stampPartialUpdate(partialUpdate, resolveWriteTime(options));
988
+ const collection = await this.collection;
989
+ await collection.updateMany(
990
+ criteria,
991
+ update
992
+ );
869
993
  }
870
- partialUpdateMany(criteria, partialUpdate) {
871
- return this.collection.then(
872
- (collection) => (
873
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
874
- collection.updateMany(criteria, partialUpdate)
875
- )
876
- ).then((res) => void 0);
994
+ async deleteByKey(key, criteria) {
995
+ const collection = await this.collection;
996
+ await collection.deleteOne({ _id: key, ...criteria });
877
997
  }
878
- deleteByKey(key, criteria) {
879
- return this.collection.then(
880
- (collection) => collection.deleteOne({ _id: key, ...criteria })
881
- ).then(() => void 0);
998
+ async deleteByKeyWithInfo(key, criteria) {
999
+ const collection = await this.collection;
1000
+ const prev = await collection.findOneAndDelete({
1001
+ _id: key,
1002
+ ...criteria
1003
+ });
1004
+ if (!prev) return void 0;
1005
+ return { prev };
882
1006
  }
883
1007
  deleteOne(object) {
884
1008
  return this.deleteByKey(object._id);
@@ -933,9 +1057,12 @@ class MongoConnection extends AbstractConnection {
933
1057
  _connection;
934
1058
  _connecting;
935
1059
  connectionFailed;
936
- // TODO interface
937
1060
  constructor(config) {
938
1061
  super();
1062
+ if ("client" in config) {
1063
+ this.useClient(config.client, "[client]");
1064
+ return;
1065
+ }
939
1066
  if (!config.database) {
940
1067
  throw new Error("Missing config database");
941
1068
  }
@@ -953,31 +1080,7 @@ class MongoConnection extends AbstractConnection {
953
1080
  logger$1.info("connecting", { connectionStringRedacted });
954
1081
  const connectPromise = mongodb.MongoClient.connect(connectionString).then((connection) => {
955
1082
  logger$1.info("connected", { connectionStringRedacted });
956
- connection.on("close", () => {
957
- logger$1.warn("close", { connectionStringRedacted });
958
- this.connectionFailed = true;
959
- this.getConnection = () => {
960
- throw new Error("MongoDB connection closed");
961
- };
962
- });
963
- connection.on("timeout", () => {
964
- logger$1.warn("timeout", { connectionStringRedacted });
965
- this.connectionFailed = true;
966
- this.getConnection = () => {
967
- throw new Error("MongoDB connection timeout");
968
- };
969
- });
970
- connection.on("reconnect", () => {
971
- logger$1.warn("reconnect", { connectionStringRedacted });
972
- this.connectionFailed = false;
973
- this.getConnection = () => Promise.resolve(this._connection);
974
- });
975
- connection.on("error", (err) => {
976
- logger$1.warn("error", { connectionStringRedacted, err });
977
- });
978
- this._connection = connection;
979
- this._connecting = void 0;
980
- this.getConnection = () => Promise.resolve(this._connection);
1083
+ this.useClient(connection, connectionStringRedacted);
981
1084
  return connection;
982
1085
  }).catch((error) => {
983
1086
  logger$1.info("not connected", { connectionStringRedacted });
@@ -990,6 +1093,33 @@ class MongoConnection extends AbstractConnection {
990
1093
  this.getConnection = () => Promise.resolve(connectPromise);
991
1094
  this._connecting = this.getConnection();
992
1095
  }
1096
+ useClient(connection, connectionStringRedacted) {
1097
+ connection.on("close", () => {
1098
+ logger$1.warn("close", { connectionStringRedacted });
1099
+ this.connectionFailed = true;
1100
+ this.getConnection = () => {
1101
+ throw new Error("MongoDB connection closed");
1102
+ };
1103
+ });
1104
+ connection.on("timeout", () => {
1105
+ logger$1.warn("timeout", { connectionStringRedacted });
1106
+ this.connectionFailed = true;
1107
+ this.getConnection = () => {
1108
+ throw new Error("MongoDB connection timeout");
1109
+ };
1110
+ });
1111
+ connection.on("reconnect", () => {
1112
+ logger$1.warn("reconnect", { connectionStringRedacted });
1113
+ this.connectionFailed = false;
1114
+ this.getConnection = () => Promise.resolve(this._connection);
1115
+ });
1116
+ connection.on("error", (err) => {
1117
+ logger$1.warn("error", { connectionStringRedacted, err });
1118
+ });
1119
+ this._connection = connection;
1120
+ this._connecting = void 0;
1121
+ this.getConnection = () => Promise.resolve(this._connection);
1122
+ }
993
1123
  getConnection() {
994
1124
  throw new Error("call connect()");
995
1125
  }