@classytic/mongokit 1.0.1 → 2.0.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 (87) hide show
  1. package/README.md +564 -157
  2. package/package.json +20 -12
  3. package/src/Repository.js +296 -225
  4. package/src/actions/aggregate.js +266 -191
  5. package/src/actions/create.js +59 -58
  6. package/src/actions/delete.js +88 -88
  7. package/src/actions/index.js +11 -11
  8. package/src/actions/read.js +188 -155
  9. package/src/actions/update.js +176 -172
  10. package/src/hooks/lifecycle.js +146 -146
  11. package/src/index.js +71 -60
  12. package/src/pagination/PaginationEngine.js +348 -0
  13. package/src/pagination/utils/cursor.js +119 -0
  14. package/src/pagination/utils/filter.js +42 -0
  15. package/src/pagination/utils/limits.js +82 -0
  16. package/src/pagination/utils/sort.js +101 -0
  17. package/src/plugins/aggregate-helpers.plugin.js +71 -71
  18. package/src/plugins/audit-log.plugin.js +60 -60
  19. package/src/plugins/batch-operations.plugin.js +66 -66
  20. package/src/plugins/field-filter.plugin.js +27 -27
  21. package/src/plugins/index.js +19 -19
  22. package/src/plugins/method-registry.plugin.js +140 -140
  23. package/src/plugins/mongo-operations.plugin.js +317 -313
  24. package/src/plugins/soft-delete.plugin.js +46 -46
  25. package/src/plugins/subdocument.plugin.js +66 -66
  26. package/src/plugins/timestamp.plugin.js +19 -19
  27. package/src/plugins/validation-chain.plugin.js +145 -145
  28. package/src/types.d.ts +87 -0
  29. package/src/utils/error.js +12 -0
  30. package/src/utils/field-selection.js +156 -156
  31. package/src/utils/index.js +12 -12
  32. package/types/Repository.d.ts +95 -0
  33. package/types/Repository.d.ts.map +1 -0
  34. package/types/actions/aggregate.d.ts +112 -0
  35. package/types/actions/aggregate.d.ts.map +1 -0
  36. package/types/actions/create.d.ts +21 -0
  37. package/types/actions/create.d.ts.map +1 -0
  38. package/types/actions/delete.d.ts +37 -0
  39. package/types/actions/delete.d.ts.map +1 -0
  40. package/types/actions/index.d.ts +6 -113
  41. package/types/actions/index.d.ts.map +1 -0
  42. package/types/actions/read.d.ts +135 -0
  43. package/types/actions/read.d.ts.map +1 -0
  44. package/types/actions/update.d.ts +58 -0
  45. package/types/actions/update.d.ts.map +1 -0
  46. package/types/hooks/lifecycle.d.ts +44 -0
  47. package/types/hooks/lifecycle.d.ts.map +1 -0
  48. package/types/index.d.ts +25 -96
  49. package/types/index.d.ts.map +1 -0
  50. package/types/pagination/PaginationEngine.d.ts +386 -0
  51. package/types/pagination/PaginationEngine.d.ts.map +1 -0
  52. package/types/pagination/utils/cursor.d.ts +40 -0
  53. package/types/pagination/utils/cursor.d.ts.map +1 -0
  54. package/types/pagination/utils/filter.d.ts +28 -0
  55. package/types/pagination/utils/filter.d.ts.map +1 -0
  56. package/types/pagination/utils/limits.d.ts +64 -0
  57. package/types/pagination/utils/limits.d.ts.map +1 -0
  58. package/types/pagination/utils/sort.d.ts +41 -0
  59. package/types/pagination/utils/sort.d.ts.map +1 -0
  60. package/types/plugins/aggregate-helpers.plugin.d.ts +6 -0
  61. package/types/plugins/aggregate-helpers.plugin.d.ts.map +1 -0
  62. package/types/plugins/audit-log.plugin.d.ts +6 -0
  63. package/types/plugins/audit-log.plugin.d.ts.map +1 -0
  64. package/types/plugins/batch-operations.plugin.d.ts +6 -0
  65. package/types/plugins/batch-operations.plugin.d.ts.map +1 -0
  66. package/types/plugins/field-filter.plugin.d.ts +6 -0
  67. package/types/plugins/field-filter.plugin.d.ts.map +1 -0
  68. package/types/plugins/index.d.ts +11 -88
  69. package/types/plugins/index.d.ts.map +1 -0
  70. package/types/plugins/method-registry.plugin.d.ts +3 -0
  71. package/types/plugins/method-registry.plugin.d.ts.map +1 -0
  72. package/types/plugins/mongo-operations.plugin.d.ts +4 -0
  73. package/types/plugins/mongo-operations.plugin.d.ts.map +1 -0
  74. package/types/plugins/soft-delete.plugin.d.ts +6 -0
  75. package/types/plugins/soft-delete.plugin.d.ts.map +1 -0
  76. package/types/plugins/subdocument.plugin.d.ts +6 -0
  77. package/types/plugins/subdocument.plugin.d.ts.map +1 -0
  78. package/types/plugins/timestamp.plugin.d.ts +6 -0
  79. package/types/plugins/timestamp.plugin.d.ts.map +1 -0
  80. package/types/plugins/validation-chain.plugin.d.ts +31 -0
  81. package/types/plugins/validation-chain.plugin.d.ts.map +1 -0
  82. package/types/utils/error.d.ts +11 -0
  83. package/types/utils/error.d.ts.map +1 -0
  84. package/types/utils/field-selection.d.ts +9 -0
  85. package/types/utils/field-selection.d.ts.map +1 -0
  86. package/types/utils/index.d.ts +2 -24
  87. package/types/utils/index.d.ts.map +1 -0
