@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,5 +1,276 @@
1
1
  import mongoose, { Schema } from "mongoose";
2
- //#region src/lock/index.ts
2
+ import { randomUUID } from "node:crypto";
3
+ import { AsyncLocalStorage } from "node:async_hooks";
4
+ // src/lock/daily-ledger.ts
5
+ /**
6
+ * Mongo-backed daily ledger — one claim row per `(key, civilDate)`.
7
+ *
8
+ * Structurally implements arc's `DailyLedger` (`@classytic/arc` →
9
+ * `createDailyGate` / `ScheduleDefinition.daily`) without importing arc:
10
+ * pass the returned object as `ledger`. Exactly one replica wins
11
+ * `claimDay` for a given key and civil date (unique `_id` + upsert; the
12
+ * loser's upsert hits E11000 and returns `false`). An uncompleted claim
13
+ * older than `info.staleAfterMs` may be re-granted (crash mid-run); a
14
+ * completed day never is.
15
+ *
16
+ * {
17
+ * _id: 'cron.digest|2026-10-30',
18
+ * key: 'cron.digest',
19
+ * civilDate: '2026-10-30',
20
+ * claimedAt: 2026-10-30T06:00:01.000Z, // the gate's clock (info.now)
21
+ * completed: true,
22
+ * completedAt: 2026-10-30T06:00:04.000Z,
23
+ * result: { sent: 12 },
24
+ * }
25
+ *
26
+ * `civilDate` must be `YYYY-MM-DD` (what arc passes), so the composite
27
+ * `_id` stays unambiguous whatever the key contains.
28
+ *
29
+ * Fencing is per INVOCATION, not per ledger instance: every won claim
30
+ * gets a unique `claimToken` (stored on the row), and completing or
31
+ * releasing only touches the row while it still carries that token. A
32
+ * run that outlived `staleAfterMs` and was re-granted (by another
33
+ * replica, or by this same instance on a later tick) can therefore
34
+ * neither delete nor complete the newer claim, and a retry of its
35
+ * completion stays fenced.
36
+ *
37
+ * - `claim()` returns a handle carrying the token; its `complete()` /
38
+ * `release()` are fenced by it. Prefer it in host code.
39
+ * - `claimDay` / `completeDay` / `releaseDay` (arc's token-less
40
+ * `DailyLedger` shape) bind the token to the calling async context:
41
+ * `completeDay` / `releaseDay` act on the claim that THIS invocation
42
+ * (the async flow that awaited `claimDay`) won. Called from a flow
43
+ * that won nothing, they fall back to the instance's claim on the row
44
+ * only when it holds exactly one, and are otherwise a no-op.
45
+ * - `adminCompleteDay` is the separate, explicit, unfenced manual
46
+ * completion. Nothing ever falls through to it.
47
+ */
48
+ const CIVIL_DATE = /^\d{4}-\d{2}-\d{2}$/;
49
+ function rowId(key, civilDate) {
50
+ if (typeof key !== "string" || key.length === 0) throw new Error("MongoDailyLedger: key must be a non-empty string");
51
+ if (typeof civilDate !== "string" || !CIVIL_DATE.test(civilDate)) throw new Error(`MongoDailyLedger: civilDate must be YYYY-MM-DD, got "${String(civilDate)}"`);
52
+ return `${key}|${civilDate}`;
53
+ }
54
+ /**
55
+ * Build a Mongo-backed daily ledger. Idempotent per `(connection,
56
+ * modelName)` like `createMongoLockAdapter`.
57
+ *
58
+ * @example
59
+ * import { createDailyGate } from '@classytic/arc/plugins';
60
+ * import { createMongoDailyLedger } from '@classytic/mongokit/lock';
61
+ *
62
+ * const gate = createDailyGate({
63
+ * key: 'digest', zone: 'Asia/Dubai', hour: 6, staleAfterMs: 15 * 60_000,
64
+ * ledger: createMongoDailyLedger({ connection, collectionName: 'app_daily_ledger' }),
65
+ * });
66
+ */
67
+ function createMongoDailyLedger(options = {}) {
68
+ const { connection, collectionName = "mongo_daily_ledger", modelName = "MongoDailyLedger", nowFn = () => /* @__PURE__ */ new Date() } = options;
69
+ const model = getOrCreateLedgerModel(connection ?? mongoose, modelName, collectionName);
70
+ const sessionOpt = () => {
71
+ const s = options.session?.();
72
+ return s ? { session: s } : {};
73
+ };
74
+ /**
75
+ * Token-less (arc-shaped) claims this instance won and has not finished: rowId → tokens. The
76
+ * fallback for a `completeDay` / `releaseDay` called outside the invocation that claimed.
77
+ */
78
+ const live = /* @__PURE__ */ new Map();
79
+ const invocation = new AsyncLocalStorage();
80
+ const slotToken = (slot) => {
81
+ for (let s = slot; s; s = s.prev) if (s.token !== void 0) return s.token;
82
+ };
83
+ const isLive = (id, token) => live.get(id)?.has(token) === true;
84
+ const forget = (id, token) => {
85
+ const set = live.get(id);
86
+ set?.delete(token);
87
+ if (set?.size === 0) live.delete(id);
88
+ };
89
+ /** The token-less caller's claim on the row: its invocation's, else the instance's only one. */
90
+ const tokenFor = (id) => {
91
+ const own = slotToken(invocation.getStore()?.get(id));
92
+ if (own !== void 0) return own;
93
+ const set = live.get(id);
94
+ return set?.size === 1 ? [...set][0] : void 0;
95
+ };
96
+ const validate = (key, civilDate, info) => {
97
+ const _id = rowId(key, civilDate);
98
+ const now = info?.now;
99
+ if (!(now instanceof Date) || Number.isNaN(now.getTime())) throw new Error("MongoDailyLedger.claimDay: info.now must be a valid Date");
100
+ const staleAfterMs = info.staleAfterMs;
101
+ if (staleAfterMs !== void 0 && (typeof staleAfterMs !== "number" || !Number.isFinite(staleAfterMs) || staleAfterMs < 0)) throw new Error("MongoDailyLedger.claimDay: info.staleAfterMs must be a finite number >= 0");
102
+ return {
103
+ _id,
104
+ now,
105
+ staleAfterMs
106
+ };
107
+ };
108
+ /** Try to win the row; the new claim's token, or `undefined` when not won. */
109
+ const claimRow = async (key, civilDate, v) => {
110
+ const { _id, now, staleAfterMs } = v;
111
+ const claimToken = randomUUID();
112
+ try {
113
+ if (staleAfterMs === void 0) {
114
+ await model.create([{
115
+ _id,
116
+ key,
117
+ civilDate,
118
+ claimedAt: now,
119
+ claimToken,
120
+ completed: false
121
+ }]);
122
+ return claimToken;
123
+ }
124
+ const res = await model.updateOne({
125
+ _id,
126
+ completed: false,
127
+ claimedAt: { $lt: new Date(now.getTime() - staleAfterMs) }
128
+ }, {
129
+ $set: {
130
+ key,
131
+ civilDate,
132
+ claimedAt: now,
133
+ claimToken,
134
+ completed: false
135
+ },
136
+ $unset: {
137
+ result: "",
138
+ completedAt: ""
139
+ }
140
+ }, { upsert: true });
141
+ return res.upsertedCount + res.modifiedCount > 0 ? claimToken : void 0;
142
+ } catch (err) {
143
+ if (err?.code === 11e3) return void 0;
144
+ throw err;
145
+ }
146
+ };
147
+ /** Complete the row while `token` still holds it uncompleted. */
148
+ const completeRow = async (_id, token, result) => {
149
+ return (await model.updateOne({
150
+ _id,
151
+ claimToken: token,
152
+ completed: false
153
+ }, { $set: {
154
+ completed: true,
155
+ completedAt: nowFn(),
156
+ ...result !== void 0 ? { result } : {}
157
+ } }, sessionOpt())).modifiedCount > 0;
158
+ };
159
+ /** Delete the row while `token` still holds it uncompleted. */
160
+ const releaseRow = async (_id, token) => {
161
+ return (await model.deleteOne({
162
+ _id,
163
+ claimToken: token,
164
+ completed: false
165
+ }, sessionOpt())).deletedCount > 0;
166
+ };
167
+ return {
168
+ async claimDay(key, civilDate, info) {
169
+ const v = validate(key, civilDate, info);
170
+ const outer = invocation.getStore();
171
+ const scope = /* @__PURE__ */ new Map();
172
+ if (outer) for (const [id, slot] of outer) {
173
+ const t = slotToken(slot);
174
+ if (t === void 0 || isLive(id, t)) scope.set(id, slot);
175
+ }
176
+ const slot = { prev: outer?.get(v._id) };
177
+ scope.set(v._id, slot);
178
+ invocation.enterWith(scope);
179
+ const token = await claimRow(key, civilDate, v);
180
+ if (token === void 0) return false;
181
+ slot.token = token;
182
+ const set = live.get(v._id) ?? /* @__PURE__ */ new Set();
183
+ set.add(token);
184
+ live.set(v._id, set);
185
+ return true;
186
+ },
187
+ async completeDay(key, civilDate, result) {
188
+ const _id = rowId(key, civilDate);
189
+ const token = tokenFor(_id);
190
+ if (token === void 0) return;
191
+ await completeRow(_id, token, result);
192
+ forget(_id, token);
193
+ },
194
+ async releaseDay(key, civilDate) {
195
+ const _id = rowId(key, civilDate);
196
+ const token = tokenFor(_id);
197
+ if (token === void 0) return;
198
+ forget(_id, token);
199
+ await releaseRow(_id, token);
200
+ },
201
+ async claim(key, civilDate, info) {
202
+ const v = validate(key, civilDate, info);
203
+ const token = await claimRow(key, civilDate, v);
204
+ if (token === void 0) return null;
205
+ return {
206
+ key,
207
+ civilDate,
208
+ token,
209
+ claimedAt: v.now,
210
+ complete: (result) => completeRow(v._id, token, result),
211
+ release: () => releaseRow(v._id, token)
212
+ };
213
+ },
214
+ async adminCompleteDay(key, civilDate, result) {
215
+ const _id = rowId(key, civilDate);
216
+ return (await model.updateOne({
217
+ _id,
218
+ completed: false
219
+ }, { $set: {
220
+ completed: true,
221
+ completedAt: nowFn(),
222
+ ...result !== void 0 ? { result } : {}
223
+ } }, sessionOpt())).modifiedCount > 0;
224
+ },
225
+ async inspect(key, civilDate) {
226
+ const doc = await model.findById(rowId(key, civilDate), null, sessionOpt()).lean();
227
+ if (!doc) return null;
228
+ const { _id: _ignored, ...row } = doc;
229
+ return row;
230
+ }
231
+ };
232
+ }
233
+ function getOrCreateLedgerModel(conn, modelName, collectionName) {
234
+ const existing = "models" in conn ? conn.models[modelName] : void 0;
235
+ if (existing) return existing;
236
+ const schema = new Schema({
237
+ _id: {
238
+ type: String,
239
+ required: true
240
+ },
241
+ key: {
242
+ type: String,
243
+ required: true
244
+ },
245
+ civilDate: {
246
+ type: String,
247
+ required: true
248
+ },
249
+ claimedAt: {
250
+ type: Date,
251
+ required: true
252
+ },
253
+ claimToken: { type: String },
254
+ completed: {
255
+ type: Boolean,
256
+ required: true,
257
+ default: false
258
+ },
259
+ completedAt: { type: Date },
260
+ result: { type: Schema.Types.Mixed }
261
+ }, {
262
+ versionKey: false,
263
+ timestamps: false,
264
+ _id: false,
265
+ minimize: false
266
+ });
267
+ schema.index({
268
+ key: 1,
269
+ civilDate: -1
270
+ });
271
+ return conn.model(modelName, schema, collectionName);
272
+ }
273
+ // src/lock/index.ts
3
274
  /**
4
275
  * Build a Mongo-backed lock adapter. Idempotent — the underlying
5
276
  * model is registered once per `(connection, modelName)` pair, so
@@ -7,27 +278,30 @@ import mongoose, { Schema } from "mongoose";
7
278
  * collection (no duplicate-model errors).
8
279
  */
