@classytic/mongokit 3.1.0 → 3.1.1
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 +625 -465
- package/dist/actions/index.d.ts +2 -2
- package/dist/actions/index.js +3 -515
- package/dist/chunks/chunk-2ZN65ZOP.js +93 -0
- package/dist/chunks/chunk-CF6FLC2G.js +46 -0
- package/dist/chunks/chunk-CSLJ2PL2.js +1092 -0
- package/dist/chunks/chunk-IT7DCOKR.js +299 -0
- package/dist/chunks/chunk-M2XHQGZB.js +361 -0
- package/dist/chunks/chunk-SAKSLT47.js +470 -0
- package/dist/chunks/chunk-VJXDGP3C.js +14 -0
- package/dist/{index-3Nkm_Brq.d.ts → index-C2NCVxJK.d.ts} +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +49 -2387
- package/dist/{mongooseToJsonSchema-CUQma8QK.d.ts → mongooseToJsonSchema-BKMxPbPp.d.ts} +1 -1
- package/dist/pagination/PaginationEngine.d.ts +1 -1
- package/dist/pagination/PaginationEngine.js +2 -368
- package/dist/plugins/index.d.ts +1 -1
- package/dist/plugins/index.js +4 -1170
- package/dist/{types-CrSoCuWu.d.ts → types-DA0rs2Jh.d.ts} +99 -5
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js +3 -398
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -1,465 +1,625 @@
|
|
|
1
|
-
# @classytic/mongokit
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/@classytic/mongokit)
|
|
4
|
-
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
|
|
6
|
-
> Production-grade MongoDB repository pattern with zero external dependencies
|
|
7
|
-
|
|
8
|
-
**Works with:** Express, Fastify, NestJS, Next.js, Koa, Hapi, Serverless
|
|
9
|
-
|
|
10
|
-
## Features
|
|
11
|
-
|
|
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
|
|
18
|
-
|
|
19
|
-
## Installation
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
npm install @classytic/mongokit mongoose
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
> Supports Mongoose `^8.0.0` and `^9.0.0`
|
|
26
|
-
|
|
27
|
-
## Quick Start
|
|
28
|
-
|
|
29
|
-
```javascript
|
|
30
|
-
import { Repository } from '@classytic/mongokit';
|
|
31
|
-
import UserModel from './models/User.js';
|
|
32
|
-
|
|
33
|
-
const userRepo = new Repository(UserModel);
|
|
34
|
-
|
|
35
|
-
// Create
|
|
36
|
-
const user = await userRepo.create({ name: 'John', email: 'john@example.com' });
|
|
37
|
-
|
|
38
|
-
// Read with auto-detected pagination
|
|
39
|
-
const users = await userRepo.getAll({ page: 1, limit: 20 });
|
|
40
|
-
|
|
41
|
-
// Update
|
|
42
|
-
await userRepo.update(user._id, { name: 'Jane' });
|
|
43
|
-
|
|
44
|
-
// Delete
|
|
45
|
-
await userRepo.delete(user._id);
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## Pagination
|
|
49
|
-
|
|
50
|
-
`getAll()` automatically detects pagination mode based on parameters:
|
|
51
|
-
|
|
52
|
-
```javascript
|
|
53
|
-
// Offset pagination (page-based) - for dashboards
|
|
54
|
-
const result = await repo.getAll({
|
|
55
|
-
page: 1,
|
|
56
|
-
limit: 20,
|
|
57
|
-
filters: { status: 'active' },
|
|
58
|
-
sort: { createdAt: -1 }
|
|
59
|
-
});
|
|
60
|
-
// → { method: 'offset', docs, total, pages, hasNext, hasPrev }
|
|
61
|
-
|
|
62
|
-
// Keyset pagination (cursor-based) - for infinite scroll
|
|
63
|
-
const stream = await repo.getAll({
|
|
64
|
-
sort: { createdAt: -1 },
|
|
65
|
-
limit: 20
|
|
66
|
-
});
|
|
67
|
-
// → { method: 'keyset', docs, hasMore, next: 'eyJ2IjoxLC...' }
|
|
68
|
-
|
|
69
|
-
// Next page with cursor
|
|
70
|
-
const next = await repo.getAll({
|
|
71
|
-
after: stream.next,
|
|
72
|
-
sort: { createdAt: -1 },
|
|
73
|
-
limit: 20
|
|
74
|
-
});
|
|
75
|
-
```
|
|
76
|
-
|
|
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)
|
|
82
|
-
|
|
83
|
-
### Required Indexes
|
|
84
|
-
|
|
85
|
-
```javascript
|
|
86
|
-
// For keyset pagination: sort field + _id
|
|
87
|
-
PostSchema.index({ createdAt: -1, _id: -1 });
|
|
88
|
-
|
|
89
|
-
// For multi-tenant: tenant + sort field + _id
|
|
90
|
-
UserSchema.index({ organizationId: 1, createdAt: -1, _id: -1 });
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
## API Reference
|
|
94
|
-
|
|
95
|
-
### CRUD Operations
|
|
96
|
-
|
|
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 |
|
|
109
|
-
|
|
110
|
-
### Aggregation
|
|
111
|
-
|
|
112
|
-
```javascript
|
|
113
|
-
// Basic aggregation
|
|
114
|
-
const result = await repo.aggregate([
|
|
115
|
-
{ $match: { status: 'active' } },
|
|
116
|
-
{ $group: { _id: '$category', total: { $sum: 1 } } }
|
|
117
|
-
]);
|
|
118
|
-
|
|
119
|
-
// Paginated aggregation
|
|
120
|
-
const result = await repo.aggregatePaginate({
|
|
121
|
-
pipeline: [...],
|
|
122
|
-
page: 1,
|
|
123
|
-
limit: 20
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
// Distinct values
|
|
127
|
-
const categories = await repo.distinct('category', { status: 'active' });
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
### Transactions
|
|
131
|
-
|
|
132
|
-
```javascript
|
|
133
|
-
await repo.withTransaction(async (session) => {
|
|
134
|
-
await repo.create({ name: 'User 1' }, { session });
|
|
135
|
-
await repo.create({ name: 'User 2' }, { session });
|
|
136
|
-
// Auto-commits on success, auto-rollbacks on error
|
|
137
|
-
});
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
## Configuration
|
|
141
|
-
|
|
142
|
-
```javascript
|
|
143
|
-
const repo = new Repository(UserModel, plugins, {
|
|
144
|
-
defaultLimit: 20, // Default docs per page
|
|
145
|
-
maxLimit: 100, // Maximum allowed limit
|
|
146
|
-
maxPage: 10000, // Maximum page number
|
|
147
|
-
deepPageThreshold: 100, // Warn when page exceeds this
|
|
148
|
-
useEstimatedCount: false, // Use fast estimated counts
|
|
149
|
-
cursorVersion: 1 // Cursor format version
|
|
150
|
-
});
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
## Plugins
|
|
154
|
-
|
|
155
|
-
### Using Plugins
|
|
156
|
-
|
|
157
|
-
```javascript
|
|
158
|
-
import {
|
|
159
|
-
Repository,
|
|
160
|
-
timestampPlugin,
|
|
161
|
-
softDeletePlugin,
|
|
162
|
-
cachePlugin,
|
|
163
|
-
createMemoryCache
|
|
164
|
-
} from '@classytic/mongokit';
|
|
165
|
-
|
|
166
|
-
const repo = new Repository(UserModel, [
|
|
167
|
-
timestampPlugin(),
|
|
168
|
-
softDeletePlugin(),
|
|
169
|
-
cachePlugin({ adapter: createMemoryCache(), ttl: 60 })
|
|
170
|
-
]);
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
### Available Plugins
|
|
174
|
-
|
|
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 |
|
|
189
|
-
|
|
190
|
-
### Soft Delete
|
|
191
|
-
|
|
192
|
-
```javascript
|
|
193
|
-
const repo = new Repository(UserModel, [
|
|
194
|
-
softDeletePlugin({ deletedField: 'deletedAt' })
|
|
195
|
-
]);
|
|
196
|
-
|
|
197
|
-
await repo.delete(id); // Marks as deleted
|
|
198
|
-
await repo.getAll(); // Excludes deleted
|
|
199
|
-
await repo.getAll({ includeDeleted: true }); // Includes deleted
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
### Caching
|
|
203
|
-
|
|
204
|
-
```javascript
|
|
205
|
-
import { cachePlugin, createMemoryCache } from '@classytic/mongokit';
|
|
206
|
-
|
|
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
|
-
]);
|
|
215
|
-
|
|
216
|
-
// Reads are cached automatically
|
|
217
|
-
const user = await repo.getById(id);
|
|
218
|
-
|
|
219
|
-
// Skip cache for fresh data
|
|
220
|
-
const fresh = await repo.getById(id, { skipCache: true });
|
|
221
|
-
|
|
222
|
-
// Mutations auto-invalidate cache
|
|
223
|
-
await repo.update(id, { name: 'New' });
|
|
224
|
-
|
|
225
|
-
// Manual invalidation
|
|
226
|
-
await repo.invalidateCache(id);
|
|
227
|
-
await repo.invalidateAllCache();
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
**Redis adapter example:**
|
|
231
|
-
```javascript
|
|
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 */ }
|
|
237
|
-
};
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
### Validation Chain
|
|
241
|
-
|
|
242
|
-
```javascript
|
|
243
|
-
import {
|
|
244
|
-
validationChainPlugin,
|
|
245
|
-
requireField,
|
|
246
|
-
uniqueField,
|
|
247
|
-
immutableField,
|
|
248
|
-
blockIf,
|
|
249
|
-
autoInject
|
|
250
|
-
} from '@classytic/mongokit';
|
|
251
|
-
|
|
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
|
-
]);
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
### Cascade Delete
|
|
266
|
-
|
|
267
|
-
```javascript
|
|
268
|
-
import { cascadePlugin, softDeletePlugin } from '@classytic/mongokit';
|
|
269
|
-
|
|
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
|
-
]);
|
|
281
|
-
|
|
282
|
-
// Deleting product also deletes related StockEntry and Review docs
|
|
283
|
-
await repo.delete(productId);
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
### Field Filtering (RBAC)
|
|
287
|
-
|
|
288
|
-
```javascript
|
|
289
|
-
import { fieldFilterPlugin } from '@classytic/mongokit';
|
|
290
|
-
|
|
291
|
-
const repo = new Repository(UserModel, [
|
|
292
|
-
fieldFilterPlugin({
|
|
293
|
-
public: ['id', 'name', 'avatar'],
|
|
294
|
-
authenticated: ['email', 'phone'],
|
|
295
|
-
admin: ['createdAt', 'internalNotes']
|
|
296
|
-
})
|
|
297
|
-
]);
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
## Event System
|
|
301
|
-
|
|
302
|
-
```javascript
|
|
303
|
-
repo.on('before:create', async (context) => {
|
|
304
|
-
context.data.processedAt = new Date();
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
repo.on('after:create', ({ context, result }) => {
|
|
308
|
-
console.log('Created:', result);
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
repo.on('error:create', ({ context, error }) => {
|
|
312
|
-
console.error('Failed:', error);
|
|
313
|
-
});
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
**Events:** `before:*`, `after:*`, `error:*` for `create`, `createMany`, `update`, `delete`, `getById`, `getByQuery`, `getAll`, `aggregatePaginate`
|
|
317
|
-
|
|
318
|
-
##
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
```typescript
|
|
361
|
-
import {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
//
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
1
|
+
# @classytic/mongokit
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@classytic/mongokit)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
> Production-grade MongoDB repository pattern with zero external dependencies
|
|
7
|
+
|
|
8
|
+
**Works with:** Express, Fastify, NestJS, Next.js, Koa, Hapi, Serverless
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
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
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @classytic/mongokit mongoose
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
> Supports Mongoose `^8.0.0` and `^9.0.0`
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```javascript
|
|
30
|
+
import { Repository } from '@classytic/mongokit';
|
|
31
|
+
import UserModel from './models/User.js';
|
|
32
|
+
|
|
33
|
+
const userRepo = new Repository(UserModel);
|
|
34
|
+
|
|
35
|
+
// Create
|
|
36
|
+
const user = await userRepo.create({ name: 'John', email: 'john@example.com' });
|
|
37
|
+
|
|
38
|
+
// Read with auto-detected pagination
|
|
39
|
+
const users = await userRepo.getAll({ page: 1, limit: 20 });
|
|
40
|
+
|
|
41
|
+
// Update
|
|
42
|
+
await userRepo.update(user._id, { name: 'Jane' });
|
|
43
|
+
|
|
44
|
+
// Delete
|
|
45
|
+
await userRepo.delete(user._id);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Pagination
|
|
49
|
+
|
|
50
|
+
`getAll()` automatically detects pagination mode based on parameters:
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
// Offset pagination (page-based) - for dashboards
|
|
54
|
+
const result = await repo.getAll({
|
|
55
|
+
page: 1,
|
|
56
|
+
limit: 20,
|
|
57
|
+
filters: { status: 'active' },
|
|
58
|
+
sort: { createdAt: -1 }
|
|
59
|
+
});
|
|
60
|
+
// → { method: 'offset', docs, total, pages, hasNext, hasPrev }
|
|
61
|
+
|
|
62
|
+
// Keyset pagination (cursor-based) - for infinite scroll
|
|
63
|
+
const stream = await repo.getAll({
|
|
64
|
+
sort: { createdAt: -1 },
|
|
65
|
+
limit: 20
|
|
66
|
+
});
|
|
67
|
+
// → { method: 'keyset', docs, hasMore, next: 'eyJ2IjoxLC...' }
|
|
68
|
+
|
|
69
|
+
// Next page with cursor
|
|
70
|
+
const next = await repo.getAll({
|
|
71
|
+
after: stream.next,
|
|
72
|
+
sort: { createdAt: -1 },
|
|
73
|
+
limit: 20
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
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)
|
|
82
|
+
|
|
83
|
+
### Required Indexes
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
// For keyset pagination: sort field + _id
|
|
87
|
+
PostSchema.index({ createdAt: -1, _id: -1 });
|
|
88
|
+
|
|
89
|
+
// For multi-tenant: tenant + sort field + _id
|
|
90
|
+
UserSchema.index({ organizationId: 1, createdAt: -1, _id: -1 });
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## API Reference
|
|
94
|
+
|
|
95
|
+
### CRUD Operations
|
|
96
|
+
|
|
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 |
|
|
109
|
+
|
|
110
|
+
### Aggregation
|
|
111
|
+
|
|
112
|
+
```javascript
|
|
113
|
+
// Basic aggregation
|
|
114
|
+
const result = await repo.aggregate([
|
|
115
|
+
{ $match: { status: 'active' } },
|
|
116
|
+
{ $group: { _id: '$category', total: { $sum: 1 } } }
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
// Paginated aggregation
|
|
120
|
+
const result = await repo.aggregatePaginate({
|
|
121
|
+
pipeline: [...],
|
|
122
|
+
page: 1,
|
|
123
|
+
limit: 20
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Distinct values
|
|
127
|
+
const categories = await repo.distinct('category', { status: 'active' });
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Transactions
|
|
131
|
+
|
|
132
|
+
```javascript
|
|
133
|
+
await repo.withTransaction(async (session) => {
|
|
134
|
+
await repo.create({ name: 'User 1' }, { session });
|
|
135
|
+
await repo.create({ name: 'User 2' }, { session });
|
|
136
|
+
// Auto-commits on success, auto-rollbacks on error
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Configuration
|
|
141
|
+
|
|
142
|
+
```javascript
|
|
143
|
+
const repo = new Repository(UserModel, plugins, {
|
|
144
|
+
defaultLimit: 20, // Default docs per page
|
|
145
|
+
maxLimit: 100, // Maximum allowed limit
|
|
146
|
+
maxPage: 10000, // Maximum page number
|
|
147
|
+
deepPageThreshold: 100, // Warn when page exceeds this
|
|
148
|
+
useEstimatedCount: false, // Use fast estimated counts
|
|
149
|
+
cursorVersion: 1 // Cursor format version
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Plugins
|
|
154
|
+
|
|
155
|
+
### Using Plugins
|
|
156
|
+
|
|
157
|
+
```javascript
|
|
158
|
+
import {
|
|
159
|
+
Repository,
|
|
160
|
+
timestampPlugin,
|
|
161
|
+
softDeletePlugin,
|
|
162
|
+
cachePlugin,
|
|
163
|
+
createMemoryCache
|
|
164
|
+
} from '@classytic/mongokit';
|
|
165
|
+
|
|
166
|
+
const repo = new Repository(UserModel, [
|
|
167
|
+
timestampPlugin(),
|
|
168
|
+
softDeletePlugin(),
|
|
169
|
+
cachePlugin({ adapter: createMemoryCache(), ttl: 60 })
|
|
170
|
+
]);
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Available Plugins
|
|
174
|
+
|
|
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 |
|
|
189
|
+
|
|
190
|
+
### Soft Delete
|
|
191
|
+
|
|
192
|
+
```javascript
|
|
193
|
+
const repo = new Repository(UserModel, [
|
|
194
|
+
softDeletePlugin({ deletedField: 'deletedAt' })
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
await repo.delete(id); // Marks as deleted
|
|
198
|
+
await repo.getAll(); // Excludes deleted
|
|
199
|
+
await repo.getAll({ includeDeleted: true }); // Includes deleted
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Caching
|
|
203
|
+
|
|
204
|
+
```javascript
|
|
205
|
+
import { cachePlugin, createMemoryCache } from '@classytic/mongokit';
|
|
206
|
+
|
|
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
|
+
]);
|
|
215
|
+
|
|
216
|
+
// Reads are cached automatically
|
|
217
|
+
const user = await repo.getById(id);
|
|
218
|
+
|
|
219
|
+
// Skip cache for fresh data
|
|
220
|
+
const fresh = await repo.getById(id, { skipCache: true });
|
|
221
|
+
|
|
222
|
+
// Mutations auto-invalidate cache
|
|
223
|
+
await repo.update(id, { name: 'New' });
|
|
224
|
+
|
|
225
|
+
// Manual invalidation
|
|
226
|
+
await repo.invalidateCache(id);
|
|
227
|
+
await repo.invalidateAllCache();
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**Redis adapter example:**
|
|
231
|
+
```javascript
|
|
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 */ }
|
|
237
|
+
};
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Validation Chain
|
|
241
|
+
|
|
242
|
+
```javascript
|
|
243
|
+
import {
|
|
244
|
+
validationChainPlugin,
|
|
245
|
+
requireField,
|
|
246
|
+
uniqueField,
|
|
247
|
+
immutableField,
|
|
248
|
+
blockIf,
|
|
249
|
+
autoInject
|
|
250
|
+
} from '@classytic/mongokit';
|
|
251
|
+
|
|
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
|
+
]);
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Cascade Delete
|
|
266
|
+
|
|
267
|
+
```javascript
|
|
268
|
+
import { cascadePlugin, softDeletePlugin } from '@classytic/mongokit';
|
|
269
|
+
|
|
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
|
+
]);
|
|
281
|
+
|
|
282
|
+
// Deleting product also deletes related StockEntry and Review docs
|
|
283
|
+
await repo.delete(productId);
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Field Filtering (RBAC)
|
|
287
|
+
|
|
288
|
+
```javascript
|
|
289
|
+
import { fieldFilterPlugin } from '@classytic/mongokit';
|
|
290
|
+
|
|
291
|
+
const repo = new Repository(UserModel, [
|
|
292
|
+
fieldFilterPlugin({
|
|
293
|
+
public: ['id', 'name', 'avatar'],
|
|
294
|
+
authenticated: ['email', 'phone'],
|
|
295
|
+
admin: ['createdAt', 'internalNotes']
|
|
296
|
+
})
|
|
297
|
+
]);
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Event System
|
|
301
|
+
|
|
302
|
+
```javascript
|
|
303
|
+
repo.on('before:create', async (context) => {
|
|
304
|
+
context.data.processedAt = new Date();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
repo.on('after:create', ({ context, result }) => {
|
|
308
|
+
console.log('Created:', result);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
repo.on('error:create', ({ context, error }) => {
|
|
312
|
+
console.error('Failed:', error);
|
|
313
|
+
});
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
**Events:** `before:*`, `after:*`, `error:*` for `create`, `createMany`, `update`, `delete`, `getById`, `getByQuery`, `getAll`, `aggregatePaginate`
|
|
317
|
+
|
|
318
|
+
## Building REST APIs
|
|
319
|
+
|
|
320
|
+
MongoKit provides a complete toolkit for building REST APIs: QueryParser for request handling, JSON Schema generation for validation/docs, and IController interface for framework-agnostic controllers.
|
|
321
|
+
|
|
322
|
+
### IController Interface
|
|
323
|
+
|
|
324
|
+
Framework-agnostic controller contract that works with Express, Fastify, Next.js, etc:
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
import type { IController, IRequestContext, IControllerResponse } from '@classytic/mongokit';
|
|
328
|
+
|
|
329
|
+
// IRequestContext - what your controller receives
|
|
330
|
+
interface IRequestContext {
|
|
331
|
+
query: Record<string, unknown>; // URL query params
|
|
332
|
+
body: Record<string, unknown>; // Request body
|
|
333
|
+
params: Record<string, string>; // Route params (:id)
|
|
334
|
+
user?: { id: string; role?: string }; // Auth user
|
|
335
|
+
context?: Record<string, unknown>; // Tenant ID, etc.
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// IControllerResponse - what your controller returns
|
|
339
|
+
interface IControllerResponse<T> {
|
|
340
|
+
success: boolean;
|
|
341
|
+
data?: T;
|
|
342
|
+
error?: string;
|
|
343
|
+
status: number;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// IController - implement this interface
|
|
347
|
+
interface IController<TDoc> {
|
|
348
|
+
list(ctx: IRequestContext): Promise<IControllerResponse<PaginationResult<TDoc>>>;
|
|
349
|
+
get(ctx: IRequestContext): Promise<IControllerResponse<TDoc>>;
|
|
350
|
+
create(ctx: IRequestContext): Promise<IControllerResponse<TDoc>>;
|
|
351
|
+
update(ctx: IRequestContext): Promise<IControllerResponse<TDoc>>;
|
|
352
|
+
delete(ctx: IRequestContext): Promise<IControllerResponse<{ message: string }>>;
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### QueryParser
|
|
357
|
+
|
|
358
|
+
Converts HTTP query strings to MongoDB queries with built-in security:
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
import { QueryParser } from '@classytic/mongokit';
|
|
362
|
+
|
|
363
|
+
const parser = new QueryParser({
|
|
364
|
+
maxLimit: 100, // Prevent excessive queries
|
|
365
|
+
maxFilterDepth: 5, // Prevent nested injection
|
|
366
|
+
maxRegexLength: 100, // ReDoS protection
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// Parse request query
|
|
370
|
+
const { filters, limit, page, sort, search } = parser.parse(req.query);
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
**Supported query patterns:**
|
|
374
|
+
```bash
|
|
375
|
+
# Filtering
|
|
376
|
+
GET /users?status=active&role=admin
|
|
377
|
+
GET /users?age[gte]=18&age[lte]=65
|
|
378
|
+
GET /users?role[in]=admin,user
|
|
379
|
+
GET /users?email[contains]=@gmail.com
|
|
380
|
+
GET /users?name[regex]=^John
|
|
381
|
+
|
|
382
|
+
# Pagination
|
|
383
|
+
GET /users?page=2&limit=50
|
|
384
|
+
GET /users?after=eyJfaWQiOi...&limit=20 # Cursor-based
|
|
385
|
+
|
|
386
|
+
# Sorting
|
|
387
|
+
GET /users?sort=-createdAt,name
|
|
388
|
+
|
|
389
|
+
# Search (requires text index)
|
|
390
|
+
GET /users?search=john
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
**Security features:**
|
|
394
|
+
- Blocks `$where`, `$function`, `$accumulator`, `$expr` operators
|
|
395
|
+
- ReDoS protection for regex patterns
|
|
396
|
+
- Max filter depth enforcement
|
|
397
|
+
- Collection allowlists for lookups
|
|
398
|
+
|
|
399
|
+
### JSON Schema Generation
|
|
400
|
+
|
|
401
|
+
Auto-generate JSON schemas from Mongoose models for validation and OpenAPI docs:
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
import { buildCrudSchemasFromModel } from '@classytic/mongokit';
|
|
405
|
+
|
|
406
|
+
const { crudSchemas } = buildCrudSchemasFromModel(UserModel, {
|
|
407
|
+
fieldRules: {
|
|
408
|
+
organizationId: { immutable: true }, // Can't update after create
|
|
409
|
+
role: { systemManaged: true }, // Users can't set this
|
|
410
|
+
createdAt: { systemManaged: true },
|
|
411
|
+
},
|
|
412
|
+
strictAdditionalProperties: true, // Reject unknown fields
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// Generated schemas:
|
|
416
|
+
// crudSchemas.createBody - POST body validation
|
|
417
|
+
// crudSchemas.updateBody - PATCH body validation
|
|
418
|
+
// crudSchemas.params - Route params (:id)
|
|
419
|
+
// crudSchemas.listQuery - GET query validation
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### Complete Controller Example
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
import {
|
|
426
|
+
Repository,
|
|
427
|
+
QueryParser,
|
|
428
|
+
buildCrudSchemasFromModel,
|
|
429
|
+
type IController,
|
|
430
|
+
type IRequestContext,
|
|
431
|
+
type IControllerResponse,
|
|
432
|
+
} from '@classytic/mongokit';
|
|
433
|
+
|
|
434
|
+
class UserController implements IController<IUser> {
|
|
435
|
+
private repo = new Repository(UserModel);
|
|
436
|
+
private parser = new QueryParser({ maxLimit: 100 });
|
|
437
|
+
|
|
438
|
+
async list(ctx: IRequestContext): Promise<IControllerResponse> {
|
|
439
|
+
const { filters, limit, page, sort } = this.parser.parse(ctx.query);
|
|
440
|
+
|
|
441
|
+
// Inject tenant filter
|
|
442
|
+
if (ctx.context?.organizationId) {
|
|
443
|
+
filters.organizationId = ctx.context.organizationId;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const result = await this.repo.getAll({ filters, limit, page, sort });
|
|
447
|
+
return { success: true, data: result, status: 200 };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async get(ctx: IRequestContext): Promise<IControllerResponse> {
|
|
451
|
+
const doc = await this.repo.getById(ctx.params.id);
|
|
452
|
+
return { success: true, data: doc, status: 200 };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async create(ctx: IRequestContext): Promise<IControllerResponse> {
|
|
456
|
+
const doc = await this.repo.create(ctx.body);
|
|
457
|
+
return { success: true, data: doc, status: 201 };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async update(ctx: IRequestContext): Promise<IControllerResponse> {
|
|
461
|
+
const doc = await this.repo.update(ctx.params.id, ctx.body);
|
|
462
|
+
return { success: true, data: doc, status: 200 };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async delete(ctx: IRequestContext): Promise<IControllerResponse> {
|
|
466
|
+
await this.repo.delete(ctx.params.id);
|
|
467
|
+
return { success: true, data: { message: 'Deleted' }, status: 200 };
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### Fastify Integration
|
|
473
|
+
|
|
474
|
+
```typescript
|
|
475
|
+
import { buildCrudSchemasFromModel } from '@classytic/mongokit';
|
|
476
|
+
|
|
477
|
+
const controller = new UserController();
|
|
478
|
+
const { crudSchemas } = buildCrudSchemasFromModel(UserModel);
|
|
479
|
+
|
|
480
|
+
// Routes with auto-validation and OpenAPI docs
|
|
481
|
+
fastify.get('/users', { schema: { querystring: crudSchemas.listQuery } }, async (req, reply) => {
|
|
482
|
+
const ctx = { query: req.query, body: {}, params: {}, user: req.user };
|
|
483
|
+
const response = await controller.list(ctx);
|
|
484
|
+
return reply.status(response.status).send(response);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
fastify.post('/users', { schema: { body: crudSchemas.createBody } }, async (req, reply) => {
|
|
488
|
+
const ctx = { query: {}, body: req.body, params: {}, user: req.user };
|
|
489
|
+
const response = await controller.create(ctx);
|
|
490
|
+
return reply.status(response.status).send(response);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
fastify.get('/users/:id', { schema: { params: crudSchemas.params } }, async (req, reply) => {
|
|
494
|
+
const ctx = { query: {}, body: {}, params: req.params, user: req.user };
|
|
495
|
+
const response = await controller.get(ctx);
|
|
496
|
+
return reply.status(response.status).send(response);
|
|
497
|
+
});
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### Express Integration
|
|
501
|
+
|
|
502
|
+
```typescript
|
|
503
|
+
const controller = new UserController();
|
|
504
|
+
|
|
505
|
+
app.get('/users', async (req, res) => {
|
|
506
|
+
const ctx = { query: req.query, body: {}, params: {}, user: req.user };
|
|
507
|
+
const response = await controller.list(ctx);
|
|
508
|
+
res.status(response.status).json(response);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
app.post('/users', async (req, res) => {
|
|
512
|
+
const ctx = { query: {}, body: req.body, params: {}, user: req.user };
|
|
513
|
+
const response = await controller.create(ctx);
|
|
514
|
+
res.status(response.status).json(response);
|
|
515
|
+
});
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
## TypeScript
|
|
519
|
+
|
|
520
|
+
```typescript
|
|
521
|
+
import { Repository, OffsetPaginationResult, KeysetPaginationResult } from '@classytic/mongokit';
|
|
522
|
+
|
|
523
|
+
interface IUser extends Document {
|
|
524
|
+
name: string;
|
|
525
|
+
email: string;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const repo = new Repository<IUser>(UserModel);
|
|
529
|
+
|
|
530
|
+
const result = await repo.getAll({ page: 1, limit: 20 });
|
|
531
|
+
|
|
532
|
+
// Discriminated union - TypeScript knows the type
|
|
533
|
+
if (result.method === 'offset') {
|
|
534
|
+
console.log(result.total, result.pages); // Available
|
|
535
|
+
}
|
|
536
|
+
if (result.method === 'keyset') {
|
|
537
|
+
console.log(result.next, result.hasMore); // Available
|
|
538
|
+
}
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
## Extending Repository
|
|
542
|
+
|
|
543
|
+
Create custom repository classes with domain-specific methods:
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
import { Repository, softDeletePlugin, timestampPlugin } from '@classytic/mongokit';
|
|
547
|
+
import UserModel, { IUser } from './models/User.js';
|
|
548
|
+
|
|
549
|
+
class UserRepository extends Repository<IUser> {
|
|
550
|
+
constructor() {
|
|
551
|
+
super(UserModel, [
|
|
552
|
+
timestampPlugin(),
|
|
553
|
+
softDeletePlugin()
|
|
554
|
+
], {
|
|
555
|
+
defaultLimit: 20
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Custom domain methods
|
|
560
|
+
async findByEmail(email: string) {
|
|
561
|
+
return this.getByQuery({ email });
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async findActiveUsers() {
|
|
565
|
+
return this.getAll({
|
|
566
|
+
filters: { status: 'active' },
|
|
567
|
+
sort: { createdAt: -1 }
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async deactivate(id: string) {
|
|
572
|
+
return this.update(id, { status: 'inactive', deactivatedAt: new Date() });
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Usage
|
|
577
|
+
const userRepo = new UserRepository();
|
|
578
|
+
const user = await userRepo.findByEmail('john@example.com');
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
### Overriding Methods
|
|
582
|
+
|
|
583
|
+
```typescript
|
|
584
|
+
class AuditedUserRepository extends Repository<IUser> {
|
|
585
|
+
constructor() {
|
|
586
|
+
super(UserModel);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Override create to add audit trail
|
|
590
|
+
async create(data: Partial<IUser>, options = {}) {
|
|
591
|
+
const result = await super.create({
|
|
592
|
+
...data,
|
|
593
|
+
createdBy: getCurrentUserId()
|
|
594
|
+
}, options);
|
|
595
|
+
|
|
596
|
+
await auditLog('user.created', result._id);
|
|
597
|
+
return result;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
## Factory Function
|
|
603
|
+
|
|
604
|
+
For simple cases without custom methods:
|
|
605
|
+
|
|
606
|
+
```javascript
|
|
607
|
+
import { createRepository, timestampPlugin } from '@classytic/mongokit';
|
|
608
|
+
|
|
609
|
+
const userRepo = createRepository(UserModel, [timestampPlugin()], {
|
|
610
|
+
defaultLimit: 20
|
|
611
|
+
});
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
## No Breaking Changes
|
|
615
|
+
|
|
616
|
+
Extending Repository works exactly the same with Mongoose 8 and 9. The package:
|
|
617
|
+
|
|
618
|
+
- Uses its own event system (not Mongoose middleware)
|
|
619
|
+
- Defines its own `FilterQuery` type (unaffected by Mongoose 9 rename)
|
|
620
|
+
- Properly gates update pipelines (safe for Mongoose 9's stricter defaults)
|
|
621
|
+
- All 194 tests pass on both Mongoose 8 and 9
|
|
622
|
+
|
|
623
|
+
## License
|
|
624
|
+
|
|
625
|
+
MIT
|