package/README.md CHANGED
@@ -4,31 +4,31 @@
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
+ - ✅ **Event-driven** hooks for every operation
14
+ - ✅ **Plugin architecture** for reusable behaviors
15
+ - ✅ **TypeScript** first-class support with discriminated unions
16
+ - ✅ **Battle-tested** in production with 68 passing tests
16
17
 
17
18
  ---
18
19
 
19
20
  ## 📦 Installation
20
21
 
21
22
  ```bash
22
- npm install @classytic/mongokit mongoose mongoose-paginate-v2 mongoose-aggregate-paginate-v2
23
+ npm install @classytic/mongokit mongoose
23
24
  ```
24
25
 
25
- > **Peer Dependencies:**
26
- > - `mongoose ^8.0.0`
27
- > - `mongoose-paginate-v2 ^1.9.0` (for pagination support)
28
- > - `mongoose-aggregate-paginate-v2 ^1.1.0` (for aggregation pagination)
26
+ > **Peer Dependencies:**
27
+ > - `mongoose ^8.0.0 || ^9.0.0` (supports both Mongoose 8 and 9)
29
28
 
30
- ---
29
+ **That's it.** No additional pagination libraries needed.
31
30
 
31
+ ---
32
32
 
33
33
  ## 🚀 Quick Start
34
34
 
@@ -42,20 +42,21 @@ class UserRepository extends Repository {
42
42
  constructor() {
43
43
  super(UserModel);
44
44
  }
45
-
46
- async findActiveUsers() {
47
- return this.getAll({ filters: { status: 'active' } });
48
- }
49
45
  }
50
46
 
51
47
  const userRepo = new UserRepository();
52
48
 
53
49
  // Create
54
- const user = await userRepo.create({ name: 'John', email: 'john@example.com' });
50
+ const user = await userRepo.create({
51
+ name: 'John',
52
+ email: 'john@example.com'
53
+ });
55
54
 
56
- // Read
57
- const users = await userRepo.getAll({ pagination: { page: 1, limit: 10 } });
58
- const user = await userRepo.getById('user-id');
55
+ // Read - auto-detects pagination mode
56
+ const users = await userRepo.getAll({
57
+ page: 1,
58
+ limit: 20
59
+ });
59
60
 
60
61
  // Update
61
62
  await userRepo.update('user-id', { name: 'Jane' });
@@ -64,58 +65,332 @@ await userRepo.update('user-id', { name: 'Jane' });
64
65
  await userRepo.delete('user-id');
65
66
  ```
66
67
 
67
- ### With Express
68
+ ### Unified Pagination - One Method, Two Modes
69
+
70
+ The `getAll()` method automatically detects whether you want **offset** (page-based) or **keyset** (cursor-based) pagination:
68
71
 
69
72
  ```javascript
70
- import express from 'express';
71
- import { Repository } from '@classytic/mongokit';
73
+ // Offset pagination (page-based) - for admin dashboards
74
+ const page1 = await userRepo.getAll({
75
+ page: 1,
76
+ limit: 20,
77
+ filters: { status: 'active' },
78
+ sort: { createdAt: -1 }
79
+ });
80
+ // → { method: 'offset', docs: [...], total: 1523, pages: 77, page: 1, ... }
72
81
 
