@classytic/mongokit 3.2.0 → 3.2.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.
Files changed (48) hide show
  1. package/README.md +470 -193
  2. package/dist/actions/index.d.mts +9 -0
  3. package/dist/actions/index.mjs +15 -0
  4. package/dist/aggregate-BAi4Do-X.mjs +767 -0
  5. package/dist/aggregate-CCHI7F51.d.mts +269 -0
  6. package/dist/ai/index.d.mts +125 -0
  7. package/dist/ai/index.mjs +203 -0
  8. package/dist/cache-keys-C8Z9B5sw.mjs +204 -0
  9. package/dist/chunk-DQk6qfdC.mjs +18 -0
  10. package/dist/create-BuO6xt0v.mjs +55 -0
  11. package/dist/custom-id.plugin-B_zIs6gE.mjs +1818 -0
  12. package/dist/custom-id.plugin-BzZI4gnE.d.mts +893 -0
  13. package/dist/index.d.mts +1012 -0
  14. package/dist/index.mjs +1906 -0
  15. package/dist/limits-DsNeCx4D.mjs +299 -0
  16. package/dist/logger-D8ily-PP.mjs +51 -0
  17. package/dist/mongooseToJsonSchema-COdDEkIJ.mjs +317 -0
  18. package/dist/{mongooseToJsonSchema-CaRF_bCN.d.ts → mongooseToJsonSchema-Wbvjfwkn.d.mts} +16 -89
  19. package/dist/pagination/PaginationEngine.d.mts +93 -0
  20. package/dist/pagination/PaginationEngine.mjs +196 -0
  21. package/dist/plugins/index.d.mts +3 -0
  22. package/dist/plugins/index.mjs +3 -0
  23. package/dist/types-D-gploPr.d.mts +1241 -0
  24. package/dist/utils/{index.d.ts → index.d.mts} +14 -21
  25. package/dist/utils/index.mjs +5 -0
  26. package/package.json +21 -21
  27. package/dist/actions/index.d.ts +0 -3
  28. package/dist/actions/index.js +0 -5
  29. package/dist/ai/index.d.ts +0 -175
  30. package/dist/ai/index.js +0 -206
  31. package/dist/chunks/chunk-2ZN65ZOP.js +0 -93
  32. package/dist/chunks/chunk-44KXLGPO.js +0 -388
  33. package/dist/chunks/chunk-DEVXDBRL.js +0 -1226
  34. package/dist/chunks/chunk-I7CWNAJB.js +0 -46
  35. package/dist/chunks/chunk-JWUAVZ3L.js +0 -8
  36. package/dist/chunks/chunk-UE2IEXZJ.js +0 -306
  37. package/dist/chunks/chunk-URLJFIR7.js +0 -22
  38. package/dist/chunks/chunk-VWKIKZYF.js +0 -737
  39. package/dist/chunks/chunk-WSFCRVEQ.js +0 -7
  40. package/dist/index-BDn5fSTE.d.ts +0 -516
  41. package/dist/index.d.ts +0 -1422
  42. package/dist/index.js +0 -1893
  43. package/dist/pagination/PaginationEngine.d.ts +0 -117
  44. package/dist/pagination/PaginationEngine.js +0 -3
  45. package/dist/plugins/index.d.ts +0 -922
  46. package/dist/plugins/index.js +0 -6
  47. package/dist/types-Jni1KgkP.d.ts +0 -780
  48. package/dist/utils/index.js +0 -5
package/README.md CHANGED
@@ -10,12 +10,14 @@
10
10
  ## Features
11
11
 
12
12
  - **Zero dependencies** - Only Mongoose as peer dependency
13
- - **Smart pagination** - Auto-detects offset vs cursor-based
14
- - **Event-driven** - Pre/post hooks for all operations
15
- - **14 built-in plugins** - Caching, soft delete, validation, multi-tenant, observability, and more
13
+ - **Explicit + smart pagination** - Explicit `mode` control or auto-detection; offset, keyset, and aggregate
14
+ - **Event-driven** - Pre/post hooks for all operations (granular scalability hooks)
15
+ - **16 built-in plugins** - Caching, soft delete, validation, multi-tenant, custom IDs, observability, Elasticsearch, and more
16
+ - **Distributed cache safety** - List cache versions stored in the adapter (Redis) for multi-pod correctness
17
+ - **Search governance** - Text index guard (throws `400` if no index), allowlisted sort/filter fields, ReDoS protection
16
18
  - **Vector search** - MongoDB Atlas `$vectorSearch` with auto-embedding and multimodal support
17
19
  - **TypeScript first** - Full type safety with discriminated unions
18
- - **547 passing tests** - Battle-tested and production-ready
20
+ - **604 passing tests** - Battle-tested and production-ready
19
21
 
20
22
  ## Installation
21
23
 
@@ -28,19 +30,19 @@ npm install @classytic/mongokit mongoose
28
30
  ## Quick Start
29
31
 
30
32
  ```javascript
31
- import { Repository } from '@classytic/mongokit';
32
- import UserModel from './models/User.js';
33
+ import { Repository } from "@classytic/mongokit";
34
+ import UserModel from "./models/User.js";
33
35
 
34
36
  const userRepo = new Repository(UserModel);
35
37
 
36
38
  // Create
37
- const user = await userRepo.create({ name: 'John', email: 'john@example.com' });
39
+ const user = await userRepo.create({ name: "John", email: "john@example.com" });
38
40
 
39
41
  // Read with auto-detected pagination
40
42
  const users = await userRepo.getAll({ page: 1, limit: 20 });
41
43
 
42
44
  // Update
43
- await userRepo.update(user._id, { name: 'Jane' });
45
+ await userRepo.update(user._id, { name: "Jane" });
44
46
 
45
47
  // Delete
46
48
  await userRepo.delete(user._id);
@@ -48,22 +50,27 @@ await userRepo.delete(user._id);
48
50
 
49
51
  ## Pagination
50
52
 
51
- `getAll()` automatically detects pagination mode based on parameters:
53
+ `getAll()` takes an **explicit `mode`** or auto-detects based on parameters:
52
54
 
53
55
  ```javascript
54
- // Offset pagination (page-based) - for dashboards
56
+ // EXPLICIT: Offset pagination (recommended for dashboards, admin panels)
55
57
  const result = await repo.getAll({
58
+ mode: "offset", // explicit — no ambiguity
56
59
  page: 1,
57
60
  limit: 20,
58
- filters: { status: 'active' },
59
- sort: { createdAt: -1 }
61
+ filters: { status: "active" },
62
+ sort: { createdAt: -1 },
63
+ countStrategy: "exact", // 'exact' | 'estimated' | 'none'
64
+ hint: { createdAt: -1 }, // index hint for query governance
65
+ maxTimeMS: 2000, // kill slow queries
60
66
  });
