@classytic/mongokit 3.0.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.
- package/README.md +35 -4
- package/dist/actions/index.d.ts +2 -2
- package/dist/actions/index.js +0 -2
- package/dist/{index-CgOJ2pqz.d.ts → index-CKy3H2SY.d.ts} +1 -1
- package/dist/index.d.ts +10 -6
- package/dist/index.js +183 -6
- package/dist/{memory-cache-DG2oSSbx.d.ts → memory-cache-tn3v1xgG.d.ts} +1 -1
- package/dist/pagination/PaginationEngine.d.ts +1 -1
- package/dist/pagination/PaginationEngine.js +0 -2
- package/dist/plugins/index.d.ts +37 -2
- package/dist/plugins/index.js +173 -3
- package/dist/{types-Nxhmi1aI.d.ts → types-vDtcOhyx.d.ts} +19 -1
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js +0 -2
- package/package.json +1 -1
- package/dist/actions/index.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/pagination/PaginationEngine.js.map +0 -1
- package/dist/plugins/index.js.map +0 -1
- 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
|
|
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 (
|
|
997
|
-
- ✅
|
|
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
|
-
-
|
|
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
|
package/dist/actions/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { a as aggregate, c as create, _ as deleteActions, r as read, u as update } from '../index-
|
|
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-
|
|
3
|
+
import '../types-vDtcOhyx.js';
|
package/dist/actions/index.js
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
8
|
-
export { i as actions } from './index-
|
|
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.
|
|
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.
|
|
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,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-
|
|
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
|
package/dist/plugins/index.d.ts
CHANGED
|
@@ -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-
|
|
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
|
-
|
|
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 };
|
package/dist/plugins/index.js
CHANGED
|
@@ -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 };
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -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-
|
|
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-
|
|
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
|
/**
|
package/dist/utils/index.js
CHANGED
|
@@ -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
|