@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,14 +1,14 @@
1
- import { n as collectIds, r as idChunks, t as DEFAULT_ID_CHUNK } from "./id-chunks-BmAwsqNk.mjs";
2
- import { a as ALL_OPERATIONS, s as OP_REGISTRY } from "./Repository-Dj6xqPxJ.mjs";
3
- import { w as upsert } from "./update-WgeAUJ_I.mjs";
4
- import { n as warn } from "./logger-K7dPmu2U.mjs";
5
- import { t as createError } from "./error-BuByZwKU.mjs";
6
- import { c as tenantContextKeysOf, o as forwardScope } from "./timestamp.plugin-DpOWavVI.mjs";
7
- import { r as getFieldsForUser } from "./field-selection-FSQFLpsU.mjs";
1
+ import { t as createError } from "./error-DjdmPLWO.mjs";
2
+ import { a as ALL_OPERATIONS, f as readRetentionPurge, p as retentionPurgeEvent, s as OP_REGISTRY } from "./Repository-DheA7oW-.mjs";
3
+ import { n as collectIds, r as idChunks, t as DEFAULT_ID_CHUNK } from "./id-chunks-DH-RAZWG.mjs";
4
+ import { w as upsert } from "./update-CYaJMu6j.mjs";
5
+ import { n as warn } from "./logger-hbWOlj_X.mjs";
6
+ import { c as tenantContextKeysOf, o as forwardScope } from "./timestamp.plugin-B-IJrlYb.mjs";
7
+ import { r as getFieldsForUser } from "./field-selection-DPFaFw9Y.mjs";
8
8
  import mongoose from "mongoose";
9
9
  import { HOOK_PRIORITY } from "@classytic/repo-core/hooks";
10
10
  import { AsyncLocalStorage } from "node:async_hooks";
11
- //#region src/plugins/aggregate-helpers.plugin.ts
11
+ // src/plugins/aggregate-helpers.plugin.ts
12
12
  /**
13
13
  * Aggregate helpers plugin
14
14
  *
@@ -71,8 +71,7 @@ function aggregateHelpersPlugin() {
71
71
  }
72
72
  };
73
73
  }
74
- //#endregion
75
- //#region src/plugins/append-only.plugin.ts
74
+ // src/plugins/append-only.plugin.ts
76
75
  /**
77
76
  * Append-Only Plugin
78
77
  *
@@ -114,6 +113,20 @@ function aggregateHelpersPlugin() {
114
113
  * maintenance (backfill repair, migration), grep-auditable at the
115
114
  * call site.
116
115
  *
116
+ * **Retention purge** (`allowPurge: true`): the one public path for a
117
+ * retention job to physically remove rows. Pass
118
+ * `retentionPurge: { reason, actor? }` with `mode: 'hard'` on `delete` /
119
+ * `deleteMany`, or run `purgeByFilter(filter, { type: 'hard' },
120
+ * { retentionPurge })` / `purgeByField(...)`, which forward it to every
121
+ * chunk. Each successful call emits `after:append-only-purge`
122
+ * ({@link AppendOnlyPurgeEvent}) with the reason, actor, target and
123
+ * `deletedCount`. Without `allowPurge`, a missing reason or `mode: 'hard'`
124
+ * the call is refused; on any other op the option is ignored (the fence
125
+ * applies). The same option is honoured by `immutableStatesPlugin`, so a
126
+ * repository with both plugins purges through one call. Prefer it over
127
+ * `bypassAppendOnly` for retention: it is delete-only and carries a reason.
128
+ * `allowArchive: true` admits only `archiveByFilter`'s attested chunk deletes.
129
+ *
117
130
  * `skipPlugins` is deliberately IGNORED — same policy as tenant scope
118
131
  * and soft delete: integrity fences don't have a convenience bypass.
119
132
  *
@@ -125,7 +138,14 @@ function aggregateHelpersPlugin() {
125
138
  * await events.create({ ... }); // OK
126
139
  * await events.update(id, { ... }); // throws 405 APPEND_ONLY_VIOLATION
127
140
  * await events.deleteMany({ ... }, { bypassAppendOnly: true }); // audited bypass
141
+ *
142
+ * @example Retention purge (construct with `appendOnlyPlugin({ allowPurge: true })`)
143
+ * events.on('after:append-only-purge', (e) => audit('retention.purge', e));
144
+ * await events.purgeByFilter({ at: { $lt: cutoff } }, { type: 'hard' }, {
145
+ * retentionPurge: { reason: 'retention: 1095 days', actor: 'job:retention' },
146
+ * });
128
147
  */
