@classytic/mongokit 2.1.0 → 3.0.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.
Files changed (38) hide show
  1. package/README.md +35 -4
  2. package/dist/actions/index.d.ts +2 -2
  3. package/dist/actions/index.js +0 -2
  4. package/dist/{index-CgOJ2pqz.d.ts → index-CKy3H2SY.d.ts} +1 -1
  5. package/dist/index.d.ts +10 -6
  6. package/dist/index.js +183 -6
  7. package/dist/{memory-cache-DG2oSSbx.d.ts → memory-cache-tn3v1xgG.d.ts} +1 -1
  8. package/dist/pagination/PaginationEngine.d.ts +1 -1
  9. package/dist/pagination/PaginationEngine.js +0 -2
  10. package/dist/plugins/index.d.ts +37 -2
  11. package/dist/plugins/index.js +173 -3
  12. package/dist/{types-Nxhmi1aI.d.cts → types-vDtcOhyx.d.ts} +19 -1
  13. package/dist/utils/index.d.ts +2 -2
  14. package/dist/utils/index.js +0 -2
  15. package/package.json +6 -12
  16. package/dist/actions/index.cjs +0 -479
  17. package/dist/actions/index.cjs.map +0 -1
  18. package/dist/actions/index.d.cts +0 -3
  19. package/dist/actions/index.js.map +0 -1
  20. package/dist/index-BfVJZF-3.d.cts +0 -337
  21. package/dist/index.cjs +0 -2142
  22. package/dist/index.cjs.map +0 -1
  23. package/dist/index.d.cts +0 -239
  24. package/dist/index.js.map +0 -1
  25. package/dist/memory-cache-DqfFfKes.d.cts +0 -142
  26. package/dist/pagination/PaginationEngine.cjs +0 -375
  27. package/dist/pagination/PaginationEngine.cjs.map +0 -1
  28. package/dist/pagination/PaginationEngine.d.cts +0 -117
  29. package/dist/pagination/PaginationEngine.js.map +0 -1
  30. package/dist/plugins/index.cjs +0 -874
  31. package/dist/plugins/index.cjs.map +0 -1
  32. package/dist/plugins/index.d.cts +0 -275
  33. package/dist/plugins/index.js.map +0 -1
  34. package/dist/types-Nxhmi1aI.d.ts +0 -510
  35. package/dist/utils/index.cjs +0 -667
  36. package/dist/utils/index.cjs.map +0 -1
  37. package/dist/utils/index.d.cts +0 -189
  38. package/dist/utils/index.js.map +0 -1
package/README.md CHANGED
@@ -15,7 +15,7 @@
15
15
  - ✅ **Plugin architecture** for reusable behaviors
16
16
  - ✅ **TypeScript** first-class support with discriminated unions
17
17
  - ✅ **Optional caching** - Redis/Memcached with auto-invalidation
18
- - ✅ **Battle-tested** in production with 182 passing tests
18
+ - ✅ **Battle-tested** in production with 187 passing tests
19
19
 
20
20
  ---
21
21
 
@@ -682,6 +682,35 @@ const redisAdapter = {
682
682
  };
683
683
  ```
684
684
 
685
+ ### Cascade Delete
686
+
687
+ Automatically delete related documents when a parent is deleted:
688
+
689
+ ```javascript
690
+ import { Repository, cascadePlugin, softDeletePlugin } from '@classytic/mongokit';
691
+
692
+ const productRepo = new Repository(ProductModel, [
693
+ softDeletePlugin(), // optional - cascade respects soft delete behavior
694
+ cascadePlugin({
695
+ relations: [
696
+ { model: 'StockEntry', foreignKey: 'product' },
697
+ { model: 'StockMovement', foreignKey: 'product' },
698
+ ],
699
+ parallel: true, // default, runs cascade deletes in parallel
700
+ logger: console, // optional logging
701
+ })
702
+ ]);
703
+
704
+ // When product is deleted, all related StockEntry and StockMovement docs are also deleted
705
+ await productRepo.delete(productId);
706
+ ```
707
+
708
+ **Options:**
709
+ - `relations` - Array of related models to cascade delete
710
+ - `parallel` - Run cascade deletes in parallel (default: `true`)
711
+ - `logger` - Optional logger for debugging
712
+ - Per-relation `softDelete` - Override soft delete behavior per relation
713
+
685
714
  ### More Plugins
686
715
 
687
716
  - **`timestampPlugin()`** - Auto-manage `createdAt`/`updatedAt`
@@ -689,6 +718,7 @@ const redisAdapter = {
689
718
  - **`batchOperationsPlugin()`** - Adds `updateMany`, `deleteMany`
690
719
  - **`aggregateHelpersPlugin()`** - Adds `groupBy`, `sum`, `average`, etc.
691
720
  - **`subdocumentPlugin()`** - Manage subdocument arrays easily
721
+ - **`cascadePlugin()`** - Auto-delete related documents on parent delete
692
722
 
693
723
  ---
694
724
 
@@ -993,8 +1023,8 @@ const total = result.total;
993
1023
  - ✅ Framework-agnostic
994
1024
 
995
1025
  ### vs. Raw Repository Pattern
996
- - ✅ Battle-tested implementation (68 passing tests)
997
- - ✅ 11 built-in plugins ready to use
1026
+ - ✅ Battle-tested implementation (187 passing tests)
1027
+ - ✅ 12 built-in plugins ready to use
998
1028
  - ✅ Comprehensive documentation
999
1029
  - ✅ TypeScript discriminated unions
1000
1030
  - ✅ Active maintenance
@@ -1008,12 +1038,13 @@ npm test
1008
1038
  ```
