@classytic/mongokit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Classytic (Sadman Chowdhury)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md ADDED
@@ -0,0 +1,433 @@
1
+ # @classytic/mongokit
2
+
3
+ [![Test](https://github.com/classytic/mongokit/actions/workflows/test.yml/badge.svg)](https://github.com/classytic/mongokit/actions/workflows/test.yml)
4
+ [![npm version](https://badge.fury.io/js/@classytic%2Fmongokit.svg)](https://www.npmjs.com/package/@classytic/mongokit)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ > Event-driven MongoDB repositories for any Node.js framework
8
+
9
+ **Works with:** Express • Fastify • NestJS • Next.js • Koa • Hapi • Serverless
10
+
11
+ - ✅ **Plugin-based architecture**
12
+ - ✅ **Event hooks** for every operation
13
+ - ✅ **Framework-agnostic**
14
+ - ✅ **TypeScript** support
15
+ - ✅ **Battle-tested** in production
16
+
17
+ ---
18
+
19
+ ## 📦 Installation
20
+
21
+ ```bash
22
+ npm install @classytic/mongokit mongoose
23
+ ```
24
+
25
+ > **Peer Dependency:** Requires `mongoose ^8.0.0`
26
+
27
+ ---
28
+
29
+ ## 🚀 Quick Start
30
+
31
+ ### Basic Usage
32
+
33
+ ```javascript
34
+ import { Repository } from '@classytic/mongokit';
35
+ import UserModel from './models/User.js';
36
+
37
+ class UserRepository extends Repository {
38
+ constructor() {
39
+ super(UserModel);
40
+ }
41
+
42
+ async findActiveUsers() {
43
+ return this.getAll({ filters: { status: 'active' } });
44
+ }
45
+ }
46
+
47
+ const userRepo = new UserRepository();
48
+
49
+ // Create
50
+ const user = await userRepo.create({ name: 'John', email: 'john@example.com' });
51
+
52
+ // Read
53
+ const users = await userRepo.getAll({ pagination: { page: 1, limit: 10 } });
54
+ const user = await userRepo.getById('user-id');
55
+
56
+ // Update
57
+ await userRepo.update('user-id', { name: 'Jane' });
58
+
59
+ // Delete
60
+ await userRepo.delete('user-id');
61
+ ```
62
+
63
+ ### With Express
64
+
65
+ ```javascript
66
+ import express from 'express';
67
+ import { Repository } from '@classytic/mongokit';
68
+
69
+ const app = express();
70
+ const userRepo = new Repository(UserModel);
71
+
72
+ app.get('/users', async (req, res) => {
73
+ const users = await userRepo.getAll({
74
+ filters: { status: 'active' },
75
+ pagination: { page: req.query.page || 1, limit: 20 }
76
+ });
77
+ res.json(users);
78
+ });
79
+ ```
80
+
81
+ ### With Fastify
82
+
83
+ ```javascript
84
+ import Fastify from 'fastify';
85
+ import { Repository } from '@classytic/mongokit';
86
+
87
+ const fastify = Fastify();
88
+ const userRepo = new Repository(UserModel);
89
+
90
+ fastify.get('/users', async (request, reply) => {
91
+ const users = await userRepo.getAll();
92
+ return users;
93
+ });
94
+ ```
95
+
96
+ ### With Next.js API Routes
97
+
98
+ ```javascript
99
+ // pages/api/users.js
100
+ import { Repository } from '@classytic/mongokit';
101
+ import UserModel from '@/models/User';
102
+
103
+ const userRepo = new Repository(UserModel);
104
+
105
+ export default async function handler(req, res) {
106
+ if (req.method === 'GET') {
107
+ const users = await userRepo.getAll();
108
+ res.json(users);
109
+ }
110
+ }
111
+ ```
112
+
113
+ ---
114
+
115
+ ## 🔌 Built-in Plugins
116
+
117
+ ### Field Filtering (Role-based Access)
118
+
119
+ Control which fields are visible based on user roles:
120
+
121
+ ```javascript
122
+ import { Repository, fieldFilterPlugin } from '@classytic/mongokit';
123
+
124
+ const fieldPreset = {
125
+ public: ['id', 'name', 'email'],
126
+ authenticated: ['phone', 'address'],
127
+ admin: ['createdAt', 'updatedAt', 'internalNotes']
128
+ };
129
+
130
+ class UserRepository extends Repository {
131
+ constructor() {
132
+ super(User, [fieldFilterPlugin(fieldPreset)]);
133
+ }
134
+ }
135
+ ```
136
+
137
+ ### Validation Chain
138
+
139
+ Add custom validation rules:
140
+
141
+ ```javascript
142
+ import {
143
+ Repository,
144
+ validationChainPlugin,
145
+ requireField,
146
+ uniqueField,
147
+ immutableField
148
+ } from '@classytic/mongokit';
149
+
150
+ class UserRepository extends Repository {
151
+ constructor() {
152
+ super(User, [
153
+ validationChainPlugin([
154
+ requireField('email', ['create']),
155
+ uniqueField('email', 'Email already exists'),
156
+ immutableField('userId')
157
+ ])
158
+ ]);
159
+ }
160
+ }
161
+ ```
162
+
163
+ ### Soft Delete
164
+
165
+ Mark records as deleted without actually removing them:
166
+
167
+ ```javascript
168
+ import { Repository, softDeletePlugin } from '@classytic/mongokit';
169
+
170
+ class UserRepository extends Repository {
171
+ constructor() {
172
+ super(User, [softDeletePlugin({ deletedField: 'deletedAt' })]);
173
+ }
174
+ }
175
+
176
+ // repo.delete(id) → marks as deleted instead of removing
177
+ // repo.getAll() → excludes deleted records
178
+ // repo.getAll({ includeDeleted: true }) → includes deleted
179
+ ```
180
+
181
+ ### Audit Logging
182
+
183
+ Log all create, update, and delete operations:
184
+
185
+ ```javascript
186
+ import { Repository, auditLogPlugin } from '@classytic/mongokit';
187
+ import logger from './logger.js';
188
+
189
+ class UserRepository extends Repository {
190
+ constructor() {
191
+ super(User, [auditLogPlugin(logger)]);
192
+ }
193
+ }
194
+
195
+ // All CUD operations automatically logged
196
+ ```
197
+
198
+ ### More Plugins
199
+
200
+ - **`timestampPlugin()`** - Auto-manage `createdAt`/`updatedAt`
201
+ - **`mongoOperationsPlugin()`** - Adds `increment`, `pushToArray`, `upsert`, etc.
202
+ - **`batchOperationsPlugin()`** - Adds `updateMany`, `deleteMany`
203
+ - **`aggregateHelpersPlugin()`** - Adds `groupBy`, `sum`, `average`, etc.
204
+ - **`subdocumentPlugin()`** - Manage subdocument arrays easily
205
+
206
+ ---
207
+
208
+ ## 🎯 Core API
209
+
210
+ ### CRUD Operations
211
+
212
+ | Method | Description | Example |
213
+ |--------|-------------|---------|
214
+ | `create(data, opts)` | Create single document | `repo.create({ name: 'John' })` |
215
+ | `createMany(data[], opts)` | Create multiple documents | `repo.createMany([{...}, {...}])` |
216
+ | `getById(id, opts)` | Find by ID | `repo.getById('123')` |
217
+ | `getByQuery(query, opts)` | Find one by query | `repo.getByQuery({ email: 'a@b.com' })` |
218
+ | `getAll(params, opts)` | Paginated list | `repo.getAll({ filters: { active: true } })` |
219
+ | `getOrCreate(query, data, opts)` | Find or create | `repo.getOrCreate({ email }, { email, name })` |
220
+ | `update(id, data, opts)` | Update document | `repo.update('123', { name: 'Jane' })` |
221
+ | `delete(id, opts)` | Delete document | `repo.delete('123')` |
222
+ | `count(query, opts)` | Count documents | `repo.count({ status: 'active' })` |
223
+ | `exists(query, opts)` | Check existence | `repo.exists({ email: 'a@b.com' })` |
224
+
225
+ ### Aggregation
226
+
227
+ ```javascript
228
+ // Basic aggregation
229
+ const result = await repo.aggregate([
230
+ { $match: { status: 'active' } },
231
+ { $group: { _id: '$category', total: { $sum: 1 } } }
232
+ ]);
233
+
234
+ // Paginated aggregation
235
+ const result = await repo.aggregatePaginate([
236
+ { $match: { status: 'active' } }
237
+ ], { page: 1, limit: 20 });
238
+
239
+ // Distinct values
240
+ const categories = await repo.distinct('category');
241
+ ```
242
+
243
+ ### Transactions
244
+
245
+ ```javascript
246
+ await repo.withTransaction(async (session) => {
247
+ await repo.create({ name: 'User 1' }, { session });
248
+ await repo.create({ name: 'User 2' }, { session });
249
+ // Auto-commits if no errors, auto-rollbacks on errors
250
+ });
251
+ ```
252
+
253
+ ---
254
+
255
+ ## 🎨 Event System
256
+
257
+ Every operation emits lifecycle events:
258
+
259
+ ```javascript
260
+ repo.on('before:create', async (context) => {
261
+ console.log('About to create:', context.data);
262
+ // Modify context.data if needed
263
+ context.data.processedAt = new Date();
264
+ });
265
+
266
+ repo.on('after:create', ({ context, result }) => {
267
+ console.log('Created:', result);
268
+ // Send notification, update cache, etc.
269
+ });
270
+
271
+ repo.on('error:create', ({ context, error }) => {
272
+ console.error('Failed to create:', error);
273
+ // Log error, send alert, etc.
274
+ });
275
+ ```
276
+
277
+ **Available Events:**
278
+ - `before:create`, `after:create`, `error:create`
279
+ - `before:update`, `after:update`, `error:update`
280
+ - `before:delete`, `after:delete`, `error:delete`
281
+ - `before:createMany`, `after:createMany`, `error:createMany`
282
+ - `before:getAll`, `before:getById`, `before:getByQuery`
283
+
284
+ ---
285
+
286
+ ## 🔧 Custom Plugins
287
+
288
+ Create your own plugins:
289
+
290
+ ```javascript
291
+ export const timestampPlugin = () => ({
292
+ name: 'timestamp',
293
+
294
+ apply(repo) {
295
+ repo.on('before:create', (context) => {
296
+ context.data.createdAt = new Date();
297
+ context.data.updatedAt = new Date();
298
+ });
299
+
300
+ repo.on('before:update', (context) => {
301
+ context.data.updatedAt = new Date();
302
+ });
303
+ }
304
+ });
305
+
306
+ // Use it
307
+ class UserRepository extends Repository {
308
+ constructor() {
309
+ super(User, [timestampPlugin()]);
310
+ }
311
+ }
312
+ ```
313
+
314
+ ---
315
+
316
+ ## 📚 TypeScript Support
317
+
318
+ Full TypeScript definitions included:
319
+
320
+ ```typescript
321
+ import { Repository, Plugin, RepositoryContext } from '@classytic/mongokit';
322
+ import { Model, Document } from 'mongoose';
323
+
324
+ interface IUser extends Document {
325
+ name: string;
326
+ email: string;
327
+ status: 'active' | 'inactive';
328
+ }
329
+
330
+ class UserRepository extends Repository<IUser> {
331
+ constructor() {
332
+ super(UserModel);
333
+ }
334
+
335
+ async findActive(): Promise<IUser[]> {
336
+ const result = await this.getAll({
337
+ filters: { status: 'active' }
338
+ });
339
+ return result.docs;
340
+ }
341
+ }
342
+ ```
343
+
344
+ ---
345
+
346
+ ## 🏗️ Advanced Patterns
347
+
348
+ ### Custom Methods
349
+
350
+ ```javascript
351
+ class MembershipRepository extends Repository {
352
+ constructor() {
353
+ super(Membership);
354
+ }
355
+
356
+ async findActiveByCustomer(customerId) {
357
+ return this.getAll({
358
+ filters: {
359
+ customerId,
360
+ status: { $in: ['active', 'paused'] }
361
+ }
362
+ });
363
+ }
364
+
365
+ async recordVisit(membershipId) {
366
+ return this.update(membershipId, {
367
+ $set: { lastVisitedAt: new Date() },
368
+ $inc: { totalVisits: 1 }
369
+ });
370
+ }
371
+ }
372
+ ```
373
+
374
+ ### Combining Multiple Plugins
375
+
376
+ ```javascript
377
+ import {
378
+ Repository,
379
+ softDeletePlugin,
380
+ auditLogPlugin,
381
+ fieldFilterPlugin
382
+ } from '@classytic/mongokit';
383
+
384
+ class UserRepository extends Repository {
385
+ constructor() {
386
+ super(User, [
387
+ softDeletePlugin(),
388
+ auditLogPlugin(logger),
389
+ fieldFilterPlugin(userFieldPreset)
390
+ ]);
391
+ }
392
+ }
393
+ ```
394
+
395
+ ---
396
+
397
+ ## 🌟 Why MongoKit?
398
+
399
+ ### vs. Mongoose Directly
400
+ - ✅ Consistent API across all models
401
+ - ✅ Built-in pagination, filtering, sorting
402
+ - ✅ Multi-tenancy without repetitive code
403
+ - ✅ Event hooks for cross-cutting concerns
404
+ - ✅ Plugin system for reusable behaviors
405
+
406
+ ### vs. TypeORM / Prisma
407
+ - ✅ Lighter weight (works with Mongoose)
408
+ - ✅ Event-driven architecture
409
+ - ✅ More flexible plugin system
410
+ - ✅ No migration needed if using Mongoose
411
+ - ✅ Framework-agnostic
412
+
413
+ ### vs. Raw Repository Pattern
414
+ - ✅ Battle-tested implementation
415
+ - ✅ 11 built-in plugins ready to use
416
+ - ✅ Comprehensive documentation
417
+ - ✅ TypeScript support
418
+ - ✅ Active maintenance
419
+
420
+ ---
421
+
422
+ ## 🧪 Testing
423
+
424
+ ```bash
425
+ npm test
426
+ ```
427
+
428
+ ---
429
+
430
+ ## 📄 License
431
+
432
+ MIT © [Classytic](https://github.com/classytic)
433
+
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@classytic/mongokit",
3
+ "version": "1.0.0",
4
+ "description": "Event-driven MongoDB repositories for any Node.js framework",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "types": "./types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./types/index.d.ts",
11
+ "import": "./src/index.js"
12
+ },
13
+ "./plugins": {
14
+ "types": "./types/plugins/index.d.ts",
15
+ "import": "./src/plugins/index.js"
16
+ },
17
+ "./utils": {
18
+ "types": "./types/utils/index.d.ts",
19
+ "import": "./src/utils/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "src",
24
+ "types",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "keywords": [
29
+ "mongodb",
30
+ "mongoose",
31
+ "repository",
32
+ "repository-pattern",
33
+ "data-access",
34
+ "multi-tenant",
35
+ "multi-tenancy",
36
+ "event-driven",
37
+ "plugin-based",
38
+ "express",
39
+ "fastify",
40
+ "nestjs",
41
+ "nextjs",
42
+ "typescript"
43
+ ],
44
+ "author": "Sadman Chowdhury (Github: @siam923)",
45
+ "license": "MIT",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/classytic/mongokit.git"
49
+ },
50
+ "bugs": {
51
+ "url": "https://github.com/classytic/mongokit/issues"
52
+ },
53
+ "homepage": "https://github.com/classytic/mongokit#readme",
54
+ "peerDependencies": {
55
+ "mongoose": "^8.0.0"
56
+ },
57
+ "dependencies": {
58
+ "http-errors": "^2.0.0"
59
+ },
60
+ "engines": {
61
+ "node": ">=18"
62
+ },
63
+ "scripts": {
64
+ "test": "node --test test/*.test.js",
65
+ "test:watch": "node --test --watch test/*.test.js"
66
+ },
67
+ "devDependencies": {
68
+ "mongoose-paginate-v2": "^1.9.0"
69
+ }
70
+ }
71
+