61
67
  // → { method: 'offset', docs, total, pages, hasNext, hasPrev }
62
68
 
63
- // Keyset pagination (cursor-based) - for infinite scroll
69
+ // EXPLICIT: Keyset pagination (recommended for feeds, infinite scroll)
64
70
  const stream = await repo.getAll({
71
+ mode: "keyset",
65
72
  sort: { createdAt: -1 },
66
- limit: 20
73
+ limit: 20,
67
74
  });
68
75
  // → { method: 'keyset', docs, hasMore, next: 'eyJ2IjoxLC...' }
69
76
 
@@ -71,20 +78,37 @@ const stream = await repo.getAll({
71
78
  const next = await repo.getAll({
72
79
  after: stream.next,
73
80
  sort: { createdAt: -1 },
74
- limit: 20
81
+ limit: 20,
75
82
  });
83
+
84
+ // AUTO-DETECTION (backwards compatible, no mode required)
85
+ // page parameter → offset mode
86
+ // after/cursor parameter → keyset mode
87
+ // sort without page → keyset mode (first page)
88
+ // nothing/filters only → offset mode (page 1)
76
89
  ```
77
90
 
78
- **Auto-detection rules:**
79
- - `page` parameter → offset mode
80
- - `after`/`cursor` parameterkeyset mode
81
- - `sort` without `page` → keyset mode (first page)
82
- - Default offset mode (page 1)
91
+ **Auto-detection rules (when `mode` is omitted):**
92
+
93
+ - `page` present**offset** mode
94
+ - `after` or `cursor` present **keyset** mode
95
+ - Non-default `sort` provided without `page` → **keyset** mode
96
+ - Nothing / filters only → **offset** mode (page 1)
97
+
98
+ > ⚠️ **Recommended:** Always pass `mode` explicitly in new code to make intent clear and avoid surprising behavior when query params change.
99
+
100
+ ### Performance Options
101
+
102
+ | Option | Type | Description |
103
+ | --------------- | ------------------------------ | ------------------------------------------------------------------- |
104
+ | `hint` | `string \| object` | Force a specific index — prevents collection scans on large tables |
105
+ | `maxTimeMS` | `number` | Kill query if it takes longer than N ms (prevent runaway queries) |
106
+ | `countStrategy` | `'exact'\|'estimated'\|'none'` | Control cost of total-count query — use `'estimated'` for 10M+ rows |
83
107
 
84
108
  ### Required Indexes
85
109
 
86
110
  ```javascript
87
- // For keyset pagination: sort field + _id
111
+ // For keyset pagination: sort field + _id (compound)
88
112
  PostSchema.index({ createdAt: -1, _id: -1 });
89
113
 
90
114
  // For multi-tenant: tenant + sort field + _id
@@ -95,18 +119,20 @@ UserSchema.index({ organizationId: 1, createdAt: -1, _id: -1 });
95
119
 
96
120
  ### CRUD Operations
97
121
 
98
- | Method | Description |
99
- |--------|-------------|
100
- | `create(data, opts)` | Create single document |
101
- | `createMany(data[], opts)` | Create multiple documents |
102
- | `getById(id, opts)` | Find by ID |
103
- | `getByQuery(query, opts)` | Find one by query |
104
- | `getAll(params, opts)` | Paginated list (auto-detects mode) |
105
- | `getOrCreate(query, data, opts)` | Find or create |
106
- | `update(id, data, opts)` | Update document |
107
- | `delete(id, opts)` | Delete document |
108
- | `count(query, opts)` | Count documents |
109
- | `exists(query, opts)` | Check existence |
122
+ | Method | Description |
123
+ | -------------------------------- | ---------------------------------- |
124
+ | `create(data, opts)` | Create single document |
125
+ | `createMany(data[], opts)` | Create multiple documents |
126
+ | `getById(id, opts)` | Find by ID |
127
+ | `getByQuery(query, opts)` | Find one by query |
128
+ | `getAll(params, opts)` | Paginated list (auto-detects mode) |
129
+ | `getOrCreate(query, data, opts)` | Find or create |
130
+ | `update(id, data, opts)` | Update document |
131
+ | `delete(id, opts)` | Delete document |
132
+ | `count(query, opts)` | Count documents |
133
+ | `exists(query, opts)` | Check existence |
134
+
135
+ > **Note:** All read operations (`getById`, `getByQuery`, `getAll`, `count`, `exists`, `aggregate`, etc.) accept a `readPreference` option in the `opts` parameter (e.g., `readPreference: 'secondaryPreferred'`) to support scaling reads across replica sets.
110
136
 
111
137
  ### Aggregation
112
138
 
@@ -132,8 +158,8 @@ const categories = await repo.distinct('category', { status: 'active' });
132
158
 
133
159
  ```javascript
134
160
  await repo.withTransaction(async (session) => {
135
- await repo.create({ name: 'User 1' }, { session });
136
- await repo.create({ name: 'User 2' }, { session });
161
+ await repo.create({ name: "User 1" }, { session });
162
+ await repo.create({ name: "User 2" }, { session });
137
163
  // Auto-commits on success, auto-rollbacks on error
138
164
  });
139
165
  ```
@@ -142,12 +168,12 @@ await repo.withTransaction(async (session) => {
142
168
 
143
169
  ```javascript
144
170
  const repo = new Repository(UserModel, plugins, {
145
- defaultLimit: 20, // Default docs per page
146
- maxLimit: 100, // Maximum allowed limit
147
- maxPage: 10000, // Maximum page number
148
- deepPageThreshold: 100, // Warn when page exceeds this
149
- useEstimatedCount: false, // Use fast estimated counts
150
- cursorVersion: 1 // Cursor format version
171
+ defaultLimit: 20, // Default docs per page
172
+ maxLimit: 100, // Maximum allowed limit
173
+ maxPage: 10000, // Maximum page number
174
+ deepPageThreshold: 100, // Warn when page exceeds this
175
+ useEstimatedCount: false, // Use fast estimated counts
176
+ cursorVersion: 1, // Cursor format version
151
177
  });
152
178
  ```
153
179
 
@@ -161,59 +187,61 @@ import {
161
187
  timestampPlugin,
162
188
  softDeletePlugin,
163
189
  cachePlugin,
164
- createMemoryCache
165
- } from '@classytic/mongokit';
190
+ createMemoryCache,
191
+ } from "@classytic/mongokit";
166
192
 
167
193
  const repo = new Repository(UserModel, [
168
194
  timestampPlugin(),
169
195
  softDeletePlugin(),
170
- cachePlugin({ adapter: createMemoryCache(), ttl: 60 })
196
+ cachePlugin({ adapter: createMemoryCache(), ttl: 60 }),
171
197
  ]);
172
198
  ```
173
199
 
174
200
  ### Available Plugins
175
201
 
176
- | Plugin | Description |
177
- |--------|-------------|
178
- | `timestampPlugin()` | Auto-manage `createdAt`/`updatedAt` |
179
- | `softDeletePlugin(opts)` | Mark as deleted instead of removing |
180
- | `auditLogPlugin(logger)` | Log all CUD operations |
181
- | `cachePlugin(opts)` | Redis/Memcached/memory caching with auto-invalidation |
182
- | `validationChainPlugin(validators)` | Custom validation rules |
183
- | `fieldFilterPlugin(preset)` | Role-based field visibility |
184
- | `cascadePlugin(opts)` | Auto-delete related documents |
185
- | `methodRegistryPlugin()` | Dynamic method registration (required by plugins below) |
186
- | `mongoOperationsPlugin()` | Adds `increment`, `pushToArray`, `upsert`, etc. |
187
- | `batchOperationsPlugin()` | Adds `updateMany`, `deleteMany` |
188
- | `aggregateHelpersPlugin()` | Adds `groupBy`, `sum`, `average`, etc. |
189
- | `subdocumentPlugin()` | Manage subdocument arrays |
190
- | `multiTenantPlugin(opts)` | Auto-inject tenant isolation on all operations |
191
- | `observabilityPlugin(opts)` | Operation timing, metrics, slow query detection |
202
+ | Plugin | Description |
203
+ | ----------------------------------- | --------------------------------------------------------- |
204
+ | `timestampPlugin()` | Auto-manage `createdAt`/`updatedAt` |
205
+ | `softDeletePlugin(opts)` | Mark as deleted instead of removing |
206
+ | `auditLogPlugin(logger)` | Log all CUD operations |
207
+ | `cachePlugin(opts)` | Redis/Memcached/memory caching with auto-invalidation |
208
+ | `validationChainPlugin(validators)` | Custom validation rules |
209
+ | `fieldFilterPlugin(preset)` | Role-based field visibility |
210
+ | `cascadePlugin(opts)` | Auto-delete related documents |
211
+ | `methodRegistryPlugin()` | Dynamic method registration (required by plugins below) |
212
+ | `mongoOperationsPlugin()` | Adds `increment`, `pushToArray`, `upsert`, etc. |
213
+ | `batchOperationsPlugin()` | Adds `updateMany`, `deleteMany` |
214
+ | `aggregateHelpersPlugin()` | Adds `groupBy`, `sum`, `average`, etc. |
215
+ | `subdocumentPlugin()` | Manage subdocument arrays |
216
+ | `multiTenantPlugin(opts)` | Auto-inject tenant isolation on all operations |
217
+ | `customIdPlugin(opts)` | Auto-generate sequential/random IDs with atomic counters |
218
+ | `elasticSearchPlugin(opts)` | Delegate text/semantic search to Elasticsearch/OpenSearch |
219
+ | `observabilityPlugin(opts)` | Operation timing, metrics, slow query detection |
192
220
 
193
221
  ### Soft Delete
194
222
 
195
223
  ```javascript
196
224
  const repo = new Repository(UserModel, [
197
- softDeletePlugin({ deletedField: 'deletedAt' })
225
+ softDeletePlugin({ deletedField: "deletedAt" }),
198
226
  ]);
199
227
 
200
- await repo.delete(id); // Marks as deleted
201
- await repo.getAll(); // Excludes deleted
202
- await repo.getAll({ includeDeleted: true }); // Includes deleted
228
+ await repo.delete(id); // Marks as deleted
229
+ await repo.getAll(); // Excludes deleted
230
+ await repo.getAll({ includeDeleted: true }); // Includes deleted
203
231
  ```
204
232
 
205
233
  ### Caching
206
234
 
207
235
  ```javascript
208
- import { cachePlugin, createMemoryCache } from '@classytic/mongokit';
236
+ import { cachePlugin, createMemoryCache } from "@classytic/mongokit";
209
237
 
210
238
  const repo = new Repository(UserModel, [
211
239
  cachePlugin({
212
- adapter: createMemoryCache(), // or Redis adapter
213
- ttl: 60, // Default TTL (seconds)
214
- byIdTtl: 300, // TTL for getById
215
- queryTtl: 30, // TTL for lists
216
- })
240
+ adapter: createMemoryCache(), // or Redis adapter
241
+ ttl: 60, // Default TTL (seconds)
242
+ byIdTtl: 300, // TTL for getById
243
+ queryTtl: 30, // TTL for lists
244
+ }),
217
245
  ]);
218
246
 
219
247
  // Reads are cached automatically
@@ -223,7 +251,7 @@ const user = await repo.getById(id);
223
251
  const fresh = await repo.getById(id, { skipCache: true });
224
252
 
225
253
  // Mutations auto-invalidate cache
226
- await repo.update(id, { name: 'New' });
254
+ await repo.update(id, { name: "New" });
227
255
 
228
256
  // Manual invalidation
229
257
  await repo.invalidateCache(id);
@@ -231,12 +259,21 @@ await repo.invalidateAllCache();
231
259
  ```
232
260
 
233
261
  **Redis adapter example:**
262
+
234
263
  ```javascript
235
264
  const redisAdapter = {
236
- async get(key) { return JSON.parse(await redis.get(key) || 'null'); },
237
- async set(key, value, ttl) { await redis.setex(key, ttl, JSON.stringify(value)); },
238
- async del(key) { await redis.del(key); },
239
- async clear(pattern) { /* optional bulk delete */ }
265
+ async get(key) {
266
+ return JSON.parse((await redis.get(key)) || "null");
267
+ },
268
+ async set(key, value, ttl) {
269
+ await redis.setex(key, ttl, JSON.stringify(value));
270
+ },
271
+ async del(key) {
272
+ await redis.del(key);
273
+ },
274
+ async clear(pattern) {
275
+ /* optional bulk delete */
276
+ },
240
277
  };
241
278
  ```
242
279
 
@@ -249,37 +286,40 @@ import {
249
286
  uniqueField,
250
287
  immutableField,
251
288
  blockIf,
252
- autoInject
253
- } from '@classytic/mongokit';
289
+ autoInject,
290
+ } from "@classytic/mongokit";
254
291
 
255
292
  const repo = new Repository(UserModel, [
256
293
  validationChainPlugin([
257
- requireField('email', ['create']),
258
- uniqueField('email', 'Email already exists'),
259
- immutableField('userId'),
260
- blockIf('noAdminDelete', ['delete'],
261
- (ctx) => ctx.data?.role === 'admin',
262
- 'Cannot delete admin users'),
263
- autoInject('slug', (ctx) => slugify(ctx.data?.name), ['create'])
264
- ])
294
+ requireField("email", ["create"]),
295
+ uniqueField("email", "Email already exists"),
296
+ immutableField("userId"),
297
+ blockIf(
298
+ "noAdminDelete",
299
+ ["delete"],
300
+ (ctx) => ctx.data?.role === "admin",
301
+ "Cannot delete admin users",
302
+ ),
303
+ autoInject("slug", (ctx) => slugify(ctx.data?.name), ["create"]),
304
+ ]),
265
305
  ]);
266
306
  ```
267
307
 
268
308
  ### Cascade Delete
269
309
 
270
310
  ```javascript
271
- import { cascadePlugin, softDeletePlugin } from '@classytic/mongokit';
311
+ import { cascadePlugin, softDeletePlugin } from "@classytic/mongokit";
272
312
 
273
313
  const repo = new Repository(ProductModel, [
274
314
  softDeletePlugin(),
275
315
  cascadePlugin({
276
316
  relations: [
277
- { model: 'StockEntry', foreignKey: 'product' },
278
- { model: 'Review', foreignKey: 'product', softDelete: false }
317
+ { model: "StockEntry", foreignKey: "product" },
318
+ { model: "Review", foreignKey: "product", softDelete: false },
279
319
  ],
280
320
  parallel: true,
281
- logger: console
282
- })
321
+ logger: console,
322
+ }),
283
323
  ]);
284
324
 
285
325
  // Deleting product also deletes related StockEntry and Review docs
@@ -289,40 +329,40 @@ await repo.delete(productId);
289
329
  ### Field Filtering (RBAC)
290
330
 
291
331
  ```javascript
292
- import { fieldFilterPlugin } from '@classytic/mongokit';
332
+ import { fieldFilterPlugin } from "@classytic/mongokit";
293
333
 
294
334
  const repo = new Repository(UserModel, [
295
335
  fieldFilterPlugin({
296
- public: ['id', 'name', 'avatar'],
297
- authenticated: ['email', 'phone'],
298
- admin: ['createdAt', 'internalNotes']
299
- })
336
+ public: ["id", "name", "avatar"],
337
+ authenticated: ["email", "phone"],
338
+ admin: ["createdAt", "internalNotes"],
339
+ }),
300
340
  ]);