148
+ const PURGE_OPS = /* @__PURE__ */ new Set(["delete", "deleteMany"]);
129
149
  const ALWAYS_ALLOWED = /* @__PURE__ */ new Set(["create", "createMany"]);
130
150
  function appendOnlyPlugin(options = {}) {
131
151
  const allowed = /* @__PURE__ */ new Set([...ALWAYS_ALLOWED, ...options.allow ?? []]);
@@ -133,9 +153,22 @@ function appendOnlyPlugin(options = {}) {
133
153
  name: "appendOnly",
134
154
  apply(repo) {
135
155
  const modelName = repo.Model?.modelName ?? "model";
156
+ const purgeGuard = {
157
+ plugin: "appendOnlyPlugin",
158
+ allowPurge: options.allowPurge === true,
159
+ allowArchive: options.allowArchive === true
160
+ };
161
+ const priority = options.priority ?? HOOK_PRIORITY.POLICY;
136
162
  for (const [op, meta] of Object.entries(OP_REGISTRY)) {
137
- if (!meta.mutates || allowed.has(op)) continue;
163
+ if (!meta.mutates) continue;
164
+ if (allowed.has(op)) {
165
+ if (PURGE_OPS.has(op)) repo.on(`before:${op}`, (context) => {
166
+ readRetentionPurge(context, op, purgeGuard);
167
+ }, { priority });
168
+ continue;
169
+ }
138
170
  repo.on(`before:${op}`, (context) => {
171
+ if (PURGE_OPS.has(op) && readRetentionPurge(context, op, purgeGuard)) return;
139
172
  if (context.bypassAppendOnly === true) {
140
173
  repo.emit("after:append-only-bypass", {
141
174
  operation: op,
@@ -150,13 +183,16 @@ function appendOnlyPlugin(options = {}) {
150
183
  model: modelName
151
184
  }
152
185
  });
153
- }, { priority: options.priority ?? HOOK_PRIORITY.POLICY });
186
+ }, { priority });
154
187
  }
188
+ if (options.allowPurge || options.allowArchive) for (const op of ["delete", "deleteMany"]) repo.on(`after:${op}`, async (payload) => {
189
+ const event = retentionPurgeEvent(op, payload.context, payload.result);
190
+ if (event) await repo.emitAsync("after:append-only-purge", event);
191
+ });
155
192
  }
156
193
  };
157
194
  }
158
- //#endregion
159
- //#region src/plugins/cascade.plugin.ts
195
+ // src/plugins/cascade.plugin.ts
160
196
  /**
161
197
  * Cascade Delete Plugin
162
198
  *
@@ -477,8 +513,7 @@ async function runCascades(relations, fn, parallel, session) {
477
513
  }
478
514
  } else for (const relation of relations) await fn(relation);
479
515
  }
480
- //#endregion
481
- //#region src/plugins/elastic.plugin.ts
516
+ // src/plugins/elastic.plugin.ts
482
517
  function elasticSearchPlugin(options) {
483
518
  return {
484
519
  name: "elastic-search",
@@ -549,8 +584,7 @@ function elasticSearchPlugin(options) {
549
584
  }
550
585
  };
551
586
  }
552
- //#endregion
553
- //#region src/plugins/field-filter.plugin.ts
587
+ // src/plugins/field-filter.plugin.ts
554
588
  /**
555
589
  * Field filter plugin that restricts fields based on user context
556
590
  *
@@ -582,8 +616,7 @@ function fieldFilterPlugin(fieldPreset) {
582
616
  }
583
617
  };
584
618
  }
585
- //#endregion
586
- //#region src/plugins/idempotency-store.plugin.ts
619
+ // src/plugins/idempotency-store.plugin.ts
587
620
  function randomToken() {
588
621
  const bytes = /* @__PURE__ */ new Uint8Array(16);
589
622
  globalThis.crypto.getRandomValues(bytes);
@@ -735,8 +768,7 @@ function idempotencyStorePlugin(options = {}) {
735
768
  }
736
769
  };
737
770
  }
738
- //#endregion
739
- //#region src/plugins/immutable-states.plugin.ts
771
+ // src/plugins/immutable-states.plugin.ts
740
772
  /** Ops this plugin has an explicit strategy for (see module doc). */
