@classytic/mongokit 3.43.1 → 3.44.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 +14 -0
  2. package/dist/{PaginationEngine-DZnkbSrr.mjs → PaginationEngine-C9iS0U5l.mjs} +13 -27
  3. package/dist/{PaginationEngine-CRHyuUtw.d.mts → PaginationEngine-D17AthKw.d.mts} +1 -3
  4. package/dist/{Repository-DnL4xk5w.d.mts → Repository-B6oL6OMv.d.mts} +33 -11
  5. package/dist/{Repository-Dj6xqPxJ.mjs → Repository-eMRYZ_2Y.mjs} +204 -92
  6. package/dist/actions/index.d.mts +1 -1
  7. package/dist/actions/index.mjs +2 -2
  8. package/dist/{actions-ChYfkPYP.mjs → actions-DExyXMTy.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 +17 -19
  15. package/dist/better-auth/index.mjs +3 -4
  16. package/dist/{coercion-BjdPX3YF.d.mts → coercion-COp9DRNw.d.mts} +0 -2
  17. package/dist/{create-repository-CJtVdwig.d.mts → create-repository-BOFUYj1l.d.mts} +5 -7
  18. package/dist/{create-repository-UjwPWR1z.mjs → create-repository-D7MQpIZo.mjs} +4 -5
  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-8QUEsOFI.d.mts} +3 -4
  27. package/dist/{index-C0AjVqX3.d.mts → index-ehGZLbrd.d.mts} +2 -8
  28. package/dist/index.d.mts +63 -96
  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-CGAepsBU.mjs} +3 -5
  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-DfeDMiI8.d.mts} +143 -11
  43. package/dist/{operations-XiFgG5US.d.mts → operations-Dw9OFV0V.d.mts} +14 -5
  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-DAUg5zOY.d.mts} +2 -10
  51. package/dist/{plugin-options-Cirmb4Kp.d.mts → plugin-options-Bpcuz9kS.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-aUoKlEbh.mjs} +2 -3
  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-DzVIwhU2.d.mts} +25 -29
  64. package/dist/retention-purge-DqcYvRnG.d.mts +35 -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 +95 -15
  69. package/dist/sync/index.mjs +308 -62
  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-Bz_zL9PX.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-PO5r1Qoy.d.mts} +2 -16
  77. package/dist/{transaction-DmiB4k2P.mjs → transaction-C2PFxfIv.mjs} +5 -8
  78. package/dist/{type-utils-CLzRFtIA.d.mts → type-utils-CKvuewAL.d.mts} +0 -2
  79. package/dist/{update-WgeAUJ_I.mjs → update-DcfmNbJe.mjs} +12 -20
  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-B3gZCZj4.d.mts} +135 -48
  85. package/dist/{validation-chain.plugin-DRApTmkN.mjs → validation-chain.plugin-DNY6MIF6.mjs} +207 -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 +102 -0
  91. package/skills/mongokit/references/repository-api.md +83 -0
  92. package/skills/mongokit/references/transactions-outbox.md +101 -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-DcfmNbJe.mjs";