73
- const app = express();
74
- const userRepo = new Repository(UserModel);
82
+ // Keyset pagination (cursor-based) - for infinite scroll
83
+ const stream1 = await userRepo.getAll({
84
+ sort: { createdAt: -1 },
85
+ limit: 20
86
+ });
87
+ // → { method: 'keyset', docs: [...], hasMore: true, next: 'eyJ2IjoxLCJ0Ij...' }
75
88
 
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);
89
+ // Load next page with cursor
90
+ const stream2 = await userRepo.getAll({
91
+ after: stream1.next,
92
+ sort: { createdAt: -1 },
93
+ limit: 20
82
94
  });
83
95
  ```
84
96
 
85
- ### With Fastify
97
+ **Auto-detection logic:**
98
+ 1. If `page` parameter provided → **offset mode**
99
+ 2. If `after` or `cursor` parameter provided → **keyset mode**
100
+ 3. If explicit `sort` provided without `page` → **keyset mode** (first page)
101
+ 4. Otherwise → **offset mode** (default, page 1)
102
+
103
+ ---
104
+
105
+ ## 🎯 Pagination Modes Explained
106
+
107
+ ### Offset Pagination (Page-Based)
108
+
109
+ Best for: Admin dashboards, page numbers, showing total counts
86
110
 
87
111
  ```javascript
88
- import Fastify from 'fastify';
89
- import { Repository } from '@classytic/mongokit';
112
+ const result = await userRepo.getAll({
113
+ page: 1,
114
+ limit: 20,
115
+ filters: { status: 'active' },
116
+ sort: { createdAt: -1 }
117
+ });
90
118
 
91
- const fastify = Fastify();
92
- const userRepo = new Repository(UserModel);
119
+ console.log(result.method); // 'offset'
120
+ console.log(result.docs); // Array of documents
121
+ console.log(result.total); // Total count (e.g., 1523)
122
+ console.log(result.pages); // Total pages (e.g., 77)
123
+ console.log(result.page); // Current page (1)
124
+ console.log(result.hasNext); // true
125
+ console.log(result.hasPrev); // false
126
+ ```
93
127
 
94
- fastify.get('/users', async (request, reply) => {
95
- const users = await userRepo.getAll();
96
- return users;
128
+ **Performance:**
129
+ - Time complexity: O(n) where n = page × limit
130
+ - Works great for small-medium datasets
131
+ - Warning triggered for pages > 100
132
+
133
+ ### Keyset Pagination (Cursor-Based)
134
+
135
+ Best for: Infinite scroll, real-time feeds, large datasets
136
+
137
+ ```javascript
138
+ const result = await userRepo.getAll({
139
+ sort: { createdAt: -1 },
140
+ limit: 20
141
+ });
142
+
143
+ console.log(result.method); // 'keyset'
144
+ console.log(result.docs); // Array of documents
145
+ console.log(result.hasMore); // true
146
+ console.log(result.next); // 'eyJ2IjoxLCJ0IjoiZGF0ZSIsInYiO...'
147
+
148
+ // Load next page
149
+ const next = await userRepo.getAll({
150
+ after: result.next,
151
+ sort: { createdAt: -1 },
152
+ limit: 20
97
153
  });
98
154
  ```
99
155
 
100
- ### With Next.js API Routes
156
+ **Performance:**
157
+ - Time complexity: O(1) regardless of position
158
+ - Requires compound index: `{ sortField: 1, _id: 1 }`
159
+ - Ideal for millions of documents
101
160
 
161
+ **Required Index:**
102
162
  ```javascript
103
- // pages/api/users.js
104
- import { Repository } from '@classytic/mongokit';
105
- import UserModel from '@/models/User';
163
+ // For sort: { createdAt: -1 }
164
+ PostSchema.index({ createdAt: -1, _id: -1 });
106
165
 
107
- const userRepo = new Repository(UserModel);
166
+ // For sort: { publishedAt: -1, views: -1 }
167
+ PostSchema.index({ publishedAt: -1, views: -1, _id: -1 });
168
+ ```
169
+
170
+ ---
171
+
172
+ ## 💡 Real-World Examples
173
+
174
+ ### Text Search + Infinite Scroll
175
+
176
+ ```javascript
177
+ // Define schema with text index
178
+ const PostSchema = new mongoose.Schema({
179
+ title: String,
180
+ content: String,
181
+ publishedAt: { type: Date, default: Date.now }
182
+ });
183
+
184
+ PostSchema.index({ title: 'text', content: 'text' });
185
+ PostSchema.index({ publishedAt: -1, _id: -1 });
186
+
187
+ // Search and paginate
188
+ const postRepo = new Repository(PostModel);
108
189
 
