@classytic/mongokit 1.0.2 → 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.
- package/README.md +562 -155
- package/package.json +17 -10
- package/src/Repository.js +296 -225
- package/src/actions/aggregate.js +266 -191
- package/src/actions/create.js +47 -47
- package/src/actions/delete.js +88 -88
- package/src/actions/index.js +11 -11
- package/src/actions/read.js +176 -144
- package/src/actions/update.js +144 -144
- package/src/hooks/lifecycle.js +146 -146
- package/src/index.js +71 -60
- package/src/pagination/PaginationEngine.js +348 -0
- package/src/pagination/utils/cursor.js +119 -0
- package/src/pagination/utils/filter.js +42 -0
- package/src/pagination/utils/limits.js +82 -0
- package/src/pagination/utils/sort.js +101 -0
- package/src/plugins/aggregate-helpers.plugin.js +71 -71
- package/src/plugins/audit-log.plugin.js +60 -60
- package/src/plugins/batch-operations.plugin.js +66 -66
- package/src/plugins/field-filter.plugin.js +27 -27
- package/src/plugins/index.js +19 -19
- package/src/plugins/method-registry.plugin.js +140 -140
- package/src/plugins/mongo-operations.plugin.js +317 -313
- package/src/plugins/soft-delete.plugin.js +46 -46
- package/src/plugins/subdocument.plugin.js +66 -66
- package/src/plugins/timestamp.plugin.js +19 -19
- package/src/plugins/validation-chain.plugin.js +145 -145
- package/src/types.d.ts +87 -0
- package/src/utils/error.js +12 -0
- package/src/utils/field-selection.js +156 -156
- package/src/utils/index.js +12 -12
- package/types/Repository.d.ts +95 -0
- package/types/Repository.d.ts.map +1 -0
- package/types/actions/aggregate.d.ts +112 -0
- package/types/actions/aggregate.d.ts.map +1 -0
- package/types/actions/create.d.ts +21 -0
- package/types/actions/create.d.ts.map +1 -0
- package/types/actions/delete.d.ts +37 -0
- package/types/actions/delete.d.ts.map +1 -0
- package/types/actions/index.d.ts +6 -121
- package/types/actions/index.d.ts.map +1 -0
- package/types/actions/read.d.ts +135 -0
- package/types/actions/read.d.ts.map +1 -0
- package/types/actions/update.d.ts +58 -0
- package/types/actions/update.d.ts.map +1 -0
- package/types/hooks/lifecycle.d.ts +44 -0
- package/types/hooks/lifecycle.d.ts.map +1 -0
- package/types/index.d.ts +25 -104
- package/types/index.d.ts.map +1 -0
- package/types/pagination/PaginationEngine.d.ts +386 -0
- package/types/pagination/PaginationEngine.d.ts.map +1 -0
- package/types/pagination/utils/cursor.d.ts +40 -0
- package/types/pagination/utils/cursor.d.ts.map +1 -0
- package/types/pagination/utils/filter.d.ts +28 -0
- package/types/pagination/utils/filter.d.ts.map +1 -0
- package/types/pagination/utils/limits.d.ts +64 -0
- package/types/pagination/utils/limits.d.ts.map +1 -0
- package/types/pagination/utils/sort.d.ts +41 -0
- package/types/pagination/utils/sort.d.ts.map +1 -0
- package/types/plugins/aggregate-helpers.plugin.d.ts +6 -0
- package/types/plugins/aggregate-helpers.plugin.d.ts.map +1 -0
- package/types/plugins/audit-log.plugin.d.ts +6 -0
- package/types/plugins/audit-log.plugin.d.ts.map +1 -0
- package/types/plugins/batch-operations.plugin.d.ts +6 -0
- package/types/plugins/batch-operations.plugin.d.ts.map +1 -0
- package/types/plugins/field-filter.plugin.d.ts +6 -0
- package/types/plugins/field-filter.plugin.d.ts.map +1 -0
- package/types/plugins/index.d.ts +11 -88
- package/types/plugins/index.d.ts.map +1 -0
- package/types/plugins/method-registry.plugin.d.ts +3 -0
- package/types/plugins/method-registry.plugin.d.ts.map +1 -0
- package/types/plugins/mongo-operations.plugin.d.ts +4 -0
- package/types/plugins/mongo-operations.plugin.d.ts.map +1 -0
- package/types/plugins/soft-delete.plugin.d.ts +6 -0
- package/types/plugins/soft-delete.plugin.d.ts.map +1 -0
- package/types/plugins/subdocument.plugin.d.ts +6 -0
- package/types/plugins/subdocument.plugin.d.ts.map +1 -0
- package/types/plugins/timestamp.plugin.d.ts +6 -0
- package/types/plugins/timestamp.plugin.d.ts.map +1 -0
- package/types/plugins/validation-chain.plugin.d.ts +31 -0
- package/types/plugins/validation-chain.plugin.d.ts.map +1 -0
- package/types/utils/error.d.ts +11 -0
- package/types/utils/error.d.ts.map +1 -0
- package/types/utils/field-selection.d.ts +9 -0
- package/types/utils/field-selection.d.ts.map +1 -0
- package/types/utils/index.d.ts +2 -24
- package/types/utils/index.d.ts.map +1 -0
package/README.md
CHANGED
|
@@ -4,31 +4,31 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@classytic/mongokit)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
|
|
7
|
-
>
|
|
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
|
-
- ✅ **
|
|
12
|
-
- ✅ **
|
|
13
|
-
- ✅ **
|
|
14
|
-
- ✅ **
|
|
15
|
-
- ✅ **
|
|
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
|
|
23
|
+
npm install @classytic/mongokit mongoose
|
|
23
24
|
```
|
|
24
25
|
|
|
25
26
|
> **Peer Dependencies:**
|
|
26
27
|
> - `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
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({
|
|
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({
|
|
58
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
const
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
112
|
+
const result = await userRepo.getAll({
|
|
113
|
+
page: 1,
|
|
114
|
+
limit: 20,
|
|
115
|
+
filters: { status: 'active' },
|
|
116
|
+
sort: { createdAt: -1 }
|
|
117
|
+
});
|
|
90
118
|
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
104
|
-
|
|
105
|
-
import UserModel from '@/models/User';
|
|
163
|
+
// For sort: { createdAt: -1 }
|
|
164
|
+
PostSchema.index({ createdAt: -1, _id: -1 });
|
|
106
165
|
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
##
|
|
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(
|
|
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
|
|
571
|
+
Full TypeScript support with discriminated unions:
|
|
323
572
|
|
|
324
573
|
```typescript
|
|
325
|
-
import {
|
|
326
|
-
|
|
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
|
|
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
|
-
##
|
|
643
|
+
## 🏎️ Performance Tips
|
|
351
644
|
|
|
352
|
-
###
|
|
645
|
+
### 1. Use Keyset Pagination for Large Datasets
|
|
353
646
|
|
|
354
647
|
```javascript
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
###
|
|
655
|
+
### 2. Create Proper Indexes
|
|
379
656
|
|
|
380
657
|
```javascript
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
|
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
|
|
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.
|