5
+ import { n as warn } from "./logger-hbWOlj_X.mjs";
6
+ import { i as applyToQuery, n as applyCursorTimeoutMode, r as applyToAggregate, u as resolveQueryOptions } from "./query-defaults-aUoKlEbh.mjs";
7
+ import { o as withTransaction, p as resolveMongoCapabilities, s as createTxBoundRepo } from "./transaction-C2PFxfIv.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-C9iS0U5l.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,91 @@ 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
+ };
28
+ /** Validates the request's shape (reason, actor). Throws 400 on a malformed one. */
29
+ function assertRetentionPurgeShape(raw, op) {
30
+ const req = raw;
31
+ if (!req || typeof req !== "object" || typeof req.reason !== "string" || !req.reason.trim()) throw createError(400, `'${op}' with retentionPurge needs a non-blank reason.`, {
32
+ code: RETENTION_PURGE_ERROR_CODES.REASON_REQUIRED,
33
+ meta: { operation: op }
34
+ });
35
+ 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.`, {
36
+ code: RETENTION_PURGE_ERROR_CODES.INVALID_ACTOR,
37
+ meta: { operation: op }
38
+ });
39
+ return {
40
+ reason: req.reason,
41
+ actor: req.actor
42
+ };
43
+ }
44
+ /**
45
+ * The validated purge request on a delete context, or `undefined` for an
46
+ * ordinary delete. Refuses every malformed or unauthorized form rather than
47
+ * falling back to the fence: a purge that silently became a refused delete
48
+ * would hide the misconfiguration from the job.
49
+ */
50
+ function readRetentionPurge(ctx, op, guard) {
51
+ const raw = ctx.retentionPurge;
52
+ if (raw === void 0 || raw === null || raw === false) return void 0;
53
+ if (!guard.allowPurge) throw createError(403, `'${op}' with retentionPurge is not enabled on this repository: construct ${guard.plugin} with allowPurge: true for the retention path.`, {
54
+ code: RETENTION_PURGE_ERROR_CODES.DISABLED,
55
+ meta: {
56
+ operation: op,
57
+ plugin: guard.plugin
58
+ }
59
+ });
60
+ const req = assertRetentionPurgeShape(raw, op);
61
+ if (ctx.deleteMode !== "hard") throw createError(400, `'${op}' with retentionPurge must be a physical delete: pass mode: 'hard'.`, {
62
+ code: RETENTION_PURGE_ERROR_CODES.HARD_ONLY,
63
+ meta: { operation: op }
64
+ });
65
+ return req;
66
+ }
67
+ /** Builds the common audit payload from an `after:delete` / `after:deleteMany` emission. */
68
+ function retentionPurgeEvent(operation, context, result) {
69
+ const req = context.retentionPurge;
70
+ if (!req || typeof req !== "object") return void 0;
71
+ const ctx = context;
72
+ return {
73
+ operation,
74
+ reason: req.reason,
75
+ actor: req.actor,
76
+ id: operation === "delete" ? ctx.id : void 0,
77
+ filter: operation === "deleteMany" ? ctx.query : void 0,
78
+ deletedCount: operation === "delete" ? result ? 1 : 0 : result?.deletedCount ?? 0,
79
+ session: context.session,
80
+ context
81
+ };
82
+ }
83
+ // src/actions/purge.ts
84
+ /**
85
+ * Validates the run's `retentionPurge` up front (before any chunk is
86
+ * selected) and returns the port options. A purge request is a physical
87
+ * delete only: with a `soft` / `anonymize` strategy it is refused.
88
+ */
89
+ function purgePortOptions(op, strategy, options) {
90
+ const raw = options.retentionPurge;
91
+ if (raw === void 0 || raw === null || raw === false) return {};
92
+ const retentionPurge = assertRetentionPurgeShape(raw, op);
93
+ if (strategy.type !== "hard") throw createError(400, `'${op}' with retentionPurge must use the 'hard' strategy (got '${strategy.type}').`, {
94
+ code: RETENTION_PURGE_ERROR_CODES.HARD_ONLY,
95
+ meta: { operation: op }
96
+ });
97
+ return { retentionPurge };
98
+ }
21
99
  /**
22
100
  * Build a `PurgePort` bound to a repository + the `field = value`
23
101
  * predicate the purge targets.
24
102
  */
25
- function createMongoPurgePort(repo, field, value, session) {
26
- return createKeysetPurgePort(repo, { [field]: value }, session);
103
+ function createMongoPurgePort(repo, field, value, session, portOptions = {}) {
104
+ return createKeysetPurgePort(repo, { [field]: value }, session, portOptions);
27
105
  }
28
106
  /**
29
107
  * Build a `PurgePort` bound to a repository + a pre-compiled Mongo filter
@@ -42,24 +120,29 @@ function createMongoPurgePort(repo, field, value, session) {
42
120
  * Mirrors `createMongoArchivePort`, which likewise binds a pre-compiled
43
121
  * filter rather than a `field/value` pair.
44
122
  */
45
- function createMongoPurgePortFromFilter(repo, filter, session) {
46
- return createKeysetPurgePort(repo, filter, session);
123
+ function createMongoPurgePortFromFilter(repo, filter, session, portOptions = {}) {
124
+ return createKeysetPurgePort(repo, filter, session, portOptions);
47
125
  }
48
126
  /**
49
127
  * Shared keyset-progressed port both public factories delegate to. The
50
128
  * closure's `cursor` is the port's single-run progression state.
51
129
  */
52
- function createKeysetPurgePort(repo, baseFilter, session) {
130
+ function createKeysetPurgePort(repo, baseFilter, session, portOptions) {
53
131
  const baseOpts = {
54
132
  session,
55
133
  bypassTenant: true
56
134
  };
135
+ const hardOpts = {
136
+ ...baseOpts,
137
+ mode: "hard",
138
+ ...portOptions.retentionPurge ? { retentionPurge: portOptions.retentionPurge } : {}
139
+ };
57
140
  let cursor = null;
58
141
  const selectFilter = () => keysetFilter(baseFilter, cursor);
59
142
  return { async purgeChunk(strategy, limit) {
60
143
  if (strategy.type === "anonymize") {
61
144
  if (Object.values(strategy.fields).some((v) => typeof v === "function")) {
62
- const { processed, lastId } = await purgeAnonymizeFunctional(repo, selectFilter(), strategy.fields, limit, session);
145
+ const { processed, lastId } = await purgeAnonymizeFunctional(repo, selectFilter(), baseFilter, strategy.fields, limit, session, baseOpts);
63
146
  if (lastId != null) cursor = lastId;
64
147
  return processed;
65
148
  }
@@ -73,10 +156,7 @@ function createKeysetPurgePort(repo, baseFilter, session) {
73
156
  const chunkFilter = narrowToIds(baseFilter, ids);
74
157
  switch (strategy.type) {
75
158
  case "hard":
76
- await repo.deleteMany(chunkFilter, {
77
- ...baseOpts,
78
- mode: "hard"
79
- });
159
+ await repo.deleteMany(chunkFilter, hardOpts);
80
160
  break;
81
161
  case "soft":
82
162
  await repo.updateMany(chunkFilter, { $set: {
@@ -92,47 +172,68 @@ function createKeysetPurgePort(repo, baseFilter, session) {
92
172
  }
93
173
  /**
94
174
  * 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.
175
+ * full docs (function needs the row), computes each row's values, and
176
+ * writes the chunk with ONE `repo.updateMany` whose pipeline picks each
177
+ * row's value by its `_id`. Two round-trips per chunk regardless of N.
178
+ *
179
+ * Through the repository, never a raw `Model.bulkWrite`: the write meets
180
+ * the same `before:updateMany` fences as the static form —
181
+ * `immutableStatesPlugin` refuses a chunk holding a frozen row,
182
+ * `appendOnlyPlugin` refuses the edit, tenant / soft-delete scope apply —
183
+ * and `after:updateMany` reaches the change log, cache and audit plugins.
98
184
  *
99
185
  * Selection is `_id`-ascending against the caller's keyset-narrowed
100
186
  * 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.
187
+ * cursor after the write commits. Values are cast by the field's schema
188
+ * type where it is a scalar (a pipeline update is not cast by mongoose);
189
+ * `undefined` is written as `null`. Idempotent per row, so the
190
+ * at-least-once contract holds.
106
191
  */
107
- async function purgeAnonymizeFunctional(repo, selectFilter, fields, limit, session) {
192
+ async function purgeAnonymizeFunctional(repo, selectFilter, baseFilter, fields, limit, session, writeOpts) {
108
193
  const docs = await selectKeysetChunk(repo.Model, selectFilter, limit, { session });
109
194
  if (docs.length === 0) return {
110
195
  processed: 0,
111
196
  lastId: null
112
197
  };
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 }
198
+ const ids = docs.map((doc) => doc._id);
199
+ const set = {};
200
+ for (const [k, v] of Object.entries(fields)) {
201
+ const values = docs.map((doc) => castScalar(repo.Model, k, typeof v === "function" ? v(doc) : v));
202
+ set[k] = { $let: {
203
+ vars: { i: { $indexOfArray: [{ $literal: ids }, "$_id"] } },
204
+ in: { $arrayElemAt: [{ $literal: values }, "$$i"] }
119
205
  } };
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) }
206
+ }
207
+ await repo.updateMany(narrowToIds(baseFilter, ids), [{ $set: set }], {
208
+ ...writeOpts,
209
+ updatePipeline: true
128
210
  });
129
211
  return {
130
212
  processed: docs.length,
131
213
  lastId: docs[docs.length - 1]?._id ?? null
132
214
  };
133
215
  }
134
- //#endregion
135
- //#region src/operations.ts
216
+ /** Scalar schema types a pipeline write must be cast to by hand (mongoose casts `$set`, not pipelines). */
217
+ const SCALAR_INSTANCES = /* @__PURE__ */ new Set([
218
+ "String",
219
+ "Number",
220
+ "Date",
221
+ "Boolean",
222
+ "ObjectId",
223
+ "ObjectID",
224
+ "Decimal128",
225
+ "UUID",
226
+ "BigInt",
227
+ "Double",
228
+ "Int32"
229
+ ]);
230
+ function castScalar(model, path, value) {
231
+ if (value === void 0 || value === null) return null;
232
+ const type = model.schema.path(path);
233
+ if (!type?.instance || !SCALAR_INSTANCES.has(type.instance) || !type.cast) return value;
234
+ return type.cast(value);
235
+ }
236
+ // src/operations.ts
136
237
  /**
137
238
  * The registry. Order intentionally groups by category for readability.
138
239
  * Adding a new RepositoryOperation requires exactly one entry here, after
@@ -295,8 +396,7 @@ const READ_OPERATIONS = ALL_OPERATIONS.filter((op) => !OP_REGISTRY[op].mutates);
295
396
  function operationsByPolicyKey(key) {
296
397
  return ALL_OPERATIONS.filter((op) => OP_REGISTRY[op].policyKey === key);
297
398
  }
298
- //#endregion
299
- //#region src/actions/aggregate-ir/hints.ts
399
+ // src/actions/aggregate-ir/hints.ts
300
400
  function applyExecutionHints(aggregation, hints) {
301
401
  if (!hints) return;
302
402
  if (hints.allowDiskUse) aggregation.allowDiskUse(true);
@@ -328,8 +428,7 @@ function assertExpectedIndex(model, leadingKeys) {
328
428
  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
429
  checked.set(model, (done ?? /* @__PURE__ */ new Set()).add(id));
330
430
  }
331
- //#endregion
332
- //#region src/actions/aggregate-ir/dateBucket.ts
431
+ // src/actions/aggregate-ir/dateBucket.ts
333
432
  /**
334
433
  * Compile a `AggDateBucket` to the mongo expression that produces the
335
434
  * bucket label string for a single document. Used in two places:
@@ -458,8 +557,7 @@ function formatForUnit(unit) {
458
557
  case "year": return "%Y";
459
558
  }
460
559
  }
461
- //#endregion
462
- //#region src/filter/compile-expr.ts
560
+ // src/filter/compile-expr.ts
463
561
  /**
464
562
  * Compile a Filter IR node (or already-built expression) to a MongoDB
465
563
  * aggregation expression. Returns the literal `true` for empty `and`
@@ -773,8 +871,7 @@ function likeToRegexPattern(pattern) {
773
871
  function escapeRegexChar(ch) {
774
872
  return /[.*+?^${}()|[\]\\]/.test(ch) ? `\\${ch}` : ch;
775
873
  }
776
- //#endregion
777
- //#region src/actions/aggregate-ir/measure.ts
874
+ // src/actions/aggregate-ir/measure.ts
778
875
  function compileMeasure(measure) {
779
876
  const whereExpr = measure.where !== void 0 ? compileFilterToMongoExpr(measure.where) : void 0;
780
877
  switch (measure.op) {
@@ -843,8 +940,7 @@ function compileMeasure(measure) {
843
940
  };
844
941
  }
845
942
  }
846
- //#endregion
847
- //#region src/actions/aggregate-ir/normalize.ts
943
+ // src/actions/aggregate-ir/normalize.ts
848
944
  /**
849
945
  * AggRequest → input normalization (mongokit binding).
850
946
  *
@@ -857,8 +953,7 @@ function compileMeasure(measure) {
857
953
  function validateMeasures$1(measures) {
858
954
  validateMeasures(measures, "mongokit");
859
955
  }
860
- //#endregion
861
- //#region src/actions/aggregate-ir/pipeline.ts
956
+ // src/actions/aggregate-ir/pipeline.ts
862
957
  function buildAggPipeline(req, schema) {
863
958
  validateMeasures$1(req.measures);
864
959
  const groupCols = normalizeGroupBy(req.groupBy);
@@ -1251,8 +1346,7 @@ function irReferencesAlias(filter, aliasSet) {
1251
1346
  if (node.child) return irReferencesAlias(node.child, aliasSet);
1252
1347
  return false;
1253
1348
  }
1254
- //#endregion
1255
- //#region src/actions/aggregate-ir/execute.ts
1349
+ // src/actions/aggregate-ir/execute.ts
1256
1350
  /** Build the aggregate for `stages` with every execution option applied, unexecuted. */
1257
1351
  function prepareAgg(Model, stages, req, options = {}) {
1258
1352
  const aggregation = Model.aggregate(options.finalize ? options.finalize(stages) : stages);
@@ -1271,8 +1365,7 @@ async function executeAgg(Model, req, options = {}) {
1271
1365
  const { pipeline } = buildAggPipeline(req, Model.schema);
1272
1366
  return runAgg(Model, pipeline, req, options);
1273
1367
  }
1274
- //#endregion
1275
- //#region src/actions/aggregate-ir/count.ts
1368
+ // src/actions/aggregate-ir/count.ts
1276
1369
  async function countAggGroups(Model, req, options = {}) {
1277
1370
  const groupCols = normalizeGroupBy(req.groupBy);
1278
1371
  const hasDottedGroupBy = groupCols.some((f) => f.includes("."));
@@ -1302,8 +1395,7 @@ async function countAggGroups(Model, req, options = {}) {
1302
1395
  const [row] = await runAgg(Model, pipeline, req, options);
1303
1396
  return row?.n ?? 0;
1304
1397
  }
1305
- //#endregion
1306
- //#region src/actions/aggregate-ir/keyset.ts
1398
+ // src/actions/aggregate-ir/keyset.ts
1307
1399
  /**
1308
1400
  * Keyset (cursor) pagination helpers — mongokit binding.
1309
1401
  *
@@ -1366,8 +1458,7 @@ function buildKeysetPredicate(sort, cursor) {
1366
1458
  }
1367
1459
  return { $match: branches.length === 1 ? branches[0] : { $or: branches } };
1368
1460
  }
1369
- //#endregion
1370
- //#region src/actions/archive.ts
1461
+ // src/actions/archive.ts
1371
1462
  /**
1372
1463
  * Build an `ArchivePort` bound to a repository + a pre-compiled Mongo
1373
1464
  * filter (the repository compiles Filter IR before calling this).
@@ -1387,8 +1478,7 @@ function createMongoArchivePort(repo, filter, session) {
1387
1478
  }
1388
1479
  };
1389
1480
  }
1390
- //#endregion
1391
- //#region src/actions/bulk-upsert.ts
1481
+ // src/actions/bulk-upsert.ts
1392
1482
  /**
1393
1483
  * `bulkUpsert` execution: keyed `updateOne` + `upsert` sub-ops in ONE `client.bulkWrite` with
1394
1484
  * `verboseResults`, the only form that reports matched / modified / upserted PER OP (server 8.0,
@@ -1569,8 +1659,7 @@ async function executeUpserts(model, ops, options) {
1569
1659
  });
1570
1660
  return results;
1571
1661
  }
1572
- //#endregion
1573
- //#region src/plugins/search-keys.plugin.ts
1662
+ // src/plugins/search-keys.plugin.ts
1574
1663
  /**
1575
1664
  * Search-keys plugin: keeps the `searchMode: 'prefix'` keys field in step with
1576
1665
  * its source fields on EVERY write path, inside the same write.
@@ -1680,8 +1769,7 @@ function searchKeysPlugin(options) {
1680
1769
  }
1681
1770
  };
1682
1771
  }
1683
- //#endregion
1684
- //#region src/query/AggregationBuilder.ts
1772
+ // src/query/AggregationBuilder.ts
1685
1773
  /**
1686
1774
  * Normalize SortSpec to MongoDB's strict format (1 | -1)
1687
1775
  * Converts 'asc' -> 1, 'desc' -> -1
@@ -2212,8 +2300,7 @@ var AggregationBuilder = class AggregationBuilder {
2212
2300
  * .exec(Model)
2213
2301
  * ```
2214
2302
  */
2215
- //#endregion
2216
- //#region src/repository/lookup-populate.ts
2303
+ // src/repository/lookup-populate.ts
2217
2304
  /**
2218
2305
  * Inclusion projections must carry the keyset sort fields (plus `_id`) or
2219
2306
  * cursor encoding has no values to serialize. No-op for exclusion
@@ -2265,8 +2352,16 @@ function appendLookupStages(pipeline, lookups, projection, keysetSort) {
2265
2352
  const finalProjection = ensureLookupProjectionIncludesCursorFields(projection, keysetSort);
2266
2353
  if (finalProjection) pipeline.push({ $project: finalProjection });
2267
2354
  }
2268
- //#endregion
2269
- //#region src/Repository.ts
2355
+ // src/Repository.ts
2356
+ /**
2357
+ * A CAS guard (`claim` / `claimVersion` `where`) as Mongo: Filter IR compiled, a plain
2358
+ * record (Mongo operators) as is. Spreading an IR node raw made the match require
2359
+ * `doc.op === 'eq'` — no document matched, and the miss read as a lost race.
2360
+ */
2361
+ function casGuard(where) {
2362
+ if (where === void 0) return {};
2363
+ return isFilter(where) ? compileFilterToMongo(where) : where;
2364
+ }
2270
2365
  /**
2271
2366
  * Validates that an update patch is homogeneous — either all top-level
2272
2367
  * keys are `$`-prefixed Mongo operators OR none are. Mixed shapes (e.g.
@@ -3885,7 +3980,7 @@ var Repository = class extends RepositoryBase {
3885
3980
  const effectiveIdField = options.idField ?? this.idField;
3886
3981
  const fromSpec = Array.isArray(transition.from) ? { $in: transition.from } : transition.from;
3887
3982
  const filter = {
3888
- ...transition.where ?? {},
3983
+ ...casGuard(transition.where),
3889
3984
  [effectiveIdField]: id,
3890
3985
  [stateField]: fromSpec
3891
3986
  };
@@ -4008,13 +4103,32 @@ var Repository = class extends RepositoryBase {
4008
4103
  machine.assertTransition(entityId, from, args.to);
4009
4104
  }
4010
4105
  const historyField = args.history === false ? null : args.history ?? "statusHistory";
4106
+ 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" });
4107
+ 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" });
4108
+ 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" });
4109
+ 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" });
4110
+ if (args.historyExtra !== void 0) {
4111
+ const reserved = [
4112
+ "status",
4113
+ "occurredAt",
4114
+ "by",
4115
+ "note",
4116
+ "durationInPriorMs"
4117
+ ].filter((k) => k in args.historyExtra);
4118
+ if (reserved.length > 0) throw createError(400, `applyTransition: historyExtra may not set reserved key(s) ${reserved.join(", ")}`, { code: "TRANSITION_HISTORY_RESERVED_KEY" });
4119
+ }
4011
4120
  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
- };
4121
+ if (historyField !== null) {
4122
+ const occurredAt = args.at ?? /* @__PURE__ */ new Date();
4123
+ pushSpec[historyField] = {
4124
+ ...args.historyExtra ?? {},
4125
+ status: args.to,
4126
+ occurredAt,
4127
+ ...args.by !== void 0 ? { by: args.by } : {},
4128
+ ...args.note !== void 0 ? { note: args.note } : {},
4129
+ ...args.priorAt !== void 0 ? { durationInPriorMs: Math.max(0, occurredAt.getTime() - args.priorAt.getTime()) } : {}
4130
+ };
4131
+ }
4018
4132
  const patch = {
4019
4133
  ...args.set !== void 0 && Object.keys(args.set).length > 0 ? { $set: args.set } : {},
4020
4134
  ...Object.keys(pushSpec).length > 0 ? { $push: pushSpec } : {},
@@ -4156,7 +4270,7 @@ var Repository = class extends RepositoryBase {
4156
4270
  };
4157
4271
  const versionMatch = transition.from === void 0 ? null : transition.from;
4158
4272
  const filter = {
4159
- ...transition.where ?? {},
4273
+ ...casGuard(transition.where),
4160
4274
  [effectiveIdField]: id,
4161
4275
  [versionField]: versionMatch
4162
4276
  };
@@ -4370,7 +4484,7 @@ var Repository = class extends RepositoryBase {
4370
4484
  * the port implementation.
4371
4485
  */
4372
4486
  async purgeByField(field, value, strategy, options = {}) {
4373
- const port = createMongoPurgePort(this, field, value, options.session);
4487
+ const port = createMongoPurgePort(this, field, value, options.session, purgePortOptions("purgeByField", strategy, options));
4374
4488
  return runChunkedPurge(strategy, options, port);
4375
4489
  }
4376
4490
  /**
@@ -4388,8 +4502,7 @@ var Repository = class extends RepositoryBase {
4388
4502
  * See [actions/purge.ts](./actions/purge.ts) for the port.
4389
4503
  */
4390
4504
  async purgeByFilter(filter, strategy, options = {}) {
4391
- const compiled = compileFilterToMongo(filter);
4392
- const port = createMongoPurgePortFromFilter(this, compiled, options.session);
4505
+ const port = createMongoPurgePortFromFilter(this, compileFilterToMongo(filter), options.session, purgePortOptions("purgeByFilter", strategy, options));
4393
4506
  return runChunkedPurge(strategy, options, port);
4394
4507
  }
4395
4508
  /**
@@ -4827,9 +4940,9 @@ var Repository = class extends RepositoryBase {
4827
4940
  const appendLookupStages$2 = (pipeline) => appendLookupStages(pipeline, lookups, projection, isKeyset && sort ? this._parseSort(sort) : void 0);
4828
4941
  if (isKeyset && sort) {
4829
4942
  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);
4943
+ const { validateKeysetSort } = await import("./PaginationEngine-C9iS0U5l.mjs").then((n) => n.r);
4944
+ const { encodeCursor, resolveCursorFilter } = await import("./PaginationEngine-C9iS0U5l.mjs").then((n) => n.a);
4945
+ const { getPrimaryField } = await import("./PaginationEngine-C9iS0U5l.mjs").then((n) => n.r);
4833
4946
  const normalizedSort = validateKeysetSort(parsedSort, this._pagination.config.strictKeysetSortFields);
4834
4947
  const cursorVersion = this._pagination.config.cursorVersion ?? 1;
4835
4948
  const minCursorVersion = this._pagination.config.minCursorVersion ?? 1;
@@ -5392,5 +5505,4 @@ var Repository = class extends RepositoryBase {
5392
5505
  return createError(500, error.message || "Internal Server Error");
5393
5506
  }
5394
5507
  };
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 };
5508
+ 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 };
@@ -1,2 +1,2 @@
1
- import { c as create_d_exports, i as delete_d_exports, n as update_d_exports, r as read_d_exports, u as aggregate_d_exports } from "../index-SYF8N28D.mjs";
1
+ import { c as create_d_exports, i as delete_d_exports, n as update_d_exports, r as read_d_exports, u as aggregate_d_exports } from "../index-8QUEsOFI.mjs";
2
2
  export { aggregate_d_exports as aggregate, create_d_exports as create, delete_d_exports as deleteActions, read_d_exports as read, update_d_exports as update };
@@ -1,2 +1,2 @@
1
- export { m as aggregate, S as create, p as deleteActions, u as read, i as update } from "../update-WgeAUJ_I.mjs";
2
- import "../actions-ChYfkPYP.mjs";
1
+ export { m as aggregate, S as create, p as deleteActions, u as read, i as update } from "../update-DcfmNbJe.mjs";
2
+ import "../actions-DExyXMTy.mjs";
@@ -1,6 +1,6 @@
1
- import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
- import { S as create_exports, i as update_exports, m as aggregate_exports, p as delete_exports, u as read_exports } from "./update-WgeAUJ_I.mjs";
3
- //#region src/actions/index.ts
1
+ import { t as __exportAll } from "./rolldown-runtime-D8jQpDG7.mjs";
2
+ import { S as create_exports, i as update_exports, m as aggregate_exports, p as delete_exports, u as read_exports } from "./update-DcfmNbJe.mjs";
3
+ // src/actions/index.ts
4
4
  var actions_exports = /* @__PURE__ */ __exportAll({
5
5
  aggregate: () => aggregate_exports,
6
6
  create: () => create_exports,
@@ -8,5 +8,4 @@ var actions_exports = /* @__PURE__ */ __exportAll({
8
8
  read: () => read_exports,
9
9
  update: () => update_exports
10
10
  });
11
- //#endregion
12
11
  export { actions_exports as t };
@@ -2,7 +2,6 @@ import { StandardRepo } from "@classytic/repo-core/repository";
2
2
  import { Document, Model } from "mongoose";
3
3
  import { SchemaBuilderOptions, SchemaGenerator } from "@classytic/repo-core/schema";
4
4
  import { AdapterRepositoryInput, AdapterSchemaContext, AdapterValidationResult, DataAdapter, OpenApiSchemas, RepositoryLike, SchemaMetadata } from "@classytic/repo-core/adapter";
5
- //#region src/adapter/mongoose-adapter.d.ts
6
5
  interface MongooseAdapterOptions<TDoc = unknown, TModelDoc = unknown> {
7
6
  /**
8
7
  * The Mongoose model. Still a real `Model`, so a Schema or a hand-built
@@ -68,8 +67,8 @@ interface MongooseAdapterOptions<TDoc = unknown, TModelDoc = unknown> {
68
67
  * Mongoose data adapter — implements the `DataAdapter<TDoc>` contract from
69
68
  * `@classytic/repo-core/adapter`.
70
69
  */
71
- declare class MongooseAdapter<TDoc = unknown, TModelDoc = unknown> implements DataAdapter<TDoc> {
72
- readonly type: "mongoose";
70
+ export declare class MongooseAdapter<TDoc = unknown, TModelDoc = unknown> implements DataAdapter<TDoc> {
71
+ readonly type: 'mongoose';
73
72
  readonly name: string;
74
73
  /**
75
74
  * The model as given. Its document type is NOT `TDoc` — a lean repository
@@ -196,10 +195,8 @@ declare class MongooseAdapter<TDoc = unknown, TModelDoc = unknown> implements Da
196
195
  * const adapter = createMongooseAdapter(ProductModel, productRepository);
197
196
  * ```
198
197
  */
199
- declare function createMongooseAdapter<TDoc = unknown, TModelDoc = unknown>(model: Model<TModelDoc>, repository: AdapterRepositoryInput<TDoc>): DataAdapter<TDoc>;
200
- declare function createMongooseAdapter<TDoc = unknown, TModelDoc = unknown>(options: MongooseAdapterOptions<TDoc, TModelDoc>): DataAdapter<TDoc>;
201
- //#endregion
202
- //#region src/adapter/types.d.ts
198
+ export declare function createMongooseAdapter<TDoc = unknown, TModelDoc = unknown>(model: Model<TModelDoc>, repository: AdapterRepositoryInput<TDoc>): DataAdapter<TDoc>;
199
+ export declare function createMongooseAdapter<TDoc = unknown, TModelDoc = unknown>(options: MongooseAdapterOptions<TDoc, TModelDoc>): DataAdapter<TDoc>;
203
200
  /**
204
201
  * Infer document type from a Mongoose model.
205
202
  *
@@ -233,7 +230,7 @@ type MatchingModel<TDoc> = Model<TDoc & Document>;
233
230
  /**
234
231
  * Runtime guard: is `value` a Mongoose model?
235
232
  */
236
- declare function isMongooseModel(value: unknown): value is Model<Document>;
233
+ export declare function isMongooseModel(value: unknown): value is Model<Document>;
237
234
  /**
238
235
  * Mongoose SchemaType internal shape (not fully exposed by `@types/mongoose`).
239
236
  * Used to extract field metadata from schema paths.
@@ -252,5 +249,4 @@ interface MongooseSchemaType {
252
249
  };
253
250
  [key: string]: unknown;
254
251
  }
255
- //#endregion
256
- export { type CleanDoc, type InferAdapterDoc, type InferMongooseDoc, type InferRepoDoc, type MatchingModel, MongooseAdapter, type MongooseAdapterOptions, type MongooseDocument, type MongooseSchemaType, createMongooseAdapter, isMongooseModel };
252
+ export type { CleanDoc, InferAdapterDoc, InferMongooseDoc, InferRepoDoc, MatchingModel, MongooseAdapterOptions, MongooseDocument, MongooseSchemaType };
@@ -1 +1 @@
1
- export { t as MongooseAdapter, n as createMongooseAdapter, r as isMongooseModel } from "../adapter-CkMoMF83.mjs";
1
+ export { t as MongooseAdapter, n as createMongooseAdapter, r as isMongooseModel } from "../adapter-Qzgc3ZD_.mjs";
@@ -1,17 +1,16 @@
1
- import { n as warn } from "./logger-K7dPmu2U.mjs";
2
- import { t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-C3k4ide0.mjs";
1
+ import { n as warn } from "./logger-hbWOlj_X.mjs";
2
+ import { t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-DMDotz4_.mjs";
3
3
  import { matchesRecordFilter } from "@classytic/repo-core/filter";
4
4
  import { mergeFieldRuleConstraints } from "@classytic/repo-core/schema";
5
5
  import { asRepositoryLike, isRepository } from "@classytic/repo-core/adapter";
6
- //#region src/adapter/types.ts
6
+ // src/adapter/types.ts
7
7
  /**
8
8
  * Runtime guard: is `value` a Mongoose model?
9
9
  */
10
10
  function isMongooseModel(value) {
11
11
  return typeof value === "function" && value.prototype && "modelName" in value && "schema" in value;
12
12
  }
13
- //#endregion
14
- //#region src/adapter/mongoose-adapter.ts
13
+ // src/adapter/mongoose-adapter.ts
15
14
  const REQUIRED_REPO_METHODS = [
16
15
  "getAll",
17
16
  "getById",
@@ -325,5 +324,4 @@ function createMongooseAdapter(modelOrOptions, repository) {
325
324
  }
326
325
  return new MongooseAdapter(modelOrOptions);
327
326
  }
328
- //#endregion
329
327
  export { createMongooseAdapter as n, isMongooseModel as r, MongooseAdapter as t };