@classytic/mongokit 1.0.2 → 2.1.0

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 (64) hide show
  1. package/README.md +772 -151
  2. package/dist/actions/index.cjs +479 -0
  3. package/dist/actions/index.cjs.map +1 -0
  4. package/dist/actions/index.d.cts +3 -0
  5. package/dist/actions/index.d.ts +3 -0
  6. package/dist/actions/index.js +473 -0
  7. package/dist/actions/index.js.map +1 -0
  8. package/dist/index-BfVJZF-3.d.cts +337 -0
  9. package/dist/index-CgOJ2pqz.d.ts +337 -0
  10. package/dist/index.cjs +2142 -0
  11. package/dist/index.cjs.map +1 -0
  12. package/dist/index.d.cts +239 -0
  13. package/dist/index.d.ts +239 -0
  14. package/dist/index.js +2108 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/memory-cache-DG2oSSbx.d.ts +142 -0
  17. package/dist/memory-cache-DqfFfKes.d.cts +142 -0
  18. package/dist/pagination/PaginationEngine.cjs +375 -0
  19. package/dist/pagination/PaginationEngine.cjs.map +1 -0
  20. package/dist/pagination/PaginationEngine.d.cts +117 -0
  21. package/dist/pagination/PaginationEngine.d.ts +117 -0
  22. package/dist/pagination/PaginationEngine.js +369 -0
  23. package/dist/pagination/PaginationEngine.js.map +1 -0
  24. package/dist/plugins/index.cjs +874 -0
  25. package/dist/plugins/index.cjs.map +1 -0
  26. package/dist/plugins/index.d.cts +275 -0
  27. package/dist/plugins/index.d.ts +275 -0
  28. package/dist/plugins/index.js +857 -0
  29. package/dist/plugins/index.js.map +1 -0
  30. package/dist/types-Nxhmi1aI.d.cts +510 -0
  31. package/dist/types-Nxhmi1aI.d.ts +510 -0
  32. package/dist/utils/index.cjs +667 -0
  33. package/dist/utils/index.cjs.map +1 -0
  34. package/dist/utils/index.d.cts +189 -0
  35. package/dist/utils/index.d.ts +189 -0
  36. package/dist/utils/index.js +643 -0
  37. package/dist/utils/index.js.map +1 -0
  38. package/package.json +54 -24
  39. package/src/Repository.js +0 -225
  40. package/src/actions/aggregate.js +0 -191
  41. package/src/actions/create.js +0 -59
  42. package/src/actions/delete.js +0 -88
  43. package/src/actions/index.js +0 -11
  44. package/src/actions/read.js +0 -156
  45. package/src/actions/update.js +0 -176
  46. package/src/hooks/lifecycle.js +0 -146
  47. package/src/index.js +0 -60
  48. package/src/plugins/aggregate-helpers.plugin.js +0 -71
  49. package/src/plugins/audit-log.plugin.js +0 -60
  50. package/src/plugins/batch-operations.plugin.js +0 -66
  51. package/src/plugins/field-filter.plugin.js +0 -27
  52. package/src/plugins/index.js +0 -19
  53. package/src/plugins/method-registry.plugin.js +0 -140
  54. package/src/plugins/mongo-operations.plugin.js +0 -313
  55. package/src/plugins/soft-delete.plugin.js +0 -46
  56. package/src/plugins/subdocument.plugin.js +0 -66
  57. package/src/plugins/timestamp.plugin.js +0 -19
  58. package/src/plugins/validation-chain.plugin.js +0 -145
  59. package/src/utils/field-selection.js +0 -156
  60. package/src/utils/index.js +0 -12
  61. package/types/actions/index.d.ts +0 -121
  62. package/types/index.d.ts +0 -104
  63. package/types/plugins/index.d.ts +0 -88
  64. package/types/utils/index.d.ts +0 -24