301
341
  ```
302
342
 
303
343
  ### Multi-Tenant
304
344
 
305
345
  ```javascript
306
- import { multiTenantPlugin } from '@classytic/mongokit';
346
+ import { multiTenantPlugin } from "@classytic/mongokit";
307
347
 
308
348
  const repo = new Repository(UserModel, [
309
349
  multiTenantPlugin({
310
- tenantField: 'organizationId',
311
- contextKey: 'organizationId', // reads from context
350
+ tenantField: "organizationId",
351
+ contextKey: "organizationId", // reads from context
312
352
  required: true,
313
- })
353
+ }),
314
354
  ]);
315
355
 
316
356
  // All operations are automatically scoped to the tenant
317
- const users = await repo.getAll({ organizationId: 'org_123' });
318
- await repo.update(userId, { name: 'New' }, { organizationId: 'org_123' });
357
+ const users = await repo.getAll({ organizationId: "org_123" });
358
+ await repo.update(userId, { name: "New" }, { organizationId: "org_123" });
319
359
  // Cross-tenant update/delete is blocked — returns "not found"
320
360
  ```
321
361
 
322
362
  ### Observability
323
363
 
324
364
  ```javascript
325
- import { observabilityPlugin } from '@classytic/mongokit';
365
+ import { observabilityPlugin } from "@classytic/mongokit";
326
366
 