1009
1039
 
1010
1040
  **Test Coverage:**
1011
- - 184 tests (182 passing, 2 skipped - require replica set)
1041
+ - 189 tests (187 passing, 2 skipped - require replica set)
1012
1042
  - CRUD operations
1013
1043
  - Offset pagination
1014
1044
  - Keyset pagination
1015
1045
  - Aggregation pagination
1016
1046
  - Caching (hit/miss, invalidation)
1047
+ - Cascade delete (hard & soft delete)
1017
1048
  - Multi-tenancy
1018
1049
  - Text search + infinite scroll
1019
1050
  - Real-world scenarios
@@ -1,3 +1,3 @@
1
- export { a as aggregate, c as create, _ as deleteActions, r as read, u as update } from '../index-CgOJ2pqz.js';
1
+ export { a as aggregate, c as create, _ as deleteActions, r as read, u as update } from '../index-CKy3H2SY.js';
2
2
  import 'mongoose';
3
- import '../types-Nxhmi1aI.js';
3
+ import '../types-vDtcOhyx.js';
@@ -469,5 +469,3 @@ async function minMax(Model, field, query = {}, options = {}) {
469
469
  }
470
470
 
471
471
  export { aggregate_exports as aggregate, create_exports as create, delete_exports as deleteActions, read_exports as read, update_exports as update };
472
- //# sourceMappingURL=index.js.map
473
- //# sourceMappingURL=index.js.map
@@ -1,5 +1,5 @@
1
1
  import { Model, ClientSession, PipelineStage } from 'mongoose';
2
- import { A as AnyDocument, C as CreateOptions, l as OperationOptions, S as SelectSpec, f as PopulateSpec, g as SortSpec, U as UpdateOptions, n as UpdateWithValidationResult, m as UpdateManyResult, D as DeleteResult, I as GroupResult, G as LookupOptions, M as MinMaxResult } from './types-Nxhmi1aI.js';
2
+ import { A as AnyDocument, C as CreateOptions, l as OperationOptions, S as SelectSpec, f as PopulateSpec, g as SortSpec, U as UpdateOptions, n as UpdateWithValidationResult, m as UpdateManyResult, D as DeleteResult, I as GroupResult, G as LookupOptions, M as MinMaxResult } from './types-vDtcOhyx.js';
3
3
 