741
773
  const STRATEGY_OPS = /* @__PURE__ */ new Set([
742
774
  "create",
@@ -778,10 +810,12 @@ function immutableStatesPlugin(options) {
778
810
  if (options.tenantField && ctx.query && options.tenantField in ctx.query) q[options.tenantField] = ctx.query[options.tenantField];
779
811
  return q;
780
812
  };
813
+ /** The call's session: every guard read runs in it, so a row frozen earlier in the same transaction counts. */
814
+ const sessionOf = (ctx) => ctx.session ?? null;
781
815
  /** Raw-Model read — never re-enters the repo hook pipeline. */
782
816
  const readStateById = async (ctx) => {
783
817
  if (ctx.id === void 0 || ctx.id === null) return void 0;
784
- const doc = await Model.findOne(scoped({ _id: ctx.id }, ctx)).select({ [field]: 1 }).lean();
818
+ const doc = await Model.findOne(scoped({ _id: ctx.id }, ctx)).select({ [field]: 1 }).session(sessionOf(ctx)).lean();
785
819
  return doc ? getState(doc) : void 0;
786
820
  };
787
821
  const guardById = (op) => async (ctx) => {
@@ -793,19 +827,42 @@ function immutableStatesPlugin(options) {
793
827
  state
794
828
  });
795
829
  };
830
+ const purgeGuard = {
831
+ plugin: "immutableStatesPlugin",
832
+ allowPurge: options.allowPurge === true,
833
+ allowArchive: options.allowArchive === true
834
+ };
835
+ const purgeRequest = (op, ctx) => readRetentionPurge(ctx, op, purgeGuard);
796
836
  for (const op of [
797
837
  "update",
798
- "delete",
799
838
  "claimVersion",
800
839
  "restore"
801
840
  ]) repo.on(`before:${op}`, guardById(op));
841
+ const guardDelete = guardById("delete");
842
+ repo.on("before:delete", async (ctx) => {
843
+ if (ctx[internalFlag]) return;
844
+ if (purgeRequest("delete", ctx)) return;
845
+ await guardDelete(ctx);
846
+ });
847
+ const emitPurge = async (operation, context, result) => {
848
+ if (!(options.allowPurge || options.allowArchive) || context[internalFlag]) return;
849
+ const base = retentionPurgeEvent(operation, context, result);
850
+ if (!base) return;
851
+ const event = {
852
+ ...base,
853
+ states: [...frozen]
854
+ };
855
+ await repo.emitAsync("after:immutable-states-purge", event);
856
+ };
857
+ repo.on("after:delete", (payload) => emitPurge("delete", payload.context, payload.result));
858
+ repo.on("after:deleteMany", (payload) => emitPurge("deleteMany", payload.context, payload.result));
802
859
  repo.on("before:findOneAndUpdate", async (ctx) => {
803
860
  if (ctx[internalFlag]) return;
804
861
  const filter = ctx.query ?? {};
805
862
  const doc = await Model.findOne(filter).select({
806
863
  [field]: 1,
807
864
  _id: 1
808
- }).lean();
865
+ }).session(sessionOf(ctx)).lean();
809
866
  if (!doc) return;
810
867
  const state = getState(doc);
811
868
  if (state !== void 0 && frozen.has(state)) throwViolation({
@@ -817,7 +874,7 @@ function immutableStatesPlugin(options) {
817
874
  const guardMany = (op) => async (ctx) => {
818
875
  if (ctx[internalFlag]) return;
819
876
  const filter = ctx.query ?? {};
820
- const hit = await Model.exists({ $and: [filter, { [field]: { $in: [...frozen] } }] });
877
+ const hit = await Model.exists({ $and: [filter, { [field]: { $in: [...frozen] } }] }).session(sessionOf(ctx));
821
878
  if (hit) throwViolation({
822
879
  id: hit._id,
823
880
  operation: op,
@@ -825,7 +882,12 @@ function immutableStatesPlugin(options) {
825
882
  });
826
883
  };
827
884
  repo.on("before:updateMany", guardMany("updateMany"));
828
- repo.on("before:deleteMany", guardMany("deleteMany"));
885
+ const guardDeleteMany = guardMany("deleteMany");
886
+ repo.on("before:deleteMany", async (ctx) => {
887
+ if (ctx[internalFlag]) return;
888
+ if (purgeRequest("deleteMany", ctx)) return;
889
+ await guardDeleteMany(ctx);
890
+ });
829
891
  repo.on("before:bulkUpsert", async (ctx) => {
830
892
  if (ctx[internalFlag]) return;
831
893
  const filters = (ctx.operations ?? []).flatMap((op) => op.updateOne?.filter ? [op.updateOne.filter] : []);
@@ -881,8 +943,10 @@ function immutableStatesPlugin(options) {
881
943
  }
882
944
  };
883
945
  }
884
- //#endregion
885
- //#region src/plugins/lease.plugin.ts
946
+ // src/plugins/lease.plugin.ts
947
+ function isNonEmpty(obj) {
948
+ return obj !== void 0 && obj !== null && Object.keys(obj).length > 0;
949
+ }
886
950
  function leasePlugin(options = {}) {
887
951
  const statusField = options.statusField ?? "status";
888
952
  const leasedByField = options.leasedByField ?? "leasedBy";
@@ -891,6 +955,57 @@ function leasePlugin(options = {}) {
891
955
  const processingStatus = options.processingStatus ?? "processing";
892
956
  const doneStatus = options.doneStatus ?? "done";
893
957
  const sort = options.sort ?? { createdAt: 1 };
958
+ const nowFn = options.nowFn ?? (() => /* @__PURE__ */ new Date());
959
+ const deadLeaseStatuses = options.deadLeaseStatuses ?? [processingStatus];
960
+ const attemptsField = options.attemptsField;
961
+ const maxAttempts = options.maxAttempts;
962
+ if (deadLeaseStatuses.length === 0) throw new Error("leasePlugin: deadLeaseStatuses must name at least one status");
963
+ if (maxAttempts !== void 0) {
964
+ if (attemptsField === void 0) throw new Error("leasePlugin: maxAttempts requires attemptsField");
965
+ if (!Number.isInteger(maxAttempts) || maxAttempts < 1) throw new Error("leasePlugin: maxAttempts must be a positive integer");
966
+ }
967
+ const exhaustedStatus = options.exhaustedStatus;
968
+ const exhaustedAtField = options.exhaustedAtField;
969
+ const assertTerminal = (where, status) => {
970
+ if (status === pendingStatus || status === processingStatus || deadLeaseStatuses.includes(status)) throw new Error(`leasePlugin: ${where} "${status}" must differ from the pending, processing and dead-lease statuses`);
971
+ };
972
+ if (exhaustedStatus !== void 0) {
973
+ if (maxAttempts === void 0) throw new Error("leasePlugin: exhaustedStatus requires maxAttempts");
974
+ assertTerminal("exhaustedStatus", exhaustedStatus);
975
+ }
976
+ if (exhaustedAtField !== void 0 && maxAttempts === void 0) throw new Error("leasePlugin: exhaustedAtField requires maxAttempts");
977
+ /**
978
+ * The claim condition: pending (narrowed by `pendingFilter`) OR a dead
979
+ * lease, a row still in a dead-lease status whose `leaseExpiresAt <
980
+ * now` (worker crashed mid-lease). The status guard on the dead arm
981
+ * keeps a cancelled / terminal row with a stale expiry from being
982
+ * revived. `lease()` adds "under the attempts cap"; `reapExhausted()`
983
+ * adds "at or over it".
984
+ */
985
+ const claimable = (now, pendingSource) => {
986
+ const pendingFilter = typeof pendingSource === "function" ? pendingSource(now) : pendingSource;
987
+ return { $or: [isNonEmpty(pendingFilter) ? { $and: [{ [statusField]: pendingStatus }, pendingFilter] } : { [statusField]: pendingStatus }, {
988
+ [statusField]: deadLeaseStatuses.length === 1 ? deadLeaseStatuses[0] : { $in: deadLeaseStatuses },
989
+ [leaseExpiresAtField]: { $lt: now }
990
+ }] };
991
+ };
992
+ /** The caller's filter as given, with `conditions` ANDed beside it. */
993
+ const withConditions = (method, baseFilter, conditions) => {
994
+ if (baseFilter.$and !== void 0 && !Array.isArray(baseFilter.$and)) throw createError(400, `leasePlugin.${method}: filter.$and must be an array`);
995
+ const callerAnd = baseFilter.$and ?? [];
996
+ return {
997
+ ...baseFilter,
998
+ $and: [...callerAnd, ...conditions]
999
+ };
1000
+ };
1001
+ const resolveNow = (method, perCall) => {
1002
+ const now = perCall ?? nowFn();
1003
+ if (!(now instanceof Date) || Number.isNaN(now.getTime())) throw createError(400, `leasePlugin.${method}: now must be a valid Date`);
1004
+ return now;
1005
+ };
1006
+ const assertLeasedBy = (method, leasedBy) => {
1007
+ if (typeof leasedBy !== "string" || leasedBy.length === 0) throw createError(400, `leasePlugin.${method}: leasedBy must be a non-empty string`);
1008
+ };
894
1009
  return {
895
1010
  name: "lease",
896
1011
  apply(repo) {
@@ -904,18 +1019,20 @@ function leasePlugin(options = {}) {
904
1019
  */
905
1020
  repo.registerMethod("lease", async function(opts) {
906
1021
  if (typeof opts?.leaseFor !== "number" || opts.leaseFor <= 0) throw createError(400, "leasePlugin.lease: leaseFor must be a positive number (ms)");
907
- if (typeof opts.leasedBy !== "string" || opts.leasedBy.length === 0) throw createError(400, "leasePlugin.lease: leasedBy must be a non-empty string");
908
- const now = /* @__PURE__ */ new Date();
1022
+ assertLeasedBy("lease", opts.leasedBy);
1023
+ const now = resolveNow("lease", opts.now);
909
1024
  const leaseUntil = new Date(now.getTime() + opts.leaseFor);
910
- const claimFilter = {
911
- ...opts.filter ?? {},
912
- $or: [{ [statusField]: pendingStatus }, { [leaseExpiresAtField]: { $lt: now } }]
913
- };
914
- return this.findOneAndUpdate(claimFilter, { $set: {
915
- [statusField]: processingStatus,
916
- [leasedByField]: opts.leasedBy,
917
- [leaseExpiresAtField]: leaseUntil
918
- } }, {
1025
+ const conditions = [claimable(now, opts.pendingFilter ?? options.pendingFilter)];
1026
+ if (attemptsField !== void 0 && maxAttempts !== void 0) conditions.push({ [attemptsField]: { $not: { $gte: maxAttempts } } });
1027
+ const claimFilter = withConditions("lease", opts.filter ?? {}, conditions);
1028
+ return this.findOneAndUpdate(claimFilter, {
1029
+ $set: {
1030
+ [statusField]: processingStatus,
1031
+ [leasedByField]: opts.leasedBy,
1032
+ [leaseExpiresAtField]: leaseUntil
1033
+ },
1034
+ ...attemptsField !== void 0 ? { $inc: { [attemptsField]: 1 } } : {}
1035
+ }, {
919
1036
  ...opts.options ?? {},
920
1037
  sort,
921
1038
  returnDocument: "after"
@@ -923,11 +1040,13 @@ function leasePlugin(options = {}) {
923
1040
  });
924
1041
  repo.registerMethod("extend", async function(id, opts) {
925
1042
  if (typeof opts?.leaseFor !== "number" || opts.leaseFor <= 0) throw createError(400, "leasePlugin.extend: leaseFor must be a positive number (ms)");
926
- const now = /* @__PURE__ */ new Date();
1043
+ assertLeasedBy("extend", opts.leasedBy);
1044
+ const now = resolveNow("extend", opts.now);
927
1045
  const leaseUntil = new Date(now.getTime() + opts.leaseFor);
928
1046
  const idField = this.idField || "_id";
929
1047
  return this.findOneAndUpdate({
930
1048
  [idField]: id,
1049
+ [statusField]: processingStatus,
931
1050
  [leasedByField]: opts.leasedBy,
932
1051
  [leaseExpiresAtField]: { $gt: now }
933
1052
  }, { $set: { [leaseExpiresAtField]: leaseUntil } }, {
@@ -938,10 +1057,11 @@ function leasePlugin(options = {}) {
938
1057
  repo.registerMethod("release", async function(id, opts) {
939
1058
  if (typeof opts?.leasedBy !== "string" || opts.leasedBy.length === 0) throw createError(400, "leasePlugin.release: leasedBy must be a non-empty string (the holder finalising the lease)");
940
1059
  const finalStatus = opts.finalStatus ?? doneStatus;
941
- const now = /* @__PURE__ */ new Date();
1060
+ const now = resolveNow("release", opts.now);
942
1061
  const idField = this.idField || "_id";
943
1062
  return this.findOneAndUpdate({
944
1063
  [idField]: id,
1064
+ [statusField]: processingStatus,
945
1065
  [leasedByField]: opts.leasedBy,
946
1066
  [leaseExpiresAtField]: { $gt: now }
947
1067
  }, {
@@ -955,11 +1075,52 @@ function leasePlugin(options = {}) {
955
1075
  returnDocument: "after"
956
1076
  });
957
1077
  });
1078
+ repo.registerMethod("reapExhausted", async function(opts = {}) {
1079
+ if (attemptsField === void 0 || maxAttempts === void 0) throw createError(400, "leasePlugin.reapExhausted: requires maxAttempts");
1080
+ const status = opts.status ?? exhaustedStatus;
1081
+ if (typeof status !== "string" || status.length === 0) throw createError(400, "leasePlugin.reapExhausted: pass a status, or configure exhaustedStatus");
1082
+ if (opts.status !== void 0) try {
1083
+ assertTerminal("reapExhausted status", status);
1084
+ } catch (err) {
1085
+ throw createError(400, err.message);
1086
+ }
1087
+ const limit = opts.limit ?? 100;
1088
+ if (!Number.isInteger(limit) || limit < 1) throw createError(400, "leasePlugin.reapExhausted: limit must be a positive integer");
1089
+ const now = resolveNow("reapExhausted", opts.now);
1090
+ const exhausted = withConditions("reapExhausted", opts.filter ?? {}, [claimable(now, opts.pendingFilter ?? options.pendingFilter), { [attemptsField]: { $gte: maxAttempts } }]);
1091
+ const idField = this.idField || "_id";
1092
+ const candidates = await this.findAll.call(this, exhausted, {
1093
+ ...opts.options ?? {},
1094
+ sort,
1095
+ limit,
1096
+ lean: true
1097
+ });
1098
+ const reaped = [];
1099
+ for (const row of candidates) {
1100
+ const moved = await this.findOneAndUpdate({
1101
+ ...exhausted,
1102
+ [idField]: row[idField]
1103
+ }, {
1104
+ $set: {
1105
+ [statusField]: status,
1106
+ ...exhaustedAtField !== void 0 ? { [exhaustedAtField]: now } : {}
1107
+ },
1108
+ $unset: {
1109
+ [leasedByField]: "",
1110
+ [leaseExpiresAtField]: ""
1111
+ }
1112
+ }, {
1113
+ ...opts.options ?? {},
1114
+ returnDocument: "after"
1115
+ });
1116
+ if (moved) reaped.push(moved);
1117
+ }
1118
+ return reaped;
1119
+ });
958
1120
  }
959
1121
  };
960
1122
  }
961
- //#endregion
962
- //#region src/plugins/mongo-operations.plugin.ts
1123
+ // src/plugins/mongo-operations.plugin.ts
963
1124
  /**
964
1125
  * MongoDB Operations Plugin
965
1126
  *
@@ -1196,8 +1357,7 @@ function mongoOperationsPlugin() {
1196
1357
  }
1197
1358
  };
1198
1359
  }
1199
- //#endregion
1200
- //#region src/plugins/names.ts
1360
+ // src/plugins/names.ts
1201
1361
  /**
1202
1362
  * Canonical identifiers exposed by mongokit's bundled plugins.
1203
1363
  *
@@ -1229,8 +1389,7 @@ const MONGOKIT_PLUGIN_NAMES = {
1229
1389
  timestamp: "timestamp",
1230
1390
  validationChain: "validation-chain"
1231
1391
  };
1232
- //#endregion
1233
- //#region src/plugins/observability.plugin.ts
1392
+ // src/plugins/observability.plugin.ts
1234
1393
  /**
1235
1394
  * Observability Plugin
1236
1395
  *
@@ -1305,8 +1464,7 @@ function observabilityPlugin(options) {
1305
1464
  }
1306
1465
  };
1307
1466
  }
1308
- //#endregion
1309
- //#region src/plugins/subdocument.plugin.ts
1467
+ // src/plugins/subdocument.plugin.ts
1310
1468
  /**
1311
1469
  * Subdocument plugin for managing nested arrays
1312
1470
  *
@@ -1375,8 +1533,7 @@ function subdocumentPlugin() {
1375
1533
  }
1376
1534
  };
1377
1535
  }
1378
- //#endregion
1379
- //#region src/plugins/tenant-context.ts
1536
+ // src/plugins/tenant-context.ts
1380
1537
  /**
1381
1538
  * Request-scoped tenant context via AsyncLocalStorage.
1382
1539
  *
@@ -1426,8 +1583,7 @@ function createTenantContext() {
1426
1583
  }
1427
1584
  };
1428
1585
  }
1429
- //#endregion
1430
- //#region src/plugins/validation-chain.plugin.ts
1586
+ // src/plugins/validation-chain.plugin.ts
1431
1587
  /**
1432
1588
  * Validation chain plugin
1433
1589
  *
@@ -1576,5 +1732,4 @@ function uniqueField(field, errorMessage) {
1576
1732
  }
1577
1733
  };
1578
1734
  }
1579
- //#endregion
1580
1735
  export { cascadePlugin as _, uniqueField as a, subdocumentPlugin as c, mongoOperationsPlugin as d, leasePlugin as f, elasticSearchPlugin as g, fieldFilterPlugin as h, requireField as i, observabilityPlugin as l, idempotencyStorePlugin as m, blockIf as n, validationChainPlugin as o, immutableStatesPlugin as p, immutableField as r, createTenantContext as s, autoInject as t, MONGOKIT_PLUGIN_NAMES as u, appendOnlyPlugin as v, aggregateHelpersPlugin as y };
package/llms.txt ADDED
@@ -0,0 +1,33 @@
1
+ # @classytic/mongokit
2
+
3
+ Mongoose 9 repositories implementing repo-core StandardRepo. Skill: skills/mongokit/SKILL.md. Peers: @classytic/repo-core, mongoose.
4
+
5
+ | Need | Use |
6
+ |---|---|
7
+ | Register a model once, safely | `defineModel`, `defineModels` |
8
+ | Repository with tenant, soft delete, timestamps, cache, audit | `createRepository` |
9
+ | List page (offset or keyset) | `repo.getAll({ mode, page, after, sort })`, rows in `data` |
10
+ | HTTP query string to repo params | `QueryParser` |
11
+ | Status change with history and race diagnosis | `repo.applyTransition` with a `TransitionMachine` |
12
+ | Optimistic lock | `repo.claimVersion` |
13
+ | Atomic multi-write | `withTransaction`, `batchTransaction` |
14
+ | Duplicate key error | `isDuplicateKeyError` |
15
+ | Search as you type | `searchMode: 'prefix'`, `searchKeysPath`, `rebuildSearchKeys` |
16
+ | Outbox table | `createOutboxRepository` from `@classytic/mongokit/outbox` |
17
+ | Idempotency claims | `createIdempotencyStore` from `@classytic/mongokit/idempotency` |
18
+ | Cross-process lock | `createMongoLockAdapter` from `@classytic/mongokit/lock` |
19
+ | Once-per-day job ledger for the arc daily gate | `createMongoDailyLedger` from `@classytic/mongokit/lock` |
20
+ | Atomic counter in your own collection | `createSequenceCounter` |
21
+ | Counter that resets per civil year in a time zone | `getNextPeriodSequence` |
22
+ | Worker lease with clock and attempts cap | `leasePlugin` |
23
+ | Check declared vs built indexes | `verifyIndexes` from `@classytic/mongokit/indexes` |
24
+ | Tenant-led indexes on a schema | `injectTenantField` from `@classytic/mongokit/tenant` |
25
+ | Query timeout, read concern, profiler comment | `configureQueryDefaults` |
26
+ | Drop the per-page count | `configurePaginationDefaults` |
27
+ | Tenant bypass for jobs | `systemContext` |
28
+ | Forward request scope | `repoOptionsFromCtx` |
29
+ | arc adapter | `createMongooseAdapter` from `@classytic/mongokit/adapter` |
30
+ | Test database | `createTestRepository` from `@classytic/mongokit/testkit` |
31
+
32
+ Read first: SKILL.md. Details: references/repository-api.md, pagination-search.md, plugins-tenancy.md, transactions-outbox.md.
33
+ Envelope and error types: skill repo-core.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/mongokit",
3
- "version": "3.43.1",
3
+ "version": "3.45.0",
4
4
  "description": "Production-grade MongoDB repositories - smart pagination, events, and plugins. Built on @classytic/repo-core.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -105,7 +105,9 @@
105
105
  "dist",
106
106
  "README.md",
107
107
  "LICENSE",
108
- "TRADEMARK.md"
108
+ "TRADEMARK.md",
109
+ "llms.txt",
110
+ "skills/**/*.md"
109
111
  ],
110
112
  "keywords": [
111
113
  "mongodb",
@@ -150,9 +152,9 @@
150
152
  },
151
153
  "homepage": "https://github.com/classytic/mongokit#readme",
152
154
  "peerDependencies": {
153
- "@classytic/repo-core": ">=0.31.1",
154
- "mongodb-memory-server": ">=10.4.3",
155
- "mongoose": ">=9.11.2"
155
+ "@classytic/repo-core": ">=0.38.0",
156
+ "mongodb-memory-server": ">=11.3.0",
157
+ "mongoose": ">=9.12.0"
156
158
  },
157
159
  "peerDependenciesMeta": {
158
160
  "mongodb-memory-server": {
@@ -185,7 +187,7 @@
185
187
  "check": "biome ci src/ --diagnostic-level=error",
186
188
  "check:dead-code": "knip",
187
189
  "check:peer-skew": "node scripts/check-peer-skew.mjs",
188
- "ci": "npm run check:peer-skew && npm run check && npm run typecheck && npm run typecheck:tests && npm run build && npm run lint:package && npm run check:dead-code && npm run check:forgotten-exports && npm test",
190
+ "ci": "npm run check:peer-skew && npm run check && npm run typecheck && npm run typecheck:tests && npm run build && npm run lint:package && npm run check:dead-code && npm run check:forgotten-exports && npm run check:skills && npm test",
189
191
  "push": "classytic-push",
190
192
  "prepublishOnly": "npm run ci",
191
193
  "publish:dry": "npm publish --dry-run --access public",
@@ -195,29 +197,31 @@
195
197
  "release:patch": "npm version patch && npm run release",
196
198
  "release:minor": "npm version minor && npm run release",
197
199
  "release:major": "npm version major && npm run release",
198
- "check:forgotten-exports": "node scripts/check-forgotten-exports.mjs"
200
+ "check:forgotten-exports": "node scripts/check-forgotten-exports.mjs",
201
+ "check:skills": "node scripts/check-skills.mjs"
199
202
  },
200
203
  "devDependencies": {
201
204
  "@arethetypeswrong/cli": "^0.18.5",
202
- "@better-auth/api-key": "^1.6.29",
203
- "@better-auth/mongo-adapter": "^1.6.29",
204
- "@biomejs/biome": "^2.5.8",
205
- "@classytic/dev-tools": "^0.2.0",
206
- "@classytic/repo-core": "^0.31.1",
207
- "@types/node": "^26.2.0",
208
- "@vitest/coverage-v8": "^4.1.10",
205
+ "@better-auth/api-key": "^1.7.18",
206
+ "@better-auth/mongo-adapter": "^1.7.18",
207
+ "@biomejs/biome": "^2.6.5",
208
+ "@classytic/dev-tools": "^0.2.1",
209
+ "@classytic/repo-core": "^0.38.0",
210
+ "@types/node": "^26.6.6",
211
+ "@vitest/coverage-v8": "^5.1.0",
209
212
  "ajv": "^8.20.0",
210
213
  "ajv-formats": "^3.0.1",
211
- "better-auth": "^1.6.29",
212
- "dotenv": "^17.4.2",
213
- "fast-check": "^4.7.0",
214
- "knip": "^6.32.2",
215
- "mongodb": "^7.5.0",
216
- "mongodb-memory-server": "^11.2.0",
217
- "mongoose": "^9.11.2",
218
- "publint": "^0.3.23",
219
- "tsdown": "^0.22.14",
220
- "typescript": "~5.9.3",
221
- "vitest": "^4.1.10"
214
+ "better-auth": "^1.7.18",
215
+ "dotenv": "^18.0.9",
216
+ "fast-check": "^4.10.2",
217
+ "knip": "^6.44.1",
218
+ "mongodb": "~7.8.0",
219
+ "mongodb-memory-server": "^11.3.0",
220
+ "mongoose": "^9.12.0",
221
+ "publint": "^0.3.25",
222
+ "ts-api": "npm:typescript@~5.9.3",
223
+ "tsdown": "^0.23.5",
224
+ "typescript": "~7.0.2",
225
+ "vitest": "^5.1.0"
222
226
  }
223
227
  }