9
280
  function createMongoLockAdapter(options = {}) {
10
- const { connection, collectionName = "mongo_locks", modelName = "MongoLock", defaultLeaseMs = 3e4 } = options;
281
+ const { connection, collectionName = "mongo_locks", modelName = "MongoLock", defaultLeaseMs = 3e4, nowFn = () => /* @__PURE__ */ new Date() } = options;
11
282
  const model = getOrCreateModel(connection ?? mongoose, modelName, collectionName);
12
283
  return {
13
284
  async tryAcquire(name, holderId, leaseMs) {
14
285
  const ms = leaseMs > 0 ? leaseMs : defaultLeaseMs;
15
- const now = /* @__PURE__ */ new Date();
286
+ const now = nowFn();
16
287
  const expiresAt = new Date(now.getTime() + ms);
17
288
  try {
289
+ const holder = { $literal: holderId };
18
290
  const result = await model.findOneAndUpdate({
19
291
  _id: name,
20
292
  $or: [{ expiresAt: { $lt: now } }, { holder: holderId }]
21
- }, {
22
- $set: {
23
- holder: holderId,
24
- expiresAt
25
- },
26
- $setOnInsert: { acquiredAt: now }
27
- }, {
293
+ }, [{ $set: {
294
+ acquiredAt: { $cond: [
295
+ { $and: [{ $eq: ["$holder", holder] }, { $ne: [{ $type: "$acquiredAt" }, "missing"] }] },
296
+ "$acquiredAt",
297
+ { $literal: now }
298
+ ] },
299
+ holder,
300
+ expiresAt: { $literal: expiresAt }
301
+ } }], {
28
302
  upsert: true,
29
303
  returnDocument: "after",
30
- setDefaultsOnInsert: true
304
+ updatePipeline: true
31
305
  }).lean();
32
306
  return !!result && result.holder === holderId;
33
307
  } catch (err) {
@@ -35,6 +309,68 @@ function createMongoLockAdapter(options = {}) {
35
309
  throw err;
36
310
  }
37
311
  },
312
+ async acquire(name, holderId, leaseMs) {
313
+ const ms = leaseMs > 0 ? leaseMs : defaultLeaseMs;
314
+ const now = nowFn();
315
+ const expiresAt = new Date(now.getTime() + ms);
316
+ const holder = { $literal: holderId };
317
+ const won = "$__lockWon";
318
+ const pipeline = [
319
+ { $set: { __lockWon: { $or: [{ $lt: ["$expiresAt", { $literal: now }] }, { $eq: ["$holder", holder] }] } } },
320
+ { $set: {
321
+ acquiredAt: { $cond: [
322
+ won,
323
+ { $cond: [
324
+ { $and: [{ $eq: ["$holder", holder] }, { $ne: [{ $type: "$acquiredAt" }, "missing"] }] },
325
+ "$acquiredAt",
326
+ { $literal: now }
327
+ ] },
328
+ "$acquiredAt"
329
+ ] },
330
+ holder: { $cond: [
331
+ won,
332
+ holder,
333
+ "$holder"
334
+ ] },
335
+ expiresAt: { $cond: [
336
+ won,
337
+ { $literal: expiresAt },
338
+ "$expiresAt"
339
+ ] }
340
+ } },
341
+ { $unset: "__lockWon" }
342
+ ];
343
+ for (let attempt = 0;; attempt++) try {
344
+ const before = await model.findOneAndUpdate({ _id: name }, pipeline, {
345
+ upsert: true,
346
+ returnDocument: "before",
347
+ updatePipeline: true
348
+ }).lean();
349
+ if (!before) return {
350
+ acquired: true,
351
+ holder: holderId,
352
+ acquiredAt: now,
353
+ expiresAt
354
+ };
355
+ const mine = before.holder === holderId;
356
+ if (mine || before.expiresAt.getTime() < now.getTime()) return {
357
+ acquired: true,
358
+ holder: holderId,
359
+ previousHolder: before.holder,
360
+ acquiredAt: mine && before.acquiredAt ? before.acquiredAt : now,
361
+ expiresAt
362
+ };
363
+ return {
364
+ acquired: false,
365
+ holder: before.holder,
366
+ acquiredAt: before.acquiredAt,
367
+ expiresAt: before.expiresAt
368
+ };
369
+ } catch (err) {
370
+ if (err?.code === 11e3 && attempt < 3) continue;
371
+ throw err;
372
+ }
373
+ },
38
374
  async release(name, holderId) {
39
375
  return !!await model.findOneAndDelete({
40
376
  _id: name,
@@ -44,7 +380,7 @@ function createMongoLockAdapter(options = {}) {
44
380
  async inspect(name) {
45
381
  const doc = await model.findById(name).lean();
46
382
  if (!doc) return null;
47
- if (doc.expiresAt.getTime() <= Date.now()) return null;
383
+ if (doc.expiresAt.getTime() <= nowFn().getTime()) return null;
48
384
  return {
49
385
  name,
50
386
  holder: doc.holder,
@@ -88,5 +424,4 @@ function getOrCreateModel(conn, modelName, collectionName) {
88
424
  schema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });
89
425
  return conn.model(modelName, schema, collectionName);
90
426
  }
91
- //#endregion
92
- export { createMongoLockAdapter };
427
+ export { createMongoDailyLedger, createMongoLockAdapter };
@@ -1,4 +1,4 @@
1
- //#region src/utils/logger.ts
1
+ // src/utils/logger.ts
2
2
  const noop = () => {};
3
3
  let current = {
4
4
  warn: console.warn.bind(console),
@@ -22,5 +22,4 @@ function configureLogger(config) {
22
22
  function warn(message, ...args) {
23
23
  current.warn(message, ...args);
24
24
  }
25
- //#endregion
26
25
  export { warn as n, configureLogger as t };
@@ -1,5 +1,5 @@
1
- import { t as createError } from "./error-BuByZwKU.mjs";
2
- //#region src/plugins/batch-operations.plugin.ts
1
+ import { t as createError } from "./error-DjdmPLWO.mjs";
2
+ // src/plugins/batch-operations.plugin.ts
3
3
  /**
4
4
  * Batch operations plugin.
5
5
  *
@@ -123,8 +123,7 @@ function batchOperationsPlugin() {
123
123
  }
124
124
  };
125
125
  }
126
- //#endregion
127
- //#region src/plugins/method-registry.plugin.ts
126
+ // src/plugins/method-registry.plugin.ts
128
127
  /**
129
128
  * Method registry plugin that enables dynamic method registration
130
129
  */
@@ -158,5 +157,4 @@ function methodRegistryPlugin() {
158
157
  }
159
158
  };
160
159
  }
161
- //#endregion
162
160
  export { batchOperationsPlugin as n, methodRegistryPlugin as t };
@@ -1,5 +1,5 @@
1
1
  import mongoose from "mongoose";
2
- //#region src/model-registry.ts
2
+ // src/model-registry.ts
3
3
  /**
4
4
  * Model registration — the ONE place `mongoose.models` is touched.
5
5
  *
@@ -63,5 +63,4 @@ function defineModel(name, schema, options) {
63
63
  }
64
64
  return target ? target.model(name, schema(), options.collection) : mongoose.model(name, schema(), options.collection);
65
65
  }
66
- //#endregion
67
66
  export { defineModel as n, isModelRegistered as r, ModelCollisionError as t };
@@ -1,7 +1,7 @@
1
- import { r as isObjectIdInstance } from "./id-resolution-BqDrk0S2.mjs";
1
+ import { r as isObjectIdInstance } from "./id-resolution-Cqh49d9J.mjs";
2
2
  import mongoose from "mongoose";
3
3
  import { applyFieldRules, collectFieldsToOmit } from "@classytic/repo-core/schema";
4
- //#region src/utils/mongooseToJsonSchema.ts
4
+ // src/utils/mongooseToJsonSchema.ts
5
5
  /**
6
6
  * Mongoose → JSON Schema converter.
7
7
  *
@@ -600,5 +600,4 @@ function buildJsonSchemaForQuery(_tree, options) {
600
600
  for (const [k, v] of Object.entries(filterable)) if (basePagination.properties) basePagination.properties[k] = v && typeof v === "object" && "type" in v ? v : { type: "string" };
601
601
  return basePagination;
602
602
  }
603
- //#endregion
604
603
  export { buildCrudSchemasFromMongooseSchema as n, buildCrudSchemasFromModel as t };
@@ -1,9 +1,8 @@
1
- import { A as ObjectId } from "./pagination-D6psses0.mjs";
2
- import { l as Plugin, p as RepositoryContext } from "./repository-MqAP409e.mjs";
1
+ import { A as ObjectId } from "./pagination-DAUg5zOY.mjs";
2
+ import { l as Plugin, p as RepositoryContext } from "./repository-DzVIwhU2.mjs";
3
3
  import mongoose, { Connection } from "mongoose";
4
4
  import { CacheAdapter as CacheAdapter$1, CacheOptions, RepositoryCacheHandle as RepositoryCacheHandle$1, RepositoryCachePluginOptions, cachePlugin } from "@classytic/repo-core/cache";
5
5
  import { TenantConfig } from "@classytic/repo-core/tenant";
6
- //#region src/plugins/audit-trail.plugin.d.ts
7
6
  /**
8
7
  * Audit write delivery mode.
9
8
  *
@@ -262,8 +261,6 @@ interface AuditTrailMethods {
262
261
  operation?: AuditOperation;
263
262
  }): Promise<AuditQueryResult>;
264
263
  }
265
- //#endregion
266
- //#region src/plugins/custom-id.plugin.d.ts
267
264
  /**
268
265
  * Generator function that produces a unique ID.
269
266
  * Receives the full repository context for conditional logic.
@@ -277,6 +274,45 @@ interface CustomIdOptions {
277
274
  /** Only generate if the field is missing/empty (default: true) */
278
275
  generateOnlyIfEmpty?: boolean;
279
276
  }
277
+ /** Options for {@link createSequenceCounter}. */
278
+ interface SequenceCounterOptions {
279
+ /** Mongoose connection. @default the default `mongoose.connection` */
280
+ connection?: mongoose.Connection;
281
+ /**
282
+ * Collection holding the counters, for hosts with a collection-naming
283
+ * rule (e.g. an app prefix). @default `'_mongokit_counters'`
284
+ */
285
+ collection?: string;
286
+ }
287
+ /**
288
+ * The atomic counter API bound to one connection + collection. Same
289
+ * semantics as {@link getNextSequence} / {@link readSequence} /
290
+ * {@link ensureSequenceAtLeast}, with an optional session on every call.
291
+ */
292
+ interface SequenceCounter {
293
+ /** Atomic `$inc` + upsert; returns the value after the increment. */
294
+ next(counterKey: string, increment?: number, session?: mongoose.ClientSession): Promise<number>;
295
+ /** Current value, or `null` when the counter was never created. */
296
+ read(counterKey: string, session?: mongoose.ClientSession): Promise<number | null>;
297
+ /** Raise to at least `floor` (`$max` + upsert); never lowers. Returns the value after. */
298
+ ensureAtLeast(counterKey: string, floor: number, session?: mongoose.ClientSession): Promise<number>;
299
+ /**
300
+ * Next value of the counter `'<baseKey><sep><civil period>'`, which resets
301
+ * every civil year / month / day in `timeZone`. See {@link getNextPeriodSequence}.
302
+ */
303
+ nextInPeriod(baseKey: string, options: PeriodSequenceOptions): Promise<PeriodSequenceResult>;
304
+ }
305
+ /**
306
+ * Bind the atomic counter API to a connection and, optionally, a
307
+ * collection other than `_mongokit_counters`.
308
+ *
309
+ * @example
310
+ * const counters = createSequenceCounter({ connection, collection: 'app_counters' });
311
+ * await withTransaction(connection, async (session) => {
312
+ * const seq = await counters.next('alert.stream', 1, session);
313
+ * });
314
+ */
315
+ declare function createSequenceCounter(options?: SequenceCounterOptions): SequenceCounter;
280
316
  /**
281
317
  * Atomically increment and return the next sequence value for a given key.
282
318
  * Uses `findOneAndUpdate` with `upsert` + `$inc` — fully atomic even under
@@ -311,7 +347,7 @@ declare function getNextSequence(counterKey: string, increment?: number, connect
311
347
  * Current value of a counter, or `null` when it has never been created.
312
348
  * `null` and `0` differ: only a missing counter needs seeding.
313
349
  */
314
- declare function readSequence(counterKey: string, connection?: mongoose.Connection): Promise<number | null>;
350
+ declare function readSequence(counterKey: string, connection?: mongoose.Connection, session?: mongoose.ClientSession): Promise<number | null>;
315
351
  /**
316
352
  * Raise a counter to at least `floor` — never lowers it. Atomic (`$max` +
317
353
  * upsert), so concurrent seeders converge on the highest floor.
@@ -322,7 +358,77 @@ declare function readSequence(counterKey: string, connection?: mongoose.Connecti
322
358
  *
323
359
  * @returns The counter's value after the raise.
324
360
  */
325
- declare function ensureSequenceAtLeast(counterKey: string, floor: number, connection?: mongoose.Connection): Promise<number>;
361
+ declare function ensureSequenceAtLeast(counterKey: string, floor: number, connection?: mongoose.Connection, session?: mongoose.ClientSession): Promise<number>;
362
+ /** The civil period a period-scoped counter resets on. */
363
+ type SequencePeriod = 'year' | 'month' | 'day';
364
+ /**
365
+ * The civil period of an instant in an IANA time zone: `'2026'` (year),
366
+ * `'2026-03'` (month) or `'2026-03-14'` (day). The period suffix every
367
+ * period-scoped counter key ends with.
368
+ *
369
+ * @example
370
+ * civilPeriodKey(new Date('2026-12-31T21:00:00Z'), 'year', 'Asia/Riyadh'); // '2027'
371
+ * civilPeriodKey(new Date('2026-12-31T21:00:00Z'), 'year', 'UTC'); // '2026'
372
+ */
373
+ declare function civilPeriodKey(at: Date, period: SequencePeriod, timeZone: string): string;
374
+ /** Options for {@link periodCounterKey}, {@link getNextPeriodSequence} and `SequenceCounter.nextInPeriod`. */
375
+ interface PeriodSequenceKeyOptions {
376
+ /** Civil period the counter resets on. */
377
+ period: SequencePeriod;
378
+ /** IANA time zone whose civil calendar decides the period (e.g. `'Asia/Riyadh'`, `'UTC'`). */
379
+ timeZone: string;
380
+ /**
381
+ * The instant whose civil date picks the period (e.g. the record's
382
+ * `reportedAt`, so a backdated record numbers in its own year).
383
+ * @default now
384
+ */
385
+ at?: Date;
386
+ /**
387
+ * Joins the base key and the period: `'<base><sep><period>'`.
388
+ * Pass `'.'` to keep counters an app already created as `occ.occurrence.2026`.
389
+ * @default ':'
390
+ */
391
+ keySeparator?: string;
392
+ }
393
+ /** Options for {@link getNextPeriodSequence} / `SequenceCounter.nextInPeriod`. */
394
+ interface PeriodSequenceOptions extends PeriodSequenceKeyOptions {
395
+ /** @default 1 */
396
+ increment?: number;
397
+ /** Client session: the bump joins the caller's transaction. */
398
+ session?: mongoose.ClientSession;
399
+ }
400
+ /** A period-scoped counter value plus the period it was issued in. */
401
+ interface PeriodSequenceResult {
402
+ /** Value after the increment; `1` for the first value of a new period. */
403
+ seq: number;
404
+ /** The civil period (`'2026'`, `'2026-03'`, `'2026-03-14'`), for formatting the id. */
405
+ period: string;
406
+ /** The counter document's key (`'<base><sep><period>'`), for `read` / `ensureAtLeast`. */
407
+ counterKey: string;
408
+ }
409
+ /**
410
+ * The counter key and civil period for a base key at an instant. Use it to
411
+ * `read` / `ensureAtLeast` the same counter `getNextPeriodSequence` bumps.
412
+ */
413
+ declare function periodCounterKey(baseKey: string, options: PeriodSequenceKeyOptions): {
414
+ counterKey: string;
415
+ period: string;
416
+ };
417
+ /**
418
+ * Atomically issue the next value of a counter that resets every civil
419
+ * year / month / day in an IANA time zone. The period is derived from `at`
420
+ * (default now) in `timeZone`, so every replica agrees on it whatever its
421
+ * `TZ`, and DST never moves a boundary.
422
+ *
423
+ * @example Yearly reference `OC-2026-000123` in Riyadh
424
+ * const { seq, period } = await getNextPeriodSequence('occ.occurrence', {
425
+ * period: 'year', timeZone: 'Asia/Riyadh', at: reportedAt, connection, session,
426
+ * });
427
+ * const ref = `OC-${period}-${String(seq).padStart(6, '0')}`;
428
+ */
429
+ declare function getNextPeriodSequence(baseKey: string, options: PeriodSequenceOptions & {
430
+ connection?: mongoose.Connection;
431
+ }): Promise<PeriodSequenceResult>;
326
432
  interface SequentialIdOptions {
327
433
  /** Prefix string (e.g., 'INV', 'ORD') */
328
434
  prefix: string;
@@ -337,6 +443,11 @@ interface SequentialIdOptions {
337
443
  separator?: string;
338
444
  /** Custom counter key override (default: model.modelName) */
339
445
  counterKey?: string;
446
+ /**
447
+ * Counter collection, same as `createSequenceCounter({ collection })`
448
+ * (for hosts with a collection-naming rule). @default `'_mongokit_counters'`
449
+ */
450
+ counterCollection?: string;
340
451
  }
341
452
  /**
342
453
  * Generator: Simple sequential counter.
@@ -413,6 +524,30 @@ interface DateSequentialIdOptions {
413
524
  * stays invisible until one host is configured otherwise.
414
525
  */
415
526
  timezone?: 'local' | 'utc';
527
+ /**
528
+ * IANA time zone whose civil calendar decides the period (e.g.
529
+ * `'Asia/Riyadh'`). Wins over `timezone`. Read through Intl, so every
530
+ * replica agrees on the boundary whatever its `TZ`, and DST never moves it.
531
+ * Invalid zones throw when the generator is built.
532
+ */
533
+ timeZone?: string;
534
+ /**
535
+ * The instant whose civil date picks the period, e.g.
536
+ * `(ctx) => ctx.data?.reportedAt` so a backdated record numbers in its own
537
+ * period. `undefined` / `null` falls back to now; any other non-Date throws.
538
+ * @default now
539
+ */
540
+ at?: (context: RepositoryContext) => Date | undefined | null;
541
+ /**
542
+ * Base counter key (the tenant id, under `scope: 'tenant'`, and the period
543
+ * are appended). @default `model.modelName`
544
+ */
545
+ counterKey?: string;
546
+ /**
547
+ * Counter collection, same as `createSequenceCounter({ collection })`
548
+ * (for hosts with a collection-naming rule). @default `'_mongokit_counters'`
549
+ */
550
+ counterCollection?: string;
416
551
  }
417
552
  /**
418
553
  * Generator: Date-partitioned sequential counter.
@@ -493,8 +628,6 @@ declare function prefixedId(options: PrefixedIdOptions): IdGenerator;
493
628
  * ```
494
629
  */
495
630
  declare function customIdPlugin(options: CustomIdOptions): Plugin;
496
- //#endregion
497
- //#region src/plugins/multi-tenant.plugin.d.ts
498
631
  /**
499
632
  * Static tenant fields are picked from `@classytic/repo-core/tenant` so the
500
633
  * vocabulary (`tenantField`, `contextKey`, `required`, `fieldType`) stays
@@ -653,5 +786,4 @@ declare function adminBypass(options: {
653
786
  roleField?: string;
654
787
  adminRoles: readonly string[];
655
788
  }): (context: RepositoryContext, operation: string) => boolean;
656
- //#endregion
657
- export { EnsureAuditTrailReadyOptions as A, AuditQueryOptions as C, AuditTrailOptions as D, AuditTrailMode as E, ensureAuditTrailReady as M, AuditTrailQuery as O, AuditOperation as S, AuditTrailMethods as T, CacheOptions as _, DateSequentialIdOptions as a, cachePlugin as b, SequentialIdOptions as c, ensureSequenceAtLeast as d, getNextSequence as f, CacheAdapter$1 as g, sequentialId as h, CustomIdOptions as i, auditTrailPlugin as j, AuditTrailQueryOptions as k, customIdPlugin as l, readSequence as m, adminBypass as n, IdGenerator as o, prefixedId as p, multiTenantPlugin as r, PrefixedIdOptions as s, MultiTenantOptions as t, dateSequentialId as u, RepositoryCacheHandle$1 as v, AuditQueryResult as w, AuditEntry as x, RepositoryCachePluginOptions as y };
789
+ export { cachePlugin as A, EnsureAuditTrailReadyOptions as B, prefixedId as C, CacheOptions as D, CacheAdapter$1 as E, AuditTrailMethods as F, ensureAuditTrailReady as H, AuditTrailMode as I, AuditTrailOptions as L, AuditOperation as M, AuditQueryOptions as N, RepositoryCacheHandle$1 as O, AuditQueryResult as P, AuditTrailQuery as R, periodCounterKey as S, sequentialId as T, auditTrailPlugin as V, customIdPlugin as _, DateSequentialIdOptions as a, getNextPeriodSequence as b, PeriodSequenceOptions as c, SequenceCounter as d, SequenceCounterOptions as f, createSequenceCounter as g, civilPeriodKey as h, CustomIdOptions as i, AuditEntry as j, RepositoryCachePluginOptions as k, PeriodSequenceResult as l, SequentialIdOptions as m, adminBypass as n, IdGenerator as o, SequencePeriod as p, multiTenantPlugin as r, PeriodSequenceKeyOptions as s, MultiTenantOptions as t, PrefixedIdOptions as u, dateSequentialId as v, readSequence as w, getNextSequence as x, ensureSequenceAtLeast as y, AuditTrailQueryOptions as z };