109
- export default async function handler(req, res) {
110
- if (req.method === 'GET') {
111
- const users = await userRepo.getAll();
112
- res.json(users);
190
+ const page1 = await postRepo.getAll({
191
+ search: 'JavaScript',
192
+ sort: { publishedAt: -1 },
193
+ limit: 20
194
+ });
195
+ // → Returns first 20 posts matching "JavaScript"
196
+
197
+ // User scrolls down - load more
198
+ const page2 = await postRepo.getAll({
199
+ after: page1.next,
200
+ search: 'JavaScript',
201
+ sort: { publishedAt: -1 },
202
+ limit: 20
203
+ });
204
+ // → Next 20 posts with same search query
205
+ ```
206
+
207
+ ### Admin Dashboard with Filters
208
+
209
+ ```javascript
210
+ const result = await userRepo.getAll({
211
+ page: req.query.page || 1,
212
+ limit: 50,
213
+ filters: {
214
+ status: 'active',
215
+ role: { $in: ['admin', 'moderator'] }
216
+ },
217
+ sort: { lastLoginAt: -1 }
218
+ });
219
+
220
+ res.json({
221
+ users: result.docs,
222
+ pagination: {
223
+ page: result.page,
224
+ pages: result.pages,
225
+ total: result.total,
226
+ hasNext: result.hasNext,
227
+ hasPrev: result.hasPrev
228
+ }
229
+ });
230
+ ```
231
+
232
+ ### Multi-Tenant Applications
233
+
234
+ ```javascript
235
+ class TenantUserRepository extends Repository {
236
+ constructor() {
237
+ super(UserModel, [], {
238
+ defaultLimit: 20,
239
+ maxLimit: 100
240
+ });
241
+ }
242
+
243
+ async getAllForTenant(organizationId, params = {}) {
244
+ return this.getAll({
245
+ ...params,
246
+ filters: {
247
+ organizationId,
248
+ ...params.filters
249
+ }
250
+ });
113
251
  }
114
252
  }
253
+
254
+ // Use it
255
+ const users = await tenantRepo.getAllForTenant('org-123', {
256
+ page: 1,
257
+ limit: 50,
258
+ filters: { status: 'active' }
259
+ });
260
+ ```
261
+
262
+ ### Switching Between Modes Seamlessly
263
+
264
+ ```javascript
265
+ // Admin view - needs page numbers and total count
266
+ const adminView = await postRepo.getAll({
267
+ page: 1,
268
+ limit: 20,
269
+ sort: { createdAt: -1 }
270
+ });
271
+ // → method: 'offset', total: 1523, pages: 77
272
+
273
+ // Public feed - infinite scroll
274
+ const feedView = await postRepo.getAll({
275
+ sort: { createdAt: -1 },
276
+ limit: 20
277
+ });
278
+ // → method: 'keyset', next: 'eyJ2IjoxLC...'
279
+
280
+ // Both return same first 20 results!
115
281
  ```
116
282
 
117
283
  ---
118
284
 
285
+ ## 📘 Complete API Reference
286
+
287
+ ### CRUD Operations
288
+
289
+ | Method | Description | Example |
290
+ |--------|-------------|---------|
291
+ | `create(data, opts)` | Create single document | `repo.create({ name: 'John' })` |
292
+ | `createMany(data[], opts)` | Create multiple documents | `repo.createMany([{...}, {...}])` |
293
+ | `getById(id, opts)` | Find by ID | `repo.getById('123')` |
294
+ | `getByQuery(query, opts)` | Find one by query | `repo.getByQuery({ email: 'a@b.com' })` |
295
+ | `getAll(params, opts)` | Paginated list | `repo.getAll({ page: 1, limit: 20 })` |
296
+ | `getOrCreate(query, data, opts)` | Find or create | `repo.getOrCreate({ email }, { email, name })` |
297
+ | `update(id, data, opts)` | Update document | `repo.update('123', { name: 'Jane' })` |
298
+ | `delete(id, opts)` | Delete document | `repo.delete('123')` |
299
+ | `count(query, opts)` | Count documents | `repo.count({ status: 'active' })` |
300
+ | `exists(query, opts)` | Check existence | `repo.exists({ email: 'a@b.com' })` |
301
+
302
+ ### getAll() Parameters
303
+
304
+ ```javascript
305
+ await repo.getAll({
306
+ // Pagination mode (auto-detected)
307
+ page: 1, // Offset mode: page number
308
+ after: 'cursor...', // Keyset mode: cursor token
309
+ cursor: 'cursor...', // Alias for 'after'
310
+
311
+ // Common parameters
312
+ limit: 20, // Documents per page
313
+ filters: { ... }, // MongoDB query filters
314
+ sort: { createdAt: -1 }, // Sort specification
315
+ search: 'keyword', // Full-text search (requires text index)
316
+
317
+ // Additional options (in options parameter)
318
+ select: 'name email', // Field projection
319
+ populate: 'author', // Population
320
+ lean: true, // Return plain objects (default: true)
321
+ session: session // Transaction session
322
+ });
323
+ ```
324
+
325
+ ### Aggregation
326
+
327
+ ```javascript
328
+ // Basic aggregation
329
+ const result = await repo.aggregate([
330
+ { $match: { status: 'active' } },
331
+ { $group: { _id: '$category', total: { $sum: 1 } } }
332
+ ]);
333
+
334
+ // Paginated aggregation
335
+ const result = await repo.aggregatePaginate({
336
+ pipeline: [
337
+ { $match: { status: 'active' } },
338
+ { $lookup: { from: 'users', localField: 'userId', foreignField: '_id', as: 'user' } }
339
+ ],
340
+ page: 1,
341
+ limit: 20
342
+ });
343
+
344
+ // Distinct values
345
+ const categories = await repo.distinct('category', { status: 'active' });
346
+ ```
347
+
348
+ ### Transactions
349
+
350
+ ```javascript
351
+ await repo.withTransaction(async (session) => {
352
+ await repo.create({ name: 'User 1' }, { session });
353
+ await repo.create({ name: 'User 2' }, { session });
354
+ // Auto-commits if no errors, auto-rollbacks on errors
355
+ });
356
+ ```
357
+
358
+ ---
359
+
360
+ ## 🔧 Configuration
361
+
362
+ ### Pagination Configuration
363
+
364
+ ```javascript
365
+ import { Repository } from '@classytic/mongokit';
366
+
367
+ const userRepo = new Repository(UserModel, [], {
368
+ defaultLimit: 20, // Default documents per page
369
+ maxLimit: 100, // Maximum allowed limit
370
+ maxPage: 10000, // Maximum page number (offset mode)
371
+ deepPageThreshold: 100, // Warn when page exceeds this
372
+ useEstimatedCount: false, // Use estimatedDocumentCount() for speed
373
+ cursorVersion: 1 // Cursor format version
374
+ });
375
+ ```
376
+
377
+ ### Estimated Counts (for large collections)
378
+
379
+ For collections with millions of documents, counting can be slow. Use estimated counts:
380
+
381
+ ```javascript
382
+ const repo = new Repository(UserModel, [], {
383
+ useEstimatedCount: true // O(1) metadata lookup instead of O(n) count
384
+ });
385
+
386
+ const result = await repo.getAll({ page: 1, limit: 20 });
387
+ // Uses estimatedDocumentCount() - instant but approximate
388
+ ```
389
+
390
+ **Note:** Estimated counts ignore filters and sessions by design (reads metadata, not documents).
391
+
392
+ ---
393
+
119
394
  ## 🔌 Built-in Plugins
120
395
 
121
396
  ### Field Filtering (Role-based Access)
@@ -133,7 +408,7 @@ const fieldPreset = {
133
408
 
134
409
  class UserRepository extends Repository {
135
410
  constructor() {
136
- super(User, [fieldFilterPlugin(fieldPreset)]);
411
+ super(UserModel, [fieldFilterPlugin(fieldPreset)]);
137
412
  }
138
413
  }
139
414
  ```
@@ -143,17 +418,17 @@ class UserRepository extends Repository {
143
418
  Add custom validation rules:
144
419
 
145
420
  ```javascript
146
- import {
147
- Repository,
148
- validationChainPlugin,
149
- requireField,
421
+ import {
422
+ Repository,
423
+ validationChainPlugin,
424
+ requireField,
150
425
  uniqueField,
151
- immutableField
426
+ immutableField
152
427
  } from '@classytic/mongokit';
153
428
 
154
429
  class UserRepository extends Repository {
155
430
  constructor() {
156
- super(User, [
431
+ super(UserModel, [
157
432
  validationChainPlugin([
158
433
  requireField('email', ['create']),
159
434
  uniqueField('email', 'Email already exists'),
@@ -173,7 +448,7 @@ import { Repository, softDeletePlugin } from '@classytic/mongokit';
173
448
 
174
449
  class UserRepository extends Repository {
175
450
  constructor() {
176
- super(User, [softDeletePlugin({ deletedField: 'deletedAt' })]);
451
+ super(UserModel, [softDeletePlugin({ deletedField: 'deletedAt' })]);
177
452
  }
178
453
  }
179
454
 
@@ -192,7 +467,7 @@ import logger from './logger.js';
192
467
 
193
468
  class UserRepository extends Repository {
194
469
  constructor() {
195
- super(User, [auditLogPlugin(logger)]);
470
+ super(UserModel, [auditLogPlugin(logger)]);
196
471
  }
197
472
  }
198
473
 
@@ -209,53 +484,6 @@ class UserRepository extends Repository {
209
484
 
210
485
  ---
211
486
 
212
- ## 🎯 Core API
213
-
214
- ### CRUD Operations
215
-
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' })` |
228
-
229
- ### Aggregation
230
-
231
- ```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');
245
- ```
246
-
247
- ### Transactions
248
-
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
- ```
256
-
257
- ---
258
-
259
487
  ## 🎨 Event System
260
488
 
261
489
  Every operation emits lifecycle events:
@@ -287,20 +515,20 @@ repo.on('error:create', ({ context, error }) => {
287
515
 
288
516
  ---
289
517
 
290
- ## 🔧 Custom Plugins
518
+ ## 🎯 Custom Plugins
291
519
 
292
520
  Create your own plugins:
293
521
 
294
522
  ```javascript
295
523
  export const timestampPlugin = () => ({
296
524
  name: 'timestamp',
297
-
525
+
298
526
  apply(repo) {
299
527
  repo.on('before:create', (context) => {
300
528
  context.data.createdAt = new Date();
301
529
  context.data.updatedAt = new Date();
302
530
  });
303
-
531
+
304
532
  repo.on('before:update', (context) => {
305
533
  context.data.updatedAt = new Date();
306
534
  });
@@ -310,7 +538,28 @@ export const timestampPlugin = () => ({
310
538
  // Use it
311
539
  class UserRepository extends Repository {
312
540
  constructor() {
313
- super(User, [timestampPlugin()]);
541
+ super(UserModel, [timestampPlugin()]);
542
+ }
543
+ }
544
+ ```
545
+
546
+ ### Combining Multiple Plugins
547
+
548
+ ```javascript
549
+ import {
550
+ Repository,
551
+ softDeletePlugin,
552
+ auditLogPlugin,
553
+ fieldFilterPlugin
554
+ } from '@classytic/mongokit';
555
+
556
+ class UserRepository extends Repository {
557
+ constructor() {
558
+ super(UserModel, [
559
+ softDeletePlugin(),
560
+ auditLogPlugin(logger),
561
+ fieldFilterPlugin(userFieldPreset)
562
+ ]);
314
563
  }
315
564
  }
316
565
  ```
@@ -319,11 +568,15 @@ class UserRepository extends Repository {
319
568
 
320
569
  ## 📚 TypeScript Support
321
570
 
322
- Full TypeScript definitions included:
571
+ Full TypeScript support with discriminated unions:
323
572
 
324
573
  ```typescript
325
- import { Repository, Plugin, RepositoryContext } from '@classytic/mongokit';
326
- import { Model, Document } from 'mongoose';
574
+ import {
575
+ Repository,
576
+ OffsetPaginationResult,
577
+ KeysetPaginationResult
578
+ } from '@classytic/mongokit';
579
+ import { Document } from 'mongoose';
327
580
 
328
581
  interface IUser extends Document {
329
582
  name: string;
@@ -331,69 +584,174 @@ interface IUser extends Document {
331
584
  status: 'active' | 'inactive';
332
585
  }
333
586
 
334
- class UserRepository extends Repository<IUser> {
587
+ class UserRepository extends Repository {
335
588
  constructor() {
336
589
  super(UserModel);
337
590
  }
338
-
591
+
339
592
  async findActive(): Promise<IUser[]> {
340
- const result = await this.getAll({
341
- filters: { status: 'active' }
593
+ const result = await this.getAll({
594
+ filters: { status: 'active' },
595
+ page: 1,
596
+ limit: 50
342
597
  });
598
+
599
+ // TypeScript knows result is OffsetPaginationResult
600
+ if (result.method === 'offset') {
601
+ console.log(result.total); // ✅ Type-safe
602
+ console.log(result.pages); // ✅ Type-safe
603
+ // console.log(result.next); // ❌ Type error
604
+ }
605
+
606
+ return result.docs;
607
+ }
608
+
609
+ async getFeed(): Promise<IUser[]> {
610
+ const result = await this.getAll({
611
+ sort: { createdAt: -1 },
612
+ limit: 20
613
+ });
614
+
615
+ // TypeScript knows result is KeysetPaginationResult
616
+ if (result.method === 'keyset') {
617
+ console.log(result.next); // ✅ Type-safe
618
+ console.log(result.hasMore); // ✅ Type-safe
619
+ // console.log(result.total); // ❌ Type error
620
+ }
621
+
343
622
  return result.docs;
344
623
  }
345
624
  }
346
625
  ```
347
626
 
627
+ ### Import Types
628
+
629
+ ```typescript
630
+ import type {
631
+ PaginationConfig,
632
+ OffsetPaginationOptions,
633
+ KeysetPaginationOptions,
634
+ AggregatePaginationOptions,
635
+ OffsetPaginationResult,
636
+ KeysetPaginationResult,
637
+ AggregatePaginationResult
638
+ } from '@classytic/mongokit';
639
+ ```
640
+
348
641
  ---
349
642
 
350
- ## 🏗️ Advanced Patterns
643
+ ## 🏎️ Performance Tips
351
644
 
352
- ### Custom Methods
645
+ ### 1. Use Keyset Pagination for Large Datasets
353
646
 
354
647
  ```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
- }
648
+ // Slow for large datasets (millions of documents)
649
+ await repo.getAll({ page: 1000, limit: 50 }); // O(50000)
650
+
651
+ // ✅ Fast regardless of position
652
+ await repo.getAll({ after: cursor, limit: 50 }); // O(1)
376
653
  ```
377
654
 
378
- ### Combining Multiple Plugins
655
+ ### 2. Create Proper Indexes
379
656
 
380
657
  ```javascript
381
- import {
382
- Repository,
383
- softDeletePlugin,
384
- auditLogPlugin,
385
- fieldFilterPlugin
386
- } from '@classytic/mongokit';
658
+ // For keyset pagination with sort
659
+ PostSchema.index({ createdAt: -1, _id: -1 });
387
660
 
388
- class UserRepository extends Repository {
389
- constructor() {
390
- super(User, [
391
- softDeletePlugin(),
392
- auditLogPlugin(logger),
393
- fieldFilterPlugin(userFieldPreset)
394
- ]);
395
- }
396
- }
661
+ // For multi-tenant keyset pagination
662
+ UserSchema.index({ organizationId: 1, createdAt: -1, _id: -1 });
663
+
664
+ // For text search
665
+ PostSchema.index({ title: 'text', content: 'text' });
666
+ ```
667
+
668
+ ### 3. Use Estimated Counts for Large Collections
669
+
670
+ ```javascript
671
+ const repo = new Repository(UserModel, [], {
672
+ useEstimatedCount: true // Instant counts for >10M documents
673
+ });
674
+ ```
675
+
676
+ ### 4. Use Lean Queries (Enabled by Default)
677
+
678
+ ```javascript
679
+ // Lean is true by default - returns plain objects
680
+ const result = await repo.getAll({ page: 1 });
681
+
682
+ // Disable for Mongoose documents (if you need methods)
683
+ const result = await repo.getAll({ page: 1 }, { lean: false });
684
+ ```
685
+
686
+ ### 5. Limit $facet Results in Aggregation
687
+
688
+ ```javascript
689
+ // ⚠️ Warning triggered automatically at limit > 1000
690
+ await repo.aggregatePaginate({
691
+ pipeline: [...],
692
+ limit: 2000 // Warning: $facet results must be <16MB
693
+ });
694
+ ```
695
+
696
+ ---
697
+
698
+ ## 🔄 Migration Guide
699
+
700
+ ### From mongoose-paginate-v2
701
+
702
+ ```javascript
703
+ // Before
704
+ import mongoosePaginate from 'mongoose-paginate-v2';
705
+ UserSchema.plugin(mongoosePaginate);
706
+ const result = await UserModel.paginate({ status: 'active' }, { page: 1, limit: 10 });
707
+
708
+ // After
709
+ import { Repository } from '@classytic/mongokit';
710
+ const userRepo = new Repository(UserModel);
711
+ const result = await userRepo.getAll({
712
+ filters: { status: 'active' },
713
+ page: 1,
714
+ limit: 10
715
+ });
716
+ ```
717
+
718
+ ### From Prisma
719
+
720
+ ```javascript
721
+ // Before (Prisma)
722
+ const users = await prisma.user.findMany({
723
+ where: { status: 'active' },
724
+ skip: 20,
725
+ take: 10
726
+ });
727
+
728
+ // After (MongoKit)
729
+ const result = await userRepo.getAll({
730
+ filters: { status: 'active' },
731
+ page: 3,
732
+ limit: 10
733
+ });
734
+ const users = result.docs;
735
+ ```
736
+
737
+ ### From TypeORM
738
+
739
+ ```javascript
740
+ // Before (TypeORM)
741
+ const [users, total] = await userRepository.findAndCount({
742
+ where: { status: 'active' },
743
+ skip: 20,
744
+ take: 10
745
+ });
746
+
747
+ // After (MongoKit)
748
+ const result = await userRepo.getAll({
749
+ filters: { status: 'active' },
750
+ page: 3,
751
+ limit: 10
752
+ });
753
+ const users = result.docs;
754
+ const total = result.total;
397
755
  ```
398
756
 
399
757
  ---
@@ -402,11 +760,18 @@ class UserRepository extends Repository {
402
760
 
403
761
  ### vs. Mongoose Directly
404
762
  - ✅ Consistent API across all models
405
- - ✅ Built-in pagination, filtering, sorting
763
+ - ✅ Built-in pagination (offset + cursor) with zero dependencies
406
764
  - ✅ Multi-tenancy without repetitive code
407
765
  - ✅ Event hooks for cross-cutting concerns
408
766
  - ✅ Plugin system for reusable behaviors
409
767
 
768
+ ### vs. mongoose-paginate-v2
769
+ - ✅ Zero external dependencies (no mongoose-paginate-v2 needed)
770
+ - ✅ Cursor-based pagination for infinite scroll
771
+ - ✅ Unified API that auto-detects pagination mode
772
+ - ✅ Native MongoDB implementation ($facet, cursors)
773
+ - ✅ Better TypeScript support
774
+
410
775
  ### vs. TypeORM / Prisma
411
776
  - ✅ Lighter weight (works with Mongoose)
412
777
  - ✅ Event-driven architecture
@@ -415,10 +780,10 @@ class UserRepository extends Repository {
415
780
  - ✅ Framework-agnostic
416
781
 
417
782
  ### vs. Raw Repository Pattern
418
- - ✅ Battle-tested implementation
783
+ - ✅ Battle-tested implementation (68 passing tests)
419
784
  - ✅ 11 built-in plugins ready to use
420
785
  - ✅ Comprehensive documentation
421
- - ✅ TypeScript support
786
+ - ✅ TypeScript discriminated unions
422
787
  - ✅ Active maintenance
423
788
 
424
789
  ---
@@ -429,9 +794,51 @@ class UserRepository extends Repository {
429
794
  npm test
430
795
  ```
431
796
 
797
+ **Test Coverage:**
798
+ - 68 tests (67 passing, 1 skipped - requires replica set)
799
+ - CRUD operations
800
+ - Offset pagination
801
+ - Keyset pagination
802
+ - Aggregation pagination
803
+ - Multi-tenancy
804
+ - Text search + infinite scroll
805
+ - Real-world scenarios
806
+
807
+ ---
808
+
809
+ ## 📖 Examples
810
+
811
+ Check out the [examples](./examples) directory for:
812
+ - Express REST API
813
+ - Fastify REST API
814
+ - Next.js API routes
815
+ - Multi-tenant SaaS
816
+ - Infinite scroll feed
817
+ - Admin dashboard
818
+
819
+ ---
820
+
821
+ ## 🤝 Contributing
822
+
823
+ Contributions are welcome! Please check out our [contributing guide](CONTRIBUTING.md).
824
+
432
825
  ---
433
826
 
434
827
  ## 📄 License
435
828
 
436
829
  MIT © [Classytic](https://github.com/classytic)
437
830
 
831
+ ---
832
+
833
+ ## 🔗 Links
834
+
835
+ - [GitHub Repository](https://github.com/classytic/mongokit)
836
+ - [npm Package](https://www.npmjs.com/package/@classytic/mongokit)
837
+ - [Documentation](https://github.com/classytic/mongokit#readme)
838
+ - [Issue Tracker](https://github.com/classytic/mongokit/issues)
839
+
840
+ ---
841
+
842
+ **Built with ❤️ by developers, for developers.**
843
+
844
+ Zero dependencies. Zero compromises. Production-grade MongoDB pagination.