@classytic/mongokit 3.5.1 → 3.5.2
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 +21 -2
- package/dist/actions/index.mjs +1 -1
- package/dist/index.mjs +2 -2
- package/dist/plugins/index.mjs +1 -1
- package/dist/{update-DcWUpWBk.mjs → update-AVfKWNGt.mjs} +36 -0
- package/dist/{validation-chain.plugin-DboBJfGs.mjs → validation-chain.plugin-vxvcv1dg.mjs} +63 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
- **Search governance** - Text index guard (throws `400` if no index), allowlisted sort/filter fields, ReDoS protection
|
|
18
18
|
- **Vector search** - MongoDB Atlas `$vectorSearch` with auto-embedding and multimodal support
|
|
19
19
|
- **TypeScript first** - Full type safety with discriminated unions, typed events, and field autocomplete
|
|
20
|
-
- **
|
|
20
|
+
- **Extensively tested** — battle-tested and production-ready
|
|
21
21
|
|
|
22
22
|
## Installation
|
|
23
23
|
|
|
@@ -1423,6 +1423,25 @@ if (dupErr) {
|
|
|
1423
1423
|
}
|
|
1424
1424
|
```
|
|
1425
1425
|
|
|
1426
|
+
## Custom ID Field
|
|
1427
|
+
|
|
1428
|
+
Use `idField` when your documents use a custom identifier (slug, UUID, code) instead of `_id`:
|
|
1429
|
+
|
|
1430
|
+
```typescript
|
|
1431
|
+
// Repo-level: all calls use slug
|
|
1432
|
+
const productRepo = new Repository(ProductModel, [], {}, { idField: 'slug' });
|
|
1433
|
+
await productRepo.getById('laptop'); // { slug: 'laptop' }
|
|
1434
|
+
await productRepo.update('laptop', { price: 999 }); // { slug: 'laptop' }
|
|
1435
|
+
await productRepo.delete('laptop'); // { slug: 'laptop' }
|
|
1436
|
+
|
|
1437
|
+
// Per-call override: same repo, different lookups
|
|
1438
|
+
const repo = new Repository(ChatModel);
|
|
1439
|
+
await repo.getById('507f1f77bcf86cd799439011'); // by _id
|
|
1440
|
+
await repo.getById('my-chat-uuid', { idField: 'chatId' }); // by chatId
|
|
1441
|
+
```
|
|
1442
|
+
|
|
1443
|
+
All plugins respect `idField`: soft-delete, cascade, audit-log, audit-trail, validation-chain, elastic, cache, observability.
|
|
1444
|
+
|
|
1426
1445
|
## No Breaking Changes
|
|
1427
1446
|
|
|
1428
1447
|
Extending Repository works exactly the same with Mongoose 8 and 9. The package:
|
|
@@ -1430,7 +1449,7 @@ Extending Repository works exactly the same with Mongoose 8 and 9. The package:
|
|
|
1430
1449
|
- Uses its own event system (not Mongoose middleware)
|
|
1431
1450
|
- Defines its own `FilterQuery` type (unaffected by Mongoose 9 rename)
|
|
1432
1451
|
- Properly gates update pipelines (safe for Mongoose 9's stricter defaults)
|
|
1433
|
-
-
|
|
1452
|
+
- Full test suite passes on Mongoose 9
|
|
1434
1453
|
|
|
1435
1454
|
## License
|
|
1436
1455
|
|
package/dist/actions/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "../chunk-CfYAbeIz.mjs";
|
|
2
|
-
import { _ as aggregate_exports, f as delete_exports, h as create_exports, l as read_exports, r as update_exports } from "../update-
|
|
2
|
+
import { _ as aggregate_exports, f as delete_exports, h as create_exports, l as read_exports, r as update_exports } from "../update-AVfKWNGt.mjs";
|
|
3
3
|
//#region src/actions/index.ts
|
|
4
4
|
var actions_exports = /* @__PURE__ */ __exportAll({
|
|
5
5
|
aggregate: () => aggregate_exports,
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { a as warn, n as parseDuplicateKeyError, r as configureLogger, t as createError } from "./error-Bpbi_NKo.mjs";
|
|
2
|
-
import { y as LookupBuilder } from "./update-
|
|
2
|
+
import { y as LookupBuilder } from "./update-AVfKWNGt.mjs";
|
|
3
3
|
import { t as actions_exports } from "./actions/index.mjs";
|
|
4
4
|
import { t as PaginationEngine } from "./PaginationEngine-DCs-zKwZ.mjs";
|
|
5
|
-
import { A as aggregateHelpersPlugin, C as HOOK_PRIORITY, D as AuditTrailQuery, E as batchOperationsPlugin, O as auditTrailPlugin, S as cachePlugin, T as AggregationBuilder, _ as dateSequentialId, a as uniqueField, b as sequentialId, c as subdocumentPlugin, d as multiTenantPlugin, f as mongoOperationsPlugin, g as customIdPlugin, h as elasticSearchPlugin, i as requireField, k as auditLogPlugin, l as softDeletePlugin, m as fieldFilterPlugin, n as blockIf, o as validationChainPlugin, p as methodRegistryPlugin, r as immutableField, s as timestampPlugin, t as autoInject, u as observabilityPlugin, v as getNextSequence, w as Repository, x as cascadePlugin, y as prefixedId } from "./validation-chain.plugin-
|
|
5
|
+
import { A as aggregateHelpersPlugin, C as HOOK_PRIORITY, D as AuditTrailQuery, E as batchOperationsPlugin, O as auditTrailPlugin, S as cachePlugin, T as AggregationBuilder, _ as dateSequentialId, a as uniqueField, b as sequentialId, c as subdocumentPlugin, d as multiTenantPlugin, f as mongoOperationsPlugin, g as customIdPlugin, h as elasticSearchPlugin, i as requireField, k as auditLogPlugin, l as softDeletePlugin, m as fieldFilterPlugin, n as blockIf, o as validationChainPlugin, p as methodRegistryPlugin, r as immutableField, s as timestampPlugin, t as autoInject, u as observabilityPlugin, v as getNextSequence, w as Repository, x as cascadePlugin, y as prefixedId } from "./validation-chain.plugin-vxvcv1dg.mjs";
|
|
6
6
|
import { i as getMongooseProjection, n as filterResponseData, r as getFieldsForUser, t as createFieldPreset } from "./field-selection-reyDRzXf.mjs";
|
|
7
7
|
import { a as isFieldUpdateAllowed, i as getSystemManagedFields, n as buildCrudSchemasFromMongooseSchema, o as validateUpdateBody, r as getImmutableFields, s as createMemoryCache, t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-B6Qyl8BK.mjs";
|
|
8
8
|
import mongoose from "mongoose";
|
package/dist/plugins/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as aggregateHelpersPlugin, D as AuditTrailQuery, E as batchOperationsPlugin, O as auditTrailPlugin, S as cachePlugin, _ as dateSequentialId, a as uniqueField, b as sequentialId, c as subdocumentPlugin, d as multiTenantPlugin, f as mongoOperationsPlugin, g as customIdPlugin, h as elasticSearchPlugin, i as requireField, k as auditLogPlugin, l as softDeletePlugin, m as fieldFilterPlugin, n as blockIf, o as validationChainPlugin, p as methodRegistryPlugin, r as immutableField, s as timestampPlugin, t as autoInject, u as observabilityPlugin, v as getNextSequence, x as cascadePlugin, y as prefixedId } from "../validation-chain.plugin-
|
|
1
|
+
import { A as aggregateHelpersPlugin, D as AuditTrailQuery, E as batchOperationsPlugin, O as auditTrailPlugin, S as cachePlugin, _ as dateSequentialId, a as uniqueField, b as sequentialId, c as subdocumentPlugin, d as multiTenantPlugin, f as mongoOperationsPlugin, g as customIdPlugin, h as elasticSearchPlugin, i as requireField, k as auditLogPlugin, l as softDeletePlugin, m as fieldFilterPlugin, n as blockIf, o as validationChainPlugin, p as methodRegistryPlugin, r as immutableField, s as timestampPlugin, t as autoInject, u as observabilityPlugin, v as getNextSequence, x as cascadePlugin, y as prefixedId } from "../validation-chain.plugin-vxvcv1dg.mjs";
|
|
2
2
|
export { AuditTrailQuery, aggregateHelpersPlugin, auditLogPlugin, auditTrailPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, cascadePlugin, customIdPlugin, dateSequentialId, elasticSearchPlugin, fieldFilterPlugin, getNextSequence, immutableField, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, prefixedId, requireField, sequentialId, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin };
|
|
@@ -279,6 +279,42 @@ var LookupBuilder = class LookupBuilder {
|
|
|
279
279
|
return sanitized;
|
|
280
280
|
}
|
|
281
281
|
};
|
|
282
|
+
/**
|
|
283
|
+
* Performance Guidelines for $lookup at Scale:
|
|
284
|
+
*
|
|
285
|
+
* 1. **Index Requirements** (Critical for millions of records):
|
|
286
|
+
* - localField should be indexed on source collection
|
|
287
|
+
* - foreignField should be indexed on target collection (unique index preferred)
|
|
288
|
+
*
|
|
289
|
+
* Example:
|
|
290
|
+
* ```typescript
|
|
291
|
+
* // Employee collection
|
|
292
|
+
* employeeSchema.index({ departmentSlug: 1 });
|
|
293
|
+
*
|
|
294
|
+
* // Department collection
|
|
295
|
+
* departmentSchema.index({ slug: 1 }, { unique: true });
|
|
296
|
+
* ```
|
|
297
|
+
*
|
|
298
|
+
* 2. **Query Performance**:
|
|
299
|
+
* - With proper indexes: O(log n) per lookup
|
|
300
|
+
* - Without indexes: O(n * m) - AVOID THIS!
|
|
301
|
+
* - Use explain() to verify index usage: IXSCAN (good) vs COLLSCAN (bad)
|
|
302
|
+
*
|
|
303
|
+
* 3. **Pipeline Optimization**:
|
|
304
|
+
* - Place $match stages as early as possible
|
|
305
|
+
* - Use $project to reduce field size before lookups
|
|
306
|
+
* - Limit joined results with pipeline: [{ $match: {...} }, { $limit: n }]
|
|
307
|
+
*
|
|
308
|
+
* 4. **Memory Considerations**:
|
|
309
|
+
* - Each lookup creates a new field in memory
|
|
310
|
+
* - Use $project after lookup to remove unnecessary fields
|
|
311
|
+
* - Consider allowDiskUse: true for very large datasets
|
|
312
|
+
*
|
|
313
|
+
* 5. **Alternative Approaches**:
|
|
314
|
+
* - For 1:1 relationships with high read frequency: Consider storing ObjectId + slug
|
|
315
|
+
* - For read-heavy workloads: Consider caching or materialized views
|
|
316
|
+
* - For real-time dashboards: Consider separate aggregation collections
|
|
317
|
+
*/
|
|
282
318
|
//#endregion
|
|
283
319
|
//#region src/actions/aggregate.ts
|
|
284
320
|
var aggregate_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as warn, i as debug, n as parseDuplicateKeyError, t as createError } from "./error-Bpbi_NKo.mjs";
|
|
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-
|
|
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
3
|
import { t as PaginationEngine } from "./PaginationEngine-DCs-zKwZ.mjs";
|
|
4
4
|
import { a as byIdKey, c as listQueryKey, l as modelPattern, o as byQueryKey, r as getFieldsForUser, u as versionKey } from "./field-selection-reyDRzXf.mjs";
|
|
5
5
|
import mongoose from "mongoose";
|
|
@@ -79,9 +79,10 @@ function auditLogPlugin(logger) {
|
|
|
79
79
|
name: "auditLog",
|
|
80
80
|
apply(repo) {
|
|
81
81
|
repo.on("after:create", ({ context, result }) => {
|
|
82
|
+
const idKey = repo.idField || "_id";
|
|
82
83
|
logger?.info?.("Document created", {
|
|
83
84
|
model: context.model || repo.model,
|
|
84
|
-
id: result?.
|
|
85
|
+
id: result?.[idKey],
|
|
85
86
|
userId: context.user?._id || context.user?.id,
|
|
86
87
|
organizationId: context.organizationId
|
|
87
88
|
});
|
|
@@ -89,7 +90,7 @@ function auditLogPlugin(logger) {
|
|
|
89
90
|
repo.on("after:update", ({ context, result }) => {
|
|
90
91
|
logger?.info?.("Document updated", {
|
|
91
92
|
model: context.model || repo.model,
|
|
92
|
-
id: context.id || result?._id,
|
|
93
|
+
id: context.id || result?.[repo.idField || "_id"],
|
|
93
94
|
userId: context.user?._id || context.user?.id,
|
|
94
95
|
organizationId: context.organizationId
|
|
95
96
|
});
|
|
@@ -282,10 +283,11 @@ function auditTrailPlugin(options = {}) {
|
|
|
282
283
|
const AuditModel = getAuditModel(collectionName, ttlDays);
|
|
283
284
|
if (opsSet.has("create")) repo.on("after:create", ({ context, result }) => {
|
|
284
285
|
const doc = toPlainObject(result);
|
|
286
|
+
const idKey = repo.idField || "_id";
|
|
285
287
|
writeAudit(AuditModel, {
|
|
286
288
|
model: context.model || repo.model,
|
|
287
289
|
operation: "create",
|
|
288
|
-
documentId: doc?.
|
|
290
|
+
documentId: doc?.[idKey],
|
|
289
291
|
userId: getUserId(context),
|
|
290
292
|
orgId: context.organizationId,
|
|
291
293
|
document: trackDocument ? sanitizeDoc(doc, excludeFields) : void 0,
|
|
@@ -313,7 +315,7 @@ function auditTrailPlugin(options = {}) {
|
|
|
313
315
|
writeAudit(AuditModel, {
|
|
314
316
|
model: context.model || repo.model,
|
|
315
317
|
operation: "update",
|
|
316
|
-
documentId: context.id || doc?._id,
|
|
318
|
+
documentId: context.id || doc?.[repo.idField || "_id"],
|
|
317
319
|
userId: getUserId(context),
|
|
318
320
|
orgId: context.organizationId,
|
|
319
321
|
changes,
|
|
@@ -1103,6 +1105,52 @@ var AggregationBuilder = class AggregationBuilder {
|
|
|
1103
1105
|
return new AggregationBuilder().match(query);
|
|
1104
1106
|
}
|
|
1105
1107
|
};
|
|
1108
|
+
/**
|
|
1109
|
+
* Optimized Aggregation Patterns for Scale
|
|
1110
|
+
*
|
|
1111
|
+
* 1. **Early Filtering** - Always place $match as early as possible:
|
|
1112
|
+
* ```typescript
|
|
1113
|
+
* new AggregationBuilder()
|
|
1114
|
+
* .match({ status: 'active' }) // ✅ Filter first
|
|
1115
|
+
* .lookup(...) // Then join
|
|
1116
|
+
* .sort(...)
|
|
1117
|
+
* ```
|
|
1118
|
+
*
|
|
1119
|
+
* 2. **Index Usage** - Ensure indexes on:
|
|
1120
|
+
* - Fields in $match
|
|
1121
|
+
* - Fields in $sort (especially with $limit)
|
|
1122
|
+
* - Fields in $lookup (both local and foreign)
|
|
1123
|
+
*
|
|
1124
|
+
* 3. **Projection** - Remove unnecessary fields early:
|
|
1125
|
+
* ```typescript
|
|
1126
|
+
* .project({ password: 0, internalNotes: 0 }) // Remove before joins
|
|
1127
|
+
* .lookup(...)
|
|
1128
|
+
* ```
|
|
1129
|
+
*
|
|
1130
|
+
* 4. **Faceted Pagination** - Get count and data in one query:
|
|
1131
|
+
* ```typescript
|
|
1132
|
+
* .facet({
|
|
1133
|
+
* metadata: [{ $count: 'total' }],
|
|
1134
|
+
* data: [{ $skip: skip }, { $limit: limit }]
|
|
1135
|
+
* })
|
|
1136
|
+
* ```
|
|
1137
|
+
*
|
|
1138
|
+
* 5. **allowDiskUse** - For large datasets:
|
|
1139
|
+
* ```typescript
|
|
1140
|
+
* new AggregationBuilder()
|
|
1141
|
+
* .match({ status: 'active' })
|
|
1142
|
+
* .allowDiskUse()
|
|
1143
|
+
* .exec(Model)
|
|
1144
|
+
* ```
|
|
1145
|
+
*
|
|
1146
|
+
* 6. **Vector Search** - Semantic similarity (Atlas only):
|
|
1147
|
+
* ```typescript
|
|
1148
|
+
* new AggregationBuilder()
|
|
1149
|
+
* .vectorSearch({ index: 'vec_idx', path: 'embedding', queryVector: vec, limit: 10 })
|
|
1150
|
+
* .withVectorScore()
|
|
1151
|
+
* .exec(Model)
|
|
1152
|
+
* ```
|
|
1153
|
+
*/
|
|
1106
1154
|
//#endregion
|
|
1107
1155
|
//#region src/Repository.ts
|
|
1108
1156
|
function ensureLookupProjectionIncludesCursorFields(projection, sort) {
|
|
@@ -2726,7 +2774,8 @@ function cascadePlugin(options) {
|
|
|
2726
2774
|
repo.on("before:deleteMany", async (context) => {
|
|
2727
2775
|
const query = context.query;
|
|
2728
2776
|
if (!query || Object.keys(query).length === 0) return;
|
|
2729
|
-
|
|
2777
|
+
const idField = repo.idField || "_id";
|
|
2778
|
+
context._cascadeIds = (await repo.Model.find(query, { [idField]: 1 }).lean().session(context.session ?? null)).map((doc) => doc[idField]);
|
|
2730
2779
|
});
|
|
2731
2780
|
repo.on("after:deleteMany", async (payload) => {
|
|
2732
2781
|
const { context } = payload;
|
|
@@ -3093,17 +3142,18 @@ function elasticSearchPlugin(options) {
|
|
|
3093
3142
|
limit,
|
|
3094
3143
|
from
|
|
3095
3144
|
};
|
|
3096
|
-
const
|
|
3145
|
+
const mongoIdField = repo.idField || "_id";
|
|
3146
|
+
const mongoQuery = this.Model.find({ [mongoIdField]: { $in: ids } });
|
|
3097
3147
|
if (searchOptions.mongoOptions?.select) mongoQuery.select(searchOptions.mongoOptions.select);
|
|
3098
3148
|
if (searchOptions.mongoOptions?.populate) mongoQuery.populate(searchOptions.mongoOptions.populate);
|
|
3099
3149
|
if (searchOptions.mongoOptions?.lean !== false) mongoQuery.lean();
|
|
3100
3150
|
return {
|
|
3101
3151
|
docs: (await mongoQuery.exec()).sort((a, b) => {
|
|
3102
|
-
const aId = String(a
|
|
3103
|
-
const bId = String(b
|
|
3152
|
+
const aId = String(a[mongoIdField]);
|
|
3153
|
+
const bId = String(b[mongoIdField]);
|
|
3104
3154
|
return (docsOrder.get(aId) ?? Number.MAX_SAFE_INTEGER) - (docsOrder.get(bId) ?? Number.MAX_SAFE_INTEGER);
|
|
3105
3155
|
}).map((doc) => {
|
|
3106
|
-
const strId = String(doc
|
|
3156
|
+
const strId = String(doc[mongoIdField]);
|
|
3107
3157
|
if (searchOptions.mongoOptions?.lean !== false) return {
|
|
3108
3158
|
...doc,
|
|
3109
3159
|
_score: scores.get(strId)
|
|
@@ -4103,12 +4153,13 @@ function uniqueField(field, errorMessage) {
|
|
|
4103
4153
|
warn(`[mongokit] uniqueField('${field}'): getByQuery not available on repo, skipping uniqueness check`);
|
|
4104
4154
|
return;
|
|
4105
4155
|
}
|
|
4156
|
+
const idKey = repo.idField || "_id";
|
|
4106
4157
|
const existing = await getByQuery.call(repo, query, {
|
|
4107
|
-
select:
|
|
4158
|
+
select: idKey,
|
|
4108
4159
|
lean: true,
|
|
4109
4160
|
throwOnNotFound: false
|
|
4110
4161
|
});
|
|
4111
|
-
if (existing && String(existing
|
|
4162
|
+
if (existing && String(existing[idKey]) !== String(context.id)) throw createError(409, errorMessage || `${field} already exists`);
|
|
4112
4163
|
}
|
|
4113
4164
|
};
|
|
4114
4165
|
}
|
package/package.json
CHANGED