327
367
  const repo = new Repository(UserModel, [
328
368
  observabilityPlugin({
@@ -330,11 +370,154 @@ const repo = new Repository(UserModel, [
330
370
  // Send to DataDog, New Relic, OpenTelemetry, etc.
331
371
  statsd.histogram(`mongokit.${metric.operation}`, metric.duration);
332
372
  },
333
- slowThresholdMs: 200, // log operations slower than 200ms
334
- })
373
+ slowThresholdMs: 200, // log operations slower than 200ms
374
+ }),
375
+ ]);
376
+ ```
377
+
378
+ ### Custom ID Generation
379
+
380
+ Generate human-readable sequential IDs (e.g., `INV-0001`, `BILL-2026-02-0001`) using atomic MongoDB counters — safe under concurrency with zero duplicates.
381
+
382
+ ```typescript
383
+ import {
384
+ Repository,
385
+ customIdPlugin,
386
+ sequentialId,
387
+ dateSequentialId,
388
+ prefixedId,
389
+ } from "@classytic/mongokit";
390
+ ```
391
+
392
+ #### Sequential Counter
393
+
394
+ ```typescript
395
+ const invoiceRepo = new Repository(InvoiceModel, [
396
+ customIdPlugin({
397
+ field: "invoiceNumber",
398
+ generator: sequentialId({
399
+ prefix: "INV",
400
+ model: InvoiceModel,
401
+ }),
402
+ }),
403
+ ]);
404
+
405
+ const inv1 = await invoiceRepo.create({ amount: 100 });
406
+ // inv1.invoiceNumber → "INV-0001"
407
+
408
+ const inv2 = await invoiceRepo.create({ amount: 200 });
409
+ // inv2.invoiceNumber → "INV-0002"
410
+ ```
411
+
412
+ **Options:**
413
+
414
+ | Option | Default | Description |
415
+ | ------------ | ------------ | ---------------------------------------------------- |
416
+ | `prefix` | _(required)_ | Prefix string (e.g., `'INV'`, `'ORD'`) |
417
+ | `model` | _(required)_ | Mongoose model (counter key derived from model name) |
418
+ | `padding` | `4` | Number of digits (`4` → `0001`) |
419
+ | `separator` | `'-'` | Separator between prefix and number |
420
+ | `counterKey` | model name | Custom counter key to avoid collisions |
421
+
422
+ #### Date-Partitioned Counter
423
+
424
+ Counter resets per period — ideal for invoice/bill numbering:
425
+
426
+ ```typescript
427
+ const billRepo = new Repository(BillModel, [
428
+ customIdPlugin({
429
+ field: "billNumber",
430
+ generator: dateSequentialId({
431
+ prefix: "BILL",
432
+ model: BillModel,
433
+ partition: "monthly", // resets each month
434
+ }),
435
+ }),
436
+ ]);
437
+
438
+ const bill = await billRepo.create({ total: 250 });
439
+ // bill.billNumber → "BILL-2026-02-0001"
440
+ ```
441
+
442
+ **Partition modes:**
443
+
444
+ - `'yearly'` → `BILL-2026-0001` (resets every January)
445
+ - `'monthly'` → `BILL-2026-02-0001` (resets every month)
446
+ - `'daily'` → `BILL-2026-02-20-0001` (resets every day)
447
+
448
+ #### Prefixed Random ID
449
+
450
+ No database round-trip — purely in-memory random suffix:
451
+
452
+ ```typescript
453
+ const orderRepo = new Repository(OrderModel, [
454
+ customIdPlugin({
455
+ field: "orderRef",
456
+ generator: prefixedId({ prefix: "ORD", length: 10 }),
457
+ }),
458
+ ]);
459
+
460
+ const order = await orderRepo.create({ total: 99 });
461
+ // order.orderRef → "ORD_a7b3xk9m2p"
462
+ ```
463
+
464
+ #### Custom Generator
465
+
466
+ Write your own generator function for full control:
467
+
468
+ ```typescript
469
+ const repo = new Repository(OrderModel, [
470
+ customIdPlugin({
471
+ field: "orderRef",
472
+ generator: async (context) => {
473
+ const region = context.data?.region || "US";
474
+ const seq = await getNextSequence("orders");
475
+ return `ORD-${region}-${String(seq).padStart(4, "0")}`;
476
+ },
477
+ }),
335
478
  ]);
