@classytic/mongokit 3.5.6 → 3.6.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 +189 -1580
- package/dist/{PaginationEngine-CpsQl471.mjs → PaginationEngine-CunXkR9p.mjs} +100 -4
- package/dist/actions/index.d.mts +1 -1
- package/dist/ai/index.d.mts +1 -1
- package/dist/{index-DmVbUzDL.d.mts → index-BSRSsRRx.d.mts} +1 -1
- package/dist/index.d.mts +64 -7
- package/dist/index.mjs +3 -3
- package/dist/{mongooseToJsonSchema-DQMU0Dlv.d.mts → mongooseToJsonSchema-D2YjHo7W.d.mts} +1 -1
- package/dist/pagination/PaginationEngine.d.mts +8 -1
- package/dist/pagination/PaginationEngine.mjs +1 -1
- package/dist/plugins/index.d.mts +1 -1
- package/dist/plugins/index.mjs +1 -1
- package/dist/{types-BwO4u3JN.d.mts → types-DiQfDesD.d.mts} +29 -3
- package/dist/utils/index.d.mts +2 -2
- package/dist/{validation-chain.plugin-DCYjnPau.mjs → validation-chain.plugin-3rr7dlw6.mjs} +251 -131
- package/dist/{validation-chain.plugin-BtmT-FFB.d.mts → validation-chain.plugin-COKNRAwp.d.mts} +15 -12
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { a as warn, i as debug, n as parseDuplicateKeyError, t as createError } from "./error-Bpbi_NKo.mjs";
|
|
2
2
|
import { a as exists, c as getOrCreate, d as deleteByQuery, g as upsert, i as count, m as createMany, n as updateByQuery, o as getById, p as create, s as getByQuery, t as update, u as deleteById, v as distinct, y as LookupBuilder } from "./update-AVfKWNGt.mjs";
|
|
3
|
-
import { t as PaginationEngine } from "./PaginationEngine-
|
|
3
|
+
import { t as PaginationEngine } from "./PaginationEngine-CunXkR9p.mjs";
|
|
4
4
|
import { hasNearOperator, rewriteNearForCount } from "./query/primitives/geo.mjs";
|
|
5
5
|
import { r as isValidIdForType, t as getSchemaIdType } from "./id-resolution-LZ-cCwR5.mjs";
|
|
6
6
|
import { a as byIdKey, c as listQueryKey, l as modelPattern, o as byQueryKey, r as getFieldsForUser, u as versionKey } from "./field-selection-C5o7jyyA.mjs";
|
|
@@ -583,12 +583,23 @@ function batchOperationsPlugin() {
|
|
|
583
583
|
}
|
|
584
584
|
});
|
|
585
585
|
/**
|
|
586
|
-
* Delete multiple documents
|
|
586
|
+
* Delete multiple documents matching a query.
|
|
587
|
+
*
|
|
588
|
+
* Behavior mirrors `Repository.delete`: defaults to soft-delete when
|
|
589
|
+
* `softDeletePlugin` is wired, physical delete otherwise. Pass
|
|
590
|
+
* `{ mode: 'hard' }` to bypass soft-delete for GDPR / cleanup paths —
|
|
591
|
+
* multi-tenant scoping and audit hooks still fire.
|
|
592
|
+
*
|
|
593
|
+
* Rejects empty filters even after policy hooks run, to prevent
|
|
594
|
+
* accidental collection wipes.
|
|
587
595
|
*/
|
|
588
596
|
repo.registerMethod("deleteMany", async function(query, options = {}) {
|
|
597
|
+
if (!query || Object.keys(query).length === 0) throw createError(400, "deleteMany requires a non-empty query filter. Pass an explicit filter to prevent accidental mass deletes.");
|
|
598
|
+
const mode = options.mode;
|
|
589
599
|
const context = await this._buildContext("deleteMany", {
|
|
590
600
|
query,
|
|
591
|
-
...options
|
|
601
|
+
...options,
|
|
602
|
+
...mode ? { deleteMode: mode } : {}
|
|
592
603
|
});
|
|
593
604
|
try {
|
|
594
605
|
if (context.softDeleted) {
|
|
@@ -603,7 +614,7 @@ function batchOperationsPlugin() {
|
|
|
603
614
|
return result;
|
|
604
615
|
}
|
|
605
616
|
const finalQuery = context.query || query;
|
|
606
|
-
if (!finalQuery || Object.keys(finalQuery).length === 0) throw createError(400, "deleteMany requires a non-empty query filter
|
|
617
|
+
if (!finalQuery || Object.keys(finalQuery).length === 0) throw createError(400, "deleteMany requires a non-empty query filter after policy hooks.");
|
|
607
618
|
const result = await this.Model.deleteMany(finalQuery, { session: options.session }).exec();
|
|
608
619
|
await this.emitAsync("after:deleteMany", {
|
|
609
620
|
context,
|
|
@@ -1154,6 +1165,60 @@ var AggregationBuilder = class AggregationBuilder {
|
|
|
1154
1165
|
* ```
|
|
1155
1166
|
*/
|
|
1156
1167
|
//#endregion
|
|
1168
|
+
//#region src/transaction.ts
|
|
1169
|
+
/**
|
|
1170
|
+
* Run a callback inside a MongoDB transaction on the given connection.
|
|
1171
|
+
*
|
|
1172
|
+
* - Starts a session, runs `session.withTransaction(callback)` (which auto-retries
|
|
1173
|
+
* on `TransientTransactionError` and `UnknownTransactionCommitResult`), and
|
|
1174
|
+
* always ends the session in `finally`.
|
|
1175
|
+
* - When `allowFallback` is true and the deployment doesn't support transactions
|
|
1176
|
+
* (e.g. standalone MongoDB in dev), the callback runs once without a
|
|
1177
|
+
* transaction on the same session. `onFallback` is invoked with the original
|
|
1178
|
+
* error so hosts can log the degradation.
|
|
1179
|
+
*
|
|
1180
|
+
* @example
|
|
1181
|
+
* ```ts
|
|
1182
|
+
* import mongoose from 'mongoose';
|
|
1183
|
+
* import { withTransaction } from '@classytic/mongokit';
|
|
1184
|
+
*
|
|
1185
|
+
* await withTransaction(mongoose.connection, async (session) => {
|
|
1186
|
+
* const order = await orderRepo.create(data, { session });
|
|
1187
|
+
* await inventoryRepo.decrement(order.items, { session });
|
|
1188
|
+
* return order;
|
|
1189
|
+
* });
|
|
1190
|
+
* ```
|
|
1191
|
+
*/
|
|
1192
|
+
async function withTransaction(connection, callback, options = {}) {
|
|
1193
|
+
const session = await connection.startSession();
|
|
1194
|
+
try {
|
|
1195
|
+
return await session.withTransaction(() => callback(session), options.transactionOptions);
|
|
1196
|
+
} catch (error) {
|
|
1197
|
+
const err = error;
|
|
1198
|
+
if (options.allowFallback && isTransactionUnsupported(err)) {
|
|
1199
|
+
options.onFallback?.(err);
|
|
1200
|
+
return await callback(session);
|
|
1201
|
+
}
|
|
1202
|
+
throw err;
|
|
1203
|
+
} finally {
|
|
1204
|
+
await session.endSession();
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Detect whether an error indicates the MongoDB deployment does not support
|
|
1209
|
+
* multi-document transactions (standalone server, older topology, etc.).
|
|
1210
|
+
*
|
|
1211
|
+
* Checks MongoDB error codes first — 263 (standalone) and 20 (unsupported
|
|
1212
|
+
* topology) — with a message-matching fallback for edge cases surfaced by
|
|
1213
|
+
* older driver versions.
|
|
1214
|
+
*/
|
|
1215
|
+
function isTransactionUnsupported(error) {
|
|
1216
|
+
const code = error.code;
|
|
1217
|
+
if (code === 263 || code === 20) return true;
|
|
1218
|
+
const message = (error.message || "").toLowerCase();
|
|
1219
|
+
return message.includes("transaction numbers are only allowed on a replica set member") || message.includes("transaction is not supported");
|
|
1220
|
+
}
|
|
1221
|
+
//#endregion
|
|
1157
1222
|
//#region src/Repository.ts
|
|
1158
1223
|
function ensureLookupProjectionIncludesCursorFields(projection, sort) {
|
|
1159
1224
|
if (!projection || !sort) return projection;
|
|
@@ -1770,12 +1835,31 @@ var Repository = class {
|
|
|
1770
1835
|
}
|
|
1771
1836
|
}
|
|
1772
1837
|
/**
|
|
1773
|
-
* Delete document by ID
|
|
1838
|
+
* Delete a document by ID.
|
|
1839
|
+
*
|
|
1840
|
+
* By default the behavior is decided by the plugin stack:
|
|
1841
|
+
* - With `softDeletePlugin` wired → soft delete (sets `deletedAt`)
|
|
1842
|
+
* - Without the plugin → physical delete
|
|
1843
|
+
*
|
|
1844
|
+
* Pass `mode: 'hard'` to force physical deletion regardless of plugins.
|
|
1845
|
+
* All policy hooks (multi-tenant scope, audit trails, cache invalidation,
|
|
1846
|
+
* cascade) still fire — only the soft-delete interception is skipped. This
|
|
1847
|
+
* is the GDPR / admin-cleanup path.
|
|
1848
|
+
*
|
|
1849
|
+
* @example
|
|
1850
|
+
* ```ts
|
|
1851
|
+
* // Respects softDeletePlugin — soft if wired
|
|
1852
|
+
* await userRepo.delete(userId);
|
|
1853
|
+
*
|
|
1854
|
+
* // GDPR erasure — physical delete, audit hooks still fire
|
|
1855
|
+
* await userRepo.delete(userId, { mode: 'hard', organizationId: 'org_123' });
|
|
1856
|
+
* ```
|
|
1774
1857
|
*/
|
|
1775
1858
|
async delete(id, options = {}) {
|
|
1776
1859
|
const context = await this._buildContext("delete", {
|
|
1777
1860
|
id,
|
|
1778
|
-
...options
|
|
1861
|
+
...options,
|
|
1862
|
+
...options.mode ? { deleteMode: options.mode } : {}
|
|
1779
1863
|
});
|
|
1780
1864
|
try {
|
|
1781
1865
|
if (context.softDeleted) {
|
|
@@ -2150,25 +2234,7 @@ var Repository = class {
|
|
|
2150
2234
|
* ```
|
|
2151
2235
|
*/
|
|
2152
2236
|
async withTransaction(callback, options = {}) {
|
|
2153
|
-
|
|
2154
|
-
try {
|
|
2155
|
-
return await session.withTransaction(() => callback(session), options.transactionOptions);
|
|
2156
|
-
} catch (error) {
|
|
2157
|
-
const err = error;
|
|
2158
|
-
if (options.allowFallback && this._isTransactionUnsupported(err)) {
|
|
2159
|
-
options.onFallback?.(err);
|
|
2160
|
-
return await callback(session);
|
|
2161
|
-
}
|
|
2162
|
-
throw err;
|
|
2163
|
-
} finally {
|
|
2164
|
-
await session.endSession();
|
|
2165
|
-
}
|
|
2166
|
-
}
|
|
2167
|
-
_isTransactionUnsupported(error) {
|
|
2168
|
-
const code = error.code;
|
|
2169
|
-
if (code === 263 || code === 20) return true;
|
|
2170
|
-
const message = (error.message || "").toLowerCase();
|
|
2171
|
-
return message.includes("transaction numbers are only allowed on a replica set member") || message.includes("transaction is not supported");
|
|
2237
|
+
return withTransaction(this.Model.db, callback, options);
|
|
2172
2238
|
}
|
|
2173
2239
|
/**
|
|
2174
2240
|
* Execute custom query with event emission
|
|
@@ -2782,39 +2848,73 @@ function cachePlugin(options) {
|
|
|
2782
2848
|
//#region src/plugins/cascade.plugin.ts
|
|
2783
2849
|
/**
|
|
2784
2850
|
* Cascade Delete Plugin
|
|
2785
|
-
* Automatically deletes related documents when a parent document is deleted
|
|
2786
2851
|
*
|
|
2787
|
-
*
|
|
2788
|
-
*
|
|
2789
|
-
*
|
|
2790
|
-
*
|
|
2852
|
+
* Automatically deletes related documents when a parent document is deleted.
|
|
2853
|
+
*
|
|
2854
|
+
* Two routing modes per relation:
|
|
2855
|
+
*
|
|
2856
|
+
* 1. **Repo-routed (preferred)** — pass `repo: targetRepo` on the relation.
|
|
2857
|
+
* Cascade calls `targetRepo.delete(id, { mode })` / `targetRepo.deleteMany(
|
|
2858
|
+
* query, { mode })`, so the target's `before:delete` / `before:deleteMany`
|
|
2859
|
+
* hooks fire. Multi-tenant scoping, audit logging, cache invalidation,
|
|
2860
|
+
* and the target's own `softDeletePlugin` (with its configured
|
|
2861
|
+
* `deletedField`) all run correctly.
|
|
2862
|
+
*
|
|
2863
|
+
* 2. **Model-routed (legacy)** — pass `model: 'TargetModelName'`. Cascade
|
|
2864
|
+
* writes directly via `mongoose.models[name].updateMany / deleteMany`,
|
|
2865
|
+
* **bypassing** the target's hooks. Safe only for trivial targets with
|
|
2866
|
+
* no policy plugins. Retained for backwards compatibility — prefer the
|
|
2867
|
+
* repo-routed form for new code.
|
|
2868
|
+
*
|
|
2869
|
+
* The parent's delete mode propagates: a hard-deleted parent cascades hard,
|
|
2870
|
+
* a soft-deleted parent cascades soft — unless `relation.softDelete` overrides
|
|
2871
|
+
* the decision per-relation.
|
|
2791
2872
|
*
|
|
2873
|
+
* @example Repo-routed (new)
|
|
2874
|
+
* ```ts
|
|
2792
2875
|
* const productRepo = new Repository(Product, [
|
|
2793
2876
|
* methodRegistryPlugin(),
|
|
2794
2877
|
* cascadePlugin({
|
|
2795
2878
|
* relations: [
|
|
2796
|
-
* {
|
|
2797
|
-
* {
|
|
2798
|
-
* ]
|
|
2799
|
-
* })
|
|
2879
|
+
* { repo: stockEntryRepo, foreignKey: 'product' },
|
|
2880
|
+
* { repo: stockMovementRepo, foreignKey: 'product' },
|
|
2881
|
+
* ],
|
|
2882
|
+
* }),
|
|
2800
2883
|
* ]);
|
|
2884
|
+
* ```
|
|
2801
2885
|
*
|
|
2802
|
-
*
|
|
2803
|
-
*
|
|
2886
|
+
* @example Model-routed (legacy)
|
|
2887
|
+
* ```ts
|
|
2888
|
+
* cascadePlugin({
|
|
2889
|
+
* relations: [
|
|
2890
|
+
* { model: 'StockEntry', foreignKey: 'product' },
|
|
2891
|
+
* ],
|
|
2892
|
+
* });
|
|
2804
2893
|
* ```
|
|
2805
2894
|
*/
|
|
2806
2895
|
/**
|
|
2807
|
-
*
|
|
2808
|
-
*
|
|
2809
|
-
*
|
|
2810
|
-
*
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2896
|
+
* Collect top-level parent-context fields that a target plugin (multi-tenant,
|
|
2897
|
+
* audit) will likely need. We can't know the target's contextKey, so we
|
|
2898
|
+
* forward a well-known allow-list: `organizationId`, `tenantId`, and `user`.
|
|
2899
|
+
* These are the conventions enforced by the built-in plugins.
|
|
2900
|
+
*/
|
|
2901
|
+
function collectScopeForward(context) {
|
|
2902
|
+
const out = {};
|
|
2903
|
+
if (context.organizationId !== void 0) out.organizationId = context.organizationId;
|
|
2904
|
+
if (context.tenantId !== void 0) out.tenantId = context.tenantId;
|
|
2905
|
+
if (context.user !== void 0) out.user = context.user;
|
|
2906
|
+
return out;
|
|
2907
|
+
}
|
|
2908
|
+
/**
|
|
2909
|
+
* Cascade delete plugin.
|
|
2814
2910
|
*/
|
|
2815
2911
|
function cascadePlugin(options) {
|
|
2816
2912
|
const { relations, parallel = true, logger } = options;
|
|
2817
2913
|
if (!relations || relations.length === 0) throw new Error("cascadePlugin requires at least one relation");
|
|
2914
|
+
for (const rel of relations) {
|
|
2915
|
+
if (!rel.repo && !rel.model) throw new Error("cascadePlugin: each relation needs either `repo` (preferred) or `model` (legacy)");
|
|
2916
|
+
if (!rel.foreignKey) throw new Error("cascadePlugin: each relation needs `foreignKey`");
|
|
2917
|
+
}
|
|
2818
2918
|
return {
|
|
2819
2919
|
name: "cascade",
|
|
2820
2920
|
apply(repo) {
|
|
@@ -2825,59 +2925,32 @@ function cascadePlugin(options) {
|
|
|
2825
2925
|
logger?.warn?.("Cascade delete skipped: no document ID in context", { model: context.model });
|
|
2826
2926
|
return;
|
|
2827
2927
|
}
|
|
2828
|
-
const
|
|
2829
|
-
const
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2928
|
+
const isSoftParent = context.softDeleted === true;
|
|
2929
|
+
const scopeForward = collectScopeForward(context);
|
|
2930
|
+
const cascadeOne = async (relation) => {
|
|
2931
|
+
const shouldSoftDelete = relation.softDelete ?? isSoftParent;
|
|
2932
|
+
const cascadeCtx = {
|
|
2933
|
+
targetModelName: relation.repo ? relation.repo.Model?.modelName ?? "<unknown>" : relation.model ?? "<unknown>",
|
|
2934
|
+
session: context.session,
|
|
2935
|
+
shouldSoftDelete,
|
|
2936
|
+
user: context.user,
|
|
2937
|
+
scopeForward
|
|
2938
|
+
};
|
|
2839
2939
|
try {
|
|
2840
|
-
if (relation.
|
|
2841
|
-
|
|
2842
|
-
deletedAt: /* @__PURE__ */ new Date(),
|
|
2843
|
-
...context.user ? { deletedBy: context.user._id || context.user.id } : {}
|
|
2844
|
-
}, { session: context.session });
|
|
2845
|
-
logger?.info?.(`Cascade soft-deleted ${updateResult.modifiedCount} documents`, {
|
|
2846
|
-
parentModel: context.model,
|
|
2847
|
-
parentId: String(deletedId),
|
|
2848
|
-
relatedModel: relation.model,
|
|
2849
|
-
foreignKey: relation.foreignKey,
|
|
2850
|
-
count: updateResult.modifiedCount
|
|
2851
|
-
});
|
|
2852
|
-
} else {
|
|
2853
|
-
const deleteResult = await RelatedModel.deleteMany(query, { session: context.session });
|
|
2854
|
-
logger?.info?.(`Cascade deleted ${deleteResult.deletedCount} documents`, {
|
|
2855
|
-
parentModel: context.model,
|
|
2856
|
-
parentId: String(deletedId),
|
|
2857
|
-
relatedModel: relation.model,
|
|
2858
|
-
foreignKey: relation.foreignKey,
|
|
2859
|
-
count: deleteResult.deletedCount
|
|
2860
|
-
});
|
|
2861
|
-
}
|
|
2940
|
+
if (relation.repo) await cascadeViaRepoBulk(relation, deletedId, cascadeCtx);
|
|
2941
|
+
else await cascadeViaLegacyModel(relation, deletedId, cascadeCtx, logger);
|
|
2862
2942
|
} catch (error) {
|
|
2863
|
-
logger?.error?.(`Cascade delete failed for
|
|
2943
|
+
logger?.error?.(`Cascade delete failed for '${cascadeCtx.targetModelName}'`, {
|
|
2864
2944
|
parentModel: context.model,
|
|
2865
2945
|
parentId: String(deletedId),
|
|
2866
|
-
relatedModel:
|
|
2946
|
+
relatedModel: cascadeCtx.targetModelName,
|
|
2867
2947
|
foreignKey: relation.foreignKey,
|
|
2868
2948
|
error: error.message
|
|
2869
2949
|
});
|
|
2870
2950
|
throw error;
|
|
2871
2951
|
}
|
|
2872
2952
|
};
|
|
2873
|
-
|
|
2874
|
-
const failures = (await Promise.allSettled(relations.map(cascadeDelete))).filter((r) => r.status === "rejected");
|
|
2875
|
-
if (failures.length) {
|
|
2876
|
-
const err = failures[0].reason;
|
|
2877
|
-
if (failures.length > 1) err.message = `${failures.length} cascade deletes failed. First: ${err.message}`;
|
|
2878
|
-
throw err;
|
|
2879
|
-
}
|
|
2880
|
-
} else for (const relation of relations) await cascadeDelete(relation);
|
|
2953
|
+
await runCascades(relations, cascadeOne, parallel);
|
|
2881
2954
|
});
|
|
2882
2955
|
repo.on("before:deleteMany", async (context) => {
|
|
2883
2956
|
const query = context.query;
|
|
@@ -2889,60 +2962,106 @@ function cascadePlugin(options) {
|
|
|
2889
2962
|
const { context } = payload;
|
|
2890
2963
|
const ids = context._cascadeIds;
|
|
2891
2964
|
if (!ids || ids.length === 0) return;
|
|
2892
|
-
const
|
|
2893
|
-
const
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2965
|
+
const isSoftParent = context.softDeleted === true;
|
|
2966
|
+
const scopeForward = collectScopeForward(context);
|
|
2967
|
+
const cascadeBulk = async (relation) => {
|
|
2968
|
+
const shouldSoftDelete = relation.softDelete ?? isSoftParent;
|
|
2969
|
+
const cascadeCtx = {
|
|
2970
|
+
targetModelName: relation.repo ? relation.repo.Model?.modelName ?? "<unknown>" : relation.model ?? "<unknown>",
|
|
2971
|
+
session: context.session,
|
|
2972
|
+
shouldSoftDelete,
|
|
2973
|
+
user: context.user,
|
|
2974
|
+
scopeForward
|
|
2975
|
+
};
|
|
2901
2976
|
try {
|
|
2902
|
-
if (
|
|
2903
|
-
|
|
2904
|
-
deletedAt: /* @__PURE__ */ new Date(),
|
|
2905
|
-
...context.user ? { deletedBy: context.user._id || context.user.id } : {}
|
|
2906
|
-
}, { session: context.session });
|
|
2907
|
-
logger?.info?.(`Cascade soft-deleted ${updateResult.modifiedCount} documents (bulk)`, {
|
|
2908
|
-
parentModel: context.model,
|
|
2909
|
-
parentCount: ids.length,
|
|
2910
|
-
relatedModel: relation.model,
|
|
2911
|
-
foreignKey: relation.foreignKey,
|
|
2912
|
-
count: updateResult.modifiedCount
|
|
2913
|
-
});
|
|
2914
|
-
} else {
|
|
2915
|
-
const deleteResult = await RelatedModel.deleteMany(query, { session: context.session });
|
|
2916
|
-
logger?.info?.(`Cascade deleted ${deleteResult.deletedCount} documents (bulk)`, {
|
|
2917
|
-
parentModel: context.model,
|
|
2918
|
-
parentCount: ids.length,
|
|
2919
|
-
relatedModel: relation.model,
|
|
2920
|
-
foreignKey: relation.foreignKey,
|
|
2921
|
-
count: deleteResult.deletedCount
|
|
2922
|
-
});
|
|
2923
|
-
}
|
|
2977
|
+
if (relation.repo) await cascadeViaRepoBulkMany(relation, ids, cascadeCtx);
|
|
2978
|
+
else await cascadeViaLegacyModelMany(relation, ids, cascadeCtx, logger);
|
|
2924
2979
|
} catch (error) {
|
|
2925
|
-
logger?.error?.(`Cascade deleteMany failed for
|
|
2980
|
+
logger?.error?.(`Cascade deleteMany failed for '${cascadeCtx.targetModelName}'`, {
|
|
2926
2981
|
parentModel: context.model,
|
|
2927
|
-
relatedModel:
|
|
2982
|
+
relatedModel: cascadeCtx.targetModelName,
|
|
2928
2983
|
foreignKey: relation.foreignKey,
|
|
2929
2984
|
error: error.message
|
|
2930
2985
|
});
|
|
2931
2986
|
throw error;
|
|
2932
2987
|
}
|
|
2933
2988
|
};
|
|
2934
|
-
|
|
2935
|
-
const failures = (await Promise.allSettled(relations.map(cascadeDeleteMany))).filter((r) => r.status === "rejected");
|
|
2936
|
-
if (failures.length) {
|
|
2937
|
-
const err = failures[0].reason;
|
|
2938
|
-
if (failures.length > 1) err.message = `${failures.length} cascade deletes failed. First: ${err.message}`;
|
|
2939
|
-
throw err;
|
|
2940
|
-
}
|
|
2941
|
-
} else for (const relation of relations) await cascadeDeleteMany(relation);
|
|
2989
|
+
await runCascades(relations, cascadeBulk, parallel);
|
|
2942
2990
|
});
|
|
2943
2991
|
}
|
|
2944
2992
|
};
|
|
2945
2993
|
}
|
|
2994
|
+
/**
|
|
2995
|
+
* Repo-routed cascade for a single parent delete.
|
|
2996
|
+
* Calls `repo.deleteMany({ [fk]: parentId }, { mode })` — respects all target
|
|
2997
|
+
* hooks (multi-tenant, audit, target's own soft-delete plugin).
|
|
2998
|
+
*/
|
|
2999
|
+
async function cascadeViaRepoBulk(relation, parentId, ctx) {
|
|
3000
|
+
const targetRepo = relation.repo;
|
|
3001
|
+
if (typeof targetRepo.deleteMany !== "function") throw new Error(`cascadePlugin: target repo for '${ctx.targetModelName}' is missing deleteMany(). Wire batchOperationsPlugin() + methodRegistryPlugin() on the target repo.`);
|
|
3002
|
+
await targetRepo.deleteMany({ [relation.foreignKey]: parentId }, {
|
|
3003
|
+
...ctx.scopeForward,
|
|
3004
|
+
mode: ctx.shouldSoftDelete ? "soft" : "hard",
|
|
3005
|
+
session: ctx.session
|
|
3006
|
+
});
|
|
3007
|
+
}
|
|
3008
|
+
/**
|
|
3009
|
+
* Repo-routed cascade for a bulk parent deleteMany — `$in` over snapshot ids.
|
|
3010
|
+
*/
|
|
3011
|
+
async function cascadeViaRepoBulkMany(relation, parentIds, ctx) {
|
|
3012
|
+
const targetRepo = relation.repo;
|
|
3013
|
+
if (typeof targetRepo.deleteMany !== "function") throw new Error(`cascadePlugin: target repo for '${ctx.targetModelName}' is missing deleteMany(). Wire batchOperationsPlugin() + methodRegistryPlugin() on the target repo.`);
|
|
3014
|
+
await targetRepo.deleteMany({ [relation.foreignKey]: { $in: parentIds } }, {
|
|
3015
|
+
...ctx.scopeForward,
|
|
3016
|
+
mode: ctx.shouldSoftDelete ? "soft" : "hard",
|
|
3017
|
+
session: ctx.session
|
|
3018
|
+
});
|
|
3019
|
+
}
|
|
3020
|
+
/**
|
|
3021
|
+
* Legacy cascade via `mongoose.models[name]`. Bypasses target hooks.
|
|
3022
|
+
* Retained for backwards compatibility with existing cascadePlugin consumers.
|
|
3023
|
+
*/
|
|
3024
|
+
async function cascadeViaLegacyModel(relation, parentId, ctx, logger) {
|
|
3025
|
+
const RelatedModel = mongoose.models[relation.model];
|
|
3026
|
+
if (!RelatedModel) {
|
|
3027
|
+
logger?.warn?.(`Cascade delete skipped: model '${relation.model}' not found`);
|
|
3028
|
+
return;
|
|
3029
|
+
}
|
|
3030
|
+
const query = { [relation.foreignKey]: parentId };
|
|
3031
|
+
if (ctx.shouldSoftDelete) await RelatedModel.updateMany(query, {
|
|
3032
|
+
deletedAt: /* @__PURE__ */ new Date(),
|
|
3033
|
+
...ctx.user ? { deletedBy: ctx.user._id || ctx.user.id } : {}
|
|
3034
|
+
}, { session: ctx.session });
|
|
3035
|
+
else await RelatedModel.deleteMany(query, { session: ctx.session });
|
|
3036
|
+
}
|
|
3037
|
+
async function cascadeViaLegacyModelMany(relation, parentIds, ctx, logger) {
|
|
3038
|
+
const RelatedModel = mongoose.models[relation.model];
|
|
3039
|
+
if (!RelatedModel) {
|
|
3040
|
+
logger?.warn?.(`Cascade deleteMany skipped: model '${relation.model}' not found`);
|
|
3041
|
+
return;
|
|
3042
|
+
}
|
|
3043
|
+
const query = { [relation.foreignKey]: { $in: parentIds } };
|
|
3044
|
+
if (ctx.shouldSoftDelete) await RelatedModel.updateMany(query, {
|
|
3045
|
+
deletedAt: /* @__PURE__ */ new Date(),
|
|
3046
|
+
...ctx.user ? { deletedBy: ctx.user._id || ctx.user.id } : {}
|
|
3047
|
+
}, { session: ctx.session });
|
|
3048
|
+
else await RelatedModel.deleteMany(query, { session: ctx.session });
|
|
3049
|
+
}
|
|
3050
|
+
/**
|
|
3051
|
+
* Execute a list of cascade operations, honoring the `parallel` flag.
|
|
3052
|
+
* Uses `allSettled` so one failure doesn't abort siblings; throws the first
|
|
3053
|
+
* rejection (with a composite message if several failed) after all complete.
|
|
3054
|
+
*/
|
|
3055
|
+
async function runCascades(relations, fn, parallel) {
|
|
3056
|
+
if (parallel) {
|
|
3057
|
+
const failures = (await Promise.allSettled(relations.map(fn))).filter((r) => r.status === "rejected");
|
|
3058
|
+
if (failures.length) {
|
|
3059
|
+
const err = failures[0].reason;
|
|
3060
|
+
if (failures.length > 1) err.message = `${failures.length} cascade deletes failed. First: ${err.message}`;
|
|
3061
|
+
throw err;
|
|
3062
|
+
}
|
|
3063
|
+
} else for (const relation of relations) await fn(relation);
|
|
3064
|
+
}
|
|
2946
3065
|
//#endregion
|
|
2947
3066
|
//#region src/plugins/custom-id.plugin.ts
|
|
2948
3067
|
/**
|
|
@@ -3837,6 +3956,7 @@ function softDeletePlugin(options = {}) {
|
|
|
3837
3956
|
});
|
|
3838
3957
|
}
|
|
3839
3958
|
repo.on("before:delete", async (context) => {
|
|
3959
|
+
if (context.deleteMode === "hard") return;
|
|
3840
3960
|
if (options.soft !== false) {
|
|
3841
3961
|
const updateData = { [deletedField]: /* @__PURE__ */ new Date() };
|
|
3842
3962
|
if (context.user) updateData[deletedByField] = context.user._id || context.user.id;
|
|
@@ -3896,6 +4016,7 @@ function softDeletePlugin(options = {}) {
|
|
|
3896
4016
|
}
|
|
3897
4017
|
}, { priority: HOOK_PRIORITY.POLICY });
|
|
3898
4018
|
repo.on("before:deleteMany", async (context) => {
|
|
4019
|
+
if (context.deleteMode === "hard") return;
|
|
3899
4020
|
if (options.soft !== false) {
|
|
3900
4021
|
const deleteFilter = buildDeletedFilter(deletedField, filterMode, false);
|
|
3901
4022
|
const finalQuery = {
|
|
@@ -3948,9 +4069,8 @@ function softDeletePlugin(options = {}) {
|
|
|
3948
4069
|
throw error;
|
|
3949
4070
|
}
|
|
3950
4071
|
await this.emitAsync("after:restore", {
|
|
3951
|
-
|
|
3952
|
-
result
|
|
3953
|
-
context
|
|
4072
|
+
context,
|
|
4073
|
+
result
|
|
3954
4074
|
});
|
|
3955
4075
|
return result;
|
|
3956
4076
|
};
|
|
@@ -4254,4 +4374,4 @@ function uniqueField(field, errorMessage) {
|
|
|
4254
4374
|
};
|
|
4255
4375
|
}
|
|
4256
4376
|
//#endregion
|
|
4257
|
-
export {
|
|
4377
|
+
export { auditTrailPlugin as A, HOOK_PRIORITY as C, AggregationBuilder as D, withTransaction as E, aggregateHelpersPlugin as M, batchOperationsPlugin as O, cachePlugin as S, isTransactionUnsupported as T, dateSequentialId as _, uniqueField as a, sequentialId as b, subdocumentPlugin as c, multiTenantPlugin as d, mongoOperationsPlugin as f, customIdPlugin as g, elasticSearchPlugin as h, requireField as i, auditLogPlugin as j, AuditTrailQuery as k, softDeletePlugin as l, fieldFilterPlugin as m, blockIf as n, validationChainPlugin as o, methodRegistryPlugin as p, immutableField as r, timestampPlugin as s, autoInject as t, observabilityPlugin as u, getNextSequence as v, Repository as w, cascadePlugin as x, prefixedId as y };
|
package/dist/{validation-chain.plugin-BtmT-FFB.d.mts → validation-chain.plugin-COKNRAwp.d.mts}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { I as Logger, J as Plugin, St as ValidatorDefinition, T as FieldPreset, U as OffsetPaginationResult, V as ObjectId, Z as PopulateSpec, bt as ValidationChainOptions, et as RepositoryContext, f as CascadeOptions, l as CacheOptions, nt as RepositoryInstance, pt as SortSpec, st as SelectSpec, u as CacheStats, ut as SoftDeleteOptions } from "./types-
|
|
1
|
+
import { I as Logger, J as Plugin, St as ValidatorDefinition, T as FieldPreset, U as OffsetPaginationResult, V as ObjectId, Z as PopulateSpec, bt as ValidationChainOptions, et as RepositoryContext, f as CascadeOptions, l as CacheOptions, nt as RepositoryInstance, pt as SortSpec, st as SelectSpec, u as CacheStats, ut as SoftDeleteOptions } from "./types-DiQfDesD.mjs";
|
|
2
2
|
import mongoose, { ClientSession } from "mongoose";
|
|
3
3
|
|
|
4
4
|
//#region src/plugins/aggregate-helpers.plugin.d.ts
|
|
@@ -307,12 +307,21 @@ interface BatchOperationsMethods {
|
|
|
307
307
|
upsertedId: unknown;
|
|
308
308
|
}>;
|
|
309
309
|
/**
|
|
310
|
-
* Delete multiple documents matching the query
|
|
311
|
-
*
|
|
312
|
-
*
|
|
310
|
+
* Delete multiple documents matching the query.
|
|
311
|
+
*
|
|
312
|
+
* Defaults to soft delete when `softDeletePlugin` is wired, physical delete
|
|
313
|
+
* otherwise. Pass `{ mode: 'hard' }` for GDPR / admin cleanup — multi-tenant
|
|
314
|
+
* scoping and audit hooks still fire.
|
|
315
|
+
*
|
|
316
|
+
* @param query - Query to match documents (must be non-empty)
|
|
317
|
+
* @param options - Operation options (session, mode, tenant keys)
|
|
313
318
|
* @returns Delete result with deletedCount
|
|
314
319
|
*/
|
|
315
|
-
deleteMany(query: Record<string, unknown>, options?:
|
|
320
|
+
deleteMany(query: Record<string, unknown>, options?: {
|
|
321
|
+
session?: ClientSession;
|
|
322
|
+
mode?: 'hard' | 'soft';
|
|
323
|
+
[key: string]: unknown;
|
|
324
|
+
}): Promise<{
|
|
316
325
|
acknowledged: boolean;
|
|
317
326
|
deletedCount: number;
|
|
318
327
|
}>;
|
|
@@ -398,13 +407,7 @@ interface CacheMethods {
|
|
|
398
407
|
//#endregion
|
|
399
408
|
//#region src/plugins/cascade.plugin.d.ts
|
|
400
409
|
/**
|
|
401
|
-
* Cascade delete plugin
|
|
402
|
-
*
|
|
403
|
-
* Deletes related documents after the parent document is deleted.
|
|
404
|
-
* Works with both hard delete and soft delete scenarios.
|
|
405
|
-
*
|
|
406
|
-
* @param options - Cascade configuration
|
|
407
|
-
* @returns Plugin
|
|
410
|
+
* Cascade delete plugin.
|
|
408
411
|
*/
|
|
409
412
|
declare function cascadePlugin(options: CascadeOptions): Plugin;
|
|
410
413
|
//#endregion
|
package/package.json
CHANGED