@classytic/mongokit 3.5.1 → 3.5.3

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 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
- - **1170+ passing tests** - Battle-tested and production-ready
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
- - All 1090+ tests pass on Mongoose 9
1452
+ - Full test suite passes on Mongoose 9
1434
1453
 
1435
1454
  ## License
1436
1455
 
@@ -1,2 +1,2 @@
1
- import { a as create_d_exports, i as delete_d_exports, n as update_d_exports, o as aggregate_d_exports, r as read_d_exports } from "../index-BjXxDBgx.mjs";
1
+ import { a as create_d_exports, i as delete_d_exports, n as update_d_exports, o as aggregate_d_exports, r as read_d_exports } from "../index-D3ydSXYC.mjs";
2
2
  export { aggregate_d_exports as aggregate, create_d_exports as create, delete_d_exports as deleteActions, read_d_exports as read, update_d_exports as update };
@@ -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-DcWUpWBk.mjs";
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,
@@ -1,4 +1,4 @@
1
- import { J as Plugin } from "../types-DK6ELT6o.mjs";
1
+ import { J as Plugin } from "../types-wCkH8jE3.mjs";
2
2
  import { ClientSession, PipelineStage } from "mongoose";
3
3
 
4
4
  //#region src/ai/types.d.ts
@@ -1,4 +1,4 @@
1
- import { $ as ReadPreferenceType, At as LookupOptions, D as GroupResult, V as ObjectId, W as OperationOptions, Z as PopulateSpec, _t as UpdateOptions, a as AnyDocument, b as DeleteResult, g as CreateOptions, gt as UpdateManyResult, pt as SortSpec, st as SelectSpec, vt as UpdateWithValidationResult, z as MinMaxResult } from "./types-DK6ELT6o.mjs";
1
+ import { $ as ReadPreferenceType, At as LookupOptions, D as GroupResult, V as ObjectId, W as OperationOptions, Z as PopulateSpec, _t as UpdateOptions, a as AnyDocument, b as DeleteResult, g as CreateOptions, gt as UpdateManyResult, pt as SortSpec, st as SelectSpec, vt as UpdateWithValidationResult, z as MinMaxResult } from "./types-wCkH8jE3.mjs";
2
2
  import { ClientSession, Model, PipelineStage } from "mongoose";
3
3
 
4
4
  //#region src/actions/aggregate.d.ts