4
4
  /**
5
5
  * Create Actions
package/dist/index.d.ts CHANGED
@@ -1,11 +1,11 @@
1
- import { A as AnyDocument, e as PluginType, P as PaginationConfig, S as SelectSpec, f as PopulateSpec, g as SortSpec, a as OffsetPaginationResult, b as KeysetPaginationResult, d as AggregatePaginationResult, R as RepositoryContext, H as HttpError } from './types-Nxhmi1aI.js';
2
- export { c as AggregatePaginationOptions, i as AnyModel, N as CacheAdapter, T as CacheOperationOptions, Q as CacheOptions, W as CacheStats, C as CreateOptions, w as CrudSchemas, x as DecodedCursor, D as DeleteResult, E as EventPayload, F as FieldPreset, u as FieldRules, I as GroupResult, J as JsonSchema, K as KeysetPaginationOptions, L as Logger, G as LookupOptions, M as MinMaxResult, h as ObjectId, O as OffsetPaginationOptions, l as OperationOptions, k as PaginationResult, t as ParsedQuery, p as Plugin, q as PluginFunction, s as RepositoryEvent, r as RepositoryInstance, v as SchemaBuilderOptions, B as SoftDeleteOptions, j as SortDirection, m as UpdateManyResult, U as UpdateOptions, n as UpdateWithValidationResult, o as UserContext, z as ValidationChainOptions, V as ValidationResult, y as ValidatorDefinition } from './types-Nxhmi1aI.js';
1
+ import { A as AnyDocument, e as PluginType, P as PaginationConfig, S as SelectSpec, f as PopulateSpec, g as SortSpec, a as OffsetPaginationResult, b as KeysetPaginationResult, d as AggregatePaginationResult, R as RepositoryContext, H as HttpError } from './types-vDtcOhyx.js';
2
+ export { c as AggregatePaginationOptions, i as AnyModel, N as CacheAdapter, T as CacheOperationOptions, Q as CacheOptions, W as CacheStats, Y as CascadeOptions, X as CascadeRelation, C as CreateOptions, w as CrudSchemas, x as DecodedCursor, D as DeleteResult, E as EventPayload, F as FieldPreset, u as FieldRules, I as GroupResult, J as JsonSchema, K as KeysetPaginationOptions, L as Logger, G as LookupOptions, M as MinMaxResult, h as ObjectId, O as OffsetPaginationOptions, l as OperationOptions, k as PaginationResult, t as ParsedQuery, p as Plugin, q as PluginFunction, s as RepositoryEvent, r as RepositoryInstance, v as SchemaBuilderOptions, B as SoftDeleteOptions, j as SortDirection, m as UpdateManyResult, U as UpdateOptions, n as UpdateWithValidationResult, o as UserContext, z as ValidationChainOptions, V as ValidationResult, y as ValidatorDefinition } from './types-vDtcOhyx.js';
3
3
  import * as mongoose from 'mongoose';
4
4
  import { Model, ClientSession, PipelineStage, PopulateOptions } from 'mongoose';
5
5
  import { PaginationEngine } from './pagination/PaginationEngine.js';
6
- export { aggregateHelpersPlugin, auditLogPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, fieldFilterPlugin, immutableField, methodRegistryPlugin, mongoOperationsPlugin, requireField, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin } from './plugins/index.js';
7
- export { b as createError, c as createFieldPreset, d as createMemoryCache, f as filterResponseData, g as getFieldsForUser, a as getMongooseProjection } from './memory-cache-DG2oSSbx.js';
8
- export { i as actions } from './index-CgOJ2pqz.js';
6
+ export { aggregateHelpersPlugin, auditLogPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, cascadePlugin, fieldFilterPlugin, immutableField, methodRegistryPlugin, mongoOperationsPlugin, requireField, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin } from './plugins/index.js';
7
+ export { b as createError, c as createFieldPreset, d as createMemoryCache, f as filterResponseData, g as getFieldsForUser, a as getMongooseProjection } from './memory-cache-tn3v1xgG.js';
8
+ export { i as actions } from './index-CKy3H2SY.js';
9
9
 
10
10
  /**
11
11
  * Repository Pattern - Data Access Layer
@@ -55,9 +55,13 @@ declare class Repository<TDoc = AnyDocument> {
55
55
  */
56
56
  on(event: string, listener: HookListener): this;
57
57
  /**
58
- * Emit event
58
+ * Emit event (sync - for backwards compatibility)
59
59
  */
60
60
  emit(event: string, data: unknown): void;
61
+ /**
62
+ * Emit event and await all async handlers
63
+ */
64
+ emitAsync(event: string, data: unknown): Promise<void>;
61
65
  /**
62
66
  * Create single document
63
67
  */
package/dist/index.js CHANGED
@@ -858,12 +858,21 @@ var Repository = class {
858
858
  return this;
859
859
  }
860
860
  /**
861
- * Emit event
861
+ * Emit event (sync - for backwards compatibility)
862
862
  */
863
863
  emit(event, data) {
864
864
  const listeners = this._hooks.get(event) || [];
865
865
  listeners.forEach((listener) => listener(data));
866
866
  }
867
+ /**
868
+ * Emit event and await all async handlers
869
+ */
870
+ async emitAsync(event, data) {
871
+ const listeners = this._hooks.get(event) || [];
872
+ for (const listener of listeners) {
873
+ await listener(data);
874
+ }
875
+ }
867
876
  /**
868
877
  * Create single document
869
878
  */
@@ -1021,11 +1030,11 @@ var Repository = class {
1021
1030
  try {
1022
1031
  if (context.softDeleted) {
1023
1032
  const result2 = { success: true, message: "Soft deleted successfully" };
1024
- this.emit("after:delete", { context, result: result2 });
1033
+ await this.emitAsync("after:delete", { context, result: result2 });
1025
1034
  return result2;
1026
1035
  }
1027
1036
  const result = await deleteById(this.Model, id, options);
1028
- this.emit("after:delete", { context, result });
1037
+ await this.emitAsync("after:delete", { context, result });
1029
1038
  return result;
1030
1039
  } catch (error) {
1031
1040
  this.emit("error:delete", { context, error });
@@ -1992,6 +2001,176 @@ function cachePlugin(options) {
1992
2001
  }
1993
2002
  };
1994
2003
  }
