@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.
- package/README.md +772 -151
- package/dist/actions/index.cjs +479 -0
- package/dist/actions/index.cjs.map +1 -0
- package/dist/actions/index.d.cts +3 -0
- package/dist/actions/index.d.ts +3 -0
- package/dist/actions/index.js +473 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/index-BfVJZF-3.d.cts +337 -0
- package/dist/index-CgOJ2pqz.d.ts +337 -0
- package/dist/index.cjs +2142 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +239 -0
- package/dist/index.d.ts +239 -0
- package/dist/index.js +2108 -0
- package/dist/index.js.map +1 -0
- package/dist/memory-cache-DG2oSSbx.d.ts +142 -0
- package/dist/memory-cache-DqfFfKes.d.cts +142 -0
- package/dist/pagination/PaginationEngine.cjs +375 -0
- package/dist/pagination/PaginationEngine.cjs.map +1 -0
- package/dist/pagination/PaginationEngine.d.cts +117 -0
- package/dist/pagination/PaginationEngine.d.ts +117 -0
- package/dist/pagination/PaginationEngine.js +369 -0
- package/dist/pagination/PaginationEngine.js.map +1 -0
- package/dist/plugins/index.cjs +874 -0
- package/dist/plugins/index.cjs.map +1 -0
- package/dist/plugins/index.d.cts +275 -0
- package/dist/plugins/index.d.ts +275 -0
- package/dist/plugins/index.js +857 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/types-Nxhmi1aI.d.cts +510 -0
- package/dist/types-Nxhmi1aI.d.ts +510 -0
- package/dist/utils/index.cjs +667 -0
- package/dist/utils/index.cjs.map +1 -0
- package/dist/utils/index.d.cts +189 -0
- package/dist/utils/index.d.ts +189 -0
- package/dist/utils/index.js +643 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +54 -24
- package/src/Repository.js +0 -225
- package/src/actions/aggregate.js +0 -191
- package/src/actions/create.js +0 -59
- package/src/actions/delete.js +0 -88
- package/src/actions/index.js +0 -11
- package/src/actions/read.js +0 -156
- package/src/actions/update.js +0 -176
- package/src/hooks/lifecycle.js +0 -146
- package/src/index.js +0 -60
- package/src/plugins/aggregate-helpers.plugin.js +0 -71
- package/src/plugins/audit-log.plugin.js +0 -60
- package/src/plugins/batch-operations.plugin.js +0 -66
- package/src/plugins/field-filter.plugin.js +0 -27
- package/src/plugins/index.js +0 -19
- package/src/plugins/method-registry.plugin.js +0 -140
- package/src/plugins/mongo-operations.plugin.js +0 -313
- package/src/plugins/soft-delete.plugin.js +0 -46
- package/src/plugins/subdocument.plugin.js +0 -66
- package/src/plugins/timestamp.plugin.js +0 -19
- package/src/plugins/validation-chain.plugin.js +0 -145
- package/src/utils/field-selection.js +0 -156
- package/src/utils/index.js +0 -12
- package/types/actions/index.d.ts +0 -121
- package/types/index.d.ts +0 -104
- package/types/plugins/index.d.ts +0 -88
- package/types/utils/index.d.ts +0 -24
package/README.md
CHANGED
|
@@ -4,31 +4,39 @@
|
|
|
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
|
+
- ✅ **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
|
|
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({
|
|
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({
|
|
58
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
const
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
120
|
+
const result = await userRepo.getAll({
|
|
121
|
+
page: 1,
|
|
122
|
+
limit: 20,
|
|
123
|
+
filters: { status: 'active' },
|
|
124
|
+
sort: { createdAt: -1 }
|
|
125
|
+
});
|
|
90
126
|
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
104
|
-
|
|
105
|
-
import UserModel from '@/models/User';
|
|
171
|
+
// For sort: { createdAt: -1 }
|
|
172
|
+
PostSchema.index({ createdAt: -1, _id: -1 });
|
|
106
173
|
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
638
|
+
super(UserModel, [auditLogPlugin(logger)]);
|
|
196
639
|
}
|
|
197
640
|
}
|
|
198
641
|
|
|
199
642
|
// All CUD operations automatically logged
|
|
200
643
|
```
|
|
201
644
|
|
|
202
|
-
###
|
|
645
|
+
### Caching (Redis, Memcached, or In-Memory)
|
|
203
646
|
|
|
204
|
-
|
|
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
|
-
|
|
661
|
+
// Reads are cached automatically
|
|
662
|
+
const user = await userRepo.getById(id); // cached on second call
|
|
213
663
|
|
|
214
|
-
|
|
664
|
+
// Skip cache for fresh data
|
|
665
|
+
const fresh = await userRepo.getById(id, { skipCache: true });
|
|
215
666
|
|
|
216
|
-
|
|
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
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
###
|
|
685
|
+
### More Plugins
|
|
248
686
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
##
|
|
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(
|
|
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
|
|
779
|
+
Full TypeScript support with discriminated unions:
|
|
323
780
|
|
|
324
781
|
```typescript
|
|
325
|
-
import {
|
|
326
|
-
|
|
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
|
|
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
|
-
##
|
|
851
|
+
## 🏎️ Performance Tips
|
|
351
852
|
|
|
352
|
-
###
|
|
853
|
+
### 1. Use Keyset Pagination for Large Datasets
|
|
353
854
|
|
|
354
855
|
```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
|
-
}
|
|
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
|
-
###
|
|
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
|
-
|
|
382
|
-
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
|
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
|
|
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.
|