package/dist/index.d.mts CHANGED
@@ -1,8 +1,8 @@
1
- import { $ as ReadPreferenceType, A as InferDocument, At as LookupOptions, B as NonNullableFields, C as EventPayload, Ct as WithPlugins, D as GroupResult, Dt as IRequestContext, E as FieldRules, Et as IControllerResponse, F as KeysetPaginationResult, G as PaginationConfig, H as OffsetPaginationOptions, I as Logger, J as Plugin, K as PaginationResult, L as LookupPopulateOptions, M as JsonSchema, N as KeysOfType, O as HookMode, Ot as IResponseFormatter, P as KeysetPaginationOptions, Q as ReadOptions, R as LookupPopulateResult, S as EventHandlers, St as ValidatorDefinition, T as FieldPreset, Tt as IController, U as OffsetPaginationResult, V as ObjectId, W as OperationOptions, X as PluginType, Y as PluginFunction, Z as PopulateSpec, _ as CrudSchemas, _t as UpdateOptions, a as AnyDocument, at as RequiredBy, b as DeleteResult, bt as ValidationChainOptions, c as CacheOperationOptions, ct as SessionOptions, d as CacheableOptions, dt as SoftDeleteRepository, et as RepositoryContext, f as CascadeOptions, ft as SortDirection, g as CreateOptions, gt as UpdateManyResult, h as CreateInput, ht as UpdateInput, i as AllPluginMethods, it as RepositoryOptions, j as InferRawDoc, k as HttpError, kt as LookupBuilder, l as CacheOptions, lt as SoftDeleteFilterMode, m as CollationOptions, mt as Strict, n as AggregatePaginationOptions, nt as RepositoryInstance, o as AnyModel, ot as SchemaBuilderOptions, p as CascadeRelation, pt as SortSpec, q as PartialBy, r as AggregatePaginationResult, rt as RepositoryOperation, s as CacheAdapter, st as SelectSpec, t as AggregateOptions, tt as RepositoryEvent, u as CacheStats, ut as SoftDeleteOptions, v as DecodedCursor, vt as UpdateWithValidationResult, w as EventPhase, wt as WithTransactionOptions, x as DocField, xt as ValidationResult, y as DeepPartial, yt as UserContext, z as MinMaxResult } from "./types-DK6ELT6o.mjs";
2
- import { t as index_d_exports } from "./index-BjXxDBgx.mjs";
1
+ import { $ as ReadPreferenceType, A as InferDocument, At as LookupOptions, B as NonNullableFields, C as EventPayload, Ct as WithPlugins, D as GroupResult, Dt as IRequestContext, E as FieldRules, Et as IControllerResponse, F as KeysetPaginationResult, G as PaginationConfig, H as OffsetPaginationOptions, I as Logger, J as Plugin, K as PaginationResult, L as LookupPopulateOptions, M as JsonSchema, N as KeysOfType, O as HookMode, Ot as IResponseFormatter, P as KeysetPaginationOptions, Q as ReadOptions, R as LookupPopulateResult, S as EventHandlers, St as ValidatorDefinition, T as FieldPreset, Tt as IController, U as OffsetPaginationResult, V as ObjectId, W as OperationOptions, X as PluginType, Y as PluginFunction, Z as PopulateSpec, _ as CrudSchemas, _t as UpdateOptions, a as AnyDocument, at as RequiredBy, b as DeleteResult, bt as ValidationChainOptions, c as CacheOperationOptions, ct as SessionOptions, d as CacheableOptions, dt as SoftDeleteRepository, et as RepositoryContext, f as CascadeOptions, ft as SortDirection, g as CreateOptions, gt as UpdateManyResult, h as CreateInput, ht as UpdateInput, i as AllPluginMethods, it as RepositoryOptions, j as InferRawDoc, k as HttpError, kt as LookupBuilder, l as CacheOptions, lt as SoftDeleteFilterMode, m as CollationOptions, mt as Strict, n as AggregatePaginationOptions, nt as RepositoryInstance, o as AnyModel, ot as SchemaBuilderOptions, p as CascadeRelation, pt as SortSpec, q as PartialBy, r as AggregatePaginationResult, rt as RepositoryOperation, s as CacheAdapter, st as SelectSpec, t as AggregateOptions, tt as RepositoryEvent, u as CacheStats, ut as SoftDeleteOptions, v as DecodedCursor, vt as UpdateWithValidationResult, w as EventPhase, wt as WithTransactionOptions, x as DocField, xt as ValidationResult, y as DeepPartial, yt as UserContext, z as MinMaxResult } from "./types-wCkH8jE3.mjs";
2
+ import { t as index_d_exports } from "./index-D3ydSXYC.mjs";
3
3
  import { PaginationEngine } from "./pagination/PaginationEngine.mjs";
