@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.
- package/README.md +18 -3
- package/dist/{PaginationEngine-DZnkbSrr.mjs → PaginationEngine-BRlLTQQz.mjs} +13 -27
- package/dist/{PaginationEngine-CRHyuUtw.d.mts → PaginationEngine-BWb-c47a.d.mts} +1 -3
- package/dist/{Repository-DnL4xk5w.d.mts → Repository-DJuRmBNx.d.mts} +44 -13
- package/dist/{Repository-Dj6xqPxJ.mjs → Repository-DheA7oW-.mjs} +330 -112
- package/dist/actions/index.d.mts +1 -1
- package/dist/actions/index.mjs +2 -2
- package/dist/{actions-ChYfkPYP.mjs → actions-DKQx9ToJ.mjs} +3 -4
- package/dist/adapter/index.d.mts +6 -10
- package/dist/adapter/index.mjs +1 -1
- package/dist/{adapter-CkMoMF83.mjs → adapter-Qzgc3ZD_.mjs} +4 -6
- package/dist/ai/index.d.mts +21 -8
- package/dist/ai/index.mjs +15 -9
- package/dist/better-auth/index.d.mts +21 -20
- package/dist/better-auth/index.mjs +12 -23
- package/dist/{coercion-BjdPX3YF.d.mts → coercion-COp9DRNw.d.mts} +0 -2
- package/dist/{create-repository-UjwPWR1z.mjs → create-repository-C9DHHcl7.mjs} +4 -5
- package/dist/{create-repository-CJtVdwig.d.mts → create-repository-DG-gwOf0.d.mts} +5 -7
- package/dist/{derive-DRBiegRH.mjs → derive-Dxsc31fV.mjs} +3 -6
- package/dist/{error-BuByZwKU.mjs → error-DjdmPLWO.mjs} +1 -2
- package/dist/{field-selection-FSQFLpsU.mjs → field-selection-DPFaFw9Y.mjs} +1 -2
- package/dist/{id-chunks-BmAwsqNk.mjs → id-chunks-DH-RAZWG.mjs} +13 -8
- package/dist/{id-resolution-BqDrk0S2.mjs → id-resolution-Cqh49d9J.mjs} +1 -2
- package/dist/idempotency/index.d.mts +4 -7
- package/dist/idempotency/index.mjs +1 -2
- package/dist/{index-SYF8N28D.d.mts → index-5mhQ63_B.d.mts} +3 -4
- package/dist/{index-C0AjVqX3.d.mts → index-ehGZLbrd.d.mts} +2 -8
- package/dist/index.d.mts +70 -102
- package/dist/index.mjs +36 -52
- package/dist/indexes/verify-indexes.d.mts +8 -11
- package/dist/indexes/verify-indexes.mjs +1 -2
- package/dist/{indexes-DIjMpYES.d.mts → indexes-FfyOuhDK.d.mts} +0 -2
- package/dist/{join-scope-DSAznE8E.mjs → join-scope-C1gOtZ59.mjs} +2 -3
- package/dist/kernel-conformance/index.d.mts +39 -13
- package/dist/kernel-conformance/index.mjs +4 -7
- package/dist/lock/index.d.mts +134 -7
- package/dist/lock/index.mjs +349 -14
- package/dist/{logger-K7dPmu2U.mjs → logger-hbWOlj_X.mjs} +1 -2
- package/dist/{method-registry.plugin-C63pGn1d.mjs → method-registry.plugin-BeJfQBJV.mjs} +5 -7
- package/dist/{model-registry-ClKtWiHa.mjs → model-registry-DrVfJ3Nt.mjs} +1 -2
- package/dist/{mongooseToJsonSchema-C3k4ide0.mjs → mongooseToJsonSchema-DMDotz4_.mjs} +2 -3
- package/dist/{multi-tenant.plugin-B_q9NUHc.d.mts → multi-tenant.plugin-BTPtCLNK.d.mts} +143 -11
- package/dist/{operations-XiFgG5US.d.mts → operations-D6XsHx4e.d.mts} +19 -11
- package/dist/outbox/index.d.mts +24 -9
- package/dist/outbox/index.mjs +17 -6
- package/dist/pagination/PaginationEngine.d.mts +1 -1
- package/dist/pagination/PaginationEngine.mjs +1 -1
- package/dist/pagination/utils/limits.d.mts +6 -9
- package/dist/pagination/utils/limits.mjs +2 -3
- package/dist/{pagination-D6psses0.d.mts → pagination-BAM9cA4N.d.mts} +6 -16
- package/dist/{plugin-options-Cirmb4Kp.d.mts → plugin-options-DrNGWV5b.d.mts} +2 -4
- package/dist/plugins/index.d.mts +6 -7
- package/dist/plugins/index.mjs +6 -7
- package/dist/query/primitives/coercion.d.mts +1 -1
- package/dist/query/primitives/coercion.mjs +2 -3
- package/dist/query/primitives/geo.d.mts +11 -14
- package/dist/query/primitives/geo.mjs +1 -2
- package/dist/query/primitives/indexes.d.mts +1 -1
- package/dist/query/primitives/indexes.mjs +1 -2
- package/dist/{query-defaults-BxUcSOoF.mjs → query-defaults-DwvWUjZU.mjs} +18 -54
- package/dist/read-model/index.d.mts +18 -21
- package/dist/read-model/index.mjs +3 -4
- package/dist/{repository-MqAP409e.d.mts → repository-B0aFVgP2.d.mts} +25 -29
- package/dist/retention-purge-CCd_VXUf.d.mts +38 -0
- package/dist/{rolldown-runtime-D7D4PA-g.mjs → rolldown-runtime-D8jQpDG7.mjs} +1 -2
- package/dist/search/index.d.mts +1 -1
- package/dist/search/index.mjs +1 -1
- package/dist/sync/index.d.mts +113 -17
- package/dist/sync/index.mjs +421 -65
- package/dist/tenant/index.d.mts +8 -11
- package/dist/tenant/index.mjs +1 -2
- package/dist/testkit/index.d.mts +179 -23
- package/dist/testkit/index.mjs +421 -23
- package/dist/{timestamp.plugin-DpOWavVI.mjs → timestamp.plugin-B-IJrlYb.mjs} +192 -49
- package/dist/{to-plain-vksRrLtc.mjs → to-plain-CEKv_ylg.mjs} +2 -4
- package/dist/{to-plain-o5tUY2nV.d.mts → to-plain-Do-8jTKC.d.mts} +2 -16
- package/dist/{transaction-DmiB4k2P.mjs → transaction-C6Oj941P.mjs} +18 -19
- package/dist/{type-utils-CLzRFtIA.d.mts → type-utils-CKvuewAL.d.mts} +0 -2
- package/dist/{update-WgeAUJ_I.mjs → update-CYaJMu6j.mjs} +13 -23
- package/dist/usage/index.d.mts +2 -5
- package/dist/usage/index.mjs +8 -6
- package/dist/utils/index.d.mts +8 -10
- package/dist/utils/index.mjs +7 -7
- package/dist/{validation-chain.plugin-BAeC5eVT.d.mts → validation-chain.plugin-COYrUdd8.d.mts} +150 -48
- package/dist/{validation-chain.plugin-DRApTmkN.mjs → validation-chain.plugin-DJplVJ12.mjs} +210 -55
- package/llms.txt +33 -0
- package/package.json +29 -25
- package/skills/mongokit/SKILL.md +112 -0
- package/skills/mongokit/references/pagination-search.md +87 -0
- package/skills/mongokit/references/plugins-tenancy.md +103 -0
- package/skills/mongokit/references/repository-api.md +83 -0
- package/skills/mongokit/references/transactions-outbox.md +103 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as ALL_OPERATIONS, s as OP_REGISTRY } from "./Repository-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { c as tenantContextKeysOf, o as forwardScope } from "./timestamp.plugin-
|
|
7
|
-
import { r as getFieldsForUser } from "./field-selection-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
885
|
-
|
|
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
|
-
|
|
908
|
-
const now =
|
|
1022
|
+
assertLeasedBy("lease", opts.leasedBy);
|
|
1023
|
+
const now = resolveNow("lease", opts.now);
|
|
909
1024
|
const leaseUntil = new Date(now.getTime() + opts.leaseFor);
|
|
910
|
-
const
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
154
|
-
"mongodb-memory-server": ">=
|
|
155
|
-
"mongoose": ">=9.
|
|
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.
|
|
203
|
-
"@better-auth/mongo-adapter": "^1.
|
|
204
|
-
"@biomejs/biome": "^2.5
|
|
205
|
-
"@classytic/dev-tools": "^0.2.
|
|
206
|
-
"@classytic/repo-core": "^0.
|
|
207
|
-
"@types/node": "^26.
|
|
208
|
-
"@vitest/coverage-v8": "^
|
|
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.
|
|
212
|
-
"dotenv": "^
|
|
213
|
-
"fast-check": "^4.
|
|
214
|
-
"knip": "^6.
|
|
215
|
-
"mongodb": "
|
|
216
|
-
"mongodb-memory-server": "^11.
|
|
217
|
-
"mongoose": "^9.
|
|
218
|
-
"publint": "^0.3.
|
|
219
|
-
"
|
|
220
|
-
"
|
|
221
|
-
"
|
|
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
|
}
|