@classytic/mongokit 3.5.5 → 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 -1486
- 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/id-resolution-LZ-cCwR5.mjs +81 -0
- package/dist/{index-Buj7P006.d.mts → index-BSRSsRRx.d.mts} +1 -1
- package/dist/index.d.mts +81 -9
- package/dist/index.mjs +5 -5
- package/dist/{mongooseToJsonSchema-DpjhFfaO.mjs → mongooseToJsonSchema-8Cyu3NFJ.mjs} +50 -45
- package/dist/{mongooseToJsonSchema-BlDfbg_0.d.mts → mongooseToJsonSchema-D2YjHo7W.d.mts} +4 -3
- 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/query/primitives/coercion.d.mts +1 -1
- package/dist/query/primitives/coercion.mjs +4 -3
- package/dist/query/primitives/indexes.d.mts +1 -1
- package/dist/{types-CWUL0yiw.d.mts → types-DiQfDesD.d.mts} +29 -3
- package/dist/utils/index.d.mts +60 -3
- package/dist/utils/index.mjs +4 -3
- package/dist/{validation-chain.plugin-BCPh2UZG.mjs → validation-chain.plugin-3rr7dlw6.mjs} +308 -164
- package/dist/{validation-chain.plugin-Dn41aAbK.d.mts → validation-chain.plugin-COKNRAwp.d.mts} +15 -12
- package/package.json +2 -2
- /package/dist/{coercion-01tEHsKF.d.mts → coercion-B19MD38s.d.mts} +0 -0
- /package/dist/{field-selection-reyDRzXf.mjs → field-selection-C5o7jyyA.mjs} +0 -0
- /package/dist/{indexes-CiIT-Zli.d.mts → indexes-qIBCZ3mY.d.mts} +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
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
|
-
import {
|
|
5
|
+
import { r as isValidIdForType, t as getSchemaIdType } from "./id-resolution-LZ-cCwR5.mjs";
|
|
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";
|
|
6
7
|
import mongoose from "mongoose";
|
|
7
8
|
//#region src/plugins/aggregate-helpers.plugin.ts
|
|
8
9
|
/**
|
|
@@ -582,12 +583,23 @@ function batchOperationsPlugin() {
|
|
|
582
583
|
}
|
|
583
584
|
});
|
|
584
585
|
/**
|
|
585
|
-
* 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.
|
|
586
595
|
*/
|
|
587
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;
|
|
588
599
|
const context = await this._buildContext("deleteMany", {
|
|
589
600
|
query,
|
|
590
|
-
...options
|
|
601
|
+
...options,
|
|
602
|
+
...mode ? { deleteMode: mode } : {}
|
|
591
603
|
});
|
|
592
604
|
try {
|
|
593
605
|
if (context.softDeleted) {
|
|
@@ -602,7 +614,7 @@ function batchOperationsPlugin() {
|
|
|
602
614
|
return result;
|
|
603
615
|
}
|
|
604
616
|
const finalQuery = context.query || query;
|
|
605
|
-
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.");
|
|
606
618
|
const result = await this.Model.deleteMany(finalQuery, { session: options.session }).exec();
|
|
607
619
|
await this.emitAsync("after:deleteMany", {
|
|
608
620
|
context,
|
|
@@ -1153,6 +1165,60 @@ var AggregationBuilder = class AggregationBuilder {
|
|
|
1153
1165
|
* ```
|
|
1154
1166
|
*/
|
|
1155
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
|
|
1156
1222
|
//#region src/Repository.ts
|
|
1157
1223
|
function ensureLookupProjectionIncludesCursorFields(projection, sort) {
|
|
1158
1224
|
if (!projection || !sort) return projection;
|
|
@@ -1370,7 +1436,7 @@ var Repository = class {
|
|
|
1370
1436
|
});
|
|
1371
1437
|
return result;
|
|
1372
1438
|
}
|
|
1373
|
-
if (
|
|
1439
|
+
if (!isValidIdForType(id, getSchemaIdType(this.Model.schema))) {
|
|
1374
1440
|
if (context.throwOnNotFound === false || options.throwOnNotFound === false) return null;
|
|
1375
1441
|
throw createError(404, "Document not found");
|
|
1376
1442
|
}
|
|
@@ -1549,41 +1615,14 @@ var Repository = class {
|
|
|
1549
1615
|
});
|
|
1550
1616
|
const filters = context.filters ?? params.filters ?? {};
|
|
1551
1617
|
const search = context.search;
|
|
1552
|
-
const isNearQuery = hasNearOperator(
|
|
1553
|
-
const
|
|
1554
|
-
if (isNearQuery && explicitSort !== void 0) warn(`[mongokit] Repository "${this.model}" dropping explicit sort (${JSON.stringify(explicitSort)}) because the filter contains $near / $nearSphere. MongoDB forbids explicit sort with $near. Use [withinRadius] instead of [near] if you need a custom sort.`);
|
|
1555
|
-
const sort = isNearQuery ? void 0 : explicitSort ?? "-createdAt";
|
|
1618
|
+
const isNearQuery = hasNearOperator(filters);
|
|
1619
|
+
const sort = this._resolveSort(context, params, isNearQuery);
|
|
1556
1620
|
const limit = context.limit ?? params.limit ?? params.pagination?.limit ?? this._pagination.config.defaultLimit;
|
|
1557
1621
|
const page = context.page ?? params.pagination?.page ?? params.page;
|
|
1558
1622
|
const after = context.after ?? params.cursor ?? params.after;
|
|
1559
1623
|
const mode = context.mode ?? params.mode;
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
else useKeyset = !page && !!(after || sort !== "-createdAt" && (context.sort ?? params.sort));
|
|
1563
|
-
const query = { ...filters };
|
|
1564
|
-
if (search) {
|
|
1565
|
-
if (this._hasTextIndex === null) this._hasTextIndex = this.Model.schema.indexes().some((idx) => idx[0] && Object.values(idx[0]).includes("text"));
|
|
1566
|
-
let effectiveMode = this.searchMode === "regex" ? "regex" : "text";
|
|
1567
|
-
if (this.searchMode === "auto") effectiveMode = this._hasTextIndex ? "text" : "regex";
|
|
1568
|
-
if (effectiveMode === "regex") {
|
|
1569
|
-
if (!this.searchFields || this.searchFields.length === 0) throw createError(400, `Repository "${this.model}" configured with searchMode: '${this.searchMode}' but no searchFields provided.`);
|
|
1570
|
-
const escaped = String(search).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1571
|
-
const orConds = this.searchFields.map((field) => ({ [field]: {
|
|
1572
|
-
$regex: escaped,
|
|
1573
|
-
$options: "i"
|
|
1574
|
-
} }));
|
|
1575
|
-
if (Array.isArray(query.$or)) {
|
|
1576
|
-
const existingOr = query.$or;
|
|
1577
|
-
delete query.$or;
|
|
1578
|
-
query.$and = [
|
|
1579
|
-
...Array.isArray(query.$and) ? query.$and : [],
|
|
1580
|
-
{ $or: existingOr },
|
|
1581
|
-
{ $or: orConds }
|
|
1582
|
-
];
|
|
1583
|
-
} else query.$or = orConds;
|
|
1584
|
-
} else if (this._hasTextIndex) query.$text = { $search: search };
|
|
1585
|
-
else throw createError(400, `No text index found for ${this.model}. Cannot perform text search. Configure Repository with searchMode: 'regex' (and searchFields) or 'auto' to enable index-free search.`);
|
|
1586
|
-
}
|
|
1624
|
+
const useKeyset = this._detectPaginationMode(mode, page, after, sort, context, params);
|
|
1625
|
+
const query = this._buildSearchQuery(filters, search);
|
|
1587
1626
|
const populateSpec = options.populateOptions || params.populateOptions || context.populate || options.populate;
|
|
1588
1627
|
const paginationOptions = {
|
|
1589
1628
|
filters: query,
|
|
@@ -1796,12 +1835,31 @@ var Repository = class {
|
|
|
1796
1835
|
}
|
|
1797
1836
|
}
|
|
1798
1837
|
/**
|
|
1799
|
-
* 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
|
+
* ```
|
|
1800
1857
|
*/
|
|
1801
1858
|
async delete(id, options = {}) {
|
|
1802
1859
|
const context = await this._buildContext("delete", {
|
|
1803
1860
|
id,
|
|
1804
|
-
...options
|
|
1861
|
+
...options,
|
|
1862
|
+
...options.mode ? { deleteMode: options.mode } : {}
|
|
1805
1863
|
});
|
|
1806
1864
|
try {
|
|
1807
1865
|
if (context.softDeleted) {
|
|
@@ -2176,25 +2234,7 @@ var Repository = class {
|
|
|
2176
2234
|
* ```
|
|
2177
2235
|
*/
|
|
2178
2236
|
async withTransaction(callback, options = {}) {
|
|
2179
|
-
|
|
2180
|
-
try {
|
|
2181
|
-
return await session.withTransaction(() => callback(session), options.transactionOptions);
|
|
2182
|
-
} catch (error) {
|
|
2183
|
-
const err = error;
|
|
2184
|
-
if (options.allowFallback && this._isTransactionUnsupported(err)) {
|
|
2185
|
-
options.onFallback?.(err);
|
|
2186
|
-
return await callback(session);
|
|
2187
|
-
}
|
|
2188
|
-
throw err;
|
|
2189
|
-
} finally {
|
|
2190
|
-
await session.endSession();
|
|
2191
|
-
}
|
|
2192
|
-
}
|
|
2193
|
-
_isTransactionUnsupported(error) {
|
|
2194
|
-
const code = error.code;
|
|
2195
|
-
if (code === 263 || code === 20) return true;
|
|
2196
|
-
const message = (error.message || "").toLowerCase();
|
|
2197
|
-
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);
|
|
2198
2238
|
}
|
|
2199
2239
|
/**
|
|
2200
2240
|
* Execute custom query with event emission
|
|
@@ -2238,6 +2278,56 @@ var Repository = class {
|
|
|
2238
2278
|
return context;
|
|
2239
2279
|
}
|
|
2240
2280
|
/**
|
|
2281
|
+
* Resolve the effective sort for getAll. Handles $near conflict detection
|
|
2282
|
+
* (warns + drops explicit sort when $near is in filters).
|
|
2283
|
+
*/
|
|
2284
|
+
_resolveSort(context, params, isNearQuery) {
|
|
2285
|
+
const explicitSort = context.sort ?? params.sort;
|
|
2286
|
+
if (isNearQuery) {
|
|
2287
|
+
if (explicitSort !== void 0) warn(`[mongokit] Repository "${this.model}" dropping explicit sort (${JSON.stringify(explicitSort)}) because the filter contains $near / $nearSphere. MongoDB forbids explicit sort with $near. Use [withinRadius] instead of [near] if you need a custom sort.`);
|
|
2288
|
+
return;
|
|
2289
|
+
}
|
|
2290
|
+
return explicitSort ?? "-createdAt";
|
|
2291
|
+
}
|
|
2292
|
+
/**
|
|
2293
|
+
* Detect whether to use keyset (cursor) or offset (page) pagination.
|
|
2294
|
+
*/
|
|
2295
|
+
_detectPaginationMode(mode, page, after, sort, context, params) {
|
|
2296
|
+
if (mode) return mode === "keyset";
|
|
2297
|
+
return !page && !!(after || sort !== "-createdAt" && (context.sort ?? params.sort));
|
|
2298
|
+
}
|
|
2299
|
+
/**
|
|
2300
|
+
* Build the MongoDB query filter with search merged in. Handles text
|
|
2301
|
+
* search ($text), regex search ($or of $regex), and the search-resolver
|
|
2302
|
+
* plugin contract (search already cleared by a before:getAll hook).
|
|
2303
|
+
*/
|
|
2304
|
+
_buildSearchQuery(filters, search) {
|
|
2305
|
+
const query = { ...filters };
|
|
2306
|
+
if (!search) return query;
|
|
2307
|
+
if (this._hasTextIndex === null) this._hasTextIndex = this.Model.schema.indexes().some((idx) => idx[0] && Object.values(idx[0]).includes("text"));
|
|
2308
|
+
let effectiveMode = this.searchMode === "regex" ? "regex" : "text";
|
|
2309
|
+
if (this.searchMode === "auto") effectiveMode = this._hasTextIndex ? "text" : "regex";
|
|
2310
|
+
if (effectiveMode === "regex") {
|
|
2311
|
+
if (!this.searchFields || this.searchFields.length === 0) throw createError(400, `Repository "${this.model}" configured with searchMode: '${this.searchMode}' but no searchFields provided.`);
|
|
2312
|
+
const escaped = String(search).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2313
|
+
const orConds = this.searchFields.map((field) => ({ [field]: {
|
|
2314
|
+
$regex: escaped,
|
|
2315
|
+
$options: "i"
|
|
2316
|
+
} }));
|
|
2317
|
+
if (Array.isArray(query.$or)) {
|
|
2318
|
+
const existingOr = query.$or;
|
|
2319
|
+
delete query.$or;
|
|
2320
|
+
query.$and = [
|
|
2321
|
+
...Array.isArray(query.$and) ? query.$and : [],
|
|
2322
|
+
{ $or: existingOr },
|
|
2323
|
+
{ $or: orConds }
|
|
2324
|
+
];
|
|
2325
|
+
} else query.$or = orConds;
|
|
2326
|
+
} else if (this._hasTextIndex) query.$text = { $search: search };
|
|
2327
|
+
else throw createError(400, `No text index found for ${this.model}. Cannot perform text search. Configure Repository with searchMode: 'regex' (and searchFields) or 'auto' to enable index-free search.`);
|
|
2328
|
+
return query;
|
|
2329
|
+
}
|
|
2330
|
+
/**
|
|
2241
2331
|
* Parse sort string or object
|
|
2242
2332
|
*/
|
|
2243
2333
|
_parseSort(sort) {
|
|
@@ -2758,39 +2848,73 @@ function cachePlugin(options) {
|
|
|
2758
2848
|
//#region src/plugins/cascade.plugin.ts
|
|
2759
2849
|
/**
|
|
2760
2850
|
* Cascade Delete Plugin
|
|
2761
|
-
* Automatically deletes related documents when a parent document is deleted
|
|
2762
2851
|
*
|
|
2763
|
-
*
|
|
2764
|
-
*
|
|
2765
|
-
*
|
|
2766
|
-
*
|
|
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.
|
|
2767
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.
|
|
2872
|
+
*
|
|
2873
|
+
* @example Repo-routed (new)
|
|
2874
|
+
* ```ts
|
|
2768
2875
|
* const productRepo = new Repository(Product, [
|
|
2769
2876
|
* methodRegistryPlugin(),
|
|
2770
2877
|
* cascadePlugin({
|
|
2771
2878
|
* relations: [
|
|
2772
|
-
* {
|
|
2773
|
-
* {
|
|
2774
|
-
* ]
|
|
2775
|
-
* })
|
|
2879
|
+
* { repo: stockEntryRepo, foreignKey: 'product' },
|
|
2880
|
+
* { repo: stockMovementRepo, foreignKey: 'product' },
|
|
2881
|
+
* ],
|
|
2882
|
+
* }),
|
|
2776
2883
|
* ]);
|
|
2884
|
+
* ```
|
|
2777
2885
|
*
|
|
2778
|
-
*
|
|
2779
|
-
*
|
|
2886
|
+
* @example Model-routed (legacy)
|
|
2887
|
+
* ```ts
|
|
2888
|
+
* cascadePlugin({
|
|
2889
|
+
* relations: [
|
|
2890
|
+
* { model: 'StockEntry', foreignKey: 'product' },
|
|
2891
|
+
* ],
|
|
2892
|
+
* });
|
|
2780
2893
|
* ```
|
|
2781
2894
|
*/
|
|
2782
2895
|
/**
|
|
2783
|
-
*
|
|
2784
|
-
*
|
|
2785
|
-
*
|
|
2786
|
-
*
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
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.
|
|
2790
2910
|
*/
|
|
2791
2911
|
function cascadePlugin(options) {
|
|
2792
2912
|
const { relations, parallel = true, logger } = options;
|
|
2793
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
|
+
}
|
|
2794
2918
|
return {
|
|
2795
2919
|
name: "cascade",
|
|
2796
2920
|
apply(repo) {
|
|
@@ -2801,59 +2925,32 @@ function cascadePlugin(options) {
|
|
|
2801
2925
|
logger?.warn?.("Cascade delete skipped: no document ID in context", { model: context.model });
|
|
2802
2926
|
return;
|
|
2803
2927
|
}
|
|
2804
|
-
const
|
|
2805
|
-
const
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
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
|
+
};
|
|
2815
2939
|
try {
|
|
2816
|
-
if (relation.
|
|
2817
|
-
|
|
2818
|
-
deletedAt: /* @__PURE__ */ new Date(),
|
|
2819
|
-
...context.user ? { deletedBy: context.user._id || context.user.id } : {}
|
|
2820
|
-
}, { session: context.session });
|
|
2821
|
-
logger?.info?.(`Cascade soft-deleted ${updateResult.modifiedCount} documents`, {
|
|
2822
|
-
parentModel: context.model,
|
|
2823
|
-
parentId: String(deletedId),
|
|
2824
|
-
relatedModel: relation.model,
|
|
2825
|
-
foreignKey: relation.foreignKey,
|
|
2826
|
-
count: updateResult.modifiedCount
|
|
2827
|
-
});
|
|
2828
|
-
} else {
|
|
2829
|
-
const deleteResult = await RelatedModel.deleteMany(query, { session: context.session });
|
|
2830
|
-
logger?.info?.(`Cascade deleted ${deleteResult.deletedCount} documents`, {
|
|
2831
|
-
parentModel: context.model,
|
|
2832
|
-
parentId: String(deletedId),
|
|
2833
|
-
relatedModel: relation.model,
|
|
2834
|
-
foreignKey: relation.foreignKey,
|
|
2835
|
-
count: deleteResult.deletedCount
|
|
2836
|
-
});
|
|
2837
|
-
}
|
|
2940
|
+
if (relation.repo) await cascadeViaRepoBulk(relation, deletedId, cascadeCtx);
|
|
2941
|
+
else await cascadeViaLegacyModel(relation, deletedId, cascadeCtx, logger);
|
|
2838
2942
|
} catch (error) {
|
|
2839
|
-
logger?.error?.(`Cascade delete failed for
|
|
2943
|
+
logger?.error?.(`Cascade delete failed for '${cascadeCtx.targetModelName}'`, {
|
|
2840
2944
|
parentModel: context.model,
|
|
2841
2945
|
parentId: String(deletedId),
|
|
2842
|
-
relatedModel:
|
|
2946
|
+
relatedModel: cascadeCtx.targetModelName,
|
|
2843
2947
|
foreignKey: relation.foreignKey,
|
|
2844
2948
|
error: error.message
|
|
2845
2949
|
});
|
|
2846
2950
|
throw error;
|
|
2847
2951
|
}
|
|
2848
2952
|
};
|
|
2849
|
-
|
|
2850
|
-
const failures = (await Promise.allSettled(relations.map(cascadeDelete))).filter((r) => r.status === "rejected");
|
|
2851
|
-
if (failures.length) {
|
|
2852
|
-
const err = failures[0].reason;
|
|
2853
|
-
if (failures.length > 1) err.message = `${failures.length} cascade deletes failed. First: ${err.message}`;
|
|
2854
|
-
throw err;
|
|
2855
|
-
}
|
|
2856
|
-
} else for (const relation of relations) await cascadeDelete(relation);
|
|
2953
|
+
await runCascades(relations, cascadeOne, parallel);
|
|
2857
2954
|
});
|
|
2858
2955
|
repo.on("before:deleteMany", async (context) => {
|
|
2859
2956
|
const query = context.query;
|
|
@@ -2865,60 +2962,106 @@ function cascadePlugin(options) {
|
|
|
2865
2962
|
const { context } = payload;
|
|
2866
2963
|
const ids = context._cascadeIds;
|
|
2867
2964
|
if (!ids || ids.length === 0) return;
|
|
2868
|
-
const
|
|
2869
|
-
const
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
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
|
+
};
|
|
2877
2976
|
try {
|
|
2878
|
-
if (
|
|
2879
|
-
|
|
2880
|
-
deletedAt: /* @__PURE__ */ new Date(),
|
|
2881
|
-
...context.user ? { deletedBy: context.user._id || context.user.id } : {}
|
|
2882
|
-
}, { session: context.session });
|
|
2883
|
-
logger?.info?.(`Cascade soft-deleted ${updateResult.modifiedCount} documents (bulk)`, {
|
|
2884
|
-
parentModel: context.model,
|
|
2885
|
-
parentCount: ids.length,
|
|
2886
|
-
relatedModel: relation.model,
|
|
2887
|
-
foreignKey: relation.foreignKey,
|
|
2888
|
-
count: updateResult.modifiedCount
|
|
2889
|
-
});
|
|
2890
|
-
} else {
|
|
2891
|
-
const deleteResult = await RelatedModel.deleteMany(query, { session: context.session });
|
|
2892
|
-
logger?.info?.(`Cascade deleted ${deleteResult.deletedCount} documents (bulk)`, {
|
|
2893
|
-
parentModel: context.model,
|
|
2894
|
-
parentCount: ids.length,
|
|
2895
|
-
relatedModel: relation.model,
|
|
2896
|
-
foreignKey: relation.foreignKey,
|
|
2897
|
-
count: deleteResult.deletedCount
|
|
2898
|
-
});
|
|
2899
|
-
}
|
|
2977
|
+
if (relation.repo) await cascadeViaRepoBulkMany(relation, ids, cascadeCtx);
|
|
2978
|
+
else await cascadeViaLegacyModelMany(relation, ids, cascadeCtx, logger);
|
|
2900
2979
|
} catch (error) {
|
|
2901
|
-
logger?.error?.(`Cascade deleteMany failed for
|
|
2980
|
+
logger?.error?.(`Cascade deleteMany failed for '${cascadeCtx.targetModelName}'`, {
|
|
2902
2981
|
parentModel: context.model,
|
|
2903
|
-
relatedModel:
|
|
2982
|
+
relatedModel: cascadeCtx.targetModelName,
|
|
2904
2983
|
foreignKey: relation.foreignKey,
|
|
2905
2984
|
error: error.message
|
|
2906
2985
|
});
|
|
2907
2986
|
throw error;
|
|
2908
2987
|
}
|
|
2909
2988
|
};
|
|
2910
|
-
|
|
2911
|
-
const failures = (await Promise.allSettled(relations.map(cascadeDeleteMany))).filter((r) => r.status === "rejected");
|
|
2912
|
-
if (failures.length) {
|
|
2913
|
-
const err = failures[0].reason;
|
|
2914
|
-
if (failures.length > 1) err.message = `${failures.length} cascade deletes failed. First: ${err.message}`;
|
|
2915
|
-
throw err;
|
|
2916
|
-
}
|
|
2917
|
-
} else for (const relation of relations) await cascadeDeleteMany(relation);
|
|
2989
|
+
await runCascades(relations, cascadeBulk, parallel);
|
|
2918
2990
|
});
|
|
2919
2991
|
}
|
|
2920
2992
|
};
|
|
2921
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
|
+
}
|
|
2922
3065
|
//#endregion
|
|
2923
3066
|
//#region src/plugins/custom-id.plugin.ts
|
|
2924
3067
|
/**
|
|
@@ -3813,6 +3956,7 @@ function softDeletePlugin(options = {}) {
|
|
|
3813
3956
|
});
|
|
3814
3957
|
}
|
|
3815
3958
|
repo.on("before:delete", async (context) => {
|
|
3959
|
+
if (context.deleteMode === "hard") return;
|
|
3816
3960
|
if (options.soft !== false) {
|
|
3817
3961
|
const updateData = { [deletedField]: /* @__PURE__ */ new Date() };
|
|
3818
3962
|
if (context.user) updateData[deletedByField] = context.user._id || context.user.id;
|
|
@@ -3872,6 +4016,7 @@ function softDeletePlugin(options = {}) {
|
|
|
3872
4016
|
}
|
|
3873
4017
|
}, { priority: HOOK_PRIORITY.POLICY });
|
|
3874
4018
|
repo.on("before:deleteMany", async (context) => {
|
|
4019
|
+
if (context.deleteMode === "hard") return;
|
|
3875
4020
|
if (options.soft !== false) {
|
|
3876
4021
|
const deleteFilter = buildDeletedFilter(deletedField, filterMode, false);
|
|
3877
4022
|
const finalQuery = {
|
|
@@ -3924,9 +4069,8 @@ function softDeletePlugin(options = {}) {
|
|
|
3924
4069
|
throw error;
|
|
3925
4070
|
}
|
|
3926
4071
|
await this.emitAsync("after:restore", {
|
|
3927
|
-
|
|
3928
|
-
result
|
|
3929
|
-
context
|
|
4072
|
+
context,
|
|
4073
|
+
result
|
|
3930
4074
|
});
|
|
3931
4075
|
return result;
|
|
3932
4076
|
};
|
|
@@ -4230,4 +4374,4 @@ function uniqueField(field, errorMessage) {
|
|
|
4230
4374
|
};
|
|
4231
4375
|
}
|
|
4232
4376
|
//#endregion
|
|
4233
|
-
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-Dn41aAbK.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
|