4
- import { A as dateSequentialId, B as AuditEntry, C as elasticSearchPlugin, D as PrefixedIdOptions, E as IdGenerator, F as CacheMethods, G as AuditTrailOptions, H as AuditQueryOptions, I as cachePlugin, J as auditLogPlugin, K as AuditTrailQuery, L as BatchOperationsMethods, M as prefixedId, N as sequentialId, O as SequentialIdOptions, P as cascadePlugin, R as BulkWriteResult, S as ElasticSearchOptions, T as DateSequentialIdOptions, U as AuditQueryResult, V as AuditOperation, W as AuditTrailMethods, X as aggregateHelpersPlugin, Y as AggregateHelpersMethods, _ as MongoOperationsMethods, a as uniqueField, b as methodRegistryPlugin, c as SubdocumentMethods, d as softDeletePlugin, f as ObservabilityOptions, g as multiTenantPlugin, h as MultiTenantOptions, i as requireField, j as getNextSequence, k as customIdPlugin, l as subdocumentPlugin, m as observabilityPlugin, n as blockIf, o as validationChainPlugin, p as OperationMetric, q as auditTrailPlugin, r as immutableField, s as timestampPlugin, t as autoInject, u as SoftDeleteMethods, v as mongoOperationsPlugin, w as CustomIdOptions, x as fieldFilterPlugin, z as batchOperationsPlugin } from "./validation-chain.plugin-DISOoL9A.mjs";
5
- import { a as isFieldUpdateAllowed, c as configureLogger, d as getFieldsForUser, f as getMongooseProjection, i as getSystemManagedFields, l as createFieldPreset, m as parseDuplicateKeyError, n as buildCrudSchemasFromMongooseSchema, o as validateUpdateBody, p as createError, r as getImmutableFields, s as createMemoryCache, t as buildCrudSchemasFromModel, u as filterResponseData } from "./mongooseToJsonSchema-C78rAP7X.mjs";
4
+ import { A as dateSequentialId, B as AuditEntry, C as elasticSearchPlugin, D as PrefixedIdOptions, E as IdGenerator, F as CacheMethods, G as AuditTrailOptions, H as AuditQueryOptions, I as cachePlugin, J as auditLogPlugin, K as AuditTrailQuery, L as BatchOperationsMethods, M as prefixedId, N as sequentialId, O as SequentialIdOptions, P as cascadePlugin, R as BulkWriteResult, S as ElasticSearchOptions, T as DateSequentialIdOptions, U as AuditQueryResult, V as AuditOperation, W as AuditTrailMethods, X as aggregateHelpersPlugin, Y as AggregateHelpersMethods, _ as MongoOperationsMethods, a as uniqueField, b as methodRegistryPlugin, c as SubdocumentMethods, d as softDeletePlugin, f as ObservabilityOptions, g as multiTenantPlugin, h as MultiTenantOptions, i as requireField, j as getNextSequence, k as customIdPlugin, l as subdocumentPlugin, m as observabilityPlugin, n as blockIf, o as validationChainPlugin, p as OperationMetric, q as auditTrailPlugin, r as immutableField, s as timestampPlugin, t as autoInject, u as SoftDeleteMethods, v as mongoOperationsPlugin, w as CustomIdOptions, x as fieldFilterPlugin, z as batchOperationsPlugin } from "./validation-chain.plugin-CGE_mKQr.mjs";
5
+ import { a as isFieldUpdateAllowed, c as configureLogger, d as getFieldsForUser, f as getMongooseProjection, i as getSystemManagedFields, l as createFieldPreset, m as parseDuplicateKeyError, n as buildCrudSchemasFromMongooseSchema, o as validateUpdateBody, p as createError, r as getImmutableFields, s as createMemoryCache, t as buildCrudSchemasFromModel, u as filterResponseData } from "./mongooseToJsonSchema-BivI9LWt.mjs";
6
6
  import * as _$mongoose from "mongoose";
7
7
  import { ClientSession, Expression, Model, PipelineStage, PopulateOptions } from "mongoose";
8
8
 
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-DcWUpWBk.mjs";
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-DboBJfGs.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-KjPV_6_N.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";
@@ -1,4 +1,4 @@
1
- import { T as FieldPreset, _ as CrudSchemas, k as HttpError, ot as SchemaBuilderOptions, s as CacheAdapter, xt as ValidationResult, yt as UserContext } from "./types-DK6ELT6o.mjs";
1
+ import { T as FieldPreset, _ as CrudSchemas, k as HttpError, ot as SchemaBuilderOptions, s as CacheAdapter, xt as ValidationResult, yt as UserContext } from "./types-wCkH8jE3.mjs";
2
2
  import mongoose, { Schema } from "mongoose";