2004
+ function cascadePlugin(options) {
2005
+ const { relations, parallel = true, logger } = options;
2006
+ if (!relations || relations.length === 0) {
2007
+ throw new Error("cascadePlugin requires at least one relation");
2008
+ }
2009
+ return {
2010
+ name: "cascade",
2011
+ apply(repo) {
2012
+ repo.on("after:delete", async (payload) => {
2013
+ const { context } = payload;
2014
+ const deletedId = context.id;
2015
+ if (!deletedId) {
2016
+ logger?.warn?.("Cascade delete skipped: no document ID in context", {
2017
+ model: context.model
2018
+ });
2019
+ return;
2020
+ }
2021
+ const isSoftDelete = context.softDeleted === true;
2022
+ const cascadeDelete = async (relation) => {
2023
+ const RelatedModel = mongoose.models[relation.model];
2024
+ if (!RelatedModel) {
2025
+ logger?.warn?.(`Cascade delete skipped: model '${relation.model}' not found`, {
2026
+ parentModel: context.model,
2027
+ parentId: String(deletedId)
2028
+ });
2029
+ return;
2030
+ }
2031
+ const query = { [relation.foreignKey]: deletedId };
2032
+ try {
2033
+ const shouldSoftDelete = relation.softDelete ?? isSoftDelete;
2034
+ if (shouldSoftDelete) {
2035
+ const updateResult = await RelatedModel.updateMany(
2036
+ query,
2037
+ {
2038
+ deletedAt: /* @__PURE__ */ new Date(),
2039
+ ...context.user ? { deletedBy: context.user._id || context.user.id } : {}
2040
+ },
2041
+ { session: context.session }
2042
+ );
2043
+ logger?.info?.(`Cascade soft-deleted ${updateResult.modifiedCount} documents`, {
2044
+ parentModel: context.model,
2045
+ parentId: String(deletedId),
2046
+ relatedModel: relation.model,
2047
+ foreignKey: relation.foreignKey,
2048
+ count: updateResult.modifiedCount
2049
+ });
2050
+ } else {
2051
+ const deleteResult = await RelatedModel.deleteMany(query, {
2052
+ session: context.session
2053
+ });
2054
+ logger?.info?.(`Cascade deleted ${deleteResult.deletedCount} documents`, {
2055
+ parentModel: context.model,
2056
+ parentId: String(deletedId),
2057
+ relatedModel: relation.model,
2058
+ foreignKey: relation.foreignKey,
2059
+ count: deleteResult.deletedCount
2060
+ });
2061
+ }
2062
+ } catch (error) {
2063
+ logger?.error?.(`Cascade delete failed for model '${relation.model}'`, {
2064
+ parentModel: context.model,
2065
+ parentId: String(deletedId),
2066
+ relatedModel: relation.model,
2067
+ foreignKey: relation.foreignKey,
2068
+ error: error.message
2069
+ });
2070
+ throw error;
2071
+ }
2072
+ };
2073
+ if (parallel) {
2074
+ await Promise.all(relations.map(cascadeDelete));
2075
+ } else {
2076
+ for (const relation of relations) {
2077
+ await cascadeDelete(relation);
2078
+ }
2079
+ }
2080
+ });
2081
+ repo.on("after:deleteMany", async (payload) => {
2082
+ const { context, result } = payload;
2083
+ const query = context.query;
2084
+ if (!query || Object.keys(query).length === 0) {
2085
+ logger?.warn?.("Cascade deleteMany skipped: empty query", {
2086
+ model: context.model
2087
+ });
2088
+ return;
2089
+ }
2090
+ logger?.warn?.("Cascade deleteMany: use before:deleteMany hook for complete cascade support", {
2091
+ model: context.model
2092
+ });
2093
+ });
2094
+ repo.on("before:deleteMany", async (context) => {
2095
+ const query = context.query;
2096
+ if (!query || Object.keys(query).length === 0) {
2097
+ return;
2098
+ }
2099
+ const docs = await repo.Model.find(query, { _id: 1 }).lean().session(context.session ?? null);
2100
+ const ids = docs.map((doc) => doc._id);
2101
+ context._cascadeIds = ids;
2102
+ });
2103
+ const originalAfterDeleteMany = repo._hooks.get("after:deleteMany") || [];
2104
+ repo._hooks.set("after:deleteMany", [
2105
+ ...originalAfterDeleteMany,
2106
+ async (payload) => {
2107
+ const { context } = payload;
2108
+ const ids = context._cascadeIds;
2109
+ if (!ids || ids.length === 0) {
2110
+ return;
2111
+ }
2112
+ const isSoftDelete = context.softDeleted === true;
2113
+ const cascadeDeleteMany = async (relation) => {
2114
+ const RelatedModel = mongoose.models[relation.model];
2115
+ if (!RelatedModel) {
2116
+ logger?.warn?.(`Cascade deleteMany skipped: model '${relation.model}' not found`, {
2117
+ parentModel: context.model
2118
+ });
2119
+ return;
2120
+ }
2121
+ const query = { [relation.foreignKey]: { $in: ids } };
2122
+ const shouldSoftDelete = relation.softDelete ?? isSoftDelete;
2123
+ try {
2124
+ if (shouldSoftDelete) {
2125
+ const updateResult = await RelatedModel.updateMany(
2126
+ query,
2127
+ {
2128
+ deletedAt: /* @__PURE__ */ new Date(),
2129
+ ...context.user ? { deletedBy: context.user._id || context.user.id } : {}
2130
+ },
2131
+ { session: context.session }
2132
+ );
2133
+ logger?.info?.(`Cascade soft-deleted ${updateResult.modifiedCount} documents (bulk)`, {
2134
+ parentModel: context.model,
2135
+ parentCount: ids.length,
2136
+ relatedModel: relation.model,
2137
+ foreignKey: relation.foreignKey,
2138
+ count: updateResult.modifiedCount
2139
+ });
2140
+ } else {
2141
+ const deleteResult = await RelatedModel.deleteMany(query, {
2142
+ session: context.session
2143
+ });
2144
+ logger?.info?.(`Cascade deleted ${deleteResult.deletedCount} documents (bulk)`, {
2145
+ parentModel: context.model,
2146
+ parentCount: ids.length,
2147
+ relatedModel: relation.model,
2148
+ foreignKey: relation.foreignKey,
2149
+ count: deleteResult.deletedCount
2150
+ });
2151
+ }
2152
+ } catch (error) {
2153
+ logger?.error?.(`Cascade deleteMany failed for model '${relation.model}'`, {
2154
+ parentModel: context.model,
2155
+ relatedModel: relation.model,
2156
+ foreignKey: relation.foreignKey,
2157
+ error: error.message
2158
+ });
2159
+ throw error;
2160
+ }
2161
+ };
2162
+ if (parallel) {
2163
+ await Promise.all(relations.map(cascadeDeleteMany));
2164
+ } else {
2165
+ for (const relation of relations) {
2166
+ await cascadeDeleteMany(relation);
2167
+ }
2168
+ }
2169
+ }
2170
+ ]);
2171
+ }
2172
+ };
2173
+ }
1995
2174
 
