@classytic/mongokit 3.39.0 → 3.40.1
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 +71 -0
- package/dist/{PaginationEngine-D-P19Y9m.d.mts → PaginationEngine-CRHyuUtw.d.mts} +17 -3
- package/dist/{PaginationEngine-DQ7beTCR.mjs → PaginationEngine-CfT2Cxkj.mjs} +320 -145
- package/dist/{Repository-D7fDpyFx.mjs → Repository-CYhSC9ut.mjs} +844 -794
- package/dist/{Repository-IxI983_m.d.mts → Repository-pnLw1rG5.d.mts} +113 -47
- package/dist/actions/index.d.mts +1 -1
- package/dist/actions/index.mjs +1 -1
- package/dist/adapter/index.mjs +1 -1
- package/dist/{adapter-BrVfPFSW.mjs → adapter-CkMoMF83.mjs} +1 -1
- package/dist/ai/index.d.mts +1 -1
- package/dist/better-auth/index.mjs +2 -2
- package/dist/{create-repository-CRXjDa3N.d.mts → create-repository-BUx9vfG9.d.mts} +5 -5
- package/dist/{create-repository-DOe9E_5F.mjs → create-repository-BYyWzLbt.mjs} +2 -2
- package/dist/idempotency/index.d.mts +23 -0
- package/dist/idempotency/index.mjs +134 -0
- package/dist/{index-CmjNyulu.d.mts → index-SYF8N28D.d.mts} +37 -19
- package/dist/index.d.mts +105 -39
- package/dist/index.mjs +332 -676
- package/dist/indexes/verify-indexes.d.mts +14 -1
- package/dist/indexes/verify-indexes.mjs +54 -7
- package/dist/join-scope-DSAznE8E.mjs +168 -0
- package/dist/{mongooseToJsonSchema-waUVQwK4.mjs → mongooseToJsonSchema-C3k4ide0.mjs} +5 -1
- package/dist/{multi-tenant.plugin-B-tzNeYL.d.mts → multi-tenant.plugin-a3UhQARj.d.mts} +19 -3
- package/dist/{operations-CrRIoc8Z.d.mts → operations-XiFgG5US.d.mts} +36 -13
- package/dist/outbox/index.d.mts +1 -1
- package/dist/outbox/index.mjs +6 -4
- package/dist/pagination/PaginationEngine.d.mts +2 -2
- package/dist/pagination/PaginationEngine.mjs +1 -1
- package/dist/pagination/utils/limits.d.mts +1 -1
- package/dist/{pagination-BpIv2bdS.d.mts → pagination-D6psses0.d.mts} +116 -13
- package/dist/{plugin-options-BuvtOSZf.d.mts → plugin-options-CRhIvSzx.d.mts} +16 -2
- package/dist/plugins/index.d.mts +8 -3
- package/dist/plugins/index.mjs +98 -25
- package/dist/query-defaults-BxUcSOoF.mjs +208 -0
- package/dist/read-model/index.d.mts +66 -0
- package/dist/read-model/index.mjs +193 -0
- package/dist/{repository-ZbLtL7Nk.d.mts → repository-DNHfjAZX.d.mts} +36 -4
- package/dist/sync/index.d.mts +53 -0
- package/dist/sync/index.mjs +371 -0
- package/dist/tenant/index.mjs +2 -0
- package/dist/testkit/index.d.mts +40 -3
- package/dist/testkit/index.mjs +113 -2
- package/dist/{timestamp.plugin-B5eiaItQ.mjs → timestamp.plugin-zfdLYZiO.mjs} +135 -32
- package/dist/{to-plain-B8ufk3Qz.d.mts → to-plain-CjgEInWV.d.mts} +2 -2
- package/dist/transaction-BrRYtH_b.mjs +655 -0
- package/dist/{update-c_01A0if.mjs → update-WgeAUJ_I.mjs} +212 -33
- package/dist/utils/index.d.mts +1 -1
- package/dist/utils/index.mjs +1 -1
- package/dist/{validation-chain.plugin-Bju1D1Sn.mjs → validation-chain.plugin-CHUHWMtk.mjs} +204 -279
- package/dist/{validation-chain.plugin-DvrLJIbJ.d.mts → validation-chain.plugin-CTcNJCmV.d.mts} +27 -5
- package/package.json +18 -4
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { i as ALL_OPERATIONS, o as OP_REGISTRY, t as HOOK_PRIORITY } from "./Repository-CYhSC9ut.mjs";
|
|
2
2
|
import { n as warn } from "./logger-K7dPmu2U.mjs";
|
|
3
|
+
import { n as declareCollectionScope } from "./join-scope-DSAznE8E.mjs";
|
|
3
4
|
import { throwIfAborted, withRetry } from "@classytic/repo-core/repository";
|
|
4
5
|
import mongoose from "mongoose";
|
|
5
6
|
import { cachePlugin } from "@classytic/repo-core/cache";
|
|
@@ -754,6 +755,33 @@ async function getNextSequence(counterKey, increment = 1, connection, session) {
|
|
|
754
755
|
return result.seq;
|
|
755
756
|
}
|
|
756
757
|
/**
|
|
758
|
+
* Current value of a counter, or `null` when it has never been created.
|
|
759
|
+
* `null` and `0` differ: only a missing counter needs seeding.
|
|
760
|
+
*/
|
|
761
|
+
async function readSequence(counterKey, connection) {
|
|
762
|
+
const doc = await getCounterModel(connection).findById(counterKey).lean();
|
|
763
|
+
return doc ? doc.seq : null;
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Raise a counter to at least `floor` — never lowers it. Atomic (`$max` +
|
|
767
|
+
* upsert), so concurrent seeders converge on the highest floor.
|
|
768
|
+
*
|
|
769
|
+
* Use before a counter's first increment when ids were already issued under
|
|
770
|
+
* another key (e.g. moving from a global to a per-tenant sequence); otherwise
|
|
771
|
+
* the new counter restarts at 1 and re-issues taken ids.
|
|
772
|
+
*
|
|
773
|
+
* @returns The counter's value after the raise.
|
|
774
|
+
*/
|
|
775
|
+
async function ensureSequenceAtLeast(counterKey, floor, connection) {
|
|
776
|
+
if (!Number.isSafeInteger(floor) || floor < 0) throw new Error(`ensureSequenceAtLeast: floor must be a non-negative integer, got ${floor}`);
|
|
777
|
+
const result = await getCounterModel(connection).findOneAndUpdate({ _id: counterKey }, { $max: { seq: floor } }, {
|
|
778
|
+
upsert: true,
|
|
779
|
+
returnDocument: "after"
|
|
780
|
+
});
|
|
781
|
+
if (!result) throw new Error(`Failed to seed counter '${counterKey}'`);
|
|
782
|
+
return result.seq;
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
757
785
|
* Generator: Simple sequential counter.
|
|
758
786
|
* Produces IDs like `INV-0001`, `INV-0002`, etc.
|
|
759
787
|
*
|
|
@@ -930,6 +958,62 @@ function customIdPlugin(options) {
|
|
|
930
958
|
};
|
|
931
959
|
}
|
|
932
960
|
//#endregion
|
|
961
|
+
//#region src/utils/scope.ts
|
|
962
|
+
/** Tenant context keys forwarded even when no tenant plugin declared one. */
|
|
963
|
+
const CONVENTIONAL_TENANT_KEYS = ["organizationId", "tenantId"];
|
|
964
|
+
const ATTRIBUTION_KEYS = [
|
|
965
|
+
"session",
|
|
966
|
+
"user",
|
|
967
|
+
"userId",
|
|
968
|
+
"requestId"
|
|
969
|
+
];
|
|
970
|
+
/** Where a repository records the tenant context keys its plugins read. */
|
|
971
|
+
const TENANT_KEYS = Symbol.for("@classytic/mongokit/tenant-context-keys");
|
|
972
|
+
/**
|
|
973
|
+
* Record that `repo` resolves its tenant from `contextKey`. Called by
|
|
974
|
+
* `multiTenantPlugin` at bind, so forwarding knows a custom key (`branchId`)
|
|
975
|
+
* without the host restating it.
|
|
976
|
+
*/
|
|
977
|
+
function declareTenantContextKey(repo, contextKey) {
|
|
978
|
+
const holder = repo;
|
|
979
|
+
if (!holder[TENANT_KEYS]) Object.defineProperty(holder, TENANT_KEYS, {
|
|
980
|
+
value: /* @__PURE__ */ new Set(),
|
|
981
|
+
enumerable: false
|
|
982
|
+
});
|
|
983
|
+
holder[TENANT_KEYS]?.add(contextKey);
|
|
984
|
+
}
|
|
985
|
+
/** Every tenant context key relevant to a call spanning these repositories. */
|
|
986
|
+
function tenantContextKeysOf(...repos) {
|
|
987
|
+
const keys = new Set(CONVENTIONAL_TENANT_KEYS);
|
|
988
|
+
for (const repo of repos) for (const key of repo?.[TENANT_KEYS] ?? []) keys.add(key);
|
|
989
|
+
return [...keys];
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* The scope to hand a second repository call made on behalf of `source`
|
|
993
|
+
* (a hook's `RepositoryContext`, or a host's request context).
|
|
994
|
+
*
|
|
995
|
+
* Absent values are omitted, never written as `undefined`, so spreading the
|
|
996
|
+
* result into an options bag cannot erase a value set there.
|
|
997
|
+
*/
|
|
998
|
+
function forwardScope(source, tenantKeys = CONVENTIONAL_TENANT_KEYS) {
|
|
999
|
+
if (!source) return {};
|
|
1000
|
+
const out = {};
|
|
1001
|
+
let hasTenant = false;
|
|
1002
|
+
for (const key of tenantKeys) {
|
|
1003
|
+
const value = source[key];
|
|
1004
|
+
if (value === void 0) continue;
|
|
1005
|
+
out[key] = value;
|
|
1006
|
+
if (value !== null && value !== "") hasTenant = true;
|
|
1007
|
+
}
|
|
1008
|
+
if (!hasTenant && source.bypassTenant === true) out.bypassTenant = true;
|
|
1009
|
+
for (const key of ATTRIBUTION_KEYS) if (source[key] !== void 0) out[key] = source[key];
|
|
1010
|
+
return out;
|
|
1011
|
+
}
|
|
1012
|
+
/** Every key `forwardScope` governs. Other forwarding helpers must not copy these themselves. */
|
|
1013
|
+
function isScopeKey(key, tenantKeys = CONVENTIONAL_TENANT_KEYS) {
|
|
1014
|
+
return key === "bypassTenant" || tenantKeys.includes(key) || ATTRIBUTION_KEYS.includes(key);
|
|
1015
|
+
}
|
|
1016
|
+
//#endregion
|
|
933
1017
|
//#region src/plugins/multi-tenant.plugin.ts
|
|
934
1018
|
/**
|
|
935
1019
|
* Multi-Tenant Plugin
|
|
@@ -983,6 +1067,42 @@ function multiTenantPlugin(options = {}) {
|
|
|
983
1067
|
return {
|
|
984
1068
|
name: "multi-tenant",
|
|
985
1069
|
apply(repo) {
|
|
1070
|
+
declareTenantContextKey(repo, contextKey);
|
|
1071
|
+
/** The one tenant decision, shared by the hook and the join rule below. */
|
|
1072
|
+
const decide = (context, op) => {
|
|
1073
|
+
if (context.bypassTenant === true) return { bypass: "option" };
|
|
1074
|
+
if (skipWhen?.(context, op) === true) return { bypass: "callback" };
|
|
1075
|
+
let tenantId = context[contextKey];
|
|
1076
|
+
if (!tenantId && resolveContext) tenantId = resolveContext();
|
|
1077
|
+
return { tenantId };
|
|
1078
|
+
};
|
|
1079
|
+
const castTenant = (tenantId) => fieldType === "objectId" ? new mongoose.Types.ObjectId(tenantId) : tenantId;
|
|
1080
|
+
const missingTenant = (op) => new Error([
|
|
1081
|
+
`[mongokit] Multi-tenant: Missing '${contextKey}' in context for '${op}'.`,
|
|
1082
|
+
"",
|
|
1083
|
+
"Three ways to provide it — pick the one that fits the call site:",
|
|
1084
|
+
"",
|
|
1085
|
+
` 1. Per-call: repo.${op}(..., { ${contextKey}: '<id>' })`,
|
|
1086
|
+
" 2. Ambient (ALS): `createTenantContext()` + `resolveContext` —",
|
|
1087
|
+
" wrap the request in `tenantContext.run({ tenantId }, fn)`,",
|
|
1088
|
+
" then every repo call inside the closure inherits the",
|
|
1089
|
+
" tenant with no options-bag plumbing. See",
|
|
1090
|
+
" `createTenantContext` in @classytic/mongokit.",
|
|
1091
|
+
" 3. Cross-tenant: `bypassTenant: true` per-call (admin / migration /",
|
|
1092
|
+
" support), or `skipWhen: adminBypass({...})` at plugin",
|
|
1093
|
+
" construction for an always-on role-based bypass.",
|
|
1094
|
+
"",
|
|
1095
|
+
"Or set `required: false` if this collection is genuinely not tenant-scoped."
|
|
1096
|
+
].join("\n"));
|
|
1097
|
+
if (repo.Model) declareCollectionScope(repo.Model, (context, op) => {
|
|
1098
|
+
const d = decide(context, op);
|
|
1099
|
+
if ("bypass" in d) return void 0;
|
|
1100
|
+
if (!d.tenantId) {
|
|
1101
|
+
if (required) throw missingTenant(op);
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
return { [tenantField]: castTenant(d.tenantId) };
|
|
1105
|
+
}, { tenantField });
|
|
986
1106
|
const allOps = [...ALL_OPERATIONS.map((op) => ({
|
|
987
1107
|
op,
|
|
988
1108
|
policyKey: OP_REGISTRY[op].policyKey
|
|
@@ -991,41 +1111,21 @@ function multiTenantPlugin(options = {}) {
|
|
|
991
1111
|
if (skipOperations.includes(op)) continue;
|
|
992
1112
|
if (policyKey === "none") continue;
|
|
993
1113
|
repo.on(`before:${op}`, (context) => {
|
|
994
|
-
const
|
|
995
|
-
|
|
996
|
-
if (perCallBypass || callbackBypass) {
|
|
1114
|
+
const decision = decide(context, op);
|
|
1115
|
+
if ("bypass" in decision) {
|
|
997
1116
|
repo.emit("after:tenant-bypass", {
|
|
998
1117
|
context,
|
|
999
1118
|
operation: op,
|
|
1000
|
-
reason:
|
|
1119
|
+
reason: decision.bypass
|
|
1001
1120
|
});
|
|
1002
1121
|
return;
|
|
1003
1122
|
}
|
|
1004
|
-
|
|
1005
|
-
if (
|
|
1006
|
-
tenantId = resolveContext();
|
|
1007
|
-
if (tenantId) context[contextKey] = tenantId;
|
|
1008
|
-
}
|
|
1123
|
+
const tenantId = decision.tenantId;
|
|
1124
|
+
if (tenantId && !context[contextKey]) context[contextKey] = tenantId;
|
|
1009
1125
|
if (!tenantId && allowDataInjection && payloadHasTenantField(context, policyKey, tenantField)) return;
|
|
1010
|
-
if (!tenantId && required) throw
|
|
1011
|
-
`[mongokit] Multi-tenant: Missing '${contextKey}' in context for '${op}'.`,
|
|
1012
|
-
"",
|
|
1013
|
-
"Three ways to provide it — pick the one that fits the call site:",
|
|
1014
|
-
"",
|
|
1015
|
-
` 1. Per-call: repo.${op}(..., { ${contextKey}: '<id>' })`,
|
|
1016
|
-
" 2. Ambient (ALS): `createTenantContext()` + `resolveContext` —",
|
|
1017
|
-
" wrap the request in `tenantContext.run({ tenantId }, fn)`,",
|
|
1018
|
-
" then every repo call inside the closure inherits the",
|
|
1019
|
-
" tenant with no options-bag plumbing. See",
|
|
1020
|
-
" `createTenantContext` in @classytic/mongokit.",
|
|
1021
|
-
" 3. Cross-tenant: `bypassTenant: true` per-call (admin / migration /",
|
|
1022
|
-
" support), or `skipWhen: adminBypass({...})` at plugin",
|
|
1023
|
-
" construction for an always-on role-based bypass.",
|
|
1024
|
-
"",
|
|
1025
|
-
"Or set `required: false` if this collection is genuinely not tenant-scoped."
|
|
1026
|
-
].join("\n"));
|
|
1126
|
+
if (!tenantId && required) throw missingTenant(op);
|
|
1027
1127
|
if (!tenantId) return;
|
|
1028
|
-
const castId =
|
|
1128
|
+
const castId = castTenant(tenantId);
|
|
1029
1129
|
const assertTenantValue = (supplied, where) => {
|
|
1030
1130
|
if (onMismatch === "overwrite") return;
|
|
1031
1131
|
if (supplied === void 0 || supplied === null) return;
|
|
@@ -1234,6 +1334,7 @@ function softDeletePlugin(options = {}) {
|
|
|
1234
1334
|
return {
|
|
1235
1335
|
name: "softDelete",
|
|
1236
1336
|
apply(repo) {
|
|
1337
|
+
if (repo.Model) declareCollectionScope(repo.Model, () => buildDeletedFilter(deletedField, filterMode, false));
|
|
1237
1338
|
try {
|
|
1238
1339
|
const schemaPaths = repo.Model.schema.paths;
|
|
1239
1340
|
if (!schemaPaths[deletedField]) warn(`[softDeletePlugin] Model '${repo.Model.modelName}' has no '${deletedField}' schema path. Soft-delete marks documents by \`$set\`-ing '${deletedField}', but Mongoose strict mode drops updates to undeclared paths — so delete() will SILENTLY no-op (documents are never soft-deleted). Declare it on the schema: { ${deletedField}: { type: Date, default: ${filterMode === "null" ? "null" : "undefined"} } }`);
|
|
@@ -1294,7 +1395,8 @@ function softDeletePlugin(options = {}) {
|
|
|
1294
1395
|
const SOFT_DELETE_SPECIAL = /* @__PURE__ */ new Set([
|
|
1295
1396
|
"updateMany",
|
|
1296
1397
|
"deleteMany",
|
|
1297
|
-
"bulkWrite"
|
|
1398
|
+
"bulkWrite",
|
|
1399
|
+
"bulkUpsert"
|
|
1298
1400
|
]);
|
|
1299
1401
|
for (const op of ALL_OPERATIONS) {
|
|
1300
1402
|
if (SOFT_DELETE_SPECIAL.has(op)) continue;
|
|
@@ -1325,7 +1427,7 @@ function softDeletePlugin(options = {}) {
|
|
|
1325
1427
|
* `deleteMode: 'hard'` still bypasses, exactly as the single-doc path does — a purge
|
|
1326
1428
|
* is a deliberate physical delete, and it reaches the driver directly anyway.
|
|
1327
1429
|
*/
|
|
1328
|
-
|
|
1430
|
+
const scopeSubOps = (context) => {
|
|
1329
1431
|
if (options.soft === false || context.deleteMode === "hard") return;
|
|
1330
1432
|
const ops = context.operations;
|
|
1331
1433
|
if (!Array.isArray(ops)) return;
|
|
@@ -1356,7 +1458,8 @@ function softDeletePlugin(options = {}) {
|
|
|
1356
1458
|
delete subOp[key];
|
|
1357
1459
|
}
|
|
1358
1460
|
}
|
|
1359
|
-
}
|
|
1461
|
+
};
|
|
1462
|
+
for (const op of ["bulkWrite", "bulkUpsert"]) repo.on(`before:${op}`, scopeSubOps, { priority: HOOK_PRIORITY.POLICY });
|
|
1360
1463
|
repo.on("before:deleteMany", async (context) => {
|
|
1361
1464
|
if (context.deleteMode === "hard") return;
|
|
1362
1465
|
if (options.soft !== false) {
|
|
@@ -1500,4 +1603,4 @@ function timestampPlugin() {
|
|
|
1500
1603
|
};
|
|
1501
1604
|
}
|
|
1502
1605
|
//#endregion
|
|
1503
|
-
export {
|
|
1606
|
+
export { AuditTrailQuery as _, declareTenantContextKey as a, auditLogPlugin as b, tenantContextKeysOf as c, ensureSequenceAtLeast as d, getNextSequence as f, cachePlugin as g, sequentialId as h, multiTenantPlugin as i, customIdPlugin as l, readSequence as m, softDeletePlugin as n, forwardScope as o, prefixedId as p, adminBypass$1 as r, isScopeKey as s, timestampPlugin as t, dateSequentialId as u, auditTrailPlugin as v, ensureAuditTrailReady as y };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { v as UserContext } from "./repository-
|
|
2
|
-
import { r as FieldPreset } from "./plugin-options-
|
|
1
|
+
import { v as UserContext } from "./repository-DNHfjAZX.mjs";
|
|
2
|
+
import { r as FieldPreset } from "./plugin-options-CRhIvSzx.mjs";
|
|
3
3
|
import mongoose, { Schema } from "mongoose";
|
|
4
4
|
import { CacheAdapter } from "@classytic/repo-core/cache";
|
|
5
5
|
import { HttpError } from "@classytic/repo-core/errors";
|