479
+ // → "ORD-US-0001", "ORD-EU-0002", ...
336
480
  ```
337
481
 
482
+ #### Plugin Options
483
+
484
+ | Option | Default | Description |
485
+ | --------------------- | ------------ | -------------------------------------------- |
486
+ | `field` | `'customId'` | Document field to store the generated ID |
487
+ | `generator` | _(required)_ | Function returning the ID (sync or async) |
488
+ | `generateOnlyIfEmpty` | `true` | Skip generation if field already has a value |
489
+
490
+ #### Batch Creation
491
+
492
+ Works with `createMany` — each document gets its own sequential ID:
493
+
494
+ ```typescript
495
+ const docs = await invoiceRepo.createMany([
496
+ { amount: 10 },
497
+ { amount: 20, invoiceNumber: "MANUAL-001" }, // skipped (already has ID)
498
+ { amount: 30 },
499
+ ]);
500
+ // docs[0].invoiceNumber → "INV-0001"
501
+ // docs[1].invoiceNumber → "MANUAL-001" (preserved)
502
+ // docs[2].invoiceNumber → "INV-0002"
503
+ ```
504
+
505
+ #### Atomic Counter API
506
+
507
+ The `getNextSequence` helper is exported for use in custom generators:
508
+
509
+ ```typescript
510
+ import { getNextSequence } from "@classytic/mongokit";
511
+
512
+ const seq = await getNextSequence("my-counter"); // → 1, 2, 3, ...
513
+ const batch = await getNextSequence("my-counter", 5); // → jumps by 5
514
+ ```
515
+
516
+ Counters are stored in the `_mongokit_counters` collection using MongoDB's atomic `findOneAndUpdate` + `$inc` — guaranteed unique under any level of concurrency.
517
+
518
+ > **Note:** Counters are monotonically increasing and never decrement on document deletion.
519
+ > This is standard behavior for business documents (invoices, bills, receipts) — you never reuse a number.
520
+
338
521
  ### Vector Search (Atlas)
339
522
 
340
523
  ```javascript
@@ -367,10 +550,65 @@ const results = await repo.searchSimilar({ query: [0.1, 0.2, ...], limit: 5 });
367
550
  const vector = await repo.embed('some text');
368
551
  ```
369
552
 
553
+ ### Elasticsearch / OpenSearch Plugin
554
+
555
+ Delegates heavy text and semantic search to an external search engine while fetching full documents from MongoDB. Keeps your OLTP (transactional) MongoDB operations fast by separating search I/O.
556
+
557
+ **Architecture:** Query ES/OpenSearch → get IDs + relevance scores → fetch full docs from MongoDB → return in ES ranking order.
558
+
559
+ ```typescript
560
+ import {
561
+ Repository,
562
+ methodRegistryPlugin,
563
+ elasticSearchPlugin,
564
+ } from "@classytic/mongokit";
565
+ import { Client } from "@elastic/elasticsearch"; // or '@opensearch-project/opensearch'
566
+
567
+ const esClient = new Client({ node: "http://localhost:9200" });
568
+
569
+ const productRepo = new Repository(ProductModel, [
570
+ methodRegistryPlugin(), // Required first
571
+ elasticSearchPlugin({
572
+ client: esClient,
573
+ index: "products",
574
+ idField: "_id", // field in ES doc that maps to MongoDB _id
575
+ }),
576
+ ]);
577
+
578
+ // Perform semantic/full-text search
579
+ const results = await productRepo.search(
580
+ { match: { description: "wireless headphones" } },
581
+ {
582
+ limit: 20, // capped to 1000 max (safety bound)
583
+ from: 0,
584
+ mongoOptions: {
585
+ select: "name price description",
586
+ lean: true,
587
+ },
588
+ },
589
+ );
590
+
591
+ // results.docs - MongoDB documents in ES ranking order
592
+ // results.docs[*]._score - ES relevance score (preserved, including 0)
593
+ // results.total - total hits count from ES
594
+ ```
595
+
596
+ **Why this exists:**
597
+
598
+ - `$text` in MongoDB requires a text index and is not scalable for fuzzy/semantic search
599
+ - ES/OpenSearch provides BM25, vector search, semantic search, analyzers, facets
600
+ - This plugin bridges both: ES rank + MongoDB's transactional documents
601
+
602
+ **Bounds enforcement:**
603
+
604
+ - `limit` is clamped to `[1, 1000]` — prevents runaway ES queries
605
+ - `from` is clamped to `>= 0` — prevents negative offsets
606
+ - Returns `{ docs: [], total: 0 }` immediately if ES returns no hits
607
+
370
608
  ### Logging
371
609
 
372
610
  ```javascript
373
- import { configureLogger } from '@classytic/mongokit';
611
+ import { configureLogger } from "@classytic/mongokit";
374
612
 
375
613
  // Silence all internal warnings
376
614
  configureLogger(false);
@@ -389,16 +627,20 @@ The `mongoOperationsPlugin` adds MongoDB-specific atomic operations like `increm
389
627
  #### Basic Usage (No TypeScript Autocomplete)
390
628
 
391
629
  ```javascript
392
- import { Repository, methodRegistryPlugin, mongoOperationsPlugin } from '@classytic/mongokit';
630
+ import {
631
+ Repository,
632
+ methodRegistryPlugin,
633
+ mongoOperationsPlugin,
634
+ } from "@classytic/mongokit";
393
635
 
394
636
  const repo = new Repository(ProductModel, [
395
- methodRegistryPlugin(), // Required first
396
- mongoOperationsPlugin()
637
+ methodRegistryPlugin(), // Required first
638
+ mongoOperationsPlugin(),
397
639
  ]);
398
640
 
399
641
  // Works at runtime but TypeScript doesn't provide autocomplete
