@classytic/mongokit 3.0.0 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +251 -846
- package/dist/actions/index.d.ts +2 -2
- package/dist/actions/index.js +13 -2
- package/dist/{index-CgOJ2pqz.d.ts → index-CMCrkd2v.d.ts} +11 -11
- package/dist/index.d.ts +19 -17
- package/dist/index.js +250 -23
- package/dist/{memory-cache-DG2oSSbx.d.ts → memory-cache-Bn_-Kk-0.d.ts} +1 -1
- package/dist/pagination/PaginationEngine.d.ts +1 -1
- package/dist/pagination/PaginationEngine.js +0 -2
- package/dist/plugins/index.d.ts +37 -2
- package/dist/plugins/index.js +181 -4
- package/dist/{types-Nxhmi1aI.d.ts → types-B3dPUKjs.d.ts} +28 -2
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js +0 -2
- package/package.json +2 -1
- package/dist/actions/index.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/pagination/PaginationEngine.js.map +0 -1
- package/dist/plugins/index.js.map +0 -1
- package/dist/utils/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,429 +1,111 @@
|
|
|
1
1
|
# @classytic/mongokit
|
|
2
2
|
|
|
3
|
-
[](https://github.com/classytic/mongokit/actions/workflows/test.yml)
|
|
4
3
|
[](https://www.npmjs.com/package/@classytic/mongokit)
|
|
5
4
|
[](https://opensource.org/licenses/MIT)
|
|
6
5
|
|
|
7
|
-
> Production-grade MongoDB
|
|
6
|
+
> Production-grade MongoDB repository pattern with zero external dependencies
|
|
8
7
|
|
|
9
|
-
**Works with:** Express
|
|
8
|
+
**Works with:** Express, Fastify, NestJS, Next.js, Koa, Hapi, Serverless
|
|
10
9
|
|
|
11
|
-
|
|
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
|
|
10
|
+
## Features
|
|
19
11
|
|
|
20
|
-
|
|
12
|
+
- **Zero dependencies** - Only Mongoose as peer dependency
|
|
13
|
+
- **Smart pagination** - Auto-detects offset vs cursor-based
|
|
14
|
+
- **Event-driven** - Pre/post hooks for all operations
|
|
15
|
+
- **12 built-in plugins** - Caching, soft delete, validation, audit logs, and more
|
|
16
|
+
- **TypeScript first** - Full type safety with discriminated unions
|
|
17
|
+
- **194 passing tests** - Battle-tested and production-ready
|
|
21
18
|
|
|
22
|
-
##
|
|
19
|
+
## Installation
|
|
23
20
|
|
|
24
21
|
```bash
|
|
25
22
|
npm install @classytic/mongokit mongoose
|
|
26
23
|
```
|
|
27
24
|
|
|
28
|
-
>
|
|
29
|
-
> - `mongoose ^8.0.0 || ^9.0.0` (supports both Mongoose 8 and 9)
|
|
25
|
+
> Supports Mongoose `^8.0.0` and `^9.0.0`
|
|
30
26
|
|
|
31
|
-
|
|
32
|
-
```javascript
|
|
33
|
-
import { MongooseRepository } from '@classytic/mongokit'; // Core repository
|
|
34
|
-
import { queryParser, buildCrudSchemasFromModel } from '@classytic/mongokit/utils'; // HTTP utilities
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
**That's it.** No additional pagination, validation, or query parsing libraries needed.
|
|
38
|
-
|
|
39
|
-
---
|
|
40
|
-
|
|
41
|
-
## 🚀 Quick Start
|
|
42
|
-
|
|
43
|
-
### Basic Usage
|
|
27
|
+
## Quick Start
|
|
44
28
|
|
|
45
29
|
```javascript
|
|
46
30
|
import { Repository } from '@classytic/mongokit';
|
|
47
31
|
import UserModel from './models/User.js';
|
|
48
32
|
|
|
49
|
-
|
|
50
|
-
constructor() {
|
|
51
|
-
super(UserModel);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const userRepo = new UserRepository();
|
|
33
|
+
const userRepo = new Repository(UserModel);
|
|
56
34
|
|
|
57
35
|
// Create
|
|
58
|
-
const user = await userRepo.create({
|
|
59
|
-
name: 'John',
|
|
60
|
-
email: 'john@example.com'
|
|
61
|
-
});
|
|
36
|
+
const user = await userRepo.create({ name: 'John', email: 'john@example.com' });
|
|
62
37
|
|
|
63
|
-
// Read
|
|
64
|
-
const users = await userRepo.getAll({
|
|
65
|
-
page: 1,
|
|
66
|
-
limit: 20
|
|
67
|
-
});
|
|
38
|
+
// Read with auto-detected pagination
|
|
39
|
+
const users = await userRepo.getAll({ page: 1, limit: 20 });
|
|
68
40
|
|
|
69
41
|
// Update
|
|
70
|
-
await userRepo.update(
|
|
42
|
+
await userRepo.update(user._id, { name: 'Jane' });
|
|
71
43
|
|
|
72
44
|
// Delete
|
|
73
|
-
await userRepo.delete(
|
|
45
|
+
await userRepo.delete(user._id);
|
|
74
46
|
```
|
|
75
47
|
|
|
76
|
-
|
|
48
|
+
## Pagination
|
|
77
49
|
|
|
78
|
-
|
|
50
|
+
`getAll()` automatically detects pagination mode based on parameters:
|
|
79
51
|
|
|
80
52
|
```javascript
|
|
81
|
-
// Offset pagination (page-based) - for
|
|
82
|
-
const
|
|
53
|
+
// Offset pagination (page-based) - for dashboards
|
|
54
|
+
const result = await repo.getAll({
|
|
83
55
|
page: 1,
|
|
84
56
|
limit: 20,
|
|
85
57
|
filters: { status: 'active' },
|
|
86
58
|
sort: { createdAt: -1 }
|
|
87
59
|
});
|
|
88
|
-
// → { method: 'offset', docs
|
|
60
|
+
// → { method: 'offset', docs, total, pages, hasNext, hasPrev }
|
|
89
61
|
|
|
90
62
|
// Keyset pagination (cursor-based) - for infinite scroll
|
|
91
|
-
const
|
|
63
|
+
const stream = await repo.getAll({
|
|
92
64
|
sort: { createdAt: -1 },
|
|
93
65
|
limit: 20
|
|
94
66
|
});
|
|
95
|
-
// → { method: 'keyset', docs
|
|
67
|
+
// → { method: 'keyset', docs, hasMore, next: 'eyJ2IjoxLC...' }
|
|
96
68
|
|
|
97
|
-
//
|
|
98
|
-
const
|
|
99
|
-
after:
|
|
69
|
+
// Next page with cursor
|
|
70
|
+
const next = await repo.getAll({
|
|
71
|
+
after: stream.next,
|
|
100
72
|
sort: { createdAt: -1 },
|
|
101
73
|
limit: 20
|
|
102
74
|
});
|
|
103
75
|
```
|
|
104
76
|
|
|
105
|
-
**Auto-detection
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
---
|
|
77
|
+
**Auto-detection rules:**
|
|
78
|
+
- `page` parameter → offset mode
|
|
79
|
+
- `after`/`cursor` parameter → keyset mode
|
|
80
|
+
- `sort` without `page` → keyset mode (first page)
|
|
81
|
+
- Default → offset mode (page 1)
|
|
112
82
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
### Offset Pagination (Page-Based)
|
|
116
|
-
|
|
117
|
-
Best for: Admin dashboards, page numbers, showing total counts
|
|
83
|
+
### Required Indexes
|
|
118
84
|
|
|
119
85
|
```javascript
|
|
120
|
-
|
|
121
|
-
page: 1,
|
|
122
|
-
limit: 20,
|
|
123
|
-
filters: { status: 'active' },
|
|
124
|
-
sort: { createdAt: -1 }
|
|
125
|
-
});
|
|
126
|
-
|
|
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
|
|
140
|
-
|
|
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
|
|
161
|
-
});
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
**Performance:**
|
|
165
|
-
- Time complexity: O(1) regardless of position
|
|
166
|
-
- Requires compound index: `{ sortField: 1, _id: 1 }`
|
|
167
|
-
- Ideal for millions of documents
|
|
168
|
-
|
|
169
|
-
**Required Index:**
|
|
170
|
-
```javascript
|
|
171
|
-
// For sort: { createdAt: -1 }
|
|
86
|
+
// For keyset pagination: sort field + _id
|
|
172
87
|
PostSchema.index({ createdAt: -1, _id: -1 });
|
|
173
88
|
|
|
174
|
-
// For
|
|
175
|
-
|
|
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
|
|
216
|
-
|
|
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
|
-
});
|
|
259
|
-
}
|
|
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
|
-
});
|
|
89
|
+
// For multi-tenant: tenant + sort field + _id
|
|
90
|
+
UserSchema.index({ organizationId: 1, createdAt: -1, _id: -1 });
|
|
370
91
|
```
|
|
371
92
|
|
|
372
|
-
|
|
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
|
|
93
|
+
## API Reference
|
|
389
94
|
|
|
390
95
|
### CRUD Operations
|
|
391
96
|
|
|
392
|
-
| Method | Description |
|
|
393
|
-
|
|
394
|
-
| `create(data, opts)` | Create single document |
|
|
395
|
-
| `createMany(data[], opts)` | Create multiple documents |
|
|
396
|
-
| `getById(id, opts)` | Find by ID |
|
|
397
|
-
| `getByQuery(query, opts)` | Find one by query |
|
|
398
|
-
| `getAll(params, opts)` | Paginated list
|
|
399
|
-
| `getOrCreate(query, data, opts)` | Find or create |
|
|
400
|
-
| `update(id, data, opts)` | Update document |
|
|
401
|
-
| `delete(id, opts)` | Delete document |
|
|
402
|
-
| `count(query, opts)` | Count documents |
|
|
403
|
-
| `exists(query, opts)` | Check existence |
|
|
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
|
-
```
|
|
97
|
+
| Method | Description |
|
|
98
|
+
|--------|-------------|
|
|
99
|
+
| `create(data, opts)` | Create single document |
|
|
100
|
+
| `createMany(data[], opts)` | Create multiple documents |
|
|
101
|
+
| `getById(id, opts)` | Find by ID |
|
|
102
|
+
| `getByQuery(query, opts)` | Find one by query |
|
|
103
|
+
| `getAll(params, opts)` | Paginated list (auto-detects mode) |
|
|
104
|
+
| `getOrCreate(query, data, opts)` | Find or create |
|
|
105
|
+
| `update(id, data, opts)` | Update document |
|
|
106
|
+
| `delete(id, opts)` | Delete document |
|
|
107
|
+
| `count(query, opts)` | Count documents |
|
|
108
|
+
| `exists(query, opts)` | Check existence |
|
|
427
109
|
|
|
428
110
|
### Aggregation
|
|
429
111
|
|
|
@@ -436,10 +118,7 @@ const result = await repo.aggregate([
|
|
|
436
118
|
|
|
437
119
|
// Paginated aggregation
|
|
438
120
|
const result = await repo.aggregatePaginate({
|
|
439
|
-
pipeline: [
|
|
440
|
-
{ $match: { status: 'active' } },
|
|
441
|
-
{ $lookup: { from: 'users', localField: 'userId', foreignField: '_id', as: 'user' } }
|
|
442
|
-
],
|
|
121
|
+
pipeline: [...],
|
|
443
122
|
page: 1,
|
|
444
123
|
limit: 20
|
|
445
124
|
});
|
|
@@ -454,605 +133,331 @@ const categories = await repo.distinct('category', { status: 'active' });
|
|
|
454
133
|
await repo.withTransaction(async (session) => {
|
|
455
134
|
await repo.create({ name: 'User 1' }, { session });
|
|
456
135
|
await repo.create({ name: 'User 2' }, { session });
|
|
457
|
-
// Auto-commits
|
|
136
|
+
// Auto-commits on success, auto-rollbacks on error
|
|
458
137
|
});
|
|
459
138
|
```
|
|
460
139
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
## 🔧 Configuration
|
|
464
|
-
|
|
465
|
-
### Pagination Configuration
|
|
140
|
+
## Configuration
|
|
466
141
|
|
|
467
142
|
```javascript
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
const userRepo = new Repository(UserModel, [], {
|
|
471
|
-
defaultLimit: 20, // Default documents per page
|
|
143
|
+
const repo = new Repository(UserModel, plugins, {
|
|
144
|
+
defaultLimit: 20, // Default docs per page
|
|
472
145
|
maxLimit: 100, // Maximum allowed limit
|
|
473
|
-
maxPage: 10000, // Maximum page number
|
|
146
|
+
maxPage: 10000, // Maximum page number
|
|
474
147
|
deepPageThreshold: 100, // Warn when page exceeds this
|
|
475
|
-
useEstimatedCount: false, // Use
|
|
148
|
+
useEstimatedCount: false, // Use fast estimated counts
|
|
476
149
|
cursorVersion: 1 // Cursor format version
|
|
477
150
|
});
|
|
478
151
|
```
|
|
479
152
|
|
|
480
|
-
|
|
153
|
+
## Plugins
|
|
481
154
|
|
|
482
|
-
|
|
155
|
+
### Using Plugins
|
|
483
156
|
|
|
484
157
|
```javascript
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
158
|
+
import {
|
|
159
|
+
Repository,
|
|
160
|
+
timestampPlugin,
|
|
161
|
+
softDeletePlugin,
|
|
162
|
+
cachePlugin,
|
|
163
|
+
createMemoryCache
|
|
164
|
+
} from '@classytic/mongokit';
|
|
488
165
|
|
|
489
|
-
const
|
|
490
|
-
|
|
166
|
+
const repo = new Repository(UserModel, [
|
|
167
|
+
timestampPlugin(),
|
|
168
|
+
softDeletePlugin(),
|
|
169
|
+
cachePlugin({ adapter: createMemoryCache(), ttl: 60 })
|
|
170
|
+
]);
|
|
491
171
|
```
|
|
492
172
|
|
|
493
|
-
|
|
173
|
+
### Available Plugins
|
|
494
174
|
|
|
495
|
-
|
|
175
|
+
| Plugin | Description |
|
|
176
|
+
|--------|-------------|
|
|
177
|
+
| `timestampPlugin()` | Auto-manage `createdAt`/`updatedAt` |
|
|
178
|
+
| `softDeletePlugin(opts)` | Mark as deleted instead of removing |
|
|
179
|
+
| `auditLogPlugin(logger)` | Log all CUD operations |
|
|
180
|
+
| `cachePlugin(opts)` | Redis/Memcached/memory caching with auto-invalidation |
|
|
181
|
+
| `validationChainPlugin(validators)` | Custom validation rules |
|
|
182
|
+
| `fieldFilterPlugin(preset)` | Role-based field visibility |
|
|
183
|
+
| `cascadePlugin(opts)` | Auto-delete related documents |
|
|
184
|
+
| `methodRegistryPlugin()` | Dynamic method registration (required by plugins below) |
|
|
185
|
+
| `mongoOperationsPlugin()` | Adds `increment`, `pushToArray`, `upsert`, etc. |
|
|
186
|
+
| `batchOperationsPlugin()` | Adds `updateMany`, `deleteMany` |
|
|
187
|
+
| `aggregateHelpersPlugin()` | Adds `groupBy`, `sum`, `average`, etc. |
|
|
188
|
+
| `subdocumentPlugin()` | Manage subdocument arrays |
|
|
496
189
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
**Critical:** MongoDB only auto-indexes `_id`. You must create indexes for efficient pagination.
|
|
500
|
-
|
|
501
|
-
### Single-Tenant Applications
|
|
190
|
+
### Soft Delete
|
|
502
191
|
|
|
503
192
|
```javascript
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
});
|
|
193
|
+
const repo = new Repository(UserModel, [
|
|
194
|
+
softDeletePlugin({ deletedField: 'deletedAt' })
|
|
195
|
+
]);
|
|
508
196
|
|
|
509
|
-
//
|
|
510
|
-
|
|
511
|
-
//
|
|
512
|
-
// Sort field Tie-breaker
|
|
197
|
+
await repo.delete(id); // Marks as deleted
|
|
198
|
+
await repo.getAll(); // Excludes deleted
|
|
199
|
+
await repo.getAll({ includeDeleted: true }); // Includes deleted
|
|
513
200
|
```
|
|
514
201
|
|
|
515
|
-
###
|
|
202
|
+
### Caching
|
|
516
203
|
|
|
517
204
|
```javascript
|
|
518
|
-
|
|
519
|
-
organizationId: String,
|
|
520
|
-
email: String,
|
|
521
|
-
createdAt: { type: Date, default: Date.now }
|
|
522
|
-
});
|
|
205
|
+
import { cachePlugin, createMemoryCache } from '@classytic/mongokit';
|
|
523
206
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
//
|
|
527
|
-
//
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
```javascript
|
|
533
|
-
// Basic sorting
|
|
534
|
-
Schema.index({ createdAt: -1, _id: -1 });
|
|
207
|
+
const repo = new Repository(UserModel, [
|
|
208
|
+
cachePlugin({
|
|
209
|
+
adapter: createMemoryCache(), // or Redis adapter
|
|
210
|
+
ttl: 60, // Default TTL (seconds)
|
|
211
|
+
byIdTtl: 300, // TTL for getById
|
|
212
|
+
queryTtl: 30, // TTL for lists
|
|
213
|
+
})
|
|
214
|
+
]);
|
|
535
215
|
|
|
536
|
-
//
|
|
537
|
-
|
|
216
|
+
// Reads are cached automatically
|
|
217
|
+
const user = await repo.getById(id);
|
|
538
218
|
|
|
539
|
-
//
|
|
540
|
-
|
|
219
|
+
// Skip cache for fresh data
|
|
220
|
+
const fresh = await repo.getById(id, { skipCache: true });
|
|
541
221
|
|
|
542
|
-
//
|
|
543
|
-
|
|
544
|
-
Schema.index({ createdAt: -1, _id: -1 }); // Still need this for sorting
|
|
222
|
+
// Mutations auto-invalidate cache
|
|
223
|
+
await repo.update(id, { name: 'New' });
|
|
545
224
|
|
|
546
|
-
//
|
|
547
|
-
|
|
225
|
+
// Manual invalidation
|
|
226
|
+
await repo.invalidateCache(id);
|
|
227
|
+
await repo.invalidateAllCache();
|
|
548
228
|
```
|
|
549
229
|
|
|
550
|
-
|
|
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
|
-
|
|
560
|
-
---
|
|
561
|
-
|
|
562
|
-
## 🔌 Built-in Plugins
|
|
563
|
-
|
|
564
|
-
### Field Filtering (Role-based Access)
|
|
565
|
-
|
|
566
|
-
Control which fields are visible based on user roles:
|
|
567
|
-
|
|
230
|
+
**Redis adapter example:**
|
|
568
231
|
```javascript
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
admin: ['createdAt', 'updatedAt', 'internalNotes']
|
|
232
|
+
const redisAdapter = {
|
|
233
|
+
async get(key) { return JSON.parse(await redis.get(key) || 'null'); },
|
|
234
|
+
async set(key, value, ttl) { await redis.setex(key, ttl, JSON.stringify(value)); },
|
|
235
|
+
async del(key) { await redis.del(key); },
|
|
236
|
+
async clear(pattern) { /* optional bulk delete */ }
|
|
575
237
|
};
|
|
576
|
-
|
|
577
|
-
class UserRepository extends Repository {
|
|
578
|
-
constructor() {
|
|
579
|
-
super(UserModel, [fieldFilterPlugin(fieldPreset)]);
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
238
|
```
|
|
583
239
|
|
|
584
240
|
### Validation Chain
|
|
585
241
|
|
|
586
|
-
Add custom validation rules:
|
|
587
|
-
|
|
588
242
|
```javascript
|
|
589
243
|
import {
|
|
590
|
-
Repository,
|
|
591
244
|
validationChainPlugin,
|
|
592
245
|
requireField,
|
|
593
246
|
uniqueField,
|
|
594
|
-
immutableField
|
|
247
|
+
immutableField,
|
|
248
|
+
blockIf,
|
|
249
|
+
autoInject
|
|
595
250
|
} from '@classytic/mongokit';
|
|
596
251
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
])
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
```
|
|
609
|
-
|
|
610
|
-
### Soft Delete
|
|
611
|
-
|
|
612
|
-
Mark records as deleted without actually removing them:
|
|
613
|
-
|
|
614
|
-
```javascript
|
|
615
|
-
import { Repository, softDeletePlugin } from '@classytic/mongokit';
|
|
616
|
-
|
|
617
|
-
class UserRepository extends Repository {
|
|
618
|
-
constructor() {
|
|
619
|
-
super(UserModel, [softDeletePlugin({ deletedField: 'deletedAt' })]);
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
// repo.delete(id) → marks as deleted instead of removing
|
|
624
|
-
// repo.getAll() → excludes deleted records
|
|
625
|
-
// repo.getAll({ includeDeleted: true }) → includes deleted
|
|
252
|
+
const repo = new Repository(UserModel, [
|
|
253
|
+
validationChainPlugin([
|
|
254
|
+
requireField('email', ['create']),
|
|
255
|
+
uniqueField('email', 'Email already exists'),
|
|
256
|
+
immutableField('userId'),
|
|
257
|
+
blockIf('noAdminDelete', ['delete'],
|
|
258
|
+
(ctx) => ctx.data?.role === 'admin',
|
|
259
|
+
'Cannot delete admin users'),
|
|
260
|
+
autoInject('slug', (ctx) => slugify(ctx.data?.name), ['create'])
|
|
261
|
+
])
|
|
262
|
+
]);
|
|
626
263
|
```
|
|
627
264
|
|
|
628
|
-
###
|
|
629
|
-
|
|
630
|
-
Log all create, update, and delete operations:
|
|
265
|
+
### Cascade Delete
|
|
631
266
|
|
|
632
267
|
```javascript
|
|
633
|
-
import {
|
|
634
|
-
import logger from './logger.js';
|
|
268
|
+
import { cascadePlugin, softDeletePlugin } from '@classytic/mongokit';
|
|
635
269
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
}
|
|
270
|
+
const repo = new Repository(ProductModel, [
|
|
271
|
+
softDeletePlugin(),
|
|
272
|
+
cascadePlugin({
|
|
273
|
+
relations: [
|
|
274
|
+
{ model: 'StockEntry', foreignKey: 'product' },
|
|
275
|
+
{ model: 'Review', foreignKey: 'product', softDelete: false }
|
|
276
|
+
],
|
|
277
|
+
parallel: true,
|
|
278
|
+
logger: console
|
|
279
|
+
})
|
|
280
|
+
]);
|
|
641
281
|
|
|
642
|
-
//
|
|
282
|
+
// Deleting product also deletes related StockEntry and Review docs
|
|
283
|
+
await repo.delete(productId);
|
|
643
284
|
```
|
|
644
285
|
|
|
645
|
-
###
|
|
646
|
-
|
|
647
|
-
Add caching with automatic invalidation on mutations:
|
|
286
|
+
### Field Filtering (RBAC)
|
|
648
287
|
|
|
649
288
|
```javascript
|
|
650
|
-
import {
|
|
289
|
+
import { fieldFilterPlugin } from '@classytic/mongokit';
|
|
651
290
|
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
queryTtl: 30, // 30s for lists
|
|
291
|
+
const repo = new Repository(UserModel, [
|
|
292
|
+
fieldFilterPlugin({
|
|
293
|
+
public: ['id', 'name', 'avatar'],
|
|
294
|
+
authenticated: ['email', 'phone'],
|
|
295
|
+
admin: ['createdAt', 'internalNotes']
|
|
658
296
|
})
|
|
659
297
|
]);
|
|
660
|
-
|
|
661
|
-
// Reads are cached automatically
|
|
662
|
-
const user = await userRepo.getById(id); // cached on second call
|
|
663
|
-
|
|
664
|
-
// Skip cache for fresh data
|
|
665
|
-
const fresh = await userRepo.getById(id, { skipCache: true });
|
|
666
|
-
|
|
667
|
-
// Mutations auto-invalidate cache
|
|
668
|
-
await userRepo.update(id, { name: 'New' });
|
|
669
|
-
|
|
670
|
-
// Manual invalidation (microservices)
|
|
671
|
-
await userRepo.invalidateCache(id); // single doc
|
|
672
|
-
await userRepo.invalidateAllCache(); // full model
|
|
673
|
-
```
|
|
674
|
-
|
|
675
|
-
**Redis adapter example:**
|
|
676
|
-
```javascript
|
|
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
|
-
};
|
|
683
298
|
```
|
|
684
299
|
|
|
685
|
-
|
|
686
|
-
|
|
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
|
|
692
|
-
|
|
693
|
-
---
|
|
694
|
-
|
|
695
|
-
## 🎨 Event System
|
|
696
|
-
|
|
697
|
-
Every operation emits lifecycle events:
|
|
300
|
+
## Event System
|
|
698
301
|
|
|
699
302
|
```javascript
|
|
700
303
|
repo.on('before:create', async (context) => {
|
|
701
|
-
console.log('About to create:', context.data);
|
|
702
|
-
// Modify context.data if needed
|
|
703
304
|
context.data.processedAt = new Date();
|
|
704
305
|
});
|
|
705
306
|
|
|
706
307
|
repo.on('after:create', ({ context, result }) => {
|
|
707
308
|
console.log('Created:', result);
|
|
708
|
-
// Send notification, update cache, etc.
|
|
709
309
|
});
|
|
710
310
|
|
|
711
311
|
repo.on('error:create', ({ context, error }) => {
|
|
712
|
-
console.error('Failed
|
|
713
|
-
// Log error, send alert, etc.
|
|
312
|
+
console.error('Failed:', error);
|
|
714
313
|
});
|
|
715
314
|
```
|
|
716
315
|
|
|
717
|
-
**
|
|
718
|
-
- `before:create`, `after:create`, `error:create`
|
|
719
|
-
- `before:update`, `after:update`, `error:update`
|
|
720
|
-
- `before:delete`, `after:delete`, `error:delete`
|
|
721
|
-
- `before:createMany`, `after:createMany`, `error:createMany`
|
|
722
|
-
- `before:getAll`, `before:getById`, `before:getByQuery`
|
|
723
|
-
|
|
724
|
-
---
|
|
316
|
+
**Events:** `before:*`, `after:*`, `error:*` for `create`, `createMany`, `update`, `delete`, `getById`, `getByQuery`, `getAll`, `aggregatePaginate`
|
|
725
317
|
|
|
726
|
-
##
|
|
318
|
+
## HTTP Utilities
|
|
727
319
|
|
|
728
|
-
|
|
320
|
+
### Query Parser
|
|
729
321
|
|
|
730
322
|
```javascript
|
|
731
|
-
|
|
732
|
-
name: 'timestamp',
|
|
733
|
-
|
|
734
|
-
apply(repo) {
|
|
735
|
-
repo.on('before:create', (context) => {
|
|
736
|
-
context.data.createdAt = new Date();
|
|
737
|
-
context.data.updatedAt = new Date();
|
|
738
|
-
});
|
|
323
|
+
import { queryParser } from '@classytic/mongokit/utils';
|
|
739
324
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
325
|
+
app.get('/users', async (req, res) => {
|
|
326
|
+
const { filters, limit, page, sort } = queryParser.parseQuery(req.query);
|
|
327
|
+
const result = await userRepo.getAll({ filters, limit, page, sort });
|
|
328
|
+
res.json(result);
|
|
744
329
|
});
|
|
330
|
+
```
|
|
745
331
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
332
|
+
**Supported query patterns:**
|
|
333
|
+
```bash
|
|
334
|
+
GET /users?email=john@example.com&role=admin
|
|
335
|
+
GET /users?age[gte]=18&age[lte]=65
|
|
336
|
+
GET /users?role[in]=admin,user
|
|
337
|
+
GET /users?sort=-createdAt,name&page=2&limit=50
|
|
752
338
|
```
|
|
753
339
|
|
|
754
|
-
###
|
|
340
|
+
### Schema Generator (Fastify/OpenAPI)
|
|
755
341
|
|
|
756
342
|
```javascript
|
|
757
|
-
import {
|
|
758
|
-
Repository,
|
|
759
|
-
softDeletePlugin,
|
|
760
|
-
auditLogPlugin,
|
|
761
|
-
fieldFilterPlugin
|
|
762
|
-
} from '@classytic/mongokit';
|
|
343
|
+
import { buildCrudSchemasFromModel } from '@classytic/mongokit/utils';
|
|
763
344
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
auditLogPlugin(logger),
|
|
769
|
-
fieldFilterPlugin(userFieldPreset)
|
|
770
|
-
]);
|
|
345
|
+
const { crudSchemas } = buildCrudSchemasFromModel(UserModel, {
|
|
346
|
+
fieldRules: {
|
|
347
|
+
organizationId: { immutable: true },
|
|
348
|
+
status: { systemManaged: true }
|
|
771
349
|
}
|
|
772
|
-
}
|
|
773
|
-
```
|
|
774
|
-
|
|
775
|
-
---
|
|
350
|
+
});
|
|
776
351
|
|
|
777
|
-
|
|
352
|
+
fastify.post('/users', { schema: crudSchemas.create }, handler);
|
|
353
|
+
fastify.get('/users', { schema: crudSchemas.list }, handler);
|
|
354
|
+
```
|
|
778
355
|
|
|
779
|
-
|
|
356
|
+
## TypeScript
|
|
780
357
|
|
|
781
358
|
```typescript
|
|
782
|
-
import {
|
|
783
|
-
Repository,
|
|
784
|
-
OffsetPaginationResult,
|
|
785
|
-
KeysetPaginationResult
|
|
786
|
-
} from '@classytic/mongokit';
|
|
787
|
-
import { Document } from 'mongoose';
|
|
359
|
+
import { Repository, OffsetPaginationResult, KeysetPaginationResult } from '@classytic/mongokit';
|
|
788
360
|
|
|
789
361
|
interface IUser extends Document {
|
|
790
362
|
name: string;
|
|
791
363
|
email: string;
|
|
792
|
-
status: 'active' | 'inactive';
|
|
793
364
|
}
|
|
794
365
|
|
|
795
|
-
|
|
796
|
-
constructor() {
|
|
797
|
-
super(UserModel);
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
async findActive(): Promise<IUser[]> {
|
|
801
|
-
const result = await this.getAll({
|
|
802
|
-
filters: { status: 'active' },
|
|
803
|
-
page: 1,
|
|
804
|
-
limit: 50
|
|
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
|
-
}
|
|
366
|
+
const repo = new Repository<IUser>(UserModel);
|
|
813
367
|
|
|
814
|
-
|
|
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
|
-
}
|
|
368
|
+
const result = await repo.getAll({ page: 1, limit: 20 });
|
|
829
369
|
|
|
830
|
-
|
|
831
|
-
|
|
370
|
+
// Discriminated union - TypeScript knows the type
|
|
371
|
+
if (result.method === 'offset') {
|
|
372
|
+
console.log(result.total, result.pages); // Available
|
|
373
|
+
}
|
|
374
|
+
if (result.method === 'keyset') {
|
|
375
|
+
console.log(result.next, result.hasMore); // Available
|
|
832
376
|
}
|
|
833
377
|
```
|
|
834
378
|
|
|
835
|
-
|
|
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
|
-
|
|
849
|
-
---
|
|
850
|
-
|
|
851
|
-
## 🏎️ Performance Tips
|
|
852
|
-
|
|
853
|
-
### 1. Use Keyset Pagination for Large Datasets
|
|
854
|
-
|
|
855
|
-
```javascript
|
|
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)
|
|
861
|
-
```
|
|
862
|
-
|
|
863
|
-
### 2. Create Required Indexes
|
|
864
|
-
|
|
865
|
-
**IMPORTANT:** MongoDB only auto-indexes `_id`. You must manually create indexes for pagination.
|
|
866
|
-
|
|
867
|
-
```javascript
|
|
868
|
-
// ✅ Single-Tenant: Sort field + _id
|
|
869
|
-
PostSchema.index({ createdAt: -1, _id: -1 });
|
|
870
|
-
|
|
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)**
|
|
379
|
+
## Extending Repository
|
|
880
380
|
|
|
881
|
-
|
|
381
|
+
Create custom repository classes with domain-specific methods:
|
|
882
382
|
|
|
883
|
-
```
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
});
|
|
887
|
-
```
|
|
383
|
+
```typescript
|
|
384
|
+
import { Repository, softDeletePlugin, timestampPlugin } from '@classytic/mongokit';
|
|
385
|
+
import UserModel, { IUser } from './models/User.js';
|
|
888
386
|
|
|
889
|
-
|
|
387
|
+
class UserRepository extends Repository<IUser> {
|
|
388
|
+
constructor() {
|
|
389
|
+
super(UserModel, [
|
|
390
|
+
timestampPlugin(),
|
|
391
|
+
softDeletePlugin()
|
|
392
|
+
], {
|
|
393
|
+
defaultLimit: 20
|
|
394
|
+
});
|
|
395
|
+
}
|
|
890
396
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
397
|
+
// Custom domain methods
|
|
398
|
+
async findByEmail(email: string) {
|
|
399
|
+
return this.getByQuery({ email });
|
|
400
|
+
}
|
|
894
401
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
402
|
+
async findActiveUsers() {
|
|
403
|
+
return this.getAll({
|
|
404
|
+
filters: { status: 'active' },
|
|
405
|
+
sort: { createdAt: -1 }
|
|
406
|
+
});
|
|
407
|
+
}
|
|
898
408
|
|
|
899
|
-
|
|
409
|
+
async deactivate(id: string) {
|
|
410
|
+
return this.update(id, { status: 'inactive', deactivatedAt: new Date() });
|
|
411
|
+
}
|
|
412
|
+
}
|
|
900
413
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
await
|
|
904
|
-
pipeline: [...],
|
|
905
|
-
limit: 2000 // Warning: $facet results must be <16MB
|
|
906
|
-
});
|
|
414
|
+
// Usage
|
|
415
|
+
const userRepo = new UserRepository();
|
|
416
|
+
const user = await userRepo.findByEmail('john@example.com');
|
|
907
417
|
```
|
|
908
418
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
## 🔄 Migration Guide
|
|
419
|
+
### Overriding Methods
|
|
912
420
|
|
|
913
|
-
|
|
421
|
+
```typescript
|
|
422
|
+
class AuditedUserRepository extends Repository<IUser> {
|
|
423
|
+
constructor() {
|
|
424
|
+
super(UserModel);
|
|
425
|
+
}
|
|
914
426
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
427
|
+
// Override create to add audit trail
|
|
428
|
+
async create(data: Partial<IUser>, options = {}) {
|
|
429
|
+
const result = await super.create({
|
|
430
|
+
...data,
|
|
431
|
+
createdBy: getCurrentUserId()
|
|
432
|
+
}, options);
|
|
920
433
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
filters: { status: 'active' },
|
|
926
|
-
page: 1,
|
|
927
|
-
limit: 10
|
|
928
|
-
});
|
|
434
|
+
await auditLog('user.created', result._id);
|
|
435
|
+
return result;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
929
438
|
```
|
|
930
439
|
|
|
931
|
-
|
|
440
|
+
## Factory Function
|
|
932
441
|
|
|
933
|
-
|
|
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
|
|
442
|
+
For simple cases without custom methods:
|
|
951
443
|
|
|
952
444
|
```javascript
|
|
953
|
-
|
|
954
|
-
const [users, total] = await userRepository.findAndCount({
|
|
955
|
-
where: { status: 'active' },
|
|
956
|
-
skip: 20,
|
|
957
|
-
take: 10
|
|
958
|
-
});
|
|
445
|
+
import { createRepository, timestampPlugin } from '@classytic/mongokit';
|
|
959
446
|
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
filters: { status: 'active' },
|
|
963
|
-
page: 3,
|
|
964
|
-
limit: 10
|
|
447
|
+
const userRepo = createRepository(UserModel, [timestampPlugin()], {
|
|
448
|
+
defaultLimit: 20
|
|
965
449
|
});
|
|
966
|
-
const users = result.docs;
|
|
967
|
-
const total = result.total;
|
|
968
|
-
```
|
|
969
|
-
|
|
970
|
-
---
|
|
971
|
-
|
|
972
|
-
## 🌟 Why MongoKit?
|
|
973
|
-
|
|
974
|
-
### vs. Mongoose Directly
|
|
975
|
-
- ✅ Consistent API across all models
|
|
976
|
-
- ✅ Built-in pagination (offset + cursor) with zero dependencies
|
|
977
|
-
- ✅ Multi-tenancy without repetitive code
|
|
978
|
-
- ✅ Event hooks for cross-cutting concerns
|
|
979
|
-
- ✅ Plugin system for reusable behaviors
|
|
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
|
-
|
|
988
|
-
### vs. TypeORM / Prisma
|
|
989
|
-
- ✅ Lighter weight (works with Mongoose)
|
|
990
|
-
- ✅ Event-driven architecture
|
|
991
|
-
- ✅ More flexible plugin system
|
|
992
|
-
- ✅ No migration needed if using Mongoose
|
|
993
|
-
- ✅ Framework-agnostic
|
|
994
|
-
|
|
995
|
-
### vs. Raw Repository Pattern
|
|
996
|
-
- ✅ Battle-tested implementation (68 passing tests)
|
|
997
|
-
- ✅ 11 built-in plugins ready to use
|
|
998
|
-
- ✅ Comprehensive documentation
|
|
999
|
-
- ✅ TypeScript discriminated unions
|
|
1000
|
-
- ✅ Active maintenance
|
|
1001
|
-
|
|
1002
|
-
---
|
|
1003
|
-
|
|
1004
|
-
## 🧪 Testing
|
|
1005
|
-
|
|
1006
|
-
```bash
|
|
1007
|
-
npm test
|
|
1008
450
|
```
|
|
1009
451
|
|
|
1010
|
-
|
|
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
|
-
|
|
1039
|
-
---
|
|
1040
|
-
|
|
1041
|
-
## 📄 License
|
|
1042
|
-
|
|
1043
|
-
MIT © [Classytic](https://github.com/classytic)
|
|
1044
|
-
|
|
1045
|
-
---
|
|
1046
|
-
|
|
1047
|
-
## 🔗 Links
|
|
452
|
+
## No Breaking Changes
|
|
1048
453
|
|
|
1049
|
-
|
|
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)
|
|
454
|
+
Extending Repository works exactly the same with Mongoose 8 and 9. The package:
|
|
1053
455
|
|
|
1054
|
-
|
|
456
|
+
- Uses its own event system (not Mongoose middleware)
|
|
457
|
+
- Defines its own `FilterQuery` type (unaffected by Mongoose 9 rename)
|
|
458
|
+
- Properly gates update pipelines (safe for Mongoose 9's stricter defaults)
|
|
459
|
+
- All 194 tests pass on both Mongoose 8 and 9
|
|
1055
460
|
|
|
1056
|
-
|
|
461
|
+
## License
|
|
1057
462
|
|
|
1058
|
-
|
|
463
|
+
MIT
|