package/README.md CHANGED
@@ -4,31 +4,39 @@
4
4
  [![npm version](https://badge.fury.io/js/@classytic%2Fmongokit.svg)](https://www.npmjs.com/package/@classytic/mongokit)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
 
7
- > Event-driven MongoDB repositories for any Node.js framework
7
+ > Production-grade MongoDB repositories with zero external dependencies
8
8
 
9
9
  **Works with:** Express • Fastify • NestJS • Next.js • Koa • Hapi • Serverless
10
10
 
11
- - ✅ **Plugin-based architecture**
12
- - ✅ **Event hooks** for every operation
13
- - ✅ **Framework-agnostic**
14
- - ✅ **TypeScript** support
15
- - ✅ **Battle-tested** in production
11
+ - ✅ **Zero external dependencies** (only Mongoose peer dependency)
12
+ - ✅ **Smart pagination** - auto-detects offset vs cursor-based
13
+ - ✅ **HTTP utilities** - query parser & schema generator for controllers
14
+ - ✅ **Event-driven** hooks for every operation
15
+ - ✅ **Plugin architecture** for reusable behaviors
16
+ - ✅ **TypeScript** first-class support with discriminated unions
17
+ - ✅ **Optional caching** - Redis/Memcached with auto-invalidation
18
+ - ✅ **Battle-tested** in production with 182 passing tests
16
19
 
17
20
  ---
18
21
 
19
22
  ## 📦 Installation
20
23
 
21
24
  ```bash
22
- npm install @classytic/mongokit mongoose mongoose-paginate-v2 mongoose-aggregate-paginate-v2
25
+ npm install @classytic/mongokit mongoose
23
26
  ```
24
27
 
25
28
  > **Peer Dependencies:**
26
29
  > - `mongoose ^8.0.0 || ^9.0.0` (supports both Mongoose 8 and 9)
27
- > - `mongoose-paginate-v2 ^1.9.0` (for pagination support)
28
- > - `mongoose-aggregate-paginate-v2 ^1.1.0` (for aggregation pagination)
29
30
 
30
- ---
31
+ **Available imports:**
32
+ ```javascript
33
+ import { MongooseRepository } from '@classytic/mongokit'; // Core repository
34
+ import { queryParser, buildCrudSchemasFromModel } from '@classytic/mongokit/utils'; // HTTP utilities
35
+ ```
31
36
 
37
+ **That's it.** No additional pagination, validation, or query parsing libraries needed.
38
+
39
+ ---
32
40
 
33
41
  ## 🚀 Quick Start
34
42
 
@@ -42,20 +50,21 @@ class UserRepository extends Repository {
42
50
  constructor() {
43
51
  super(UserModel);
44
52
  }
45
-
46
- async findActiveUsers() {
47
- return this.getAll({ filters: { status: 'active' } });
48
- }
49
53
  }
50
54
 
51
55
  const userRepo = new UserRepository();
52
56
 
53
57
  // Create
54
- const user = await userRepo.create({ name: 'John', email: 'john@example.com' });
58
+ const user = await userRepo.create({
59
+ name: 'John',
60
+ email: 'john@example.com'
61
+ });
55
62
 
56
- // Read
57
- const users = await userRepo.getAll({ pagination: { page: 1, limit: 10 } });
58
- const user = await userRepo.getById('user-id');
63
+ // Read - auto-detects pagination mode
64
+ const users = await userRepo.getAll({
65
+ page: 1,
66
+ limit: 20
67
+ });
59
68
 
60
69
  // Update
61
70
  await userRepo.update('user-id', { name: 'Jane' });
@@ -64,56 +73,490 @@ await userRepo.update('user-id', { name: 'Jane' });
64
73
  await userRepo.delete('user-id');
65
74
  ```
66
75
 
67
- ### With Express
76
+ ### Unified Pagination - One Method, Two Modes
77
+
78
+ The `getAll()` method automatically detects whether you want **offset** (page-based) or **keyset** (cursor-based) pagination:
68
79
 
69
80
  ```javascript
70
- import express from 'express';
71
- import { Repository } from '@classytic/mongokit';
81
+ // Offset pagination (page-based) - for admin dashboards
82
+ const page1 = await userRepo.getAll({
83
+ page: 1,
84
+ limit: 20,
85
+ filters: { status: 'active' },
86
+ sort: { createdAt: -1 }
87
+ });
88
+ // → { method: 'offset', docs: [...], total: 1523, pages: 77, page: 1, ... }
72
89
 
73
- const app = express();
74
- const userRepo = new Repository(UserModel);
90
+ // Keyset pagination (cursor-based) - for infinite scroll
91
+ const stream1 = await userRepo.getAll({
92
+ sort: { createdAt: -1 },
93
+ limit: 20
94
+ });
95
+ // → { method: 'keyset', docs: [...], hasMore: true, next: 'eyJ2IjoxLCJ0Ij...' }
75
96
 
76
- app.get('/users', async (req, res) => {
77
- const users = await userRepo.getAll({
78
- filters: { status: 'active' },
79
- pagination: { page: req.query.page || 1, limit: 20 }
80
- });
81
- res.json(users);
97
+ // Load next page with cursor
98
+ const stream2 = await userRepo.getAll({
99
+ after: stream1.next,
100
+ sort: { createdAt: -1 },
101
+ limit: 20
82
102
  });
83
103
  ```
84
104
 
85
- ### With Fastify
105
+ **Auto-detection logic:**
106
+ 1. If `page` parameter provided → **offset mode**
107
+ 2. If `after` or `cursor` parameter provided → **keyset mode**
108
+ 3. If explicit `sort` provided without `page` → **keyset mode** (first page)
109
+ 4. Otherwise → **offset mode** (default, page 1)
110
+
111
+ ---
112
+
113
+ ## 🎯 Pagination Modes Explained
114
+
115
+ ### Offset Pagination (Page-Based)
116
+
117
+ Best for: Admin dashboards, page numbers, showing total counts
86
118
 
87
119
  ```javascript
88
- import Fastify from 'fastify';
89
- import { Repository } from '@classytic/mongokit';
120
+ const result = await userRepo.getAll({
121
+ page: 1,
122
+ limit: 20,
123
+ filters: { status: 'active' },
124
+ sort: { createdAt: -1 }
125
+ });
90
126
 
91
- const fastify = Fastify();
92
- const userRepo = new Repository(UserModel);
127
+ console.log(result.method); // 'offset'
128
+ console.log(result.docs); // Array of documents
129
+ console.log(result.total); // Total count (e.g., 1523)
130
+ console.log(result.pages); // Total pages (e.g., 77)
131
+ console.log(result.page); // Current page (1)
132
+ console.log(result.hasNext); // true
133
+ console.log(result.hasPrev); // false
134
+ ```
135
+
136
+ **Performance:**
137
+ - Time complexity: O(n) where n = page × limit
138
+ - Works great for small-medium datasets
139
+ - Warning triggered for pages > 100
93
140
 
94
- fastify.get('/users', async (request, reply) => {
95
- const users = await userRepo.getAll();
96
- return users;
141
+ ### Keyset Pagination (Cursor-Based)
142
+
143
+ Best for: Infinite scroll, real-time feeds, large datasets
144
+
145
+ ```javascript
146
+ const result = await userRepo.getAll({
147
+ sort: { createdAt: -1 },
148
+ limit: 20
149
+ });
150
+
151
+ console.log(result.method); // 'keyset'
152
+ console.log(result.docs); // Array of documents
153
+ console.log(result.hasMore); // true
154
+ console.log(result.next); // 'eyJ2IjoxLCJ0IjoiZGF0ZSIsInYiO...'
155
+
156
+ // Load next page
157
+ const next = await userRepo.getAll({
158
+ after: result.next,
159
+ sort: { createdAt: -1 },
160
+ limit: 20
97
161
  });
98
162
  ```
99
163
 
100
- ### With Next.js API Routes
164
+ **Performance:**
165
+ - Time complexity: O(1) regardless of position
166
+ - Requires compound index: `{ sortField: 1, _id: 1 }`
167
+ - Ideal for millions of documents
101
168
 
169
+ **Required Index:**
102
170
  ```javascript
103
- // pages/api/users.js
104
- import { Repository } from '@classytic/mongokit';
105
- import UserModel from '@/models/User';
171
+ // For sort: { createdAt: -1 }
172
+ PostSchema.index({ createdAt: -1, _id: -1 });
106
173
 
107
- const userRepo = new Repository(UserModel);
174
+ // For sort: { publishedAt: -1, views: -1 }
175
+ PostSchema.index({ publishedAt: -1, views: -1, _id: -1 });
176
+ ```
177
+
178
+ ---
179
+
180
+ ## 💡 Real-World Examples
181
+
182
+ ### Text Search + Infinite Scroll
183
+
184
+ ```javascript
185
+ // Define schema with text index
186
+ const PostSchema = new mongoose.Schema({
187
+ title: String,
188
+ content: String,
189
+ publishedAt: { type: Date, default: Date.now }
190
+ });
191
+
192
+ PostSchema.index({ title: 'text', content: 'text' });
193
+ PostSchema.index({ publishedAt: -1, _id: -1 });
194
+
195
+ // Search and paginate
196
+ const postRepo = new Repository(PostModel);
197
+
198
+ const page1 = await postRepo.getAll({
199
+ search: 'JavaScript',
200
+ sort: { publishedAt: -1 },
201
+ limit: 20
202
+ });
203
+ // → Returns first 20 posts matching "JavaScript"
204
+
205
+ // User scrolls down - load more
206
+ const page2 = await postRepo.getAll({
207
+ after: page1.next,
208
+ search: 'JavaScript',
209
+ sort: { publishedAt: -1 },
210
+ limit: 20
211
+ });
212
+ // → Next 20 posts with same search query
213
+ ```
214
+
215
+ ### Admin Dashboard with Filters
108
216
 
109
- export default async function handler(req, res) {
110
- if (req.method === 'GET') {
111
- const users = await userRepo.getAll();
112
- res.json(users);
217
+ ```javascript
218
+ const result = await userRepo.getAll({
219
+ page: req.query.page || 1,
220
+ limit: 50,
221
+ filters: {
222
+ status: 'active',
223
+ role: { $in: ['admin', 'moderator'] }
224
+ },
225
+ sort: { lastLoginAt: -1 }
226
+ });
227
+
228
+ res.json({
229
+ users: result.docs,
230
+ pagination: {
231
+ page: result.page,
232
+ pages: result.pages,
233
+ total: result.total,
234
+ hasNext: result.hasNext,
235
+ hasPrev: result.hasPrev
236
+ }
237
+ });
238
+ ```
239
+
240
+ ### Multi-Tenant Applications
241
+
242
+ ```javascript
243
+ class TenantUserRepository extends Repository {
244
+ constructor() {
245
+ super(UserModel, [], {
246
+ defaultLimit: 20,
247
+ maxLimit: 100
248
+ });
249
+ }
250
+
251
+ async getAllForTenant(organizationId, params = {}) {
252
+ return this.getAll({
253
+ ...params,
254
+ filters: {
255
+ organizationId,
256
+ ...params.filters
257
+ }
258
+ });
113
259
  }
114
260
  }
261
+
262
+ // Use it
263
+ const users = await tenantRepo.getAllForTenant('org-123', {
264
+ page: 1,
265
+ limit: 50,
266
+ filters: { status: 'active' }
267
+ });
268
+ ```
269
+
270
+ ### Switching Between Modes Seamlessly
271
+
272
+ ```javascript
273
+ // Admin view - needs page numbers and total count
274
+ const adminView = await postRepo.getAll({
275
+ page: 1,
276
+ limit: 20,
277
+ sort: { createdAt: -1 }
278
+ });
279
+ // → method: 'offset', total: 1523, pages: 77
280
+
281
+ // Public feed - infinite scroll
282
+ const feedView = await postRepo.getAll({
283
+ sort: { createdAt: -1 },
284
+ limit: 20
285
+ });
286
+ // → method: 'keyset', next: 'eyJ2IjoxLC...'
287
+
288
+ // Both return same first 20 results!
289
+ ```
290
+
291
+ ---
292
+
293
+ ## 🌐 HTTP Utilities for Controllers & Routes
294
+
295
+ MongoKit provides utilities to quickly build production-ready controllers and routes for Express, Fastify, NestJS, and other frameworks.
296
+
297
+ ### Query Parser
298
+
299
+ Parse HTTP query strings into MongoDB filters automatically:
300
+
301
+ ```javascript
302
+ import { queryParser } from '@classytic/mongokit/utils';
303
+
304
+ // Express/Fastify route
305
+ app.get('/users', async (req, res) => {
306
+ const { filters, limit, page, sort } = queryParser.parseQuery(req.query);
307
+
308
+ const result = await userRepo.getAll({ filters, limit, page, sort });
309
+ res.json(result);
310
+ });
311
+ ```
312
+
313
+ **Supported query patterns:**
314
+
315
+ ```bash
316
+ # Simple filtering
317
+ GET /users?email=john@example.com&role=admin
318
+
319
+ # Operators
320
+ GET /users?age[gte]=18&age[lte]=65 # Range queries
321
+ GET /users?email[contains]=gmail # Text search
322
+ GET /users?role[in]=admin,user # Multiple values
323
+ GET /users?status[ne]=deleted # Not equal
324
+
325
+ # Pagination
326
+ GET /users?page=2&limit=50 # Offset pagination
327
+ GET /users?after=eyJfaWQiOiI2M... # Cursor pagination
328
+
329
+ # Sorting
330
+ GET /users?sort=-createdAt,name # Multi-field sort (- = descending)
331
+
332
+ # Combined
333
+ GET /users?role=admin&createdAt[gte]=2024-01-01&sort=-createdAt&limit=20
334
+ ```
335
+
336
+ ### Schema Generator (Fastify/OpenAPI)
337
+
338
+ Generate JSON schemas from Mongoose models with field rules:
339
+
340
+ ```javascript
341
+ import { buildCrudSchemasFromModel } from '@classytic/mongokit/utils';
342
+
343
+ const { crudSchemas } = buildCrudSchemasFromModel(UserModel, {
344
+ strictAdditionalProperties: true, // Reject unknown fields
345
+ fieldRules: {
346
+ organizationId: { immutable: true }, // Cannot be updated
347
+ status: { systemManaged: true }, // Omitted from create/update
348
+ email: { optional: false }, // Required field
349
+ },
350
+ create: {
351
+ omitFields: ['verifiedAt'], // Custom omissions
352
+ },
353
+ });
354
+
355
+ // Use in Fastify routes
356
+ fastify.post('/users', {
357
+ schema: crudSchemas.create,
358
+ }, async (request, reply) => {
359
+ const user = await userRepo.create(request.body);
360
+ return reply.status(201).send(user);
361
+ });
362
+
363
+ fastify.get('/users', {
364
+ schema: crudSchemas.list,
365
+ }, async (request, reply) => {
366
+ const { filters, limit, page, sort } = queryParser.parseQuery(request.query);
367
+ const result = await userRepo.getAll({ filters, limit, page, sort });
368
+ return reply.send(result);
369
+ });
370
+ ```
371
+
372
+ **Generated schemas:**
373
+ - `crudSchemas.create` - POST validation (body only)
374
+ - `crudSchemas.update` - PATCH validation (body + params)
375
+ - `crudSchemas.get` - GET by ID validation (params)
376
+ - `crudSchemas.list` - GET list validation (query)
377
+ - `crudSchemas.remove` - DELETE validation (params)
378
+
379
+ **Field Rules:**
380
+ - `immutable` - Field cannot be updated after creation (omitted from update schema)
381
+ - `systemManaged` - System-only field (omitted from both create and update schemas)
382
+ - `optional` - Remove from required array
383
+
384
+ **See full example:** [`examples/fastify-controller-example.js`](examples/fastify-controller-example.js)
385
+
386
+ ---
387
+
388
+ ## 📘 Complete API Reference
389
+
390
+ ### CRUD Operations
391
+
392
+ | Method | Description | Example |
393
+ |--------|-------------|---------|
394
+ | `create(data, opts)` | Create single document | `repo.create({ name: 'John' })` |
395
+ | `createMany(data[], opts)` | Create multiple documents | `repo.createMany([{...}, {...}])` |
396
+ | `getById(id, opts)` | Find by ID | `repo.getById('123')` |
397
+ | `getByQuery(query, opts)` | Find one by query | `repo.getByQuery({ email: 'a@b.com' })` |
398
+ | `getAll(params, opts)` | Paginated list | `repo.getAll({ page: 1, limit: 20 })` |
399
+ | `getOrCreate(query, data, opts)` | Find or create | `repo.getOrCreate({ email }, { email, name })` |
400
+ | `update(id, data, opts)` | Update document | `repo.update('123', { name: 'Jane' })` |
401
+ | `delete(id, opts)` | Delete document | `repo.delete('123')` |
402
+ | `count(query, opts)` | Count documents | `repo.count({ status: 'active' })` |
403
+ | `exists(query, opts)` | Check existence | `repo.exists({ email: 'a@b.com' })` |
404
+
405
+ ### getAll() Parameters
406
+
407
+ ```javascript
408
+ await repo.getAll({
409
+ // Pagination mode (auto-detected)
410
+ page: 1, // Offset mode: page number
411
+ after: 'cursor...', // Keyset mode: cursor token
412
+ cursor: 'cursor...', // Alias for 'after'
413
+
414
+ // Common parameters
415
+ limit: 20, // Documents per page
416
+ filters: { ... }, // MongoDB query filters
417
+ sort: { createdAt: -1 }, // Sort specification
418
+ search: 'keyword', // Full-text search (requires text index)
419
+
420
+ // Additional options (in options parameter)
421
+ select: 'name email', // Field projection
422
+ populate: 'author', // Population
423
+ lean: true, // Return plain objects (default: true)
424
+ session: session // Transaction session
425
+ });
426
+ ```
427
+
428
+ ### Aggregation
429
+
430
+ ```javascript
431
+ // Basic aggregation
432
+ const result = await repo.aggregate([
433
+ { $match: { status: 'active' } },
434
+ { $group: { _id: '$category', total: { $sum: 1 } } }
435
+ ]);
436
+
437
+ // Paginated aggregation
438
+ const result = await repo.aggregatePaginate({
439
+ pipeline: [
440
+ { $match: { status: 'active' } },
441
+ { $lookup: { from: 'users', localField: 'userId', foreignField: '_id', as: 'user' } }
442
+ ],
443
+ page: 1,
444
+ limit: 20
445
+ });
446
+
447
+ // Distinct values
448
+ const categories = await repo.distinct('category', { status: 'active' });
449
+ ```
450
+
451
+ ### Transactions
452
+
453
+ ```javascript
454
+ await repo.withTransaction(async (session) => {
455
+ await repo.create({ name: 'User 1' }, { session });
456
+ await repo.create({ name: 'User 2' }, { session });
457
+ // Auto-commits if no errors, auto-rollbacks on errors
458
+ });
459
+ ```
460
+
461
+ ---
462
+
463
+ ## 🔧 Configuration
464
+
465
+ ### Pagination Configuration
466
+
467
+ ```javascript
468
+ import { Repository } from '@classytic/mongokit';
469
+
470
+ const userRepo = new Repository(UserModel, [], {
471
+ defaultLimit: 20, // Default documents per page
472
+ maxLimit: 100, // Maximum allowed limit
473
+ maxPage: 10000, // Maximum page number (offset mode)
474
+ deepPageThreshold: 100, // Warn when page exceeds this
475
+ useEstimatedCount: false, // Use estimatedDocumentCount() for speed
476
+ cursorVersion: 1 // Cursor format version
477
+ });
478
+ ```
479
+
480
+ ### Estimated Counts (for large collections)
481
+
482
+ For collections with millions of documents, counting can be slow. Use estimated counts:
483
+
484
+ ```javascript
485
+ const repo = new Repository(UserModel, [], {
486
+ useEstimatedCount: true // O(1) metadata lookup instead of O(n) count
487
+ });
488
+
489
+ const result = await repo.getAll({ page: 1, limit: 20 });
490
+ // Uses estimatedDocumentCount() - instant but approximate
491
+ ```
492
+
493
+ **Note:** Estimated counts ignore filters and sessions by design (reads metadata, not documents).
494
+
495
+ ---
496
+
497
+ ## 📊 Indexing Guide
498
+
499
+ **Critical:** MongoDB only auto-indexes `_id`. You must create indexes for efficient pagination.
500
+
501
+ ### Single-Tenant Applications
502
+
503
+ ```javascript
504
+ const PostSchema = new mongoose.Schema({
505
+ title: String,
506
+ publishedAt: { type: Date, default: Date.now }
507
+ });
508
+
509
+ // Required for keyset pagination
510
+ PostSchema.index({ publishedAt: -1, _id: -1 });
511
+ // ^^^^^^^^^^^^^^ ^^^^^^
512
+ // Sort field Tie-breaker
513
+ ```
514
+
515
+ ### Multi-Tenant Applications
516
+
517
+ ```javascript
518
+ const UserSchema = new mongoose.Schema({
519
+ organizationId: String,
520
+ email: String,
521
+ createdAt: { type: Date, default: Date.now }
522
+ });
523
+
524
+ // Required for multi-tenant keyset pagination
525
+ UserSchema.index({ organizationId: 1, createdAt: -1, _id: -1 });
526
+ // ^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^^
527
+ // Tenant filter Sort field Tie-breaker
528
+ ```
529
+
530
+ ### Common Index Patterns
531
+
532
+ ```javascript
533
+ // Basic sorting
534
+ Schema.index({ createdAt: -1, _id: -1 });
535
+
536
+ // Multi-tenant
537
+ Schema.index({ tenantId: 1, createdAt: -1, _id: -1 });
538
+
539
+ // Multi-tenant + status filter
540
+ Schema.index({ tenantId: 1, status: 1, createdAt: -1, _id: -1 });
541
+
542
+ // Text search
543
+ Schema.index({ title: 'text', content: 'text' });
544
+ Schema.index({ createdAt: -1, _id: -1 }); // Still need this for sorting
545
+
546
+ // Multi-field sort
547
+ Schema.index({ priority: -1, createdAt: -1, _id: -1 });
115
548
  ```
116
549
 
550
+ ### Performance Impact
551
+
552
+ | Scenario | Without Index | With Index |
553
+ |----------|--------------|------------|
554
+ | 10K docs | ~50ms | ~5ms |
555
+ | 1M docs | ~5000ms | ~5ms |
556
+ | 100M docs | timeout | ~5ms |
557
+
558
+ **Rule:** Index = (tenant_field +) sort_field + _id
559
+
117
560
  ---
118
561
 
119
562
  ## 🔌 Built-in Plugins
@@ -133,7 +576,7 @@ const fieldPreset = {
133
576
 
134
577
  class UserRepository extends Repository {
135
578
  constructor() {
136
- super(User, [fieldFilterPlugin(fieldPreset)]);
579
+ super(UserModel, [fieldFilterPlugin(fieldPreset)]);
137
580
  }
138
581
  }
139
582
  ```
@@ -143,17 +586,17 @@ class UserRepository extends Repository {
143
586
  Add custom validation rules:
144
587
 
145
588
  ```javascript
146
- import {
147
- Repository,
148
- validationChainPlugin,
149
- requireField,
589
+ import {
590
+ Repository,
591
+ validationChainPlugin,
592
+ requireField,
150
593
  uniqueField,
151
- immutableField
594
+ immutableField
152
595
  } from '@classytic/mongokit';
153
596
 
154
597
  class UserRepository extends Repository {
155
598
  constructor() {
156
- super(User, [
599
+ super(UserModel, [
157
600
  validationChainPlugin([
158
601
  requireField('email', ['create']),
159
602
  uniqueField('email', 'Email already exists'),
@@ -173,7 +616,7 @@ import { Repository, softDeletePlugin } from '@classytic/mongokit';
173
616
 
174
617
  class UserRepository extends Repository {
175
618
  constructor() {
176
- super(User, [softDeletePlugin({ deletedField: 'deletedAt' })]);
619
+ super(UserModel, [softDeletePlugin({ deletedField: 'deletedAt' })]);
177
620
  }
178
621
  }
179
622
 
@@ -192,67 +635,60 @@ import logger from './logger.js';
192
635
 
193
636
  class UserRepository extends Repository {
194
637
  constructor() {
195
- super(User, [auditLogPlugin(logger)]);
638
+ super(UserModel, [auditLogPlugin(logger)]);
196
639
  }
197
640
  }
198
641
 
199
642
  // All CUD operations automatically logged
200
643
  ```
201
644
 
202
- ### More Plugins
645
+ ### Caching (Redis, Memcached, or In-Memory)
203
646
 
204
- - **`timestampPlugin()`** - Auto-manage `createdAt`/`updatedAt`
205
- - **`mongoOperationsPlugin()`** - Adds `increment`, `pushToArray`, `upsert`, etc.
206
- - **`batchOperationsPlugin()`** - Adds `updateMany`, `deleteMany`
207
- - **`aggregateHelpersPlugin()`** - Adds `groupBy`, `sum`, `average`, etc.
208
- - **`subdocumentPlugin()`** - Manage subdocument arrays easily
647
+ Add caching with automatic invalidation on mutations:
209
648
 
210
- ---
649
+ ```javascript
650
+ import { Repository, cachePlugin, createMemoryCache } from '@classytic/mongokit';
651
+
652
+ const userRepo = new Repository(UserModel, [
653
+ cachePlugin({
654
+ adapter: createMemoryCache(), // or your Redis adapter
655
+ ttl: 60, // 60 seconds default
656
+ byIdTtl: 300, // 5 min for getById
657
+ queryTtl: 30, // 30s for lists
658
+ })
659
+ ]);
211
660
 
212
- ## 🎯 Core API
661
+ // Reads are cached automatically
662
+ const user = await userRepo.getById(id); // cached on second call
213
663
 
214
- ### CRUD Operations
664
+ // Skip cache for fresh data
665
+ const fresh = await userRepo.getById(id, { skipCache: true });
215
666
 
216
- | Method | Description | Example |
217
- |--------|-------------|---------|
218
- | `create(data, opts)` | Create single document | `repo.create({ name: 'John' })` |
219
- | `createMany(data[], opts)` | Create multiple documents | `repo.createMany([{...}, {...}])` |
220
- | `getById(id, opts)` | Find by ID | `repo.getById('123')` |
221
- | `getByQuery(query, opts)` | Find one by query | `repo.getByQuery({ email: 'a@b.com' })` |
222
- | `getAll(params, opts)` | Paginated list | `repo.getAll({ filters: { active: true } })` |
223
- | `getOrCreate(query, data, opts)` | Find or create | `repo.getOrCreate({ email }, { email, name })` |
224
- | `update(id, data, opts)` | Update document | `repo.update('123', { name: 'Jane' })` |
225
- | `delete(id, opts)` | Delete document | `repo.delete('123')` |
226
- | `count(query, opts)` | Count documents | `repo.count({ status: 'active' })` |
227
- | `exists(query, opts)` | Check existence | `repo.exists({ email: 'a@b.com' })` |
667
+ // Mutations auto-invalidate cache
668
+ await userRepo.update(id, { name: 'New' });
228
669
 
229
- ### Aggregation
670
+ // Manual invalidation (microservices)
671
+ await userRepo.invalidateCache(id); // single doc
672
+ await userRepo.invalidateAllCache(); // full model
673
+ ```
230
674
 
675
+ **Redis adapter example:**
231
676
  ```javascript
232
- // Basic aggregation
233
- const result = await repo.aggregate([
234
- { $match: { status: 'active' } },
235
- { $group: { _id: '$category', total: { $sum: 1 } } }
236
- ]);
237
-
238
- // Paginated aggregation
239
- const result = await repo.aggregatePaginate([
240
- { $match: { status: 'active' } }
241
- ], { page: 1, limit: 20 });
242
-
243
- // Distinct values
244
- const categories = await repo.distinct('category');
677
+ const redisAdapter = {
678
+ async get(key) { return JSON.parse(await redis.get(key) || 'null'); },
679
+ async set(key, value, ttl) { await redis.setex(key, ttl, JSON.stringify(value)); },
680
+ async del(key) { await redis.del(key); },
681
+ async clear(pattern) { /* optional: bulk delete by pattern */ }
682
+ };
245
683
  ```
246
684
 
247
- ### Transactions
685
+ ### More Plugins
248
686
 
249
- ```javascript
250
- await repo.withTransaction(async (session) => {
251
- await repo.create({ name: 'User 1' }, { session });
252
- await repo.create({ name: 'User 2' }, { session });
253
- // Auto-commits if no errors, auto-rollbacks on errors
254
- });
255
- ```
687
+ - **`timestampPlugin()`** - Auto-manage `createdAt`/`updatedAt`
688
+ - **`mongoOperationsPlugin()`** - Adds `increment`, `pushToArray`, `upsert`, etc.
689
+ - **`batchOperationsPlugin()`** - Adds `updateMany`, `deleteMany`
690
+ - **`aggregateHelpersPlugin()`** - Adds `groupBy`, `sum`, `average`, etc.
691
+ - **`subdocumentPlugin()`** - Manage subdocument arrays easily
256
692
 
257
693
  ---
258
694
 
@@ -287,20 +723,20 @@ repo.on('error:create', ({ context, error }) => {
287
723
 
288
724
  ---
289
725
 
290
- ## 🔧 Custom Plugins
726
+ ## 🎯 Custom Plugins
291
727
 
292
728
  Create your own plugins:
293
729
 
294
730
  ```javascript
295
731
  export const timestampPlugin = () => ({
296
732
  name: 'timestamp',
297
-
733
+
298
734
  apply(repo) {
299
735
  repo.on('before:create', (context) => {
300
736
  context.data.createdAt = new Date();
301
737
  context.data.updatedAt = new Date();
302
738
  });
303
-
739
+
304
740
  repo.on('before:update', (context) => {
305
741
  context.data.updatedAt = new Date();
306
742
  });
@@ -310,7 +746,28 @@ export const timestampPlugin = () => ({
310
746
  // Use it
311
747
  class UserRepository extends Repository {
312
748
  constructor() {
313
- super(User, [timestampPlugin()]);
749
+ super(UserModel, [timestampPlugin()]);
750
+ }
751
+ }
752
+ ```
753
+
754
+ ### Combining Multiple Plugins
755
+
756
+ ```javascript
757
+ import {
758
+ Repository,
759
+ softDeletePlugin,
760
+ auditLogPlugin,
761
+ fieldFilterPlugin
762
+ } from '@classytic/mongokit';
763
+
764
+ class UserRepository extends Repository {
765
+ constructor() {
766
+ super(UserModel, [
767
+ softDeletePlugin(),
768
+ auditLogPlugin(logger),
769
+ fieldFilterPlugin(userFieldPreset)
770
+ ]);
314
771
  }
315
772
  }
316
773
  ```
@@ -319,11 +776,15 @@ class UserRepository extends Repository {
319
776
 
320
777
  ## 📚 TypeScript Support
321
778
 
322
- Full TypeScript definitions included:
779
+ Full TypeScript support with discriminated unions:
323
780
 
324
781
  ```typescript
325
- import { Repository, Plugin, RepositoryContext } from '@classytic/mongokit';
326
- import { Model, Document } from 'mongoose';
782
+ import {
783
+ Repository,
784
+ OffsetPaginationResult,
785
+ KeysetPaginationResult
786
+ } from '@classytic/mongokit';
787
+ import { Document } from 'mongoose';
327
788
 
328
789
  interface IUser extends Document {
329
790
  name: string;
@@ -331,69 +792,179 @@ interface IUser extends Document {
331
792
  status: 'active' | 'inactive';
332
793
  }
333
794
 
334
- class UserRepository extends Repository<IUser> {
795
+ class UserRepository extends Repository {
335
796
  constructor() {
336
797
  super(UserModel);
337
798
  }
338
-
799
+
339
800
  async findActive(): Promise<IUser[]> {
340
- const result = await this.getAll({
341
- filters: { status: 'active' }
801
+ const result = await this.getAll({
802
+ filters: { status: 'active' },
803
+ page: 1,
804
+ limit: 50
342
805
  });
806
+
807
+ // TypeScript knows result is OffsetPaginationResult
808
+ if (result.method === 'offset') {
809
+ console.log(result.total); // ✅ Type-safe
810
+ console.log(result.pages); // ✅ Type-safe
811
+ // console.log(result.next); // ❌ Type error
812
+ }
813
+
814
+ return result.docs;
815
+ }
816
+
817
+ async getFeed(): Promise<IUser[]> {
818
+ const result = await this.getAll({
819
+ sort: { createdAt: -1 },
820
+ limit: 20
821
+ });
822
+
823
+ // TypeScript knows result is KeysetPaginationResult
824
+ if (result.method === 'keyset') {
825
+ console.log(result.next); // ✅ Type-safe
826
+ console.log(result.hasMore); // ✅ Type-safe
827
+ // console.log(result.total); // ❌ Type error
828
+ }
829
+
343
830
  return result.docs;
344
831
  }
345
832
  }
346
833
  ```
347
834
 
835
+ ### Import Types
836
+
837
+ ```typescript
838
+ import type {
839
+ PaginationConfig,
840
+ OffsetPaginationOptions,
841
+ KeysetPaginationOptions,
842
+ AggregatePaginationOptions,
843
+ OffsetPaginationResult,
844
+ KeysetPaginationResult,
845
+ AggregatePaginationResult
846
+ } from '@classytic/mongokit';
847
+ ```
848
+
348
849
  ---
349
850
 
350
- ## 🏗️ Advanced Patterns
851
+ ## 🏎️ Performance Tips
351
852
 
352
- ### Custom Methods
853
+ ### 1. Use Keyset Pagination for Large Datasets
353
854
 
354
855
  ```javascript
355
- class MembershipRepository extends Repository {
356
- constructor() {
357
- super(Membership);
358
- }
359
-
360
- async findActiveByCustomer(customerId) {
361
- return this.getAll({
362
- filters: {
363
- customerId,
364
- status: { $in: ['active', 'paused'] }
365
- }
366
- });
367
- }
368
-
369
- async recordVisit(membershipId) {
370
- return this.update(membershipId, {
371
- $set: { lastVisitedAt: new Date() },
372
- $inc: { totalVisits: 1 }
373
- });
374
- }
375
- }
856
+ // Slow for large datasets (millions of documents)
857
+ await repo.getAll({ page: 1000, limit: 50 }); // O(50000)
858
+
859
+ // ✅ Fast regardless of position
860
+ await repo.getAll({ after: cursor, limit: 50 }); // O(1)
376
861
  ```
377
862
 
378
- ### Combining Multiple Plugins
863
+ ### 2. Create Required Indexes
864
+
865
+ **IMPORTANT:** MongoDB only auto-indexes `_id`. You must manually create indexes for pagination.
379
866
 
380
867
  ```javascript
381
- import {
382
- Repository,
383
- softDeletePlugin,
384
- auditLogPlugin,
385
- fieldFilterPlugin
386
- } from '@classytic/mongokit';
868
+ // Single-Tenant: Sort field + _id
869
+ PostSchema.index({ createdAt: -1, _id: -1 });
387
870
 
388
- class UserRepository extends Repository {
389
- constructor() {
390
- super(User, [
391
- softDeletePlugin(),
392
- auditLogPlugin(logger),
393
- fieldFilterPlugin(userFieldPreset)
394
- ]);
395
- }
396
- }
871
+ // Multi-Tenant: Tenant field + Sort field + _id
872
+ UserSchema.index({ organizationId: 1, createdAt: -1, _id: -1 });
873
+
874
+ // ✅ Text Search: Text index
875
+ PostSchema.index({ title: 'text', content: 'text' });
876
+ ```
877
+
878
+ **Without indexes = slow (full collection scan)**
879
+ **With indexes = fast (O(1) index seek)**
880
+
881
+ ### 3. Use Estimated Counts for Large Collections
882
+
883
+ ```javascript
884
+ const repo = new Repository(UserModel, [], {
885
+ useEstimatedCount: true // Instant counts for >10M documents
886
+ });
887
+ ```
888
+
889
+ ### 4. Use Lean Queries (Enabled by Default)
890
+
891
+ ```javascript
892
+ // Lean is true by default - returns plain objects
893
+ const result = await repo.getAll({ page: 1 });
894
+
895
+ // Disable for Mongoose documents (if you need methods)
896
+ const result = await repo.getAll({ page: 1 }, { lean: false });
897
+ ```
898
+
899
+ ### 5. Limit $facet Results in Aggregation
900
+
901
+ ```javascript
902
+ // ⚠️ Warning triggered automatically at limit > 1000
903
+ await repo.aggregatePaginate({
904
+ pipeline: [...],
905
+ limit: 2000 // Warning: $facet results must be <16MB
906
+ });
907
+ ```
908
+
909
+ ---
910
+
911
+ ## 🔄 Migration Guide
912
+
913
+ ### From mongoose-paginate-v2
914
+
915
+ ```javascript
916
+ // Before
917
+ import mongoosePaginate from 'mongoose-paginate-v2';
918
+ UserSchema.plugin(mongoosePaginate);
919
+ const result = await UserModel.paginate({ status: 'active' }, { page: 1, limit: 10 });
920
+
921
+ // After
922
+ import { Repository } from '@classytic/mongokit';
923
+ const userRepo = new Repository(UserModel);
924
+ const result = await userRepo.getAll({
925
+ filters: { status: 'active' },
926
+ page: 1,
927
+ limit: 10
928
+ });
929
+ ```
930
+
931
+ ### From Prisma
932
+
933
+ ```javascript
934
+ // Before (Prisma)
935
+ const users = await prisma.user.findMany({
936
+ where: { status: 'active' },
937
+ skip: 20,
938
+ take: 10
939
+ });
940
+
941
+ // After (MongoKit)
942
+ const result = await userRepo.getAll({
943
+ filters: { status: 'active' },
944
+ page: 3,
945
+ limit: 10
946
+ });
947
+ const users = result.docs;
948
+ ```
949
+
950
+ ### From TypeORM
951
+
952
+ ```javascript
953
+ // Before (TypeORM)
954
+ const [users, total] = await userRepository.findAndCount({
955
+ where: { status: 'active' },
956
+ skip: 20,
957
+ take: 10
958
+ });
959
+
960
+ // After (MongoKit)
961
+ const result = await userRepo.getAll({
962
+ filters: { status: 'active' },
963
+ page: 3,
964
+ limit: 10
965
+ });
966
+ const users = result.docs;
967
+ const total = result.total;
397
968
  ```
398
969
 
399
970
  ---
@@ -402,11 +973,18 @@ class UserRepository extends Repository {
402
973
 
403
974
  ### vs. Mongoose Directly
404
975
  - ✅ Consistent API across all models
405
- - ✅ Built-in pagination, filtering, sorting
976
+ - ✅ Built-in pagination (offset + cursor) with zero dependencies
406
977
  - ✅ Multi-tenancy without repetitive code
407
978
  - ✅ Event hooks for cross-cutting concerns
408
979
  - ✅ Plugin system for reusable behaviors
409
980
 
981
+ ### vs. mongoose-paginate-v2
982
+ - ✅ Zero external dependencies (no mongoose-paginate-v2 needed)
983
+ - ✅ Cursor-based pagination for infinite scroll
984
+ - ✅ Unified API that auto-detects pagination mode
985
+ - ✅ Native MongoDB implementation ($facet, cursors)
986
+ - ✅ Better TypeScript support
987
+
410
988
  ### vs. TypeORM / Prisma
411
989
  - ✅ Lighter weight (works with Mongoose)
412
990
  - ✅ Event-driven architecture
@@ -415,10 +993,10 @@ class UserRepository extends Repository {
415
993
  - ✅ Framework-agnostic
416
994
 
417
995
  ### vs. Raw Repository Pattern
418
- - ✅ Battle-tested implementation
996
+ - ✅ Battle-tested implementation (68 passing tests)
419
997
  - ✅ 11 built-in plugins ready to use
420
998
  - ✅ Comprehensive documentation
421
- - ✅ TypeScript support
999
+ - ✅ TypeScript discriminated unions
422
1000
  - ✅ Active maintenance
423
1001
 
424
1002
  ---
@@ -429,9 +1007,52 @@ class UserRepository extends Repository {
429
1007
  npm test
430
1008
  ```
431
1009
 
1010
+ **Test Coverage:**
1011
+ - 184 tests (182 passing, 2 skipped - require replica set)
1012
+ - CRUD operations
1013
+ - Offset pagination
1014
+ - Keyset pagination
1015
+ - Aggregation pagination
1016
+ - Caching (hit/miss, invalidation)
1017
+ - Multi-tenancy
1018
+ - Text search + infinite scroll
1019
+ - Real-world scenarios
1020
+
1021
+ ---
1022
+
1023
+ ## 📖 Examples
1024
+
1025
+ Check out the [examples](./examples) directory for:
1026
+ - Express REST API
1027
+ - Fastify REST API
1028
+ - Next.js API routes
1029
+ - Multi-tenant SaaS
1030
+ - Infinite scroll feed
1031
+ - Admin dashboard
1032
+
1033
+ ---
1034
+
1035
+ ## 🤝 Contributing
1036
+
1037
+ Contributions are welcome! Please check out our [contributing guide](CONTRIBUTING.md).
1038
+
432
1039
  ---
433
1040
 
434
1041
  ## 📄 License
435
1042
 
436
1043
  MIT © [Classytic](https://github.com/classytic)
437
1044
 
1045
+ ---
1046
+
1047
+ ## 🔗 Links
1048
+
1049
+ - [GitHub Repository](https://github.com/classytic/mongokit)
1050
+ - [npm Package](https://www.npmjs.com/package/@classytic/mongokit)
1051
+ - [Documentation](https://github.com/classytic/mongokit#readme)
1052
+ - [Issue Tracker](https://github.com/classytic/mongokit/issues)
1053
+
1054
+ ---
1055
+
1056
+ **Built with ❤️ by developers, for developers.**
1057
+
1058
+ Zero dependencies. Zero compromises. Production-grade MongoDB pagination.