@classytic/mongokit 3.43.1 → 3.45.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 (92) hide show
  1. package/README.md +18 -3
  2. package/dist/{PaginationEngine-DZnkbSrr.mjs → PaginationEngine-BRlLTQQz.mjs} +13 -27
  3. package/dist/{PaginationEngine-CRHyuUtw.d.mts → PaginationEngine-BWb-c47a.d.mts} +1 -3
  4. package/dist/{Repository-DnL4xk5w.d.mts → Repository-DJuRmBNx.d.mts} +44 -13
  5. package/dist/{Repository-Dj6xqPxJ.mjs → Repository-DheA7oW-.mjs} +330 -112
  6. package/dist/actions/index.d.mts +1 -1
  7. package/dist/actions/index.mjs +2 -2
  8. package/dist/{actions-ChYfkPYP.mjs → actions-DKQx9ToJ.mjs} +3 -4
  9. package/dist/adapter/index.d.mts +6 -10
  10. package/dist/adapter/index.mjs +1 -1
  11. package/dist/{adapter-CkMoMF83.mjs → adapter-Qzgc3ZD_.mjs} +4 -6
  12. package/dist/ai/index.d.mts +21 -8
  13. package/dist/ai/index.mjs +15 -9
  14. package/dist/better-auth/index.d.mts +21 -20
  15. package/dist/better-auth/index.mjs +12 -23
  16. package/dist/{coercion-BjdPX3YF.d.mts → coercion-COp9DRNw.d.mts} +0 -2
  17. package/dist/{create-repository-UjwPWR1z.mjs → create-repository-C9DHHcl7.mjs} +4 -5
  18. package/dist/{create-repository-CJtVdwig.d.mts → create-repository-DG-gwOf0.d.mts} +5 -7
  19. package/dist/{derive-DRBiegRH.mjs → derive-Dxsc31fV.mjs} +3 -6
  20. package/dist/{error-BuByZwKU.mjs → error-DjdmPLWO.mjs} +1 -2
  21. package/dist/{field-selection-FSQFLpsU.mjs → field-selection-DPFaFw9Y.mjs} +1 -2
  22. package/dist/{id-chunks-BmAwsqNk.mjs → id-chunks-DH-RAZWG.mjs} +13 -8
  23. package/dist/{id-resolution-BqDrk0S2.mjs → id-resolution-Cqh49d9J.mjs} +1 -2
  24. package/dist/idempotency/index.d.mts +4 -7
  25. package/dist/idempotency/index.mjs +1 -2
  26. package/dist/{index-SYF8N28D.d.mts → index-5mhQ63_B.d.mts} +3 -4
  27. package/dist/{index-C0AjVqX3.d.mts → index-ehGZLbrd.d.mts} +2 -8
  28. package/dist/index.d.mts +70 -102
  29. package/dist/index.mjs +36 -52
  30. package/dist/indexes/verify-indexes.d.mts +8 -11
  31. package/dist/indexes/verify-indexes.mjs +1 -2
  32. package/dist/{indexes-DIjMpYES.d.mts → indexes-FfyOuhDK.d.mts} +0 -2
  33. package/dist/{join-scope-DSAznE8E.mjs → join-scope-C1gOtZ59.mjs} +2 -3
  34. package/dist/kernel-conformance/index.d.mts +39 -13
  35. package/dist/kernel-conformance/index.mjs +4 -7
  36. package/dist/lock/index.d.mts +134 -7
  37. package/dist/lock/index.mjs +349 -14
  38. package/dist/{logger-K7dPmu2U.mjs → logger-hbWOlj_X.mjs} +1 -2
  39. package/dist/{method-registry.plugin-C63pGn1d.mjs → method-registry.plugin-BeJfQBJV.mjs} +5 -7
  40. package/dist/{model-registry-ClKtWiHa.mjs → model-registry-DrVfJ3Nt.mjs} +1 -2
  41. package/dist/{mongooseToJsonSchema-C3k4ide0.mjs → mongooseToJsonSchema-DMDotz4_.mjs} +2 -3
  42. package/dist/{multi-tenant.plugin-B_q9NUHc.d.mts → multi-tenant.plugin-BTPtCLNK.d.mts} +143 -11
  43. package/dist/{operations-XiFgG5US.d.mts → operations-D6XsHx4e.d.mts} +19 -11
  44. package/dist/outbox/index.d.mts +24 -9
  45. package/dist/outbox/index.mjs +17 -6
  46. package/dist/pagination/PaginationEngine.d.mts +1 -1
  47. package/dist/pagination/PaginationEngine.mjs +1 -1
  48. package/dist/pagination/utils/limits.d.mts +6 -9
  49. package/dist/pagination/utils/limits.mjs +2 -3
  50. package/dist/{pagination-D6psses0.d.mts → pagination-BAM9cA4N.d.mts} +6 -16
  51. package/dist/{plugin-options-Cirmb4Kp.d.mts → plugin-options-DrNGWV5b.d.mts} +2 -4
  52. package/dist/plugins/index.d.mts +6 -7
  53. package/dist/plugins/index.mjs +6 -7
  54. package/dist/query/primitives/coercion.d.mts +1 -1
  55. package/dist/query/primitives/coercion.mjs +2 -3
  56. package/dist/query/primitives/geo.d.mts +11 -14
  57. package/dist/query/primitives/geo.mjs +1 -2
  58. package/dist/query/primitives/indexes.d.mts +1 -1
  59. package/dist/query/primitives/indexes.mjs +1 -2
  60. package/dist/{query-defaults-BxUcSOoF.mjs → query-defaults-DwvWUjZU.mjs} +18 -54
  61. package/dist/read-model/index.d.mts +18 -21
  62. package/dist/read-model/index.mjs +3 -4
  63. package/dist/{repository-MqAP409e.d.mts → repository-B0aFVgP2.d.mts} +25 -29
  64. package/dist/retention-purge-CCd_VXUf.d.mts +38 -0
  65. package/dist/{rolldown-runtime-D7D4PA-g.mjs → rolldown-runtime-D8jQpDG7.mjs} +1 -2
  66. package/dist/search/index.d.mts +1 -1
  67. package/dist/search/index.mjs +1 -1
  68. package/dist/sync/index.d.mts +113 -17
  69. package/dist/sync/index.mjs +421 -65
  70. package/dist/tenant/index.d.mts +8 -11
  71. package/dist/tenant/index.mjs +1 -2
  72. package/dist/testkit/index.d.mts +179 -23
  73. package/dist/testkit/index.mjs +421 -23
  74. package/dist/{timestamp.plugin-DpOWavVI.mjs → timestamp.plugin-B-IJrlYb.mjs} +192 -49
  75. package/dist/{to-plain-vksRrLtc.mjs → to-plain-CEKv_ylg.mjs} +2 -4
  76. package/dist/{to-plain-o5tUY2nV.d.mts → to-plain-Do-8jTKC.d.mts} +2 -16
  77. package/dist/{transaction-DmiB4k2P.mjs → transaction-C6Oj941P.mjs} +18 -19
  78. package/dist/{type-utils-CLzRFtIA.d.mts → type-utils-CKvuewAL.d.mts} +0 -2
  79. package/dist/{update-WgeAUJ_I.mjs → update-CYaJMu6j.mjs} +13 -23
  80. package/dist/usage/index.d.mts +2 -5
  81. package/dist/usage/index.mjs +8 -6
  82. package/dist/utils/index.d.mts +8 -10
  83. package/dist/utils/index.mjs +7 -7
  84. package/dist/{validation-chain.plugin-BAeC5eVT.d.mts → validation-chain.plugin-COYrUdd8.d.mts} +150 -48
  85. package/dist/{validation-chain.plugin-DRApTmkN.mjs → validation-chain.plugin-DJplVJ12.mjs} +210 -55
  86. package/llms.txt +33 -0
  87. package/package.json +29 -25
  88. package/skills/mongokit/SKILL.md +112 -0
  89. package/skills/mongokit/references/pagination-search.md +87 -0
  90. package/skills/mongokit/references/plugins-tenancy.md +103 -0
  91. package/skills/mongokit/references/repository-api.md +83 -0
  92. package/skills/mongokit/references/transactions-outbox.md +103 -0