3
3
 
4
4
  //#region src/utils/error.d.ts
@@ -1,4 +1,4 @@
1
- import { F as KeysetPaginationResult, G as PaginationConfig, H as OffsetPaginationOptions, P as KeysetPaginationOptions, U as OffsetPaginationResult, a as AnyDocument, n as AggregatePaginationOptions, r as AggregatePaginationResult } from "../types-DK6ELT6o.mjs";
1
+ import { F as KeysetPaginationResult, G as PaginationConfig, H as OffsetPaginationOptions, P as KeysetPaginationOptions, U as OffsetPaginationResult, a as AnyDocument, n as AggregatePaginationOptions, r as AggregatePaginationResult } from "../types-wCkH8jE3.mjs";
2
2
  import { Model } from "mongoose";
3
3
 
4
4
  //#region src/pagination/PaginationEngine.d.ts
@@ -1,2 +1,2 @@
1
- import { A as dateSequentialId, B as AuditEntry, C as elasticSearchPlugin, D as PrefixedIdOptions, E as IdGenerator, F as CacheMethods, G as AuditTrailOptions, H as AuditQueryOptions, I as cachePlugin, J as auditLogPlugin, K as AuditTrailQuery, L as BatchOperationsMethods, M as prefixedId, N as sequentialId, O as SequentialIdOptions, P as cascadePlugin, S as ElasticSearchOptions, T as DateSequentialIdOptions, U as AuditQueryResult, V as AuditOperation, W as AuditTrailMethods, X as aggregateHelpersPlugin, Y as AggregateHelpersMethods, _ as MongoOperationsMethods, a as uniqueField, b as methodRegistryPlugin, c as SubdocumentMethods, d as softDeletePlugin, f as ObservabilityOptions, g as multiTenantPlugin, h as MultiTenantOptions, i as requireField, j as getNextSequence, k as customIdPlugin, l as subdocumentPlugin, m as observabilityPlugin, n as blockIf, o as validationChainPlugin, p as OperationMetric, q as auditTrailPlugin, r as immutableField, s as timestampPlugin, t as autoInject, u as SoftDeleteMethods, v as mongoOperationsPlugin, w as CustomIdOptions, x as fieldFilterPlugin, y as MethodRegistryRepository, z as batchOperationsPlugin } from "../validation-chain.plugin-DISOoL9A.mjs";
1
+ import { A as dateSequentialId, B as AuditEntry, C as elasticSearchPlugin, D as PrefixedIdOptions, E as IdGenerator, F as CacheMethods, G as AuditTrailOptions, H as AuditQueryOptions, I as cachePlugin, J as auditLogPlugin, K as AuditTrailQuery, L as BatchOperationsMethods, M as prefixedId, N as sequentialId, O as SequentialIdOptions, P as cascadePlugin, S as ElasticSearchOptions, T as DateSequentialIdOptions, U as AuditQueryResult, V as AuditOperation, W as AuditTrailMethods, X as aggregateHelpersPlugin, Y as AggregateHelpersMethods, _ as MongoOperationsMethods, a as uniqueField, b as methodRegistryPlugin, c as SubdocumentMethods, d as softDeletePlugin, f as ObservabilityOptions, g as multiTenantPlugin, h as MultiTenantOptions, i as requireField, j as getNextSequence, k as customIdPlugin, l as subdocumentPlugin, m as observabilityPlugin, n as blockIf, o as validationChainPlugin, p as OperationMetric, q as auditTrailPlugin, r as immutableField, s as timestampPlugin, t as autoInject, u as SoftDeleteMethods, v as mongoOperationsPlugin, w as CustomIdOptions, x as fieldFilterPlugin, y as MethodRegistryRepository, z as batchOperationsPlugin } from "../validation-chain.plugin-CGE_mKQr.mjs";
2
2
  export { type AggregateHelpersMethods, type AuditEntry, type AuditOperation, type AuditQueryOptions, type AuditQueryResult, type AuditTrailMethods, type AuditTrailOptions, AuditTrailQuery, type BatchOperationsMethods, type CacheMethods, type CustomIdOptions, type DateSequentialIdOptions, type ElasticSearchOptions, type IdGenerator, type MethodRegistryRepository, type MongoOperationsMethods, type MultiTenantOptions, type ObservabilityOptions, type OperationMetric, type PrefixedIdOptions, type SequentialIdOptions, type SoftDeleteMethods, type SubdocumentMethods, 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 };