400
- await repo.increment(productId, 'views', 1);
401
- await repo.upsert({ sku: 'ABC' }, { name: 'Product', price: 99 });
642
+ await repo.increment(productId, "views", 1);
643
+ await repo.upsert({ sku: "ABC" }, { name: "Product", price: 99 });
402
644
  ```
403
645
 
404
646
  #### With TypeScript Type Safety (Recommended)
@@ -406,8 +648,12 @@ await repo.upsert({ sku: 'ABC' }, { name: 'Product', price: 99 });
406
648
  For full TypeScript autocomplete and type checking, use the `MongoOperationsMethods` type:
407
649
 
408
650
  ```typescript
409
- import { Repository, methodRegistryPlugin, mongoOperationsPlugin } from '@classytic/mongokit';
410
- import type { MongoOperationsMethods } from '@classytic/mongokit';
651
+ import {
652
+ Repository,
653
+ methodRegistryPlugin,
654
+ mongoOperationsPlugin,
655
+ } from "@classytic/mongokit";
656
+ import type { MongoOperationsMethods } from "@classytic/mongokit";
411
657
 
412
658
  // 1. Create your repository class
413
659
  class ProductRepo extends Repository<IProduct> {
@@ -423,17 +669,18 @@ type ProductRepoWithPlugins = ProductRepo & MongoOperationsMethods<IProduct>;
423
669
  // 3. Instantiate with type assertion
424
670
  const repo = new ProductRepo(ProductModel, [
425
671
  methodRegistryPlugin(),
426
- mongoOperationsPlugin()
672
+ mongoOperationsPlugin(),
427
673
  ]) as ProductRepoWithPlugins;
428
674
 
429
675
  // 4. Now TypeScript provides full autocomplete and type checking!
430
- await repo.increment(productId, 'views', 1); // ✅ Autocomplete works
431
- await repo.upsert({ sku: 'ABC' }, { name: 'Product' }); // ✅ Type-safe
432
- await repo.pushToArray(productId, 'tags', 'featured'); // ✅ Validated
433
- await repo.findBySku('ABC'); // ✅ Custom methods too
676
+ await repo.increment(productId, "views", 1); // ✅ Autocomplete works
677
+ await repo.upsert({ sku: "ABC" }, { name: "Product" }); // ✅ Type-safe
678
+ await repo.pushToArray(productId, "tags", "featured"); // ✅ Validated
679
+ await repo.findBySku("ABC"); // ✅ Custom methods too
434
680
  ```
435
681
 
436
682
  **Available operations:**
683
+
437
684
  - `upsert(query, data, opts)` - Create or find document
438
685
  - `increment(id, field, value, opts)` - Atomically increment field
439
686
  - `decrement(id, field, value, opts)` - Atomically decrement field
@@ -452,7 +699,7 @@ await repo.findBySku('ABC'); // ✅ Custom methods too
452
699
  Plugin methods are added at runtime. Use `WithPlugins<TDoc, TRepo>` for TypeScript autocomplete:
453
700
 
454
701
  ```typescript
455
- import type { WithPlugins } from '@classytic/mongokit';
702
+ import type { WithPlugins } from "@classytic/mongokit";
456
703
 
457
704
  class UserRepo extends Repository<IUser> {}
458
705
 
@@ -463,7 +710,7 @@ const repo = new UserRepo(Model, [
463
710
  ]) as WithPlugins<IUser, UserRepo>;
464
711
 
465
712
  // Full TypeScript autocomplete!
466
- await repo.increment(id, 'views', 1);
713
+ await repo.increment(id, "views", 1);
467
714
  await repo.restore(id);
468
715
  await repo.invalidateCache(id);
469
716
  ```
@@ -473,16 +720,16 @@ await repo.invalidateCache(id);
473
720
  ## Event System
474
721
 
475
722
  ```javascript
476
- repo.on('before:create', async (context) => {
723
+ repo.on("before:create", async (context) => {
477
724
  context.data.processedAt = new Date();
478
725
  });
479
726
 
480
- repo.on('after:create', ({ context, result }) => {
481
- console.log('Created:', result);
727
+ repo.on("after:create", ({ context, result }) => {
728
+ console.log("Created:", result);
482
729
  });
483
730
 
484
- repo.on('error:create', ({ context, error }) => {
485
- console.error('Failed:', error);
731
+ repo.on("error:create", ({ context, error }) => {
732
+ console.error("Failed:", error);
486
733
  });
487
734
  ```
488
735
 
@@ -497,15 +744,19 @@ MongoKit provides a complete toolkit for building REST APIs: QueryParser for req
497
744
  Framework-agnostic controller contract that works with Express, Fastify, Next.js, etc:
498
745
 
499
746
  ```typescript
500
- import type { IController, IRequestContext, IControllerResponse } from '@classytic/mongokit';
747
+ import type {
748
+ IController,
749
+ IRequestContext,
750
+ IControllerResponse,
751
+ } from "@classytic/mongokit";
501
752
 
502
753
  // IRequestContext - what your controller receives
503
754
  interface IRequestContext {
504
- query: Record<string, unknown>; // URL query params
505
- body: Record<string, unknown>; // Request body
506
- params: Record<string, string>; // Route params (:id)
507
- user?: { id: string; role?: string }; // Auth user
508
- context?: Record<string, unknown>; // Tenant ID, etc.
755
+ query: Record<string, unknown>; // URL query params
756
+ body: Record<string, unknown>; // Request body
757
+ params: Record<string, string>; // Route params (:id)
758
+ user?: { id: string; role?: string }; // Auth user
759
+ context?: Record<string, unknown>; // Tenant ID, etc.
509
760
  }
510
761
 
511
762
  // IControllerResponse - what your controller returns
@@ -518,11 +769,15 @@ interface IControllerResponse<T> {
518
769
 
519
770
  // IController - implement this interface
520
771
  interface IController<TDoc> {
521
- list(ctx: IRequestContext): Promise<IControllerResponse<PaginationResult<TDoc>>>;
772
+ list(
773
+ ctx: IRequestContext,
774
+ ): Promise<IControllerResponse<PaginationResult<TDoc>>>;
522
775
  get(ctx: IRequestContext): Promise<IControllerResponse<TDoc>>;
523
776
  create(ctx: IRequestContext): Promise<IControllerResponse<TDoc>>;
524
777
  update(ctx: IRequestContext): Promise<IControllerResponse<TDoc>>;
525
- delete(ctx: IRequestContext): Promise<IControllerResponse<{ message: string }>>;
778
+ delete(
779
+ ctx: IRequestContext,
780
+ ): Promise<IControllerResponse<{ message: string }>>;
526
781
  }
527
782
  ```
528
783
 
@@ -531,12 +786,12 @@ interface IController<TDoc> {
531
786
  Converts HTTP query strings to MongoDB queries with built-in security:
532
787
 
533
788
  ```typescript
534
- import { QueryParser } from '@classytic/mongokit';
789
+ import { QueryParser } from "@classytic/mongokit";
535
790
 
536
791
  const parser = new QueryParser({
537
- maxLimit: 100, // Prevent excessive queries
538
- maxFilterDepth: 5, // Prevent nested injection
539
- maxRegexLength: 100, // ReDoS protection
792
+ maxLimit: 100, // Prevent excessive queries
793
+ maxFilterDepth: 5, // Prevent nested injection
794
+ maxRegexLength: 100, // ReDoS protection
540
795
  });
541
796
 
542
797
  // Parse request query
@@ -544,6 +799,7 @@ const { filters, limit, page, sort, search } = parser.parse(req.query);
544
799
  ```
545
800
 
546
801
  **Supported query patterns:**
802
+
547
803
  ```bash
548
804
  # Filtering
549
805
  GET /users?status=active&role=admin
@@ -574,6 +830,7 @@ GET /posts?populate[author][populate][department][select]=name # Nested
574
830
  ```
575
831
 
576
832
  **Security features:**
833
+
577
834
  - Blocks `$where`, `$function`, `$accumulator`, `$expr` operators
578
835
  - ReDoS protection for regex patterns
579
836
  - Max filter depth enforcement
@@ -586,7 +843,7 @@ GET /posts?populate[author][populate][department][select]=name # Nested
586
843
  QueryParser supports Mongoose populate options via URL query parameters:
587
844
 
588
845
  ```typescript
589
- import { QueryParser } from '@classytic/mongokit';
846
+ import { QueryParser } from "@classytic/mongokit";
590
847
 
591
848
  const parser = new QueryParser();
592
849
 
@@ -596,19 +853,19 @@ const parsed = parser.parse(req.query);
596
853
  // Use with Repository
597
854
  const posts = await postRepo.getAll(
598
855
  { filters: parsed.filters, page: parsed.page, limit: parsed.limit },
599
- { populateOptions: parsed.populateOptions }
856
+ { populateOptions: parsed.populateOptions },
600
857
  );
601
858
  ```
602
859
 
603
860
  **Supported populate options:**
604
861
 
605
- | Option | URL Syntax | Description |
606
- |--------|------------|-------------|
607
- | `select` | `populate[path][select]=field1,field2` | Fields to include (space-separated in Mongoose) |
608
- | `match` | `populate[path][match][field]=value` | Filter populated documents |
609
- | `limit` | `populate[path][limit]=10` | Limit number of populated docs |
610
- | `sort` | `populate[path][sort]=-createdAt` | Sort populated documents |
611
- | `populate` | `populate[path][populate][nested][select]=field` | Nested populate (max depth: 5) |
862
+ | Option | URL Syntax | Description |
863
+ | ---------- | ------------------------------------------------ | ----------------------------------------------- |
864
+ | `select` | `populate[path][select]=field1,field2` | Fields to include (space-separated in Mongoose) |
865
+ | `match` | `populate[path][match][field]=value` | Filter populated documents |
866
+ | `limit` | `populate[path][limit]=10` | Limit number of populated docs |
867
+ | `sort` | `populate[path][sort]=-createdAt` | Sort populated documents |
868
+ | `populate` | `populate[path][populate][nested][select]=field` | Nested populate (max depth: 5) |
612
869
 
613
870
  **Example - Complex populate:**
614
871
 
@@ -632,15 +889,15 @@ const parsed = parser.parse(req.query);
632
889
  Auto-generate JSON schemas from Mongoose models for validation and OpenAPI docs:
633
890
 
634
891
  ```typescript
635
- import { buildCrudSchemasFromModel } from '@classytic/mongokit';
892
+ import { buildCrudSchemasFromModel } from "@classytic/mongokit";
636
893
 
637
894
  const { crudSchemas } = buildCrudSchemasFromModel(UserModel, {
638
895
  fieldRules: {
639
- organizationId: { immutable: true }, // Can't update after create
640
- role: { systemManaged: true }, // Users can't set this
896
+ organizationId: { immutable: true }, // Can't update after create
897
+ role: { systemManaged: true }, // Users can't set this
641
898
  createdAt: { systemManaged: true },
642
899
  },
643
- strictAdditionalProperties: true, // Reject unknown fields
900
+ strictAdditionalProperties: true, // Reject unknown fields
644
901
  });
645
902
 
646
903
  // Generated schemas:
@@ -660,7 +917,7 @@ import {
660
917
  type IController,
661
918
  type IRequestContext,
662
919
  type IControllerResponse,
663
- } from '@classytic/mongokit';
920
+ } from "@classytic/mongokit";
664
921
 
665
922
  class UserController implements IController<IUser> {
666
923
  private repo = new Repository(UserModel);
@@ -695,7 +952,7 @@ class UserController implements IController<IUser> {
695
952
 
696
953
  async delete(ctx: IRequestContext): Promise<IControllerResponse> {
697
954
  await this.repo.delete(ctx.params.id);
698
- return { success: true, data: { message: 'Deleted' }, status: 200 };
955
+ return { success: true, data: { message: "Deleted" }, status: 200 };
699
956
  }
700
957
  }
701
958
  ```
@@ -703,29 +960,41 @@ class UserController implements IController<IUser> {
703
960
  ### Fastify Integration
704
961
 
705
962
  ```typescript
706
- import { buildCrudSchemasFromModel } from '@classytic/mongokit';
963
+ import { buildCrudSchemasFromModel } from "@classytic/mongokit";
707
964
 
708
965
  const controller = new UserController();
709
966
  const { crudSchemas } = buildCrudSchemasFromModel(UserModel);
710
967
 
711
968
  // Routes with auto-validation and OpenAPI docs
712
- fastify.get('/users', { schema: { querystring: crudSchemas.listQuery } }, async (req, reply) => {
713
- const ctx = { query: req.query, body: {}, params: {}, user: req.user };
714
- const response = await controller.list(ctx);
715
- return reply.status(response.status).send(response);
716
- });
969
+ fastify.get(
970
+ "/users",
971
+ { schema: { querystring: crudSchemas.listQuery } },
972
+ async (req, reply) => {
973
+ const ctx = { query: req.query, body: {}, params: {}, user: req.user };
974
+ const response = await controller.list(ctx);
975
+ return reply.status(response.status).send(response);
976
+ },
977
+ );
717
978
 
718
- fastify.post('/users', { schema: { body: crudSchemas.createBody } }, async (req, reply) => {
719
- const ctx = { query: {}, body: req.body, params: {}, user: req.user };
720
- const response = await controller.create(ctx);
721
- return reply.status(response.status).send(response);
722
- });
979
+ fastify.post(
980
+ "/users",
981
+ { schema: { body: crudSchemas.createBody } },
982
+ async (req, reply) => {
983
+ const ctx = { query: {}, body: req.body, params: {}, user: req.user };
984
+ const response = await controller.create(ctx);
985
+ return reply.status(response.status).send(response);
986
+ },
987
+ );
723
988
 
724
- fastify.get('/users/:id', { schema: { params: crudSchemas.params } }, async (req, reply) => {
725
- const ctx = { query: {}, body: {}, params: req.params, user: req.user };
726
- const response = await controller.get(ctx);
727
- return reply.status(response.status).send(response);
728
- });
989
+ fastify.get(
990
+ "/users/:id",
991
+ { schema: { params: crudSchemas.params } },
992
+ async (req, reply) => {
993
+ const ctx = { query: {}, body: {}, params: req.params, user: req.user };
994
+ const response = await controller.get(ctx);
995
+ return reply.status(response.status).send(response);
996
+ },
997
+ );
729
998
  ```
730
999
 
731
1000
  ### Express Integration
@@ -733,13 +1002,13 @@ fastify.get('/users/:id', { schema: { params: crudSchemas.params } }, async (req
733
1002
  ```typescript
734
1003
  const controller = new UserController();
735
1004
 
736
- app.get('/users', async (req, res) => {
1005
+ app.get("/users", async (req, res) => {
737
1006
  const ctx = { query: req.query, body: {}, params: {}, user: req.user };
738
1007
  const response = await controller.list(ctx);
739
1008
  res.status(response.status).json(response);
740
1009
  });
741
1010
 
742
- app.post('/users', async (req, res) => {
1011
+ app.post("/users", async (req, res) => {
743
1012
  const ctx = { query: {}, body: req.body, params: {}, user: req.user };
744
1013
  const response = await controller.create(ctx);
745
1014
  res.status(response.status).json(response);
@@ -749,7 +1018,11 @@ app.post('/users', async (req, res) => {
749
1018
  ## TypeScript
750
1019
 
751
1020
  ```typescript
752
- import { Repository, OffsetPaginationResult, KeysetPaginationResult } from '@classytic/mongokit';
1021
+ import {
1022
+ Repository,
1023
+ OffsetPaginationResult,
1024
+ KeysetPaginationResult,
1025
+ } from "@classytic/mongokit";
753
1026
 
754
1027
  interface IUser extends Document {
755
1028
  name: string;
@@ -761,10 +1034,10 @@ const repo = new Repository<IUser>(UserModel);
761
1034
  const result = await repo.getAll({ page: 1, limit: 20 });
762
1035
 
763
1036
  // Discriminated union - TypeScript knows the type
764
- if (result.method === 'offset') {
765
- console.log(result.total, result.pages); // Available
1037
+ if (result.method === "offset") {
1038
+ console.log(result.total, result.pages); // Available
766
1039
  }
767
- if (result.method === 'keyset') {
1040
+ if (result.method === "keyset") {
768
1041
  console.log(result.next, result.hasMore); // Available
769
1042
  }
770
1043
  ```
@@ -774,16 +1047,17 @@ if (result.method === 'keyset') {
774
1047
  Create custom repository classes with domain-specific methods:
775
1048
 
776
1049
  ```typescript
777
- import { Repository, softDeletePlugin, timestampPlugin } from '@classytic/mongokit';
778
- import UserModel, { IUser } from './models/User.js';
1050
+ import {
1051
+ Repository,
1052
+ softDeletePlugin,
1053
+ timestampPlugin,
1054
+ } from "@classytic/mongokit";
1055
+ import UserModel, { IUser } from "./models/User.js";
779
1056
 
780
1057
  class UserRepository extends Repository<IUser> {
781
1058
  constructor() {
782
- super(UserModel, [
783
- timestampPlugin(),
784
- softDeletePlugin()
785
- ], {
786
- defaultLimit: 20
1059
+ super(UserModel, [timestampPlugin(), softDeletePlugin()], {
1060
+ defaultLimit: 20,
787
1061
  });
788
1062
  }
789
1063
 
@@ -794,19 +1068,19 @@ class UserRepository extends Repository<IUser> {
794
1068
 
795
1069
  async findActiveUsers() {
796
1070
  return this.getAll({
797
- filters: { status: 'active' },
798
- sort: { createdAt: -1 }
1071
+ filters: { status: "active" },
1072
+ sort: { createdAt: -1 },
799
1073
  });
800
1074
  }
801
1075
 
802
1076
  async deactivate(id: string) {
803
- return this.update(id, { status: 'inactive', deactivatedAt: new Date() });
1077
+ return this.update(id, { status: "inactive", deactivatedAt: new Date() });
804
1078
  }
805
1079
  }
806
1080
 
807
1081
  // Usage
808
1082
  const userRepo = new UserRepository();
809
- const user = await userRepo.findByEmail('john@example.com');
1083
+ const user = await userRepo.findByEmail("john@example.com");
810
1084
  ```
811
1085
 
812
1086
  ### Overriding Methods
@@ -819,12 +1093,15 @@ class AuditedUserRepository extends Repository<IUser> {
819
1093
 
820
1094
  // Override create to add audit trail
821
1095
  async create(data: Partial<IUser>, options = {}) {
822
- const result = await super.create({
823
- ...data,
824
- createdBy: getCurrentUserId()
825
- }, options);
826
-
827
- await auditLog('user.created', result._id);
1096
+ const result = await super.create(
1097
+ {
1098
+ ...data,
1099
+ createdBy: getCurrentUserId(),
1100
+ },
1101
+ options,
1102
+ );
1103
+
1104
+ await auditLog("user.created", result._id);
828
1105
  return result;
829
1106
  }
830
1107
  }
@@ -835,10 +1112,10 @@ class AuditedUserRepository extends Repository<IUser> {
835
1112
  For simple cases without custom methods:
836
1113
 
837
1114
  ```javascript
838
- import { createRepository, timestampPlugin } from '@classytic/mongokit';
1115
+ import { createRepository, timestampPlugin } from "@classytic/mongokit";
839
1116
 
840
1117
  const userRepo = createRepository(UserModel, [timestampPlugin()], {
841
- defaultLimit: 20
1118
+ defaultLimit: 20,
842
1119
  });
843
1120
  ```
844
1121
 
@@ -849,7 +1126,7 @@ Extending Repository works exactly the same with Mongoose 8 and 9. The package:
849
1126
  - Uses its own event system (not Mongoose middleware)
850
1127
  - Defines its own `FilterQuery` type (unaffected by Mongoose 9 rename)
851
1128
  - Properly gates update pipelines (safe for Mongoose 9's stricter defaults)
852
- - All 547 tests pass on Mongoose 9
1129
+ - All 604 tests pass on Mongoose 9
853
1130
 
854
1131
  ## License
855
1132