@@ -1,15 +1,15 @@
1
- import { i as isValidIdForType, t as getSchemaIdType } from "./id-resolution-BqDrk0S2.mjs";
2
- import { a as narrowToIds, i as keysetFilter, o as selectKeysetChunk, r as idChunks, t as DEFAULT_ID_CHUNK } from "./id-chunks-BmAwsqNk.mjs";
3
- import { _ as SHORTHAND_OPS, a as count, b as create, c as getByQuery, d as deleteById, f as deleteByQuery, g as LookupBuilder, h as distinct, l as getOrCreate, n as update, o as exists, r as updateByQuery, s as getById, t as findOneAndUpdate, v as compileFilterToMongo, x as createMany, y as castFilterToSchema } from "./update-WgeAUJ_I.mjs";
4
- import { n as warn } from "./logger-K7dPmu2U.mjs";
5
- import { n as isDuplicateKeyError, r as parseDuplicateKeyError, t as createError } from "./error-BuByZwKU.mjs";
6
- import { i as applyToQuery, n as applyCursorTimeoutMode, r as applyToAggregate, u as resolveQueryOptions } from "./query-defaults-BxUcSOoF.mjs";
7
- import { o as withTransaction, p as resolveMongoCapabilities, s as createTxBoundRepo } from "./transaction-DmiB4k2P.mjs";
8
- import { _ as resolveCursorSecrets, d as withGroupTiebreak, f as withIdTiebreak, g as attachSignature, i as validateKeysetSort, l as assertOffsetWithinCap, n as getPrimaryField, o as encodeCursor, s as resolveCursorFilter, t as PaginationEngine, u as cursorScope, v as verifySignature } from "./PaginationEngine-DZnkbSrr.mjs";
1
+ import { i as isValidIdForType, t as getSchemaIdType } from "./id-resolution-Cqh49d9J.mjs";
2
+ import { n as isDuplicateKeyError, r as parseDuplicateKeyError, t as createError } from "./error-DjdmPLWO.mjs";
3
+ import { a as narrowToIds, i as keysetFilter, o as selectKeysetChunk, r as idChunks, t as DEFAULT_ID_CHUNK } from "./id-chunks-DH-RAZWG.mjs";
4
+ import { _ as SHORTHAND_OPS, a as count, b as create, c as getByQuery, d as deleteById, f as deleteByQuery, g as LookupBuilder, h as distinct, l as getOrCreate, n as update, o as exists, r as updateByQuery, s as getById, t as findOneAndUpdate, v as compileFilterToMongo, x as createMany, y as castFilterToSchema } from "./update-CYaJMu6j.mjs";
5
+ import { n as warn } from "./logger-hbWOlj_X.mjs";
6
+ import { l as resolveQueryOptions, n as applyToAggregate, r as applyToQuery } from "./query-defaults-DwvWUjZU.mjs";
7
+ import { o as withTransaction, p as resolveMongoCapabilities, s as createTxBoundRepo } from "./transaction-C6Oj941P.mjs";
8
+ import { _ as resolveCursorSecrets, d as withGroupTiebreak, f as withIdTiebreak, g as attachSignature, i as validateKeysetSort, l as assertOffsetWithinCap, n as getPrimaryField, o as encodeCursor, s as resolveCursorFilter, t as PaginationEngine, u as cursorScope, v as verifySignature } from "./PaginationEngine-BRlLTQQz.mjs";
9
9
  import { calculateTotalPages } from "./pagination/utils/limits.mjs";
10
- import { a as insertValues, c as writeKeys, d as buildSearchKeys, f as prefixSearchFilter, i as fieldValuesOf, n as SearchKeysWriteError, o as keysFromValues, r as analyzeUpdate, s as unresolvedForUpdate, t as SEARCH_KEYS_ERROR_CODES } from "./derive-DRBiegRH.mjs";
10
+ import { a as insertValues, c as writeKeys, d as buildSearchKeys, f as prefixSearchFilter, i as fieldValuesOf, n as SearchKeysWriteError, o as keysFromValues, r as analyzeUpdate, s as unresolvedForUpdate, t as SEARCH_KEYS_ERROR_CODES } from "./derive-Dxsc31fV.mjs";
11
11
  import { hasNearOperator, rewriteNearForCount } from "./query/primitives/geo.mjs";
12
- import { a as scopeJoins, r as markGoverned } from "./join-scope-DSAznE8E.mjs";
12
+ import { a as scopeJoins, r as markGoverned } from "./join-scope-C1gOtZ59.mjs";
13
13
  import { BULK_UPSERT_ERROR_CODES, BULK_UPSERT_FAILURE_CODES, RepositoryBase, bulkUpsertOptionsError, createDistributionGuard, runChunkedArchive, runChunkedPurge, summarizeBulkUpsert, throwIfAborted, validatePluginOrder, withRetry } from "@classytic/repo-core/repository";
14
14
  import mongoose from "mongoose";
15
15
  import { isFilter } from "@classytic/repo-core/filter";
@@ -17,13 +17,135 @@ import { VersionConflictError, conservativeMongoIsTransientConflict } from "@cla
17
17
  import { HOOK_PRIORITY } from "@classytic/repo-core/hooks";
18
18
  import { compileUpdateSpecToMongo, isUpdateSpec } from "@classytic/repo-core/update";
19
19
  import { decodeAggCursor, encodeAggCursor, isKeysetMode, normalizeGroupBy, validateMeasures } from "@classytic/repo-core/aggregate";