@@ -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-DboBJfGs.mjs";
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-KjPV_6_N.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 };
@@ -684,6 +684,10 @@ type PaginationResult<T = unknown> = OffsetPaginationResult<T> | KeysetPaginatio
684
684
  interface SessionOptions {
685
685
  /** MongoDB session for transactions */
686
686
  session?: ClientSession;
687
+ /** Organization/tenant ID for multi-tenant plugin scoping */
688
+ organizationId?: string | ObjectId;
689
+ /** Extensible — plugins can read custom fields from options */
690
+ [key: string]: unknown;
687
691
  }
688
692
  /** Read options — session + readPreference for read-only operations */
689
693
  interface ReadOptions extends SessionOptions {
@@ -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
- import { Z as PopulateSpec, pt as SortSpec, st as SelectSpec } from "../types-DK6ELT6o.mjs";
2
- import { a as isFieldUpdateAllowed, c as configureLogger, d as getFieldsForUser, f as getMongooseProjection, i as getSystemManagedFields, l as createFieldPreset, n as buildCrudSchemasFromMongooseSchema, o as validateUpdateBody, p as createError, r as getImmutableFields, s as createMemoryCache, t as buildCrudSchemasFromModel, u as filterResponseData } from "../mongooseToJsonSchema-C78rAP7X.mjs";
1
+ import { Z as PopulateSpec, pt as SortSpec, st as SelectSpec } from "../types-wCkH8jE3.mjs";
2
+ import { a as isFieldUpdateAllowed, c as configureLogger, d as getFieldsForUser, f as getMongooseProjection, i as getSystemManagedFields, l as createFieldPreset, n as buildCrudSchemasFromMongooseSchema, o as validateUpdateBody, p as createError, r as getImmutableFields, s as createMemoryCache, t as buildCrudSchemasFromModel, u as filterResponseData } from "../mongooseToJsonSchema-BivI9LWt.mjs";
3
3
 
4
4
  //#region src/utils/cache-keys.d.ts
5
5
  /**
@@ -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-DK6ELT6o.mjs";
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-wCkH8jE3.mjs";
2
2
  import mongoose, { ClientSession } from "mongoose";
3
3
 
4
4
  //#region src/plugins/aggregate-helpers.plugin.d.ts
@@ -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-DcWUpWBk.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-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?._id,
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?._id,
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) {
@@ -2383,6 +2431,34 @@ function cachePlugin(options) {
2383
2431
  }
2384
2432
  }, { priority: HOOK_PRIORITY.CACHE });
2385
2433
  /**
2434
+ * before:getOne - Same cache logic as getByQuery (single-doc)
2435
+ */
2436
+ repo.on("before:getOne", async (context) => {
2437
+ if (context.skipCache) return;
2438
+ try {
2439
+ const collectionVersion = await getVersion();
2440
+ const query = context.query || {};
2441
+ const key = byQueryKey(config.prefix, model, collectionVersion, query, {
2442
+ select: context.select,
2443
+ populate: context.populate
2444
+ });
2445
+ const cached = await config.adapter.get(key);
2446
+ if (cached !== null && cached !== void 0) {
2447
+ log(`Cache HIT for getOne: ${key}`);
2448
+ stats.hits++;
2449
+ context._cacheHit = true;
2450
+ context._cachedResult = cached;
2451
+ } else {
2452
+ log(`Cache MISS for getOne: ${key}`);
2453
+ stats.misses++;
2454
+ context._cacheKey = key;
2455
+ }
2456
+ } catch (e) {
2457
+ log(`Cache error for getOne:`, e);
2458
+ stats.errors++;
2459
+ }
2460
+ }, { priority: HOOK_PRIORITY.CACHE });
2461
+ /**
2386
2462
  * before:getAll - Check cache for list query
2387
2463
  * Runs at CACHE priority (200) — after policy hooks inject filters
2388
2464
  */
@@ -2478,6 +2554,25 @@ function cachePlugin(options) {
2478
2554
  }
2479
2555
  });