1996
2175
  // src/utils/memory-cache.ts
1997
2176
  function createMemoryCache(maxEntries = 1e3) {
@@ -2103,6 +2282,4 @@ var index_default = Repository;
2103
2282
  * ```
2104
2283
  */
2105
2284
 
2106
- export { PaginationEngine, Repository, actions_exports as actions, aggregateHelpersPlugin, auditLogPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, createError, createFieldPreset, createMemoryCache, createRepository, index_default as default, fieldFilterPlugin, filterResponseData, getFieldsForUser, getMongooseProjection, immutableField, methodRegistryPlugin, mongoOperationsPlugin, requireField, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin };
2107
- //# sourceMappingURL=index.js.map
2108
- //# sourceMappingURL=index.js.map
2285
+ export { PaginationEngine, Repository, actions_exports as actions, aggregateHelpersPlugin, auditLogPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, cascadePlugin, createError, createFieldPreset, createMemoryCache, createRepository, index_default as default, fieldFilterPlugin, filterResponseData, getFieldsForUser, getMongooseProjection, immutableField, methodRegistryPlugin, mongoOperationsPlugin, requireField, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin };
@@ -1,4 +1,4 @@
1
- import { o as UserContext, F as FieldPreset, H as HttpError, N as CacheAdapter } from './types-Nxhmi1aI.js';
1
+ import { o as UserContext, F as FieldPreset, H as HttpError, N as CacheAdapter } from './types-vDtcOhyx.js';
2
2
 
3
3
  /**
4
4
  * Field Selection Utilities
@@ -1,5 +1,5 @@
1
1
  import { Model } from 'mongoose';
2
- import { A as AnyDocument, P as PaginationConfig, O as OffsetPaginationOptions, a as OffsetPaginationResult, K as KeysetPaginationOptions, b as KeysetPaginationResult, c as AggregatePaginationOptions, d as AggregatePaginationResult } from '../types-Nxhmi1aI.js';
2
+ import { A as AnyDocument, P as PaginationConfig, O as OffsetPaginationOptions, a as OffsetPaginationResult, K as KeysetPaginationOptions, b as KeysetPaginationResult, c as AggregatePaginationOptions, d as AggregatePaginationResult } from '../types-vDtcOhyx.js';
3
3
 
4
4
  /**
5
5
  * Pagination Engine
@@ -365,5 +365,3 @@ var PaginationEngine = class {
365
365
  };
366
366
 
367
367
  export { PaginationEngine };
368
- //# sourceMappingURL=PaginationEngine.js.map
369
- //# sourceMappingURL=PaginationEngine.js.map
@@ -1,4 +1,4 @@
1
- import { F as FieldPreset, p as Plugin, L as Logger, B as SoftDeleteOptions, r as RepositoryInstance, y as ValidatorDefinition, z as ValidationChainOptions, R as RepositoryContext, Q as CacheOptions } from '../types-Nxhmi1aI.js';
1
+ import { F as FieldPreset, p as Plugin, L as Logger, B as SoftDeleteOptions, r as RepositoryInstance, y as ValidatorDefinition, z as ValidationChainOptions, R as RepositoryContext, Q as CacheOptions, Y as CascadeOptions } from '../types-vDtcOhyx.js';
2
2
  import 'mongoose';
3
3
 
4
4
  /**
@@ -272,4 +272,39 @@ declare function subdocumentPlugin(): Plugin;
272
272
  */
273
273
  declare function cachePlugin(options: CacheOptions): Plugin;
274
274
 
275
- export { type MethodRegistryRepository, aggregateHelpersPlugin, auditLogPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, fieldFilterPlugin, immutableField, methodRegistryPlugin, mongoOperationsPlugin, requireField, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin };
275
+ /**
276
+ * Cascade Delete Plugin
277
+ * Automatically deletes related documents when a parent document is deleted
278
+ *
279
+ * @example
280
+ * ```typescript
281
+ * import mongoose from 'mongoose';
282
+ * import { Repository, cascadePlugin, methodRegistryPlugin } from '@classytic/mongokit';
283
+ *
284
+ * const productRepo = new Repository(Product, [
285
+ * methodRegistryPlugin(),
286
+ * cascadePlugin({
287
+ * relations: [
288
+ * { model: 'StockEntry', foreignKey: 'product' },
289
+ * { model: 'StockMovement', foreignKey: 'product' },
290
+ * ]
291
+ * })
292
+ * ]);
293
+ *
294
+ * // When a product is deleted, all related StockEntry and StockMovement docs are also deleted
295
+ * await productRepo.delete(productId);
296
+ * ```
297
+ */
298
+
299
+ /**
300
+ * Cascade delete plugin
301
+ *
302
+ * Deletes related documents after the parent document is deleted.
303
+ * Works with both hard delete and soft delete scenarios.
304
+ *
305
+ * @param options - Cascade configuration
306
+ * @returns Plugin
307
+ */
308
+ declare function cascadePlugin(options: CascadeOptions): Plugin;
309
+
310
+ export { type MethodRegistryRepository, aggregateHelpersPlugin, auditLogPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, cascadePlugin, fieldFilterPlugin, immutableField, methodRegistryPlugin, mongoOperationsPlugin, requireField, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin };
@@ -1,3 +1,5 @@
1
+ import mongoose from 'mongoose';
2
+
1
3
  // src/utils/field-selection.ts
2
4
  function getFieldsForUser(user, preset) {
3
5
  if (!preset) {
@@ -851,7 +853,175 @@ function cachePlugin(options) {
851
853
  }
852
854
  };
853
855
  }
856
+ function cascadePlugin(options) {
857
+ const { relations, parallel = true, logger } = options;
858
+ if (!relations || relations.length === 0) {
859
+ throw new Error("cascadePlugin requires at least one relation");
860
+ }
861
+ return {
862
+ name: "cascade",
863
+ apply(repo) {
864
+ repo.on("after:delete", async (payload) => {
865
+ const { context } = payload;
866
+ const deletedId = context.id;
867
+ if (!deletedId) {
868
+ logger?.warn?.("Cascade delete skipped: no document ID in context", {
869
+ model: context.model
870
+ });
871
+ return;
872
+ }
873
+ const isSoftDelete = context.softDeleted === true;
874
+ const cascadeDelete = async (relation) => {
875
+ const RelatedModel = mongoose.models[relation.model];
876
+ if (!RelatedModel) {
877
+ logger?.warn?.(`Cascade delete skipped: model '${relation.model}' not found`, {
878
+ parentModel: context.model,
879
+ parentId: String(deletedId)
880
+ });
881
+ return;
882
+ }
883
+ const query = { [relation.foreignKey]: deletedId };
884
+ try {
885
+ const shouldSoftDelete = relation.softDelete ?? isSoftDelete;
886
+ if (shouldSoftDelete) {
887
+ const updateResult = await RelatedModel.updateMany(
888
+ query,
889
+ {
890
+ deletedAt: /* @__PURE__ */ new Date(),
891
+ ...context.user ? { deletedBy: context.user._id || context.user.id } : {}
892
+ },
893
+ { session: context.session }
894
+ );
895
+ logger?.info?.(`Cascade soft-deleted ${updateResult.modifiedCount} documents`, {
896
+ parentModel: context.model,
897
+ parentId: String(deletedId),
898
+ relatedModel: relation.model,
899
+ foreignKey: relation.foreignKey,
900
+ count: updateResult.modifiedCount
901
+ });
902
+ } else {
903
+ const deleteResult = await RelatedModel.deleteMany(query, {
904
+ session: context.session
905
+ });
906
+ logger?.info?.(`Cascade deleted ${deleteResult.deletedCount} documents`, {
907
+ parentModel: context.model,
908
+ parentId: String(deletedId),
909
+ relatedModel: relation.model,
910
+ foreignKey: relation.foreignKey,
911
+ count: deleteResult.deletedCount
912
+ });
913
+ }
914
+ } catch (error) {
915
+ logger?.error?.(`Cascade delete failed for model '${relation.model}'`, {
916
+ parentModel: context.model,
917
+ parentId: String(deletedId),
918
+ relatedModel: relation.model,
919
+ foreignKey: relation.foreignKey,
920
+ error: error.message
921
+ });
922
+ throw error;
923
+ }
924
+ };
925
+ if (parallel) {
926
+ await Promise.all(relations.map(cascadeDelete));
927
+ } else {
928
+ for (const relation of relations) {
929
+ await cascadeDelete(relation);
930
+ }
931
+ }
932
+ });
933
+ repo.on("after:deleteMany", async (payload) => {
934
+ const { context, result } = payload;
935
+ const query = context.query;
936
+ if (!query || Object.keys(query).length === 0) {
937
+ logger?.warn?.("Cascade deleteMany skipped: empty query", {
938
+ model: context.model
939
+ });
940
+ return;
941
+ }
942
+ logger?.warn?.("Cascade deleteMany: use before:deleteMany hook for complete cascade support", {
943
+ model: context.model
944
+ });
945
+ });
946
+ repo.on("before:deleteMany", async (context) => {
947
+ const query = context.query;
948
+ if (!query || Object.keys(query).length === 0) {
949
+ return;
950
+ }
951
+ const docs = await repo.Model.find(query, { _id: 1 }).lean().session(context.session ?? null);
952
+ const ids = docs.map((doc) => doc._id);
953
+ context._cascadeIds = ids;
954
+ });
955
+ const originalAfterDeleteMany = repo._hooks.get("after:deleteMany") || [];
956
+ repo._hooks.set("after:deleteMany", [
957
+ ...originalAfterDeleteMany,
958
+ async (payload) => {
959
+ const { context } = payload;
960
+ const ids = context._cascadeIds;
961
+ if (!ids || ids.length === 0) {
962
+ return;
963
+ }
964
+ const isSoftDelete = context.softDeleted === true;
965
+ const cascadeDeleteMany = async (relation) => {
966
+ const RelatedModel = mongoose.models[relation.model];
967
+ if (!RelatedModel) {
968
+ logger?.warn?.(`Cascade deleteMany skipped: model '${relation.model}' not found`, {
969
+ parentModel: context.model
970
+ });
971
+ return;
972
+ }
973
+ const query = { [relation.foreignKey]: { $in: ids } };
974
+ const shouldSoftDelete = relation.softDelete ?? isSoftDelete;
975
+ try {
976
+ if (shouldSoftDelete) {
977
+ const updateResult = await RelatedModel.updateMany(
978
+ query,
979
+ {
980
+ deletedAt: /* @__PURE__ */ new Date(),
981
+ ...context.user ? { deletedBy: context.user._id || context.user.id } : {}
982
+ },
983
+ { session: context.session }
984
+ );
985
+ logger?.info?.(`Cascade soft-deleted ${updateResult.modifiedCount} documents (bulk)`, {
986
+ parentModel: context.model,
987
+ parentCount: ids.length,
988
+ relatedModel: relation.model,
989
+ foreignKey: relation.foreignKey,
990
+ count: updateResult.modifiedCount
991
+ });
992
+ } else {
993
+ const deleteResult = await RelatedModel.deleteMany(query, {
994
+ session: context.session
995
+ });
996
+ logger?.info?.(`Cascade deleted ${deleteResult.deletedCount} documents (bulk)`, {
997
+ parentModel: context.model,
998
+ parentCount: ids.length,
999
+ relatedModel: relation.model,
1000
+ foreignKey: relation.foreignKey,
1001
+ count: deleteResult.deletedCount
1002
+ });
1003
+ }
1004
+ } catch (error) {
1005
+ logger?.error?.(`Cascade deleteMany failed for model '${relation.model}'`, {
1006
+ parentModel: context.model,
1007
+ relatedModel: relation.model,
1008
+ foreignKey: relation.foreignKey,
1009
+ error: error.message
1010
+ });
1011
+ throw error;
1012
+ }
1013
+ };
1014
+ if (parallel) {
1015
+ await Promise.all(relations.map(cascadeDeleteMany));
1016
+ } else {
1017
+ for (const relation of relations) {
1018
+ await cascadeDeleteMany(relation);
1019
+ }
1020
+ }
1021
+ }
1022
+ ]);
1023
+ }
1024
+ };
1025
+ }
854
1026
 
855
- export { aggregateHelpersPlugin, auditLogPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, fieldFilterPlugin, immutableField, methodRegistryPlugin, mongoOperationsPlugin, requireField, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin };
856
- //# sourceMappingURL=index.js.map
857
- //# sourceMappingURL=index.js.map
1027
+ export { aggregateHelpersPlugin, auditLogPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, cascadePlugin, fieldFilterPlugin, immutableField, methodRegistryPlugin, mongoOperationsPlugin, requireField, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin };
@@ -498,6 +498,24 @@ interface CacheStats {
498
498
  sets: number;
499
499
  invalidations: number;
500
500
  }
501
+ /** Cascade relation definition */
502
+ interface CascadeRelation {
503
+ /** Model name to cascade delete to */
504
+ model: string;
505
+ /** Foreign key field in the related model that references the deleted document */
506
+ foreignKey: string;
507
+ /** Whether to use soft delete if available (default: follows parent behavior) */
508
+ softDelete?: boolean;
509
+ }
510
+ /** Cascade delete plugin options */
511
+ interface CascadeOptions {
512
+ /** Relations to cascade delete */
513
+ relations: CascadeRelation[];
514
+ /** Run cascade deletes in parallel (default: true) */
515
+ parallel?: boolean;
516
+ /** Logger for cascade operations */
517
+ logger?: Logger;
518
+ }
501
519
  /** HTTP Error with status code */
502
520
  interface HttpError extends Error {
503
521
  status: number;
@@ -507,4 +525,4 @@ interface HttpError extends Error {
507
525
  }>;
508
526
  }
509
527
 
510
- export type { AnyDocument as A, SoftDeleteOptions as B, CreateOptions as C, DeleteResult as D, EventPayload as E, FieldPreset as F, LookupOptions as G, HttpError as H, GroupResult as I, JsonSchema as J, KeysetPaginationOptions as K, Logger as L, MinMaxResult as M, CacheAdapter as N, OffsetPaginationOptions as O, PaginationConfig as P, CacheOptions as Q, RepositoryContext as R, SelectSpec as S, CacheOperationOptions as T, UpdateOptions as U, ValidationResult as V, CacheStats as W, OffsetPaginationResult as a, KeysetPaginationResult as b, AggregatePaginationOptions as c, AggregatePaginationResult as d, PluginType as e, PopulateSpec as f, SortSpec as g, ObjectId as h, AnyModel as i, SortDirection as j, PaginationResult as k, OperationOptions as l, UpdateManyResult as m, UpdateWithValidationResult as n, UserContext as o, Plugin as p, PluginFunction as q, RepositoryInstance as r, RepositoryEvent as s, ParsedQuery as t, FieldRules as u, SchemaBuilderOptions as v, CrudSchemas as w, DecodedCursor as x, ValidatorDefinition as y, ValidationChainOptions as z };
528
+ export type { AnyDocument as A, SoftDeleteOptions as B, CreateOptions as C, DeleteResult as D, EventPayload as E, FieldPreset as F, LookupOptions as G, HttpError as H, GroupResult as I, JsonSchema as J, KeysetPaginationOptions as K, Logger as L, MinMaxResult as M, CacheAdapter as N, OffsetPaginationOptions as O, PaginationConfig as P, CacheOptions as Q, RepositoryContext as R, SelectSpec as S, CacheOperationOptions as T, UpdateOptions as U, ValidationResult as V, CacheStats as W, CascadeRelation as X, CascadeOptions as Y, OffsetPaginationResult as a, KeysetPaginationResult as b, AggregatePaginationOptions as c, AggregatePaginationResult as d, PluginType as e, PopulateSpec as f, SortSpec as g, ObjectId as h, AnyModel as i, SortDirection as j, PaginationResult as k, OperationOptions as l, UpdateManyResult as m, UpdateWithValidationResult as n, UserContext as o, Plugin as p, PluginFunction as q, RepositoryInstance as r, RepositoryEvent as s, ParsedQuery as t, FieldRules as u, SchemaBuilderOptions as v, CrudSchemas as w, DecodedCursor as x, ValidatorDefinition as y, ValidationChainOptions as z };
@@ -1,5 +1,5 @@
1
- export { b as createError, c as createFieldPreset, d as createMemoryCache, f as filterResponseData, g as getFieldsForUser, a as getMongooseProjection } from '../memory-cache-DG2oSSbx.js';
2
- import { t as ParsedQuery, v as SchemaBuilderOptions, w as CrudSchemas, V as ValidationResult, S as SelectSpec, f as PopulateSpec, g as SortSpec } from '../types-Nxhmi1aI.js';
1
+ export { b as createError, c as createFieldPreset, d as createMemoryCache, f as filterResponseData, g as getFieldsForUser, a as getMongooseProjection } from '../memory-cache-tn3v1xgG.js';
2
+ import { t as ParsedQuery, v as SchemaBuilderOptions, w as CrudSchemas, V as ValidationResult, S as SelectSpec, f as PopulateSpec, g as SortSpec } from '../types-vDtcOhyx.js';
3
3
  import mongoose__default, { Schema } from 'mongoose';
4
4
 
5
5
  /**
@@ -639,5 +639,3 @@ function listPattern(prefix, model) {
639
639
  }
640
640
 
641
641
  export { buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, byIdKey, byQueryKey, createError, createFieldPreset, createMemoryCache, filterResponseData, getFieldsForUser, getImmutableFields, getMongooseProjection, getSystemManagedFields, isFieldUpdateAllowed, listPattern, listQueryKey, modelPattern, queryParser_default as queryParser, validateUpdateBody, versionKey };
642
- //# sourceMappingURL=index.js.map
643
- //# sourceMappingURL=index.js.map