@classytic/mongokit 3.43.0 → 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.
- package/README.md +14 -0
- package/dist/{PaginationEngine-DZnkbSrr.mjs → PaginationEngine-C9iS0U5l.mjs} +13 -27
- package/dist/{PaginationEngine-CRHyuUtw.d.mts → PaginationEngine-D17AthKw.d.mts} +1 -3
- package/dist/{Repository-DnL4xk5w.d.mts → Repository-B6oL6OMv.d.mts} +33 -11
- package/dist/{Repository-Dj6xqPxJ.mjs → Repository-eMRYZ_2Y.mjs} +204 -92
- package/dist/actions/index.d.mts +1 -1
- package/dist/actions/index.mjs +2 -12
- package/dist/actions-DExyXMTy.mjs +11 -0
- package/dist/adapter/index.d.mts +6 -10
- package/dist/adapter/index.mjs +1 -2
- 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 +18 -32
- package/dist/better-auth/index.mjs +3 -4
- package/dist/{coercion-BjdPX3YF.d.mts → coercion-COp9DRNw.d.mts} +0 -2
- package/dist/{create-repository-CJtVdwig.d.mts → create-repository-BOFUYj1l.d.mts} +5 -7
- package/dist/{create-repository-UjwPWR1z.mjs → create-repository-D7MQpIZo.mjs} +4 -5
- 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-8QUEsOFI.d.mts} +3 -4
- package/dist/{index-C0AjVqX3.d.mts → index-ehGZLbrd.d.mts} +2 -8
- package/dist/index.d.mts +63 -96
- 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-CGAepsBU.mjs} +3 -5
- 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-DfeDMiI8.d.mts} +143 -11
- package/dist/{operations-XiFgG5US.d.mts → operations-Dw9OFV0V.d.mts} +14 -5
- 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 -2
- package/dist/pagination/utils/limits.d.mts +6 -9
- package/dist/pagination/utils/limits.mjs +2 -3
- package/dist/{pagination-D6psses0.d.mts → pagination-DAUg5zOY.d.mts} +2 -10
- package/dist/{plugin-options-Cirmb4Kp.d.mts → plugin-options-Bpcuz9kS.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-aUoKlEbh.mjs} +2 -3
- package/dist/read-model/index.d.mts +18 -21
- package/dist/read-model/index.mjs +3 -4
- package/dist/{repository-MqAP409e.d.mts → repository-DzVIwhU2.d.mts} +25 -29
- package/dist/retention-purge-DqcYvRnG.d.mts +35 -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 -2
- package/dist/sync/index.d.mts +95 -15
- package/dist/sync/index.mjs +308 -62
- 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-Bz_zL9PX.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-PO5r1Qoy.d.mts} +2 -16
- package/dist/{transaction-DmiB4k2P.mjs → transaction-C2PFxfIv.mjs} +5 -8
- package/dist/{type-utils-CLzRFtIA.d.mts → type-utils-CKvuewAL.d.mts} +0 -2
- package/dist/{update-WgeAUJ_I.mjs → update-DcfmNbJe.mjs} +12 -20
- 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-B3gZCZj4.d.mts} +135 -48
- package/dist/{validation-chain.plugin-DRApTmkN.mjs → validation-chain.plugin-DNY6MIF6.mjs} +207 -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 +102 -0
- package/skills/mongokit/references/repository-api.md +83 -0
- package/skills/mongokit/references/transactions-outbox.md +101 -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-eMRYZ_2Y.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-DcfmNbJe.mjs";
|
|
5
|
+
import { n as warn } from "./logger-hbWOlj_X.mjs";
|
|
6
|
+
import { c as tenantContextKeysOf, o as forwardScope } from "./timestamp.plugin-Bz_zL9PX.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,19 @@ 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
|
+
*
|
|
117
129
|
* `skipPlugins` is deliberately IGNORED — same policy as tenant scope
|
|
118
130
|
* and soft delete: integrity fences don't have a convenience bypass.
|
|
119
131
|
*
|
|
@@ -125,7 +137,14 @@ function aggregateHelpersPlugin() {
|
|
|
125
137
|
* await events.create({ ... }); // OK
|
|
126
138
|
* await events.update(id, { ... }); // throws 405 APPEND_ONLY_VIOLATION
|
|
127
139
|
* await events.deleteMany({ ... }, { bypassAppendOnly: true }); // audited bypass
|
|
140
|
+
*
|
|
141
|
+
* @example Retention purge (construct with `appendOnlyPlugin({ allowPurge: true })`)
|
|
142
|
+
* events.on('after:append-only-purge', (e) => audit('retention.purge', e));
|
|
143
|
+
* await events.purgeByFilter({ at: { $lt: cutoff } }, { type: 'hard' }, {
|
|
144
|
+
* retentionPurge: { reason: 'retention: 1095 days', actor: 'job:retention' },
|
|
145
|
+
* });
|
|
128
146
|
*/
|
|
147
|
+
const PURGE_OPS = /* @__PURE__ */ new Set(["delete", "deleteMany"]);
|
|
129
148
|
const ALWAYS_ALLOWED = /* @__PURE__ */ new Set(["create", "createMany"]);
|
|
130
149
|
function appendOnlyPlugin(options = {}) {
|
|
131
150
|
const allowed = /* @__PURE__ */ new Set([...ALWAYS_ALLOWED, ...options.allow ?? []]);
|
|
@@ -133,9 +152,21 @@ function appendOnlyPlugin(options = {}) {
|
|
|
133
152
|
name: "appendOnly",
|
|
134
153
|
apply(repo) {
|
|
135
154
|
const modelName = repo.Model?.modelName ?? "model";
|
|
155
|
+
const purgeGuard = {
|
|
156
|
+
plugin: "appendOnlyPlugin",
|
|
157
|
+
allowPurge: options.allowPurge === true
|
|
158
|
+
};
|
|
159
|
+
const priority = options.priority ?? HOOK_PRIORITY.POLICY;
|
|
136
160
|
for (const [op, meta] of Object.entries(OP_REGISTRY)) {
|
|
137
|
-
if (!meta.mutates
|
|
161
|
+
if (!meta.mutates) continue;
|
|
162
|
+
if (allowed.has(op)) {
|
|
163
|
+
if (PURGE_OPS.has(op)) repo.on(`before:${op}`, (context) => {
|
|
164
|
+
readRetentionPurge(context, op, purgeGuard);
|
|
165
|
+
}, { priority });
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
138
168
|
repo.on(`before:${op}`, (context) => {
|
|
169
|
+
if (PURGE_OPS.has(op) && readRetentionPurge(context, op, purgeGuard)) return;
|
|
139
170
|
if (context.bypassAppendOnly === true) {
|
|
140
171
|
repo.emit("after:append-only-bypass", {
|
|
141
172
|
operation: op,
|
|
@@ -150,13 +181,16 @@ function appendOnlyPlugin(options = {}) {
|
|
|
150
181
|
model: modelName
|
|
151
182
|
}
|
|
152
183
|
});
|
|
153
|
-
}, { priority
|
|
184
|
+
}, { priority });
|
|
154
185
|
}
|
|
186
|
+
if (options.allowPurge) for (const op of ["delete", "deleteMany"]) repo.on(`after:${op}`, async (payload) => {
|
|
187
|
+
const event = retentionPurgeEvent(op, payload.context, payload.result);
|
|
188
|
+
if (event) await repo.emitAsync("after:append-only-purge", event);
|
|
189
|
+
});
|
|
155
190
|
}
|
|
156
191
|
};
|
|
157
192
|
}
|
|
158
|
-
|
|
159
|
-
//#region src/plugins/cascade.plugin.ts
|
|
193
|
+
// src/plugins/cascade.plugin.ts
|
|
160
194
|
/**
|
|
161
195
|
* Cascade Delete Plugin
|
|
162
196
|
*
|
|
@@ -477,8 +511,7 @@ async function runCascades(relations, fn, parallel, session) {
|
|
|
477
511
|
}
|
|
478
512
|
} else for (const relation of relations) await fn(relation);
|
|
479
513
|
}
|
|
480
|
-
|
|
481
|
-
//#region src/plugins/elastic.plugin.ts
|
|
514
|
+
// src/plugins/elastic.plugin.ts
|
|
482
515
|
function elasticSearchPlugin(options) {
|
|
483
516
|
return {
|
|
484
517
|
name: "elastic-search",
|
|
@@ -549,8 +582,7 @@ function elasticSearchPlugin(options) {
|
|
|
549
582
|
}
|
|
550
583
|
};
|
|
551
584
|
}
|
|
552
|
-
|
|
553
|
-
//#region src/plugins/field-filter.plugin.ts
|
|
585
|
+
// src/plugins/field-filter.plugin.ts
|
|
554
586
|
/**
|
|
555
587
|
* Field filter plugin that restricts fields based on user context
|
|
556
588
|
*
|
|
@@ -582,8 +614,7 @@ function fieldFilterPlugin(fieldPreset) {
|
|
|
582
614
|
}
|
|
583
615
|
};
|
|
584
616
|
}
|
|
585
|
-
|
|
586
|
-
//#region src/plugins/idempotency-store.plugin.ts
|
|
617
|
+
// src/plugins/idempotency-store.plugin.ts
|
|
587
618
|
function randomToken() {
|
|
588
619
|
const bytes = /* @__PURE__ */ new Uint8Array(16);
|
|
589
620
|
globalThis.crypto.getRandomValues(bytes);
|
|
@@ -735,8 +766,7 @@ function idempotencyStorePlugin(options = {}) {
|
|
|
735
766
|
}
|
|
736
767
|
};
|
|
737
768
|
}
|
|
738
|
-
|
|
739
|
-
//#region src/plugins/immutable-states.plugin.ts
|
|
769
|
+
// src/plugins/immutable-states.plugin.ts
|
|
740
770
|
/** Ops this plugin has an explicit strategy for (see module doc). */
|
|
741
771
|
const STRATEGY_OPS = /* @__PURE__ */ new Set([
|
|
742
772
|
"create",
|
|
@@ -778,10 +808,12 @@ function immutableStatesPlugin(options) {
|
|
|
778
808
|
if (options.tenantField && ctx.query && options.tenantField in ctx.query) q[options.tenantField] = ctx.query[options.tenantField];
|
|
779
809
|
return q;
|
|
780
810
|
};
|
|
811
|
+
/** The call's session: every guard read runs in it, so a row frozen earlier in the same transaction counts. */
|
|
812
|
+
const sessionOf = (ctx) => ctx.session ?? null;
|
|
781
813
|
/** Raw-Model read — never re-enters the repo hook pipeline. */
|
|
782
814
|
const readStateById = async (ctx) => {
|
|
783
815
|
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();
|
|
816
|
+
const doc = await Model.findOne(scoped({ _id: ctx.id }, ctx)).select({ [field]: 1 }).session(sessionOf(ctx)).lean();
|
|
785
817
|
return doc ? getState(doc) : void 0;
|
|
786
818
|
};
|
|
787
819
|
const guardById = (op) => async (ctx) => {
|
|
@@ -793,19 +825,41 @@ function immutableStatesPlugin(options) {
|
|
|
793
825
|
state
|
|
794
826
|
});
|
|
795
827
|
};
|
|
828
|
+
const purgeGuard = {
|
|
829
|
+
plugin: "immutableStatesPlugin",
|
|
830
|
+
allowPurge: options.allowPurge === true
|
|
831
|
+
};
|
|
832
|
+
const purgeRequest = (op, ctx) => readRetentionPurge(ctx, op, purgeGuard);
|
|
796
833
|
for (const op of [
|
|
797
834
|
"update",
|
|
798
|
-
"delete",
|
|
799
835
|
"claimVersion",
|
|
800
836
|
"restore"
|
|
801
837
|
]) repo.on(`before:${op}`, guardById(op));
|
|
838
|
+
const guardDelete = guardById("delete");
|
|
839
|
+
repo.on("before:delete", async (ctx) => {
|
|
840
|
+
if (ctx[internalFlag]) return;
|
|
841
|
+
if (purgeRequest("delete", ctx)) return;
|
|
842
|
+
await guardDelete(ctx);
|
|
843
|
+
});
|
|
844
|
+
const emitPurge = async (operation, context, result) => {
|
|
845
|
+
if (!options.allowPurge || context[internalFlag]) return;
|
|
846
|
+
const base = retentionPurgeEvent(operation, context, result);
|
|
847
|
+
if (!base) return;
|
|
848
|
+
const event = {
|
|
849
|
+
...base,
|
|
850
|
+
states: [...frozen]
|
|
851
|
+
};
|
|
852
|
+
await repo.emitAsync("after:immutable-states-purge", event);
|
|
853
|
+
};
|
|
854
|
+
repo.on("after:delete", (payload) => emitPurge("delete", payload.context, payload.result));
|
|
855
|
+
repo.on("after:deleteMany", (payload) => emitPurge("deleteMany", payload.context, payload.result));
|
|
802
856
|
repo.on("before:findOneAndUpdate", async (ctx) => {
|
|
803
857
|
if (ctx[internalFlag]) return;
|
|
804
858
|
const filter = ctx.query ?? {};
|
|
805
859
|
const doc = await Model.findOne(filter).select({
|
|
806
860
|
[field]: 1,
|
|
807
861
|
_id: 1
|
|
808
|
-
}).lean();
|
|
862
|
+
}).session(sessionOf(ctx)).lean();
|
|
809
863
|
if (!doc) return;
|
|
810
864
|
const state = getState(doc);
|
|
811
865
|
if (state !== void 0 && frozen.has(state)) throwViolation({
|
|
@@ -817,7 +871,7 @@ function immutableStatesPlugin(options) {
|
|
|
817
871
|
const guardMany = (op) => async (ctx) => {
|
|
818
872
|
if (ctx[internalFlag]) return;
|
|
819
873
|
const filter = ctx.query ?? {};
|
|
820
|
-
const hit = await Model.exists({ $and: [filter, { [field]: { $in: [...frozen] } }] });
|
|
874
|
+
const hit = await Model.exists({ $and: [filter, { [field]: { $in: [...frozen] } }] }).session(sessionOf(ctx));
|
|
821
875
|
if (hit) throwViolation({
|
|
822
876
|
id: hit._id,
|
|
823
877
|
operation: op,
|
|
@@ -825,7 +879,12 @@ function immutableStatesPlugin(options) {
|
|
|
825
879
|
});
|
|
826
880
|
};
|
|
827
881
|
repo.on("before:updateMany", guardMany("updateMany"));
|
|
828
|
-
|
|
882
|
+
const guardDeleteMany = guardMany("deleteMany");
|
|
883
|
+
repo.on("before:deleteMany", async (ctx) => {
|
|
884
|
+
if (ctx[internalFlag]) return;
|
|
885
|
+
if (purgeRequest("deleteMany", ctx)) return;
|
|
886
|
+
await guardDeleteMany(ctx);
|
|
887
|
+
});
|
|
829
888
|
repo.on("before:bulkUpsert", async (ctx) => {
|
|
830
889
|
if (ctx[internalFlag]) return;
|
|
831
890
|
const filters = (ctx.operations ?? []).flatMap((op) => op.updateOne?.filter ? [op.updateOne.filter] : []);
|
|
@@ -881,8 +940,10 @@ function immutableStatesPlugin(options) {
|
|
|
881
940
|
}
|
|
882
941
|
};
|
|
883
942
|
}
|
|
884
|
-
|
|
885
|
-
|
|
943
|
+
// src/plugins/lease.plugin.ts
|
|
944
|
+
function isNonEmpty(obj) {
|
|
945
|
+
return obj !== void 0 && obj !== null && Object.keys(obj).length > 0;
|
|
946
|
+
}
|
|
886
947
|
function leasePlugin(options = {}) {
|
|
887
948
|
const statusField = options.statusField ?? "status";
|
|
888
949
|
const leasedByField = options.leasedByField ?? "leasedBy";
|
|
@@ -891,6 +952,57 @@ function leasePlugin(options = {}) {
|
|
|
891
952
|
const processingStatus = options.processingStatus ?? "processing";
|
|
892
953
|
const doneStatus = options.doneStatus ?? "done";
|
|
893
954
|
const sort = options.sort ?? { createdAt: 1 };
|
|
955
|
+
const nowFn = options.nowFn ?? (() => /* @__PURE__ */ new Date());
|
|
956
|
+
const deadLeaseStatuses = options.deadLeaseStatuses ?? [processingStatus];
|
|
957
|
+
const attemptsField = options.attemptsField;
|
|
958
|
+
const maxAttempts = options.maxAttempts;
|
|
959
|
+
if (deadLeaseStatuses.length === 0) throw new Error("leasePlugin: deadLeaseStatuses must name at least one status");
|
|
960
|
+
if (maxAttempts !== void 0) {
|
|
961
|
+
if (attemptsField === void 0) throw new Error("leasePlugin: maxAttempts requires attemptsField");
|
|
962
|
+
if (!Number.isInteger(maxAttempts) || maxAttempts < 1) throw new Error("leasePlugin: maxAttempts must be a positive integer");
|
|
963
|
+
}
|
|
964
|
+
const exhaustedStatus = options.exhaustedStatus;
|
|
965
|
+
const exhaustedAtField = options.exhaustedAtField;
|
|
966
|
+
const assertTerminal = (where, status) => {
|
|
967
|
+
if (status === pendingStatus || status === processingStatus || deadLeaseStatuses.includes(status)) throw new Error(`leasePlugin: ${where} "${status}" must differ from the pending, processing and dead-lease statuses`);
|
|
968
|
+
};
|
|
969
|
+
if (exhaustedStatus !== void 0) {
|
|
970
|
+
if (maxAttempts === void 0) throw new Error("leasePlugin: exhaustedStatus requires maxAttempts");
|
|
971
|
+
assertTerminal("exhaustedStatus", exhaustedStatus);
|
|
972
|
+
}
|
|
973
|
+
if (exhaustedAtField !== void 0 && maxAttempts === void 0) throw new Error("leasePlugin: exhaustedAtField requires maxAttempts");
|
|
974
|
+
/**
|
|
975
|
+
* The claim condition: pending (narrowed by `pendingFilter`) OR a dead
|
|
976
|
+
* lease, a row still in a dead-lease status whose `leaseExpiresAt <
|
|
977
|
+
* now` (worker crashed mid-lease). The status guard on the dead arm
|
|
978
|
+
* keeps a cancelled / terminal row with a stale expiry from being
|
|
979
|
+
* revived. `lease()` adds "under the attempts cap"; `reapExhausted()`
|
|
980
|
+
* adds "at or over it".
|
|
981
|
+
*/
|
|
982
|
+
const claimable = (now, pendingSource) => {
|
|
983
|
+
const pendingFilter = typeof pendingSource === "function" ? pendingSource(now) : pendingSource;
|
|
984
|
+
return { $or: [isNonEmpty(pendingFilter) ? { $and: [{ [statusField]: pendingStatus }, pendingFilter] } : { [statusField]: pendingStatus }, {
|
|
985
|
+
[statusField]: deadLeaseStatuses.length === 1 ? deadLeaseStatuses[0] : { $in: deadLeaseStatuses },
|
|
986
|
+
[leaseExpiresAtField]: { $lt: now }
|
|
987
|
+
}] };
|
|
988
|
+
};
|
|
989
|
+
/** The caller's filter as given, with `conditions` ANDed beside it. */
|
|
990
|
+
const withConditions = (method, baseFilter, conditions) => {
|
|
991
|
+
if (baseFilter.$and !== void 0 && !Array.isArray(baseFilter.$and)) throw createError(400, `leasePlugin.${method}: filter.$and must be an array`);
|
|
992
|
+
const callerAnd = baseFilter.$and ?? [];
|
|
993
|
+
return {
|
|
994
|
+
...baseFilter,
|
|
995
|
+
$and: [...callerAnd, ...conditions]
|
|
996
|
+
};
|
|
997
|
+
};
|
|
998
|
+
const resolveNow = (method, perCall) => {
|
|
999
|
+
const now = perCall ?? nowFn();
|
|
1000
|
+
if (!(now instanceof Date) || Number.isNaN(now.getTime())) throw createError(400, `leasePlugin.${method}: now must be a valid Date`);
|
|
1001
|
+
return now;
|
|
1002
|
+
};
|
|
1003
|
+
const assertLeasedBy = (method, leasedBy) => {
|
|
1004
|
+
if (typeof leasedBy !== "string" || leasedBy.length === 0) throw createError(400, `leasePlugin.${method}: leasedBy must be a non-empty string`);
|
|
1005
|
+
};
|
|
894
1006
|
return {
|
|
895
1007
|
name: "lease",
|
|
896
1008
|
apply(repo) {
|
|
@@ -904,18 +1016,20 @@ function leasePlugin(options = {}) {
|
|
|
904
1016
|
*/
|
|
905
1017
|
repo.registerMethod("lease", async function(opts) {
|
|
906
1018
|
if (typeof opts?.leaseFor !== "number" || opts.leaseFor <= 0) throw createError(400, "leasePlugin.lease: leaseFor must be a positive number (ms)");
|
|
907
|
-
|
|
908
|
-
const now =
|
|
1019
|
+
assertLeasedBy("lease", opts.leasedBy);
|
|
1020
|
+
const now = resolveNow("lease", opts.now);
|
|
909
1021
|
const leaseUntil = new Date(now.getTime() + opts.leaseFor);
|
|
910
|
-
const
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1022
|
+
const conditions = [claimable(now, opts.pendingFilter ?? options.pendingFilter)];
|
|
1023
|
+
if (attemptsField !== void 0 && maxAttempts !== void 0) conditions.push({ [attemptsField]: { $not: { $gte: maxAttempts } } });
|
|
1024
|
+
const claimFilter = withConditions("lease", opts.filter ?? {}, conditions);
|
|
1025
|
+
return this.findOneAndUpdate(claimFilter, {
|
|
1026
|
+
$set: {
|
|
1027
|
+
[statusField]: processingStatus,
|
|
1028
|
+
[leasedByField]: opts.leasedBy,
|
|
1029
|
+
[leaseExpiresAtField]: leaseUntil
|
|
1030
|
+
},
|
|
1031
|
+
...attemptsField !== void 0 ? { $inc: { [attemptsField]: 1 } } : {}
|
|
1032
|
+
}, {
|
|
919
1033
|
...opts.options ?? {},
|
|
920
1034
|
sort,
|
|
921
1035
|
returnDocument: "after"
|
|
@@ -923,11 +1037,13 @@ function leasePlugin(options = {}) {
|
|
|
923
1037
|
});
|
|
924
1038
|
repo.registerMethod("extend", async function(id, opts) {
|
|
925
1039
|
if (typeof opts?.leaseFor !== "number" || opts.leaseFor <= 0) throw createError(400, "leasePlugin.extend: leaseFor must be a positive number (ms)");
|
|
926
|
-
|
|
1040
|
+
assertLeasedBy("extend", opts.leasedBy);
|
|
1041
|
+
const now = resolveNow("extend", opts.now);
|
|
927
1042
|
const leaseUntil = new Date(now.getTime() + opts.leaseFor);
|
|
928
1043
|
const idField = this.idField || "_id";
|
|
929
1044
|
return this.findOneAndUpdate({
|
|
930
1045
|
[idField]: id,
|
|
1046
|
+
[statusField]: processingStatus,
|
|
931
1047
|
[leasedByField]: opts.leasedBy,
|
|
932
1048
|
[leaseExpiresAtField]: { $gt: now }
|
|
933
1049
|
}, { $set: { [leaseExpiresAtField]: leaseUntil } }, {
|
|
@@ -938,10 +1054,11 @@ function leasePlugin(options = {}) {
|
|
|
938
1054
|
repo.registerMethod("release", async function(id, opts) {
|
|
939
1055
|
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
1056
|
const finalStatus = opts.finalStatus ?? doneStatus;
|
|
941
|
-
const now =
|
|
1057
|
+
const now = resolveNow("release", opts.now);
|
|
942
1058
|
const idField = this.idField || "_id";
|
|
943
1059
|
return this.findOneAndUpdate({
|
|
944
1060
|
[idField]: id,
|
|
1061
|
+
[statusField]: processingStatus,
|
|
945
1062
|
[leasedByField]: opts.leasedBy,
|
|
946
1063
|
[leaseExpiresAtField]: { $gt: now }
|
|
947
1064
|
}, {
|
|
@@ -955,11 +1072,52 @@ function leasePlugin(options = {}) {
|
|
|
955
1072
|
returnDocument: "after"
|
|
956
1073
|
});
|
|
957
1074
|
});
|
|
1075
|
+
repo.registerMethod("reapExhausted", async function(opts = {}) {
|
|
1076
|
+
if (attemptsField === void 0 || maxAttempts === void 0) throw createError(400, "leasePlugin.reapExhausted: requires maxAttempts");
|
|
1077
|
+
const status = opts.status ?? exhaustedStatus;
|
|
1078
|
+
if (typeof status !== "string" || status.length === 0) throw createError(400, "leasePlugin.reapExhausted: pass a status, or configure exhaustedStatus");
|
|
1079
|
+
if (opts.status !== void 0) try {
|
|
1080
|
+
assertTerminal("reapExhausted status", status);
|
|
1081
|
+
} catch (err) {
|
|
1082
|
+
throw createError(400, err.message);
|
|
1083
|
+
}
|
|
1084
|
+
const limit = opts.limit ?? 100;
|
|
1085
|
+
if (!Number.isInteger(limit) || limit < 1) throw createError(400, "leasePlugin.reapExhausted: limit must be a positive integer");
|
|
1086
|
+
const now = resolveNow("reapExhausted", opts.now);
|
|
1087
|
+
const exhausted = withConditions("reapExhausted", opts.filter ?? {}, [claimable(now, opts.pendingFilter ?? options.pendingFilter), { [attemptsField]: { $gte: maxAttempts } }]);
|
|
1088
|
+
const idField = this.idField || "_id";
|
|
1089
|
+
const candidates = await this.findAll.call(this, exhausted, {
|
|
1090
|
+
...opts.options ?? {},
|
|
1091
|
+
sort,
|
|
1092
|
+
limit,
|
|
1093
|
+
lean: true
|
|
1094
|
+
});
|
|
1095
|
+
const reaped = [];
|
|
1096
|
+
for (const row of candidates) {
|
|
1097
|
+
const moved = await this.findOneAndUpdate({
|
|
1098
|
+
...exhausted,
|
|
1099
|
+
[idField]: row[idField]
|
|
1100
|
+
}, {
|
|
1101
|
+
$set: {
|
|
1102
|
+
[statusField]: status,
|
|
1103
|
+
...exhaustedAtField !== void 0 ? { [exhaustedAtField]: now } : {}
|
|
1104
|
+
},
|
|
1105
|
+
$unset: {
|
|
1106
|
+
[leasedByField]: "",
|
|
1107
|
+
[leaseExpiresAtField]: ""
|
|
1108
|
+
}
|
|
1109
|
+
}, {
|
|
1110
|
+
...opts.options ?? {},
|
|
1111
|
+
returnDocument: "after"
|
|
1112
|
+
});
|
|
1113
|
+
if (moved) reaped.push(moved);
|
|
1114
|
+
}
|
|
1115
|
+
return reaped;
|
|
1116
|
+
});
|
|
958
1117
|
}
|
|
959
1118
|
};
|
|
960
1119
|
}
|
|
961
|
-
|
|
962
|
-
//#region src/plugins/mongo-operations.plugin.ts
|
|
1120
|
+
// src/plugins/mongo-operations.plugin.ts
|
|
963
1121
|
/**
|
|
964
1122
|
* MongoDB Operations Plugin
|
|
965
1123
|
*
|
|
@@ -1196,8 +1354,7 @@ function mongoOperationsPlugin() {
|
|
|
1196
1354
|
}
|
|
1197
1355
|
};
|
|
1198
1356
|
}
|
|
1199
|
-
|
|
1200
|
-
//#region src/plugins/names.ts
|
|
1357
|
+
// src/plugins/names.ts
|
|
1201
1358
|
/**
|
|
1202
1359
|
* Canonical identifiers exposed by mongokit's bundled plugins.
|
|
1203
1360
|
*
|
|
@@ -1229,8 +1386,7 @@ const MONGOKIT_PLUGIN_NAMES = {
|
|
|
1229
1386
|
timestamp: "timestamp",
|
|
1230
1387
|
validationChain: "validation-chain"
|
|
1231
1388
|
};
|
|
1232
|
-
|
|
1233
|
-
//#region src/plugins/observability.plugin.ts
|
|
1389
|
+
// src/plugins/observability.plugin.ts
|
|
1234
1390
|
/**
|
|
1235
1391
|
* Observability Plugin
|
|
1236
1392
|
*
|
|
@@ -1305,8 +1461,7 @@ function observabilityPlugin(options) {
|
|
|
1305
1461
|
}
|
|
1306
1462
|
};
|
|
1307
1463
|
}
|
|
1308
|
-
|
|
1309
|
-
//#region src/plugins/subdocument.plugin.ts
|
|
1464
|
+
// src/plugins/subdocument.plugin.ts
|
|
1310
1465
|
/**
|
|
1311
1466
|
* Subdocument plugin for managing nested arrays
|
|
1312
1467
|
*
|
|
@@ -1375,8 +1530,7 @@ function subdocumentPlugin() {
|
|
|
1375
1530
|
}
|
|
1376
1531
|
};
|
|
1377
1532
|
}
|
|
1378
|
-
|
|
1379
|
-
//#region src/plugins/tenant-context.ts
|
|
1533
|
+
// src/plugins/tenant-context.ts
|
|
1380
1534
|
/**
|
|
1381
1535
|
* Request-scoped tenant context via AsyncLocalStorage.
|
|
1382
1536
|
*
|
|
@@ -1426,8 +1580,7 @@ function createTenantContext() {
|
|
|
1426
1580
|
}
|
|
1427
1581
|
};
|
|
1428
1582
|
}
|
|
1429
|
-
|
|
1430
|
-
//#region src/plugins/validation-chain.plugin.ts
|
|
1583
|
+
// src/plugins/validation-chain.plugin.ts
|
|
1431
1584
|
/**
|
|
1432
1585
|
* Validation chain plugin
|
|
1433
1586
|
*
|
|
@@ -1576,5 +1729,4 @@ function uniqueField(field, errorMessage) {
|
|
|
1576
1729
|
}
|
|
1577
1730
|
};
|
|
1578
1731
|
}
|
|
1579
|
-
//#endregion
|
|
1580
1732
|
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.44.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.11.
|
|
155
|
+
"@classytic/repo-core": ">=0.37.0",
|
|
156
|
+
"mongodb-memory-server": ">=11.3.0",
|
|
157
|
+
"mongoose": ">=9.11.4"
|
|
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.
|
|
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.15",
|
|
206
|
+
"@better-auth/mongo-adapter": "^1.7.15",
|
|
207
|
+
"@biomejs/biome": "^2.6.4",
|
|
208
|
+
"@classytic/dev-tools": "^0.2.1",
|
|
209
|
+
"@classytic/repo-core": "^0.37.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.11.
|
|
218
|
-
"publint": "^0.3.
|
|
219
|
-
"
|
|
220
|
-
"
|
|
221
|
-
"
|
|
214
|
+
"better-auth": "^1.7.15",
|
|
215
|
+
"dotenv": "^18.0.9",
|
|
216
|
+
"fast-check": "^4.10.2",
|
|
217
|
+
"knip": "^6.44.1",
|
|
218
|
+
"mongodb": "~7.7.0",
|
|
219
|
+
"mongodb-memory-server": "^11.3.0",
|
|
220
|
+
"mongoose": "^9.11.4",
|
|
221
|
+
"publint": "^0.3.25",
|
|
222
|
+
"ts-api": "npm:typescript@~5.9.3",
|
|
223
|
+
"tsdown": "^0.23.4",
|
|
224
|
+
"typescript": "~7.0.2",
|
|
225
|
+
"vitest": "^5.1.0"
|
|
222
226
|
}
|
|
223
227
|
}
|