2480
2556
  /**
2557
+ * after:getOne - Cache the result (same as getByQuery)
2558
+ */
2559
+ repo.on("after:getOne", async (payload) => {
2560
+ const { context, result } = payload;
2561
+ if (context._cacheHit || context.skipCache || result === null) return;
2562
+ const collectionVersion = await getVersion();
2563
+ const query = context.query || {};
2564
+ const key = byQueryKey(config.prefix, model, collectionVersion, query, {
2565
+ select: context.select,
2566
+ populate: context.populate
2567
+ });
2568
+ try {
2569
+ await config.adapter.set(key, result, context.cacheTtl ?? config.queryTtl);
2570
+ stats.sets++;
2571
+ } catch (e) {
2572
+ log(`Failed to cache getOne:`, e);
2573
+ }
2574
+ });
2575
+ /**
2481
2576
  * after:getAll - Cache the result
2482
2577
  */
2483
2578
  repo.on("after:getAll", async (payload) => {
@@ -2726,7 +2821,8 @@ function cascadePlugin(options) {
2726
2821
  repo.on("before:deleteMany", async (context) => {
2727
2822
  const query = context.query;
2728
2823
  if (!query || Object.keys(query).length === 0) return;
2729
- context._cascadeIds = (await repo.Model.find(query, { _id: 1 }).lean().session(context.session ?? null)).map((doc) => doc._id);
2824
+ const idField = repo.idField || "_id";
2825
+ context._cascadeIds = (await repo.Model.find(query, { [idField]: 1 }).lean().session(context.session ?? null)).map((doc) => doc[idField]);
2730
2826
  });
2731
2827
  repo.on("after:deleteMany", async (payload) => {
2732
2828
  const { context } = payload;
@@ -3093,17 +3189,18 @@ function elasticSearchPlugin(options) {
3093
3189
  limit,
3094
3190
  from
3095
3191
  };
3096
- const mongoQuery = this.Model.find({ _id: { $in: ids } });
3192
+ const mongoIdField = repo.idField || "_id";
3193
+ const mongoQuery = this.Model.find({ [mongoIdField]: { $in: ids } });
3097
3194
  if (searchOptions.mongoOptions?.select) mongoQuery.select(searchOptions.mongoOptions.select);
3098
3195
  if (searchOptions.mongoOptions?.populate) mongoQuery.populate(searchOptions.mongoOptions.populate);
3099
3196
  if (searchOptions.mongoOptions?.lean !== false) mongoQuery.lean();
3100
3197
  return {
3101
3198
  docs: (await mongoQuery.exec()).sort((a, b) => {
3102
- const aId = String(a._id);
3103
- const bId = String(b._id);
3199
+ const aId = String(a[mongoIdField]);
3200
+ const bId = String(b[mongoIdField]);
3104
3201
  return (docsOrder.get(aId) ?? Number.MAX_SAFE_INTEGER) - (docsOrder.get(bId) ?? Number.MAX_SAFE_INTEGER);
3105
3202
  }).map((doc) => {
3106
- const strId = String(doc._id);
3203
+ const strId = String(doc[mongoIdField]);
3107
3204
  if (searchOptions.mongoOptions?.lean !== false) return {
3108
3205
  ...doc,
3109
3206
  _score: scores.get(strId)
@@ -3143,8 +3240,10 @@ function fieldFilterPlugin(fieldPreset) {
3143
3240
  else context.select = presetSelect;
3144
3241
  };
3145
3242
  repo.on("before:getAll", applyFieldFiltering);
3243
+ repo.on("before:findAll", applyFieldFiltering);
3146
3244
  repo.on("before:getById", applyFieldFiltering);
3147
3245
  repo.on("before:getByQuery", applyFieldFiltering);
3246
+ repo.on("before:getOne", applyFieldFiltering);
3148
3247
  }
3149
3248
  };
3150
3249
  }
@@ -3430,12 +3529,14 @@ function multiTenantPlugin(options = {}) {
3430
3529
  const { tenantField = "organizationId", contextKey = "organizationId", required = true, skipOperations = [], skipWhen, resolveContext } = options;
3431
3530
  const filterOps = [
3432
3531
  "getAll",
3532
+ "findAll",
3433
3533
  "aggregatePaginate",
3434
3534
  "lookupPopulate"
3435
3535
  ];
3436
3536
  const queryReadOps = [
3437
3537
  "getById",
3438
3538
  "getByQuery",
3539
+ "getOne",
3439
3540
  "count",
3440
3541
  "exists",
3441
3542
  "getOrCreate",
@@ -3528,7 +3629,14 @@ const DEFAULT_OPS = [
3528
3629
  "delete",
3529
3630
  "getById",
3530
3631
  "getByQuery",
3632
+ "getOne",
3531
3633
  "getAll",
3634
+ "findAll",
3635
+ "getOrCreate",
3636
+ "count",
3637
+ "exists",
3638
+ "distinct",
3639
+ "aggregate",
3532
3640
  "aggregatePaginate",
3533
3641
  "lookupPopulate"
3534
3642
  ];
@@ -3683,7 +3791,7 @@ function softDeletePlugin(options = {}) {
3683
3791
  context.softDeleted = true;
3684
3792
  }
3685
3793
  }, { priority: HOOK_PRIORITY.POLICY });
3686
- repo.on("before:getAll", (context) => {
3794
+ const injectDeleteFilterToFilters = (context) => {
3687
3795
  if (options.soft !== false) {
3688
3796
  const deleteFilter = buildDeletedFilter(deletedField, filterMode, !!context.includeDeleted);
3689
3797
  if (Object.keys(deleteFilter).length > 0) context.filters = {
@@ -3691,53 +3799,8 @@ function softDeletePlugin(options = {}) {
3691
3799
  ...deleteFilter
3692
3800
  };
3693
3801
  }
3694
- }, { priority: HOOK_PRIORITY.POLICY });
3695
- repo.on("before:getById", (context) => {
3696
- if (options.soft !== false) {
3697
- const deleteFilter = buildDeletedFilter(deletedField, filterMode, !!context.includeDeleted);
3698
- if (Object.keys(deleteFilter).length > 0) context.query = {
3699
- ...context.query || {},
3700
- ...deleteFilter
3701
- };
3702
- }
3703
- }, { priority: HOOK_PRIORITY.POLICY });
3704
- repo.on("before:getByQuery", (context) => {
3705
- if (options.soft !== false) {
3706
- const deleteFilter = buildDeletedFilter(deletedField, filterMode, !!context.includeDeleted);
3707
- if (Object.keys(deleteFilter).length > 0) context.query = {
3708
- ...context.query || {},
3709
- ...deleteFilter
3710
- };
3711
- }
3712
- }, { priority: HOOK_PRIORITY.POLICY });
3713
- repo.on("before:count", (context) => {
3714
- if (options.soft !== false) {
3715
- const deleteFilter = buildDeletedFilter(deletedField, filterMode, !!context.includeDeleted);
3716
- if (Object.keys(deleteFilter).length > 0) context.query = {
3717
- ...context.query || {},
3718
- ...deleteFilter
3719
- };
3720
- }
3721
- }, { priority: HOOK_PRIORITY.POLICY });
3722
- repo.on("before:exists", (context) => {
3723
- if (options.soft !== false) {
3724
- const deleteFilter = buildDeletedFilter(deletedField, filterMode, !!context.includeDeleted);
3725
- if (Object.keys(deleteFilter).length > 0) context.query = {
3726
- ...context.query || {},
3727
- ...deleteFilter
3728
- };
3729
- }
3730
- }, { priority: HOOK_PRIORITY.POLICY });
3731
- repo.on("before:getOrCreate", (context) => {
3732
- if (options.soft !== false) {
3733
- const deleteFilter = buildDeletedFilter(deletedField, filterMode, !!context.includeDeleted);
3734
- if (Object.keys(deleteFilter).length > 0) context.query = {
3735
- ...context.query || {},
3736
- ...deleteFilter
3737
- };
3738
- }
3739
- }, { priority: HOOK_PRIORITY.POLICY });
3740
- repo.on("before:distinct", (context) => {
3802
+ };
3803
+ const injectDeleteFilterToQuery = (context) => {
3741
3804
  if (options.soft !== false) {
3742
3805
  const deleteFilter = buildDeletedFilter(deletedField, filterMode, !!context.includeDeleted);
3743
3806
  if (Object.keys(deleteFilter).length > 0) context.query = {
@@ -3745,7 +3808,23 @@ function softDeletePlugin(options = {}) {
3745
3808
  ...deleteFilter
3746
3809
  };
3747
3810
  }
3748
- }, { priority: HOOK_PRIORITY.POLICY });
3811
+ };
3812
+ for (const op of [
3813
+ "getAll",
3814
+ "findAll",
3815
+ "aggregatePaginate",
3816
+ "lookupPopulate"
3817
+ ]) repo.on(`before:${op}`, injectDeleteFilterToFilters, { priority: HOOK_PRIORITY.POLICY });
3818
+ for (const op of [
3819
+ "getById",
3820
+ "getOne",
3821
+ "getByQuery",
3822
+ "count",
3823
+ "exists",
3824
+ "getOrCreate",
3825
+ "distinct",
3826
+ "aggregate"
3827
+ ]) repo.on(`before:${op}`, injectDeleteFilterToQuery, { priority: HOOK_PRIORITY.POLICY });
3749
3828
  repo.on("before:updateMany", (context) => {
3750
3829
  if (options.soft !== false) {
3751
3830
  const deleteFilter = buildDeletedFilter(deletedField, filterMode, !!context.includeDeleted);
@@ -4103,12 +4182,13 @@ function uniqueField(field, errorMessage) {
4103
4182
  warn(`[mongokit] uniqueField('${field}'): getByQuery not available on repo, skipping uniqueness check`);
4104
4183
  return;
4105
4184
  }
4185
+ const idKey = repo.idField || "_id";
4106
4186
  const existing = await getByQuery.call(repo, query, {
4107
- select: "_id",
4187
+ select: idKey,
4108
4188
  lean: true,
4109
4189
  throwOnNotFound: false
4110
4190
  });
4111
- if (existing && String(existing._id) !== String(context.id)) throw createError(409, errorMessage || `${field} already exists`);
4191
+ if (existing && String(existing[idKey]) !== String(context.id)) throw createError(409, errorMessage || `${field} already exists`);
4112
4192
  }
4113
4193
  };
4114
4194
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/mongokit",
3
- "version": "3.5.1",
3
+ "version": "3.5.3",
4
4
  "description": "Production-grade MongoDB repositories with zero dependencies - smart pagination, events, and plugins",
5
5
  "type": "module",
6
6
  "sideEffects": false,