20
- //#region src/actions/purge.ts
20
+ // src/plugins/retention-purge.ts
21
+ /** Error codes thrown for a malformed or unauthorized retention purge. */
22
+ const RETENTION_PURGE_ERROR_CODES = {
23
+ DISABLED: "RETENTION_PURGE_DISABLED",
24
+ REASON_REQUIRED: "RETENTION_PURGE_REASON_REQUIRED",
25
+ INVALID_ACTOR: "RETENTION_PURGE_INVALID_ACTOR",
26
+ HARD_ONLY: "RETENTION_PURGE_HARD_ONLY",
27
+ ARCHIVE_UNATTESTED: "RETENTION_PURGE_ARCHIVE_UNATTESTED"
28
+ };
29
+ /** Attested id sets, keyed by an object only `attestArchivedChunk` can mint. */
30
+ const ATTESTED_CHUNKS = /* @__PURE__ */ new WeakMap();
31
+ /**
32
+ * Mint the attestation for one archive chunk the sink has ACKNOWLEDGED. Internal to
33
+ * mongokit (not exported from the package entry): the archive port is its only caller.
34
+ */
35
+ function attestArchivedChunk(ids) {
36
+ const token = Object.freeze({});
37
+ ATTESTED_CHUNKS.set(token, new Set(ids.map(String)));
38
+ return token;
39
+ }
40
+ /** Every `_id: { $in: [...] }` list in the query, through nested `$and`. */
41
+ function idListsIn(query) {
42
+ if (!query || typeof query !== "object") return [];
43
+ const q = query;
44
+ const lists = [];
45
+ const idPredicate = q._id;
46
+ if (idPredicate && typeof idPredicate === "object" && Array.isArray(idPredicate.$in)) lists.push(idPredicate.$in);
47
+ if (Array.isArray(q.$and)) for (const arm of q.$and) lists.push(...idListsIn(arm));
48
+ return lists;
49
+ }
50
+ /**
51
+ * Whether the context carries an attestation, and whether it covers the delete. A delete
52
+ * is covered only when some `_id.$in` it ANDs in is non-empty and wholly attested — an
53
+ * extra predicate can only narrow it further.
54
+ */
55
+ function archiveAttestationOf(ctx) {
56
+ const token = ctx.archiveAttestation;
57
+ if (token === void 0) return "none";
58
+ const attested = typeof token === "object" && token ? ATTESTED_CHUNKS.get(token) : void 0;
59
+ if (!attested) return "invalid";
60
+ const query = ctx.query;
61
+ return idListsIn(query).some((ids) => ids.length > 0 && ids.every((id) => attested.has(String(id)))) ? "valid" : "invalid";
62
+ }
63
+ /** Validates the request's shape (reason, actor). Throws 400 on a malformed one. */
64
+ function assertRetentionPurgeShape(raw, op) {
65
+ const req = raw;
66
+ if (!req || typeof req !== "object" || typeof req.reason !== "string" || !req.reason.trim()) throw createError(400, `'${op}' with retentionPurge needs a non-blank reason.`, {
67
+ code: RETENTION_PURGE_ERROR_CODES.REASON_REQUIRED,
68
+ meta: { operation: op }
69
+ });
70
+ if (req.actor !== void 0 && (typeof req.actor !== "string" || !req.actor.trim())) throw createError(400, `'${op}' with retentionPurge: actor must be a non-blank string.`, {
71
+ code: RETENTION_PURGE_ERROR_CODES.INVALID_ACTOR,
72
+ meta: { operation: op }
73
+ });
74
+ return {
75
+ reason: req.reason,
76
+ actor: req.actor
77
+ };
78
+ }
79
+ /**
80
+ * The validated purge request on a delete context, or `undefined` for an
81
+ * ordinary delete. Refuses every malformed or unauthorized form rather than
82
+ * falling back to the fence: a purge that silently became a refused delete
83
+ * would hide the misconfiguration from the job.
84
+ */
85
+ function readRetentionPurge(ctx, op, guard) {
86
+ const raw = ctx.retentionPurge;
87
+ if (raw === void 0 || raw === null || raw === false) return void 0;
88
+ const attestation = archiveAttestationOf(ctx);
89
+ if (attestation === "invalid") throw createError(403, `'${op}' carries an archive attestation that does not cover the rows it deletes: an archive delete may remove only the chunk its sink acknowledged.`, {
90
+ code: RETENTION_PURGE_ERROR_CODES.ARCHIVE_UNATTESTED,
91
+ meta: {
92
+ operation: op,
93
+ plugin: guard.plugin
94
+ }
95
+ });
96
+ if (!guard.allowPurge && !(guard.allowArchive && attestation === "valid")) throw createError(403, `'${op}' with retentionPurge is not enabled on this repository: construct ${guard.plugin} with allowPurge: true for the retention path` + (attestation === "valid" ? ", or allowArchive: true for archiveByFilter." : "."), {
97
+ code: RETENTION_PURGE_ERROR_CODES.DISABLED,
98
+ meta: {
99
+ operation: op,
100
+ plugin: guard.plugin
101
+ }
102
+ });
103
+ const req = assertRetentionPurgeShape(raw, op);
104
+ if (ctx.deleteMode !== "hard") throw createError(400, `'${op}' with retentionPurge must be a physical delete: pass mode: 'hard'.`, {
105
+ code: RETENTION_PURGE_ERROR_CODES.HARD_ONLY,
106
+ meta: { operation: op }
107
+ });
108
+ return req;
109
+ }
110
+ /** Builds the common audit payload from an `after:delete` / `after:deleteMany` emission. */
111
+ function retentionPurgeEvent(operation, context, result) {
112
+ const req = context.retentionPurge;
113
+ if (!req || typeof req !== "object") return void 0;
114
+ const ctx = context;
115
+ return {
116
+ operation,
117
+ reason: req.reason,
118
+ actor: req.actor,
119
+ id: operation === "delete" ? ctx.id : void 0,
120
+ filter: operation === "deleteMany" ? ctx.query : void 0,
121
+ deletedCount: operation === "delete" ? result ? 1 : 0 : result?.deletedCount ?? 0,
122
+ archived: archiveAttestationOf(context) === "valid",
123
+ session: context.session,
124
+ context
125
+ };
126
+ }
127
+ // src/actions/purge.ts
128
+ /**
129
+ * Validates the run's `retentionPurge` up front (before any chunk is
130
+ * selected) and returns the port options. A purge request is a physical
131
+ * delete only: with a `soft` / `anonymize` strategy it is refused.
132
+ */
133
+ function purgePortOptions(op, strategy, options) {
134
+ const raw = options.retentionPurge;
135
+ if (raw === void 0 || raw === null || raw === false) return {};
136
+ const retentionPurge = assertRetentionPurgeShape(raw, op);
137
+ if (strategy.type !== "hard") throw createError(400, `'${op}' with retentionPurge must use the 'hard' strategy (got '${strategy.type}').`, {
138
+ code: RETENTION_PURGE_ERROR_CODES.HARD_ONLY,
139
+ meta: { operation: op }
140
+ });
141
+ return { retentionPurge };
142
+ }
21
143
  /**
22
144
  * Build a `PurgePort` bound to a repository + the `field = value`
23
145
  * predicate the purge targets.
24
146
  */
25
- function createMongoPurgePort(repo, field, value, session) {
26
- return createKeysetPurgePort(repo, { [field]: value }, session);
147
+ function createMongoPurgePort(repo, field, value, session, portOptions = {}) {
148
+ return createKeysetPurgePort(repo, { [field]: value }, session, portOptions);
27
149
  }
28
150
  /**
29
151
  * Build a `PurgePort` bound to a repository + a pre-compiled Mongo filter
@@ -39,27 +161,32 @@ function createMongoPurgePort(repo, field, value, session) {
39
161
  * uses, so a row that leaves the matching set between the id select and the
40
162
  * write is never touched.
41
163
  *
42
- * Mirrors `createMongoArchivePort`, which likewise binds a pre-compiled
164
+ * Mirrors `createMongoArchiveRun`, which likewise binds a pre-compiled
43
165
  * filter rather than a `field/value` pair.
44
166
  */
45
- function createMongoPurgePortFromFilter(repo, filter, session) {
46
- return createKeysetPurgePort(repo, filter, session);
167
+ function createMongoPurgePortFromFilter(repo, filter, session, portOptions = {}) {
168
+ return createKeysetPurgePort(repo, filter, session, portOptions);
47
169
  }
48
170
  /**
49
171
  * Shared keyset-progressed port both public factories delegate to. The
50
172
  * closure's `cursor` is the port's single-run progression state.
51
173
  */
52
- function createKeysetPurgePort(repo, baseFilter, session) {
174
+ function createKeysetPurgePort(repo, baseFilter, session, portOptions) {
53
175
  const baseOpts = {
54
176
  session,
55
177
  bypassTenant: true
56
178
  };
179
+ const hardOpts = {
180
+ ...baseOpts,
181
+ mode: "hard",
182
+ ...portOptions.retentionPurge ? { retentionPurge: portOptions.retentionPurge } : {}
183
+ };
57
184
  let cursor = null;
58
185
  const selectFilter = () => keysetFilter(baseFilter, cursor);
59
186
  return { async purgeChunk(strategy, limit) {
60
187
  if (strategy.type === "anonymize") {
61
188
  if (Object.values(strategy.fields).some((v) => typeof v === "function")) {
62
- const { processed, lastId } = await purgeAnonymizeFunctional(repo, selectFilter(), strategy.fields, limit, session);
189
+ const { processed, lastId } = await purgeAnonymizeFunctional(repo, selectFilter(), baseFilter, strategy.fields, limit, session, baseOpts);
63
190
  if (lastId != null) cursor = lastId;
64
191
  return processed;
65
192
  }
@@ -73,10 +200,7 @@ function createKeysetPurgePort(repo, baseFilter, session) {
73
200
  const chunkFilter = narrowToIds(baseFilter, ids);
74
201
  switch (strategy.type) {
75
202
  case "hard":
76
- await repo.deleteMany(chunkFilter, {
77
- ...baseOpts,
78
- mode: "hard"
79
- });
203
+ await repo.deleteMany(chunkFilter, hardOpts);
80
204
  break;
81
205
  case "soft":
82
206
  await repo.updateMany(chunkFilter, { $set: {
@@ -92,47 +216,68 @@ function createKeysetPurgePort(repo, baseFilter, session) {
92
216
  }
93
217
  /**
94
218
  * Anonymize with at least one function-form field replacer. Fetches the
95
- * full docs (function needs the row) and issues ONE `bulkWrite` with N
96
- * heterogeneous `updateOne` ops. Two round-trips per chunk regardless
97
- * of N — vs the N+1 of per-doc `update()` fan-out.
219
+ * full docs (function needs the row), computes each row's values, and
220
+ * writes the chunk with ONE `repo.updateMany` whose pipeline picks each
221
+ * row's value by its `_id`. Two round-trips per chunk regardless of N.
222
+ *
223
+ * Through the repository, never a raw `Model.bulkWrite`: the write meets
224
+ * the same `before:updateMany` fences as the static form —
225
+ * `immutableStatesPlugin` refuses a chunk holding a frozen row,
226
+ * `appendOnlyPlugin` refuses the edit, tenant / soft-delete scope apply —
227
+ * and `after:updateMany` reaches the change log, cache and audit plugins.
98
228
  *
99
229
  * Selection is `_id`-ascending against the caller's keyset-narrowed
100
230
  * filter; returns the chunk's last `_id` so the port can advance its
101
- * cursor after the write commits.
102
- *
103
- * Mongo's `bulkWrite` is ordered: false → independent failures don't
104
- * abort the batch. For idempotent anonymize (same row anonymized twice
105
- * is the same write), this matches the at-least-once contract.
231
+ * cursor after the write commits. Values are cast by the field's schema
232
+ * type where it is a scalar (a pipeline update is not cast by mongoose);
233
+ * `undefined` is written as `null`. Idempotent per row, so the
234
+ * at-least-once contract holds.
106
235
  */
107
- async function purgeAnonymizeFunctional(repo, selectFilter, fields, limit, session) {
236
+ async function purgeAnonymizeFunctional(repo, selectFilter, baseFilter, fields, limit, session, writeOpts) {
108
237
  const docs = await selectKeysetChunk(repo.Model, selectFilter, limit, { session });
109
238
  if (docs.length === 0) return {
110
239
  processed: 0,
111
240
  lastId: null
112
241
  };
113
- const operations = docs.map((doc) => {
114
- const set = {};
115
- for (const [k, v] of Object.entries(fields)) set[k] = typeof v === "function" ? v(doc) : v;
116
- return { updateOne: {
117
- filter: { _id: doc._id },
118
- update: { $set: set }
242
+ const ids = docs.map((doc) => doc._id);
243
+ const set = {};
244
+ for (const [k, v] of Object.entries(fields)) {
245
+ const values = docs.map((doc) => castScalar(repo.Model, k, typeof v === "function" ? v(doc) : v));
246
+ set[k] = { $let: {
247
+ vars: { i: { $indexOfArray: [{ $literal: ids }, "$_id"] } },
248
+ in: { $arrayElemAt: [{ $literal: values }, "$$i"] }
119
249
  } };
120
- });
121
- await repo.Model.bulkWrite(operations, {
122
- ordered: false,
123
- session: session ?? void 0
124
- });
125
- await repo.emitAsync("after:anonymize", {
126
- context: { ...session ? { session } : {} },
127
- result: { ids: docs.map((doc) => doc._id) }
250
+ }
251
+ await repo.updateMany(narrowToIds(baseFilter, ids), [{ $set: set }], {
252
+ ...writeOpts,
253
+ updatePipeline: true
128
254
  });
129
255
  return {
130
256
  processed: docs.length,
131
257
  lastId: docs[docs.length - 1]?._id ?? null
132
258
  };
133
259
  }
134
- //#endregion
135
- //#region src/operations.ts
260
+ /** Scalar schema types a pipeline write must be cast to by hand (mongoose casts `$set`, not pipelines). */
261
+ const SCALAR_INSTANCES = /* @__PURE__ */ new Set([
262
+ "String",
263
+ "Number",
264
+ "Date",
265
+ "Boolean",
266
+ "ObjectId",
267
+ "ObjectID",
268
+ "Decimal128",
269
+ "UUID",
270
+ "BigInt",
271
+ "Double",
272
+ "Int32"
273
+ ]);
274
+ function castScalar(model, path, value) {
275
+ if (value === void 0 || value === null) return null;
276
+ const type = model.schema.path(path);
277
+ if (!type?.instance || !SCALAR_INSTANCES.has(type.instance) || !type.cast) return value;
278
+ return type.cast(value);
279
+ }
280
+ // src/operations.ts
136
281
  /**
137
282
  * The registry. Order intentionally groups by category for readability.
138
283
  * Adding a new RepositoryOperation requires exactly one entry here, after
@@ -295,8 +440,7 @@ const READ_OPERATIONS = ALL_OPERATIONS.filter((op) => !OP_REGISTRY[op].mutates);
295
440
  function operationsByPolicyKey(key) {
296
441
  return ALL_OPERATIONS.filter((op) => OP_REGISTRY[op].policyKey === key);
297
442
  }
298
- //#endregion
299
- //#region src/actions/aggregate-ir/hints.ts
443
+ // src/actions/aggregate-ir/hints.ts
300
444
  function applyExecutionHints(aggregation, hints) {
301
445
  if (!hints) return;
302
446
  if (hints.allowDiskUse) aggregation.allowDiskUse(true);
@@ -328,8 +472,7 @@ function assertExpectedIndex(model, leadingKeys) {
328
472
  if (!keys.some((key) => startsWith(key, 0) || startsWith(key, 1))) throw new Error(`[mongokit] ${model.modelName}: an aggregation expects an index leading with (${id}), but none is declared — declared: ${keys.map((k) => `(${k.join(",")})`).join(" ") || "none"}. Declare one, or drop the hint.`);
329
473
  checked.set(model, (done ?? /* @__PURE__ */ new Set()).add(id));
330
474
  }
331
- //#endregion
332
- //#region src/actions/aggregate-ir/dateBucket.ts
475
+ // src/actions/aggregate-ir/dateBucket.ts
333
476
  /**
334
477
  * Compile a `AggDateBucket` to the mongo expression that produces the
335
478
  * bucket label string for a single document. Used in two places:
@@ -458,8 +601,7 @@ function formatForUnit(unit) {
458
601
  case "year": return "%Y";
459
602
  }
460
603
  }
461
- //#endregion
462
- //#region src/filter/compile-expr.ts
604
+ // src/filter/compile-expr.ts
463
605
  /**
464
606
  * Compile a Filter IR node (or already-built expression) to a MongoDB
465
607
  * aggregation expression. Returns the literal `true` for empty `and`
@@ -773,8 +915,7 @@ function likeToRegexPattern(pattern) {
773
915
  function escapeRegexChar(ch) {
774
916
  return /[.*+?^${}()|[\]\\]/.test(ch) ? `\\${ch}` : ch;
775
917
  }
776
- //#endregion
777
- //#region src/actions/aggregate-ir/measure.ts
918
+ // src/actions/aggregate-ir/measure.ts
778
919
  function compileMeasure(measure) {
779
920
  const whereExpr = measure.where !== void 0 ? compileFilterToMongoExpr(measure.where) : void 0;
780
921
  switch (measure.op) {
@@ -843,8 +984,7 @@ function compileMeasure(measure) {
843
984
  };
844
985
  }
845
986
  }
846
- //#endregion
847
- //#region src/actions/aggregate-ir/normalize.ts
987
+ // src/actions/aggregate-ir/normalize.ts
848
988
  /**
849
989
  * AggRequest → input normalization (mongokit binding).
850
990
  *
@@ -857,8 +997,7 @@ function compileMeasure(measure) {
857
997
  function validateMeasures$1(measures) {
858
998
  validateMeasures(measures, "mongokit");
859
999
  }
860
- //#endregion
861
- //#region src/actions/aggregate-ir/pipeline.ts
1000
+ // src/actions/aggregate-ir/pipeline.ts
862
1001
  function buildAggPipeline(req, schema) {
863
1002
  validateMeasures$1(req.measures);
864
1003
  const groupCols = normalizeGroupBy(req.groupBy);
@@ -1251,8 +1390,7 @@ function irReferencesAlias(filter, aliasSet) {
1251
1390
  if (node.child) return irReferencesAlias(node.child, aliasSet);
1252
1391
  return false;
1253
1392
  }
1254
- //#endregion
1255
- //#region src/actions/aggregate-ir/execute.ts
1393
+ // src/actions/aggregate-ir/execute.ts
1256
1394
  /** Build the aggregate for `stages` with every execution option applied, unexecuted. */
1257
1395
  function prepareAgg(Model, stages, req, options = {}) {
1258
1396
  const aggregation = Model.aggregate(options.finalize ? options.finalize(stages) : stages);
@@ -1271,8 +1409,7 @@ async function executeAgg(Model, req, options = {}) {
1271
1409
  const { pipeline } = buildAggPipeline(req, Model.schema);
1272
1410
  return runAgg(Model, pipeline, req, options);
1273
1411
  }
1274
- //#endregion
1275
- //#region src/actions/aggregate-ir/count.ts
1412
+ // src/actions/aggregate-ir/count.ts
1276
1413
  async function countAggGroups(Model, req, options = {}) {
1277
1414
  const groupCols = normalizeGroupBy(req.groupBy);
1278
1415
  const hasDottedGroupBy = groupCols.some((f) => f.includes("."));
@@ -1302,8 +1439,7 @@ async function countAggGroups(Model, req, options = {}) {
1302
1439
  const [row] = await runAgg(Model, pipeline, req, options);
1303
1440
  return row?.n ?? 0;
1304
1441
  }
1305
- //#endregion
1306
- //#region src/actions/aggregate-ir/keyset.ts
1442
+ // src/actions/aggregate-ir/keyset.ts
1307
1443
  /**
1308
1444
  * Keyset (cursor) pagination helpers — mongokit binding.
1309
1445
  *
@@ -1366,29 +1502,83 @@ function buildKeysetPredicate(sort, cursor) {
1366
1502
  }
1367
1503
  return { $match: branches.length === 1 ? branches[0] : { $or: branches } };
1368
1504
  }
1369
- //#endregion
1370
- //#region src/actions/archive.ts
1505
+ // src/actions/archive.ts
1506
+ const idOf = (doc) => doc._id;
1507
+ const valueAt = (doc, path) => path.split(".").reduce((acc, key) => acc?.[key], doc);
1371
1508
  /**
1372
- * Build an `ArchivePort` bound to a repository + a pre-compiled Mongo
1373
- * filter (the repository compiles Filter IR before calling this).
1509
+ * The fields that prove a row is unchanged since export: the `updatedAt` mongoose
1510
+ * timestamps maintain on every write path, plus the repository's `versionField` when
1511
+ * the schema has it. Empty only for a collection declared `immutable`. A collection with
1512
+ * no maintained `updatedAt` is refused before the run reads anything.
1374
1513
  */
1375
- function createMongoArchivePort(repo, filter, session) {
1514
+ function archiveChangeTokens(model, versionField, immutable) {
1515
+ if (immutable) return [];
1516
+ const ts = model.timestamps;
1517
+ const updatedAt = ts === true ? "updatedAt" : ts && typeof ts === "object" && ts.updatedAt !== false ? typeof ts.updatedAt === "string" ? ts.updatedAt : "updatedAt" : void 0;
1518
+ if (!updatedAt) throw createError(400, `archiveByFilter on '${model.modelName}': the schema maintains no updatedAt (timestamps), so a row edited between export and delete could not be told apart and would be deleted with its edit. Enable timestamps, or pass immutable: true for a collection whose rows never change.`, {
1519
+ code: "ARCHIVE_NO_CHANGE_TOKEN",
1520
+ meta: { model: model.modelName }
1521
+ });
1522
+ return model.hasVersionPath ? [updatedAt, versionField] : [updatedAt];
1523
+ }
1524
+ /** Each exported row pinned to its change tokens: an edited row no longer matches. */
1525
+ function unchangedSince(docs, tokens) {
1526
+ return { $or: docs.map((doc) => {
1527
+ const pin = { _id: idOf(doc) };
1528
+ for (const token of tokens) {
1529
+ const value = valueAt(doc, token);
1530
+ pin[token] = value === void 0 ? { $exists: false } : value;
1531
+ }
1532
+ return pin;
1533
+ }) };
1534
+ }
1535
+ /**
1536
+ * Build the sink + port pair for ONE `archiveByFilter` call. The returned sink
1537
+ * forwards to the caller's and records what it acknowledged; the port's delete
1538
+ * removes only acknowledged rows still unchanged since export, and throws on any other.
1539
+ * Progression is a keyset over `_id`, so a row left live is not revisited by this run.
1540
+ */
1541
+ function createMongoArchiveRun(repo, filter, sink, session, retentionPurge, immutable) {
1542
+ const tokens = archiveChangeTokens({
1543
+ modelName: repo.Model.modelName,
1544
+ timestamps: repo.Model.schema.get("timestamps"),
1545
+ hasVersionPath: Boolean(repo.Model.schema.path(repo.versionField))
1546
+ }, repo.versionField, immutable);
1547
+ let acknowledged = /* @__PURE__ */ new Set();
1548
+ let cursor = null;
1376
1549
  return {
1377
- async readChunk(limit) {
1378
- return selectKeysetChunk(repo.Model, filter, limit, { session });
1550
+ sink: {
1551
+ async write(docs) {
1552
+ await sink.write(docs);
1553
+ acknowledged = new Set(docs.map((doc) => String(idOf(doc))));
1554
+ },
1555
+ ...sink.flush ? { flush: () => sink.flush?.() ?? Promise.resolve() } : {}
1379
1556
  },
1380
- async deleteChunk(docs) {
1381
- const ids = docs.map((doc) => doc._id);
1382
- return (await repo.deleteMany(narrowToIds(filter, ids), {
1383
- session,
1384
- bypassTenant: true,
1385
- mode: "hard"
1386
- })).deletedCount;
1557
+ port: {
1558
+ async readChunk(limit) {
1559
+ return selectKeysetChunk(repo.Model, keysetFilter(filter, cursor), limit, { session });
1560
+ },
1561
+ async deleteChunk(docs) {
1562
+ const ids = docs.map(idOf);
1563
+ const unacknowledged = ids.filter((id) => !acknowledged.has(String(id)));
1564
+ if (unacknowledged.length > 0) throw new Error(`archiveByFilter: refusing to delete ${unacknowledged.length} row(s) the sink did not acknowledge`);
1565
+ const narrowed = narrowToIds(filter, ids);
1566
+ const result = await repo.deleteMany(tokens.length > 0 ? { $and: [narrowed, unchangedSince(docs, tokens)] } : narrowed, {
1567
+ session,
1568
+ bypassTenant: true,
1569
+ mode: "hard",
1570
+ ...retentionPurge ? {
1571
+ retentionPurge,
1572
+ archiveAttestation: attestArchivedChunk(ids)
1573
+ } : {}
1574
+ });
1575
+ if (ids.length > 0) cursor = ids[ids.length - 1];
1576
+ return result.deletedCount;
1577
+ }
1387
1578
  }
1388
1579
  };
1389
1580
  }
1390
- //#endregion
1391
- //#region src/actions/bulk-upsert.ts
1581
+ // src/actions/bulk-upsert.ts
1392
1582
  /**
1393
1583
  * `bulkUpsert` execution: keyed `updateOne` + `upsert` sub-ops in ONE `client.bulkWrite` with
1394
1584
  * `verboseResults`, the only form that reports matched / modified / upserted PER OP (server 8.0,
@@ -1529,7 +1719,7 @@ async function executeUpserts(model, ops, options) {
1529
1719
  verboseResults: true,
1530
1720
  ...options.session ? { session: options.session } : {},
1531
1721
  ...options.writeConcern ? { writeConcern: options.writeConcern } : {},
1532
- ...options.timeoutMS !== void 0 ? { timeoutMS: options.timeoutMS } : {}
1722
+ ...options.maxTimeMS !== void 0 ? { maxTimeMS: options.maxTimeMS } : {}
1533
1723
  });
1534
1724
  } catch (err) {
1535
1725
  throwIfUnsupported(err);
@@ -1569,8 +1759,7 @@ async function executeUpserts(model, ops, options) {
1569
1759
  });
1570
1760
  return results;
1571
1761
  }
1572
- //#endregion
1573
- //#region src/plugins/search-keys.plugin.ts
1762
+ // src/plugins/search-keys.plugin.ts
1574
1763
  /**
1575
1764
  * Search-keys plugin: keeps the `searchMode: 'prefix'` keys field in step with
1576
1765
  * its source fields on EVERY write path, inside the same write.
@@ -1680,8 +1869,7 @@ function searchKeysPlugin(options) {
1680
1869
  }
1681
1870
  };
1682
1871
  }
1683
- //#endregion
1684
- //#region src/query/AggregationBuilder.ts
1872
+ // src/query/AggregationBuilder.ts
1685
1873
  /**
1686
1874
  * Normalize SortSpec to MongoDB's strict format (1 | -1)
1687
1875
  * Converts 'asc' -> 1, 'desc' -> -1
@@ -2212,8 +2400,7 @@ var AggregationBuilder = class AggregationBuilder {
2212
2400
  * .exec(Model)
2213
2401
  * ```
2214
2402
  */
2215
- //#endregion
2216
- //#region src/repository/lookup-populate.ts
2403
+ // src/repository/lookup-populate.ts
2217
2404
  /**
2218
2405
  * Inclusion projections must carry the keyset sort fields (plus `_id`) or
2219
2406
  * cursor encoding has no values to serialize. No-op for exclusion
@@ -2265,8 +2452,16 @@ function appendLookupStages(pipeline, lookups, projection, keysetSort) {
2265
2452
  const finalProjection = ensureLookupProjectionIncludesCursorFields(projection, keysetSort);
2266
2453
  if (finalProjection) pipeline.push({ $project: finalProjection });
2267
2454
  }
2268
- //#endregion
2269
- //#region src/Repository.ts
2455
+ // src/Repository.ts
2456
+ /**
2457
+ * A CAS guard (`claim` / `claimVersion` `where`) as Mongo: Filter IR compiled, a plain
2458
+ * record (Mongo operators) as is. Spreading an IR node raw made the match require
2459
+ * `doc.op === 'eq'` — no document matched, and the miss read as a lost race.
2460
+ */
2461
+ function casGuard(where) {
2462
+ if (where === void 0) return {};
2463
+ return isFilter(where) ? compileFilterToMongo(where) : where;
2464
+ }
2270
2465
  /**
2271
2466
  * Validates that an update patch is homogeneous — either all top-level
2272
2467
  * keys are `$`-prefixed Mongo operators OR none are. Mixed shapes (e.g.
@@ -2990,7 +3185,7 @@ var Repository = class extends RepositoryBase {
2990
3185
  }
2991
3186
  /**
2992
3187
  * Resumable keyset batches over `_id`: each batch is ONE bounded `find` (no long-lived server
2993
- * cursor, so no cursor timeout and no CSOT lifetime budget), memory is one batch, and every
3188
+ * cursor, so no cursor timeout and a bound on every batch), memory is one batch, and every
2994
3189
  * batch carries a `checkpoint`. Pass a checkpoint back as `after` to resume: rows written
2995
3190
  * behind it are not revisited, rows ahead of it are reached. A checkpoint is bound to the
2996
3191
  * collection, the post-policy filter (tenant included) and is refused elsewhere.
@@ -3069,7 +3264,6 @@ var Repository = class extends RepositoryBase {
3069
3264
  if (options.session) query.session(options.session);
3070
3265
  const cursorOpts = this._opts("read", context);
3071
3266
  applyToQuery(query, cursorOpts);
3072
- applyCursorTimeoutMode(query, cursorOpts, this.Model.db);
3073
3267
  const stream = query.cursor();
3074
3268
  let yieldedCount = 0;
3075
3269
  try {
@@ -3885,7 +4079,7 @@ var Repository = class extends RepositoryBase {
3885
4079
  const effectiveIdField = options.idField ?? this.idField;
3886
4080
  const fromSpec = Array.isArray(transition.from) ? { $in: transition.from } : transition.from;
3887
4081
  const filter = {
3888
- ...transition.where ?? {},
4082
+ ...casGuard(transition.where),
3889
4083
  [effectiveIdField]: id,
3890
4084
  [stateField]: fromSpec
3891
4085
  };
@@ -4008,13 +4202,32 @@ var Repository = class extends RepositoryBase {
4008
4202
  machine.assertTransition(entityId, from, args.to);
4009
4203
  }
4010
4204
  const historyField = args.history === false ? null : args.history ?? "statusHistory";
4205
+ if (historyField === null && (args.priorAt !== void 0 || args.historyExtra !== void 0)) throw createError(400, "applyTransition: priorAt / historyExtra need history (got history: false)", { code: "TRANSITION_HISTORY_DISABLED" });
4206
+ if (args.priorAt !== void 0 && (!(args.priorAt instanceof Date) || Number.isNaN(args.priorAt.getTime()))) throw createError(400, "applyTransition: priorAt must be a valid Date", { code: "TRANSITION_INVALID_PRIOR_AT" });
4207
+ if (args.priorAt !== void 0 && args.at !== void 0 && (!(args.at instanceof Date) || Number.isNaN(args.at.getTime()))) throw createError(400, "applyTransition: at must be a valid Date when priorAt is given", { code: "TRANSITION_INVALID_AT" });
4208
+ if (args.historyExtra !== void 0 && (args.historyExtra === null || typeof args.historyExtra !== "object" || Array.isArray(args.historyExtra))) throw createError(400, "applyTransition: historyExtra must be a plain object", { code: "TRANSITION_INVALID_HISTORY_EXTRA" });
4209
+ if (args.historyExtra !== void 0) {
4210
+ const reserved = [
4211
+ "status",
4212
+ "occurredAt",
4213
+ "by",
4214
+ "note",
4215
+ "durationInPriorMs"
4216
+ ].filter((k) => k in args.historyExtra);
4217
+ if (reserved.length > 0) throw createError(400, `applyTransition: historyExtra may not set reserved key(s) ${reserved.join(", ")}`, { code: "TRANSITION_HISTORY_RESERVED_KEY" });
4218
+ }
4011
4219
  const pushSpec = { ...args.push ?? {} };
4012
- if (historyField !== null) pushSpec[historyField] = {
4013
- status: args.to,
4014
- occurredAt: args.at ?? /* @__PURE__ */ new Date(),
4015
- ...args.by !== void 0 ? { by: args.by } : {},
4016
- ...args.note !== void 0 ? { note: args.note } : {}
4017
- };
4220
+ if (historyField !== null) {
4221
+ const occurredAt = args.at ?? /* @__PURE__ */ new Date();
4222
+ pushSpec[historyField] = {
4223
+ ...args.historyExtra ?? {},
4224
+ status: args.to,
4225
+ occurredAt,
4226
+ ...args.by !== void 0 ? { by: args.by } : {},
4227
+ ...args.note !== void 0 ? { note: args.note } : {},
4228
+ ...args.priorAt !== void 0 ? { durationInPriorMs: Math.max(0, occurredAt.getTime() - args.priorAt.getTime()) } : {}
4229
+ };
4230
+ }
4018
4231
  const patch = {
4019
4232
  ...args.set !== void 0 && Object.keys(args.set).length > 0 ? { $set: args.set } : {},
4020
4233
  ...Object.keys(pushSpec).length > 0 ? { $push: pushSpec } : {},
@@ -4156,7 +4369,7 @@ var Repository = class extends RepositoryBase {
4156
4369
  };
4157
4370
  const versionMatch = transition.from === void 0 ? null : transition.from;
4158
4371
  const filter = {
4159
- ...transition.where ?? {},
4372
+ ...casGuard(transition.where),
4160
4373
  [effectiveIdField]: id,
4161
4374
  [versionField]: versionMatch
4162
4375
  };
@@ -4370,7 +4583,7 @@ var Repository = class extends RepositoryBase {
4370
4583
  * the port implementation.
4371
4584
  */
4372
4585
  async purgeByField(field, value, strategy, options = {}) {
4373
- const port = createMongoPurgePort(this, field, value, options.session);
4586
+ const port = createMongoPurgePort(this, field, value, options.session, purgePortOptions("purgeByField", strategy, options));
4374
4587
  return runChunkedPurge(strategy, options, port);
4375
4588
  }
4376
4589
  /**
@@ -4388,25 +4601,31 @@ var Repository = class extends RepositoryBase {
4388
4601
  * See [actions/purge.ts](./actions/purge.ts) for the port.
4389
4602
  */
4390
4603
  async purgeByFilter(filter, strategy, options = {}) {
4391
- const compiled = compileFilterToMongo(filter);
4392
- const port = createMongoPurgePortFromFilter(this, compiled, options.session);
4604
+ const port = createMongoPurgePortFromFilter(this, compileFilterToMongo(filter), options.session, purgePortOptions("purgeByFilter", strategy, options));
4393
4605
  return runChunkedPurge(strategy, options, port);
4394
4606
  }
4395
4607
  /**
4396
4608
  * Chunked cold-storage extraction — see `StandardRepo.archiveByFilter`
4397
4609
  * for the cross-kit contract (write-before-delete, at-least-once,
4398
4610
  * duplicate-tolerant sinks). Mongokit composes the kit-agnostic
4399
- * `runChunkedArchive` orchestrator with `createMongoArchivePort`
4611
+ * `runChunkedArchive` orchestrator with `createMongoArchiveRun`
4400
4612
  * (`_id`-ordered lean reads + plugin-routed deleteMany removal). See
4401
4613
  * [actions/archive.ts](./actions/archive.ts) for the port.
4402
4614
  *
4615
+ * `retentionPurge` rides every chunk delete with an attestation of the ids the
4616
+ * sink acknowledged, so a fence built with `allowArchive` (or `allowPurge`)
4617
+ * admits it; validated before any chunk is read. Each row is deleted only if its
4618
+ * `updatedAt` (+ `versionField`) still equals the exported value; an edited row stays
4619
+ * live for the next run.
4620
+ *
4403
4621
  * Accepts Filter IR or a Mongo-shaped filter record — compiled once via
4404
4622
  * `compileFilterToMongo`, same dual-dialect rule as every other verb.
4405
4623
  */
4406
4624
  async archiveByFilter(filter, sink, options = {}) {
4407
4625
  const compiled = compileFilterToMongo(filter);
4408
- const port = createMongoArchivePort(this, compiled, options.session);
4409
- return runChunkedArchive(options, sink, port);
4626
+ const retentionPurge = options.retentionPurge === void 0 ? void 0 : assertRetentionPurgeShape(options.retentionPurge, "archiveByFilter");
4627
+ const run = createMongoArchiveRun(this, compiled, sink, options.session, retentionPurge, options.immutable === true);
4628
+ return runChunkedArchive(options, run.sink, run.port);
4410
4629
  }
4411
4630
  /**
4412
4631
  * Many keyed upserts in ONE command with an exact per-row outcome (repo-core `bulkUpsert` IR).
@@ -4467,7 +4686,7 @@ var Repository = class extends RepositoryBase {
4467
4686
  ordered,
4468
4687
  session: context.session,
4469
4688
  writeConcern: writeOpts.writeConcern,
4470
- timeoutMS: writeOpts.maxTimeMS
4689
+ maxTimeMS: writeOpts.maxTimeMS
4471
4690
  });
4472
4691
  return summarizeBulkUpsert([...refused, ...sent].sort((a, b) => a.index - b.index));
4473
4692
  });
@@ -4827,9 +5046,9 @@ var Repository = class extends RepositoryBase {
4827
5046
  const appendLookupStages$2 = (pipeline) => appendLookupStages(pipeline, lookups, projection, isKeyset && sort ? this._parseSort(sort) : void 0);
4828
5047
  if (isKeyset && sort) {
4829
5048
  const parsedSort = this._parseSort(sort);
4830
- const { validateKeysetSort } = await import("./PaginationEngine-DZnkbSrr.mjs").then((n) => n.r);
4831
- const { encodeCursor, resolveCursorFilter } = await import("./PaginationEngine-DZnkbSrr.mjs").then((n) => n.a);
4832
- const { getPrimaryField } = await import("./PaginationEngine-DZnkbSrr.mjs").then((n) => n.r);
5049
+ const { validateKeysetSort } = await import("./PaginationEngine-BRlLTQQz.mjs").then((n) => n.r);
5050
+ const { encodeCursor, resolveCursorFilter } = await import("./PaginationEngine-BRlLTQQz.mjs").then((n) => n.a);
5051
+ const { getPrimaryField } = await import("./PaginationEngine-BRlLTQQz.mjs").then((n) => n.r);
4833
5052
  const normalizedSort = validateKeysetSort(parsedSort, this._pagination.config.strictKeysetSortFields);
4834
5053
  const cursorVersion = this._pagination.config.cursorVersion ?? 1;
4835
5054
  const minCursorVersion = this._pagination.config.minCursorVersion ?? 1;
@@ -5392,5 +5611,4 @@ var Repository = class extends RepositoryBase {
5392
5611
  return createError(500, error.message || "Internal Server Error");
5393
5612
  }
5394
5613
  };
5395
- //#endregion
5396
- export { ALL_OPERATIONS as a, READ_OPERATIONS as c, searchKeysPlugin as i, operationsByPolicyKey as l, Repository as n, MUTATING_OPERATIONS as o, AggregationBuilder as r, OP_REGISTRY as s, HOOK_PRIORITY$1 as t, createMongoPurgePort as u };
5614
+ export { ALL_OPERATIONS as a, READ_OPERATIONS as c, RETENTION_PURGE_ERROR_CODES as d, readRetentionPurge as f, searchKeysPlugin as i, operationsByPolicyKey as l, Repository as n, MUTATING_OPERATIONS as o, retentionPurgeEvent as p, AggregationBuilder as r, OP_REGISTRY as s, HOOK_PRIORITY$1 as t, createMongoPurgePort as u };