@ciscode/database-kit 1.0.0 β 1.0.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/CHANGELOG.md +50 -4
- package/README.md +487 -148
- package/dist/adapters/mongo.adapter.d.ts +53 -3
- package/dist/adapters/mongo.adapter.d.ts.map +1 -1
- package/dist/adapters/mongo.adapter.js +410 -27
- package/dist/adapters/mongo.adapter.js.map +1 -1
- package/dist/adapters/postgres.adapter.d.ts +50 -3
- package/dist/adapters/postgres.adapter.d.ts.map +1 -1
- package/dist/adapters/postgres.adapter.js +439 -45
- package/dist/adapters/postgres.adapter.js.map +1 -1
- package/dist/config/database.config.d.ts +1 -1
- package/dist/config/database.config.d.ts.map +1 -1
- package/dist/config/database.config.js +13 -13
- package/dist/config/database.config.js.map +1 -1
- package/dist/config/database.constants.js +7 -7
- package/dist/contracts/database.contracts.d.ts +283 -6
- package/dist/contracts/database.contracts.d.ts.map +1 -1
- package/dist/contracts/database.contracts.js +6 -1
- package/dist/contracts/database.contracts.js.map +1 -1
- package/dist/database-kit.module.d.ts +2 -2
- package/dist/database-kit.module.d.ts.map +1 -1
- package/dist/database-kit.module.js +1 -2
- package/dist/database-kit.module.js.map +1 -1
- package/dist/filters/database-exception.filter.d.ts +1 -1
- package/dist/filters/database-exception.filter.d.ts.map +1 -1
- package/dist/filters/database-exception.filter.js +43 -43
- package/dist/filters/database-exception.filter.js.map +1 -1
- package/dist/index.d.ts +10 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/database.decorators.d.ts.map +1 -1
- package/dist/middleware/database.decorators.js.map +1 -1
- package/dist/services/database.service.d.ts +83 -5
- package/dist/services/database.service.d.ts.map +1 -1
- package/dist/services/database.service.js +136 -16
- package/dist/services/database.service.js.map +1 -1
- package/dist/services/logger.service.d.ts +1 -1
- package/dist/services/logger.service.d.ts.map +1 -1
- package/dist/services/logger.service.js +1 -1
- package/dist/services/logger.service.js.map +1 -1
- package/dist/utils/pagination.utils.d.ts +2 -2
- package/dist/utils/pagination.utils.d.ts.map +1 -1
- package/dist/utils/pagination.utils.js +9 -6
- package/dist/utils/pagination.utils.js.map +1 -1
- package/dist/utils/validation.utils.d.ts.map +1 -1
- package/dist/utils/validation.utils.js +5 -5
- package/dist/utils/validation.utils.js.map +1 -1
- package/package.json +28 -8
package/README.md
CHANGED
|
@@ -5,18 +5,82 @@ A NestJS-friendly, OOP-style database library providing a unified repository API
|
|
|
5
5
|
[](https://www.npmjs.com/package/@ciscode/database-kit)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
[](https://nodejs.org)
|
|
8
|
+
[]()
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## π― How It Works
|
|
13
|
+
|
|
14
|
+
**DatabaseKit** provides a unified abstraction layer over MongoDB and PostgreSQL, allowing you to write database operations once and run them on either database system. Here's how the architecture works:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
18
|
+
β Your NestJS Application β
|
|
19
|
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
|
|
20
|
+
β β
|
|
21
|
+
β βββββββββββββββ inject βββββββββββββββββββββββββββββββββββ β
|
|
22
|
+
β β Service β ββββββββββββ β DatabaseService β β
|
|
23
|
+
β βββββββββββββββ β βββ createMongoRepository() β β
|
|
24
|
+
β β βββ createPostgresRepository() β β
|
|
25
|
+
β βββββββββββββββββ¬ββββββββββββββββββ β
|
|
26
|
+
β β β
|
|
27
|
+
β βββββββββββββββββββββββ΄ββββββββββββββββββ β
|
|
28
|
+
β β β β
|
|
29
|
+
β βββββββββββΌββββββββββ βββββββββββββββΌβββ
|
|
30
|
+
β β MongoAdapter β β PostgresAdapterβ
|
|
31
|
+
β β (Mongoose) β β (Knex.js) ββ
|
|
32
|
+
β βββββββββββ¬ββββββββββ βββββββββ¬βββββββββ
|
|
33
|
+
β β β β
|
|
34
|
+
ββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββΌβββββββββ
|
|
35
|
+
β β
|
|
36
|
+
βββββββββΌββββββββ βββββββββΌββββββββ
|
|
37
|
+
β MongoDB β β PostgreSQL β
|
|
38
|
+
βββββββββββββββββ βββββββββββββββββ
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### The Repository Pattern
|
|
42
|
+
|
|
43
|
+
Every repository (MongoDB or PostgreSQL) implements the **same interface**:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
const user = await repo.create({ name: "John" }); // Works on both!
|
|
47
|
+
const found = await repo.findById("123"); // Works on both!
|
|
48
|
+
const page = await repo.findPage({ page: 1 }); // Works on both!
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This means you can:
|
|
52
|
+
|
|
53
|
+
- **Switch databases** without changing your service code
|
|
54
|
+
- **Test with MongoDB** and deploy with PostgreSQL (or vice versa)
|
|
55
|
+
- **Use the same mental model** regardless of database
|
|
8
56
|
|
|
9
57
|
---
|
|
10
58
|
|
|
11
59
|
## β¨ Features
|
|
12
60
|
|
|
61
|
+
### Core Features
|
|
62
|
+
|
|
13
63
|
- β
**Unified Repository API** - Same interface for MongoDB and PostgreSQL
|
|
14
64
|
- β
**NestJS Integration** - First-class support with `DatabaseKitModule`
|
|
15
65
|
- β
**TypeScript First** - Full type safety and IntelliSense
|
|
16
66
|
- β
**Pagination Built-in** - Consistent pagination across databases
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
67
|
+
|
|
68
|
+
### Advanced Features
|
|
69
|
+
|
|
70
|
+
- β
**Transactions** - ACID transactions with automatic retry logic
|
|
71
|
+
- β
**Bulk Operations** - `insertMany`, `updateMany`, `deleteMany`
|
|
72
|
+
- β
**Soft Delete** - Non-destructive deletion with restore capability
|
|
73
|
+
- β
**Timestamps** - Automatic `createdAt`/`updatedAt` tracking
|
|
74
|
+
- β
**Health Checks** - Database monitoring and connection status
|
|
75
|
+
- β
**Connection Pool Config** - Fine-tune pool settings for performance
|
|
76
|
+
- β
**Event Hooks** - Lifecycle callbacks (beforeCreate, afterUpdate, etc.)
|
|
77
|
+
|
|
78
|
+
### Query Features
|
|
79
|
+
|
|
80
|
+
- β
**findOne** - Find single record by filter
|
|
81
|
+
- β
**upsert** - Update or insert in one operation
|
|
82
|
+
- β
**distinct** - Get unique values for a field
|
|
83
|
+
- β
**select** - Field projection (return only specific fields)
|
|
20
84
|
|
|
21
85
|
---
|
|
22
86
|
|
|
@@ -28,22 +92,18 @@ npm install @ciscode/database-kit
|
|
|
28
92
|
|
|
29
93
|
### Peer Dependencies
|
|
30
94
|
|
|
31
|
-
Make sure you have NestJS installed:
|
|
32
|
-
|
|
33
95
|
```bash
|
|
34
96
|
npm install @nestjs/common @nestjs/core reflect-metadata
|
|
35
97
|
```
|
|
36
98
|
|
|
37
99
|
### Database Drivers
|
|
38
100
|
|
|
39
|
-
Install the driver for your database:
|
|
40
|
-
|
|
41
101
|
```bash
|
|
42
102
|
# For MongoDB
|
|
43
103
|
npm install mongoose
|
|
44
104
|
|
|
45
105
|
# For PostgreSQL
|
|
46
|
-
npm install pg
|
|
106
|
+
npm install pg knex
|
|
47
107
|
```
|
|
48
108
|
|
|
49
109
|
---
|
|
@@ -70,7 +130,7 @@ import { DatabaseKitModule } from "@ciscode/database-kit";
|
|
|
70
130
|
export class AppModule {}
|
|
71
131
|
```
|
|
72
132
|
|
|
73
|
-
### 2.
|
|
133
|
+
### 2. Create a Repository and Use It
|
|
74
134
|
|
|
75
135
|
```typescript
|
|
76
136
|
// users.service.ts
|
|
@@ -86,6 +146,7 @@ interface User {
|
|
|
86
146
|
_id: string;
|
|
87
147
|
name: string;
|
|
88
148
|
email: string;
|
|
149
|
+
createdAt: Date;
|
|
89
150
|
}
|
|
90
151
|
|
|
91
152
|
@Injectable()
|
|
@@ -93,172 +154,360 @@ export class UsersService {
|
|
|
93
154
|
private readonly usersRepo: Repository<User>;
|
|
94
155
|
|
|
95
156
|
constructor(@InjectDatabase() private readonly db: DatabaseService) {
|
|
96
|
-
|
|
157
|
+
// For MongoDB
|
|
158
|
+
this.usersRepo = db.createMongoRepository<User>({
|
|
159
|
+
model: UserModel,
|
|
160
|
+
timestamps: true, // Auto createdAt/updatedAt
|
|
161
|
+
softDelete: true, // Enable soft delete
|
|
162
|
+
hooks: {
|
|
163
|
+
// Lifecycle hooks
|
|
164
|
+
beforeCreate: (ctx) => {
|
|
165
|
+
console.log("Creating user:", ctx.data);
|
|
166
|
+
return ctx.data; // Can modify data
|
|
167
|
+
},
|
|
168
|
+
afterCreate: (user) => {
|
|
169
|
+
console.log("User created:", user._id);
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
});
|
|
97
173
|
}
|
|
98
174
|
|
|
175
|
+
// CREATE
|
|
99
176
|
async createUser(data: Partial<User>): Promise<User> {
|
|
100
177
|
return this.usersRepo.create(data);
|
|
101
178
|
}
|
|
102
179
|
|
|
180
|
+
// READ
|
|
103
181
|
async getUser(id: string): Promise<User | null> {
|
|
104
182
|
return this.usersRepo.findById(id);
|
|
105
183
|
}
|
|
106
184
|
|
|
185
|
+
async getUserByEmail(email: string): Promise<User | null> {
|
|
186
|
+
return this.usersRepo.findOne({ email });
|
|
187
|
+
}
|
|
188
|
+
|
|
107
189
|
async listUsers(page = 1, limit = 10) {
|
|
108
|
-
return this.usersRepo.findPage({
|
|
190
|
+
return this.usersRepo.findPage({
|
|
191
|
+
page,
|
|
192
|
+
limit,
|
|
193
|
+
sort: "-createdAt",
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// UPDATE
|
|
198
|
+
async updateUser(id: string, data: Partial<User>): Promise<User | null> {
|
|
199
|
+
return this.usersRepo.updateById(id, data);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// UPSERT (update or create)
|
|
203
|
+
async upsertByEmail(email: string, data: Partial<User>): Promise<User> {
|
|
204
|
+
return this.usersRepo.upsert({ email }, data);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// DELETE (soft delete if enabled)
|
|
208
|
+
async deleteUser(id: string): Promise<boolean> {
|
|
209
|
+
return this.usersRepo.deleteById(id);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// RESTORE (only with soft delete)
|
|
213
|
+
async restoreUser(id: string): Promise<User | null> {
|
|
214
|
+
return this.usersRepo.restore!(id);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// BULK OPERATIONS
|
|
218
|
+
async createManyUsers(users: Partial<User>[]): Promise<User[]> {
|
|
219
|
+
return this.usersRepo.insertMany(users);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// DISTINCT VALUES
|
|
223
|
+
async getUniqueEmails(): Promise<string[]> {
|
|
224
|
+
return this.usersRepo.distinct("email");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// SELECT SPECIFIC FIELDS
|
|
228
|
+
async getUserNames(): Promise<Pick<User, "name" | "email">[]> {
|
|
229
|
+
return this.usersRepo.select({}, ["name", "email"]);
|
|
109
230
|
}
|
|
110
231
|
}
|
|
111
232
|
```
|
|
112
233
|
|
|
113
234
|
---
|
|
114
235
|
|
|
115
|
-
##
|
|
236
|
+
## π Complete Repository API
|
|
116
237
|
|
|
117
|
-
|
|
238
|
+
```typescript
|
|
239
|
+
interface Repository<T> {
|
|
240
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
241
|
+
// CRUD Operations
|
|
242
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
243
|
+
create(data: Partial<T>): Promise<T>;
|
|
244
|
+
findById(id: string | number): Promise<T | null>;
|
|
245
|
+
findOne(filter: Filter): Promise<T | null>;
|
|
246
|
+
findAll(filter?: Filter): Promise<T[]>;
|
|
247
|
+
findPage(options?: PageOptions): Promise<PageResult<T>>;
|
|
248
|
+
updateById(id: string | number, update: Partial<T>): Promise<T | null>;
|
|
249
|
+
deleteById(id: string | number): Promise<boolean>;
|
|
250
|
+
count(filter?: Filter): Promise<number>;
|
|
251
|
+
exists(filter?: Filter): Promise<boolean>;
|
|
252
|
+
|
|
253
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
254
|
+
// Bulk Operations
|
|
255
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
256
|
+
insertMany(data: Partial<T>[]): Promise<T[]>;
|
|
257
|
+
updateMany(filter: Filter, update: Partial<T>): Promise<number>;
|
|
258
|
+
deleteMany(filter: Filter): Promise<number>;
|
|
259
|
+
|
|
260
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
261
|
+
// Advanced Queries
|
|
262
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
263
|
+
upsert(filter: Filter, data: Partial<T>): Promise<T>;
|
|
264
|
+
distinct<K extends keyof T>(field: K, filter?: Filter): Promise<T[K][]>;
|
|
265
|
+
select<K extends keyof T>(filter: Filter, fields: K[]): Promise<Pick<T, K>[]>;
|
|
266
|
+
|
|
267
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
268
|
+
// Soft Delete (when enabled)
|
|
269
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
270
|
+
softDelete?(id: string | number): Promise<boolean>;
|
|
271
|
+
softDeleteMany?(filter: Filter): Promise<number>;
|
|
272
|
+
restore?(id: string | number): Promise<T | null>;
|
|
273
|
+
restoreMany?(filter: Filter): Promise<number>;
|
|
274
|
+
findWithDeleted?(filter?: Filter): Promise<T[]>;
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
118
279
|
|
|
119
|
-
|
|
120
|
-
| ----------------------------- | ---------------------------------------- | -------------- |
|
|
121
|
-
| `DATABASE_TYPE` | Database type (`mongo` or `postgres`) | Yes |
|
|
122
|
-
| `MONGO_URI` | MongoDB connection string | For MongoDB |
|
|
123
|
-
| `DATABASE_URL` | PostgreSQL connection string | For PostgreSQL |
|
|
124
|
-
| `DATABASE_POOL_SIZE` | Connection pool size (default: 10) | No |
|
|
125
|
-
| `DATABASE_CONNECTION_TIMEOUT` | Connection timeout in ms (default: 5000) | No |
|
|
280
|
+
## β‘ Advanced Features
|
|
126
281
|
|
|
127
|
-
###
|
|
282
|
+
### Transactions
|
|
283
|
+
|
|
284
|
+
Execute multiple operations atomically:
|
|
128
285
|
|
|
129
286
|
```typescript
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
287
|
+
// MongoDB Transaction
|
|
288
|
+
const result = await db.getMongoAdapter().withTransaction(
|
|
289
|
+
async (ctx) => {
|
|
290
|
+
const userRepo = ctx.createRepository<User>({ model: UserModel });
|
|
291
|
+
const orderRepo = ctx.createRepository<Order>({ model: OrderModel });
|
|
292
|
+
|
|
293
|
+
const user = await userRepo.create({ name: "John" });
|
|
294
|
+
const order = await orderRepo.create({ userId: user._id, total: 99.99 });
|
|
295
|
+
|
|
296
|
+
return { user, order };
|
|
134
297
|
},
|
|
135
|
-
|
|
136
|
-
|
|
298
|
+
{
|
|
299
|
+
maxRetries: 3, // Retry on transient errors
|
|
300
|
+
retryDelayMs: 100,
|
|
301
|
+
},
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
// PostgreSQL Transaction
|
|
305
|
+
const result = await db.getPostgresAdapter().withTransaction(
|
|
306
|
+
async (ctx) => {
|
|
307
|
+
const userRepo = ctx.createRepository<User>({ table: "users" });
|
|
308
|
+
const orderRepo = ctx.createRepository<Order>({ table: "orders" });
|
|
309
|
+
|
|
310
|
+
const user = await userRepo.create({ name: "John" });
|
|
311
|
+
const order = await orderRepo.create({ user_id: user.id, total: 99.99 });
|
|
312
|
+
|
|
313
|
+
return { user, order };
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
isolationLevel: "serializable",
|
|
317
|
+
},
|
|
318
|
+
);
|
|
137
319
|
```
|
|
138
320
|
|
|
139
|
-
###
|
|
321
|
+
### Event Hooks
|
|
322
|
+
|
|
323
|
+
React to repository lifecycle events:
|
|
140
324
|
|
|
141
325
|
```typescript
|
|
142
|
-
|
|
326
|
+
const repo = db.createMongoRepository<User>({
|
|
327
|
+
model: UserModel,
|
|
328
|
+
hooks: {
|
|
329
|
+
// Before create - can modify data
|
|
330
|
+
beforeCreate: (context) => {
|
|
331
|
+
console.log("Creating:", context.data);
|
|
332
|
+
return {
|
|
333
|
+
...context.data,
|
|
334
|
+
normalizedEmail: context.data.email?.toLowerCase(),
|
|
335
|
+
};
|
|
336
|
+
},
|
|
143
337
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
config: {
|
|
148
|
-
type: config.get("DATABASE_TYPE") as "mongo" | "postgres",
|
|
149
|
-
connectionString: config.get("DATABASE_URL")!,
|
|
338
|
+
// After create - for side effects
|
|
339
|
+
afterCreate: (user) => {
|
|
340
|
+
sendWelcomeEmail(user.email);
|
|
150
341
|
},
|
|
151
|
-
|
|
152
|
-
|
|
342
|
+
|
|
343
|
+
// Before update - can modify data
|
|
344
|
+
beforeUpdate: (context) => {
|
|
345
|
+
return { ...context.data, updatedBy: "system" };
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
// After update
|
|
349
|
+
afterUpdate: (user) => {
|
|
350
|
+
if (user) invalidateCache(user._id);
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
// Before delete - for validation
|
|
354
|
+
beforeDelete: (id) => {
|
|
355
|
+
console.log("Deleting user:", id);
|
|
356
|
+
},
|
|
357
|
+
|
|
358
|
+
// After delete
|
|
359
|
+
afterDelete: (success) => {
|
|
360
|
+
if (success) console.log("User deleted");
|
|
361
|
+
},
|
|
362
|
+
},
|
|
153
363
|
});
|
|
154
364
|
```
|
|
155
365
|
|
|
156
|
-
###
|
|
366
|
+
### Connection Pool Configuration
|
|
367
|
+
|
|
368
|
+
Fine-tune database connection pooling:
|
|
157
369
|
|
|
158
370
|
```typescript
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
371
|
+
// MongoDB
|
|
372
|
+
DatabaseKitModule.forRoot({
|
|
373
|
+
config: {
|
|
374
|
+
type: "mongo",
|
|
375
|
+
connectionString: process.env.MONGO_URI!,
|
|
376
|
+
pool: {
|
|
377
|
+
min: 5,
|
|
378
|
+
max: 50,
|
|
379
|
+
idleTimeoutMs: 30000,
|
|
380
|
+
acquireTimeoutMs: 60000,
|
|
381
|
+
},
|
|
382
|
+
// MongoDB-specific
|
|
383
|
+
serverSelectionTimeoutMS: 5000,
|
|
384
|
+
socketTimeoutMS: 45000,
|
|
385
|
+
},
|
|
386
|
+
});
|
|
173
387
|
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
388
|
+
// PostgreSQL
|
|
389
|
+
DatabaseKitModule.forRoot({
|
|
390
|
+
config: {
|
|
391
|
+
type: "postgres",
|
|
392
|
+
connectionString: process.env.DATABASE_URL!,
|
|
393
|
+
pool: {
|
|
394
|
+
min: 2,
|
|
395
|
+
max: 20,
|
|
396
|
+
idleTimeoutMs: 30000,
|
|
397
|
+
acquireTimeoutMs: 60000,
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
});
|
|
182
401
|
```
|
|
183
402
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
## π Repository API
|
|
403
|
+
### Health Checks
|
|
187
404
|
|
|
188
|
-
|
|
405
|
+
Monitor database health in production:
|
|
189
406
|
|
|
190
407
|
```typescript
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
408
|
+
@Controller("health")
|
|
409
|
+
export class HealthController {
|
|
410
|
+
constructor(@InjectDatabase() private readonly db: DatabaseService) {}
|
|
411
|
+
|
|
412
|
+
@Get()
|
|
413
|
+
async check() {
|
|
414
|
+
const mongoHealth = await this.db.getMongoAdapter().healthCheck();
|
|
415
|
+
// Returns:
|
|
416
|
+
// {
|
|
417
|
+
// healthy: true,
|
|
418
|
+
// responseTimeMs: 12,
|
|
419
|
+
// type: 'mongo',
|
|
420
|
+
// details: {
|
|
421
|
+
// version: 'MongoDB 6.0',
|
|
422
|
+
// activeConnections: 5,
|
|
423
|
+
// poolSize: 10,
|
|
424
|
+
// }
|
|
425
|
+
// }
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
status: mongoHealth.healthy ? "healthy" : "unhealthy",
|
|
429
|
+
database: mongoHealth,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
200
432
|
}
|
|
201
433
|
```
|
|
202
434
|
|
|
203
|
-
###
|
|
435
|
+
### Soft Delete
|
|
436
|
+
|
|
437
|
+
Non-destructive deletion with restore capability:
|
|
204
438
|
|
|
205
439
|
```typescript
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
sort: "-createdAt", // or { createdAt: -1 }
|
|
440
|
+
const repo = db.createMongoRepository<User>({
|
|
441
|
+
model: UserModel,
|
|
442
|
+
softDelete: true, // Enable soft delete
|
|
443
|
+
softDeleteField: "deletedAt", // Default field name
|
|
211
444
|
});
|
|
212
445
|
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
//
|
|
220
|
-
//
|
|
446
|
+
// "Delete" - sets deletedAt timestamp
|
|
447
|
+
await repo.deleteById("123");
|
|
448
|
+
|
|
449
|
+
// Regular queries exclude deleted records
|
|
450
|
+
await repo.findAll(); // Only non-deleted users
|
|
451
|
+
|
|
452
|
+
// Include deleted records
|
|
453
|
+
await repo.findWithDeleted!(); // All users including deleted
|
|
454
|
+
|
|
455
|
+
// Restore a deleted record
|
|
456
|
+
await repo.restore!("123");
|
|
221
457
|
```
|
|
222
458
|
|
|
223
|
-
###
|
|
459
|
+
### Timestamps
|
|
460
|
+
|
|
461
|
+
Automatic created/updated tracking:
|
|
224
462
|
|
|
225
463
|
```typescript
|
|
226
|
-
|
|
464
|
+
const repo = db.createMongoRepository<User>({
|
|
465
|
+
model: UserModel,
|
|
466
|
+
timestamps: true, // Enable timestamps
|
|
467
|
+
createdAtField: "createdAt", // Default
|
|
468
|
+
updatedAtField: "updatedAt", // Default
|
|
469
|
+
});
|
|
227
470
|
|
|
228
|
-
//
|
|
229
|
-
const
|
|
471
|
+
// create() automatically sets createdAt
|
|
472
|
+
const user = await repo.create({ name: "John" });
|
|
473
|
+
// user.createdAt = 2026-02-01T12:00:00.000Z
|
|
474
|
+
|
|
475
|
+
// updateById() automatically sets updatedAt
|
|
476
|
+
await repo.updateById(user._id, { name: "Johnny" });
|
|
477
|
+
// user.updatedAt = 2026-02-01T12:01:00.000Z
|
|
230
478
|
```
|
|
231
479
|
|
|
232
|
-
|
|
480
|
+
---
|
|
233
481
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
user_id: string;
|
|
238
|
-
total: number;
|
|
239
|
-
created_at: Date;
|
|
240
|
-
}
|
|
482
|
+
## π Query Operators
|
|
483
|
+
|
|
484
|
+
### MongoDB Queries
|
|
241
485
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
486
|
+
Standard MongoDB query syntax:
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
await repo.findAll({
|
|
490
|
+
age: { $gte: 18, $lt: 65 },
|
|
491
|
+
status: { $in: ["active", "pending"] },
|
|
492
|
+
name: { $regex: /john/i },
|
|
247
493
|
});
|
|
248
494
|
```
|
|
249
495
|
|
|
250
|
-
### PostgreSQL
|
|
496
|
+
### PostgreSQL Queries
|
|
497
|
+
|
|
498
|
+
Structured query operators:
|
|
251
499
|
|
|
252
500
|
```typescript
|
|
253
|
-
// Comparison
|
|
501
|
+
// Comparison
|
|
254
502
|
await repo.findAll({
|
|
255
|
-
price: { gt: 100, lte: 500 },
|
|
256
|
-
status: { ne: "cancelled" },
|
|
503
|
+
price: { gt: 100, lte: 500 }, // > 100 AND <= 500
|
|
504
|
+
status: { ne: "cancelled" }, // != 'cancelled'
|
|
257
505
|
});
|
|
258
506
|
|
|
259
507
|
// IN / NOT IN
|
|
260
508
|
await repo.findAll({
|
|
261
509
|
category: { in: ["electronics", "books"] },
|
|
510
|
+
brand: { nin: ["unknown"] },
|
|
262
511
|
});
|
|
263
512
|
|
|
264
513
|
// LIKE (case-insensitive)
|
|
@@ -269,34 +518,91 @@ await repo.findAll({
|
|
|
269
518
|
// NULL checks
|
|
270
519
|
await repo.findAll({
|
|
271
520
|
deleted_at: { isNull: true },
|
|
521
|
+
email: { isNotNull: true },
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// Sorting
|
|
525
|
+
await repo.findPage({
|
|
526
|
+
sort: "-created_at,name", // DESC created_at, ASC name
|
|
527
|
+
// or: { created_at: -1, name: 1 }
|
|
272
528
|
});
|
|
273
529
|
```
|
|
274
530
|
|
|
275
531
|
---
|
|
276
532
|
|
|
277
|
-
##
|
|
533
|
+
## βοΈ Configuration
|
|
278
534
|
|
|
279
|
-
###
|
|
535
|
+
### Environment Variables
|
|
536
|
+
|
|
537
|
+
| Variable | Description | Required |
|
|
538
|
+
| ------------------- | ---------------------------- | ------------------ |
|
|
539
|
+
| `DATABASE_TYPE` | `mongo` or `postgres` | Yes |
|
|
540
|
+
| `MONGO_URI` | MongoDB connection string | For MongoDB |
|
|
541
|
+
| `DATABASE_URL` | PostgreSQL connection string | For PostgreSQL |
|
|
542
|
+
| `DATABASE_POOL_MIN` | Min pool connections | No (default: 0) |
|
|
543
|
+
| `DATABASE_POOL_MAX` | Max pool connections | No (default: 10) |
|
|
544
|
+
| `DATABASE_TIMEOUT` | Connection timeout (ms) | No (default: 5000) |
|
|
280
545
|
|
|
281
|
-
|
|
546
|
+
### Async Configuration (Recommended)
|
|
282
547
|
|
|
283
548
|
```typescript
|
|
284
|
-
|
|
285
|
-
import { DatabaseExceptionFilter } from "@ciscode/database-kit";
|
|
549
|
+
import { ConfigModule, ConfigService } from "@nestjs/config";
|
|
286
550
|
|
|
287
|
-
|
|
551
|
+
DatabaseKitModule.forRootAsync({
|
|
552
|
+
imports: [ConfigModule],
|
|
553
|
+
useFactory: (config: ConfigService) => ({
|
|
554
|
+
config: {
|
|
555
|
+
type: config.get("DATABASE_TYPE") as "mongo" | "postgres",
|
|
556
|
+
connectionString: config.get("DATABASE_URL")!,
|
|
557
|
+
pool: {
|
|
558
|
+
min: config.get("DATABASE_POOL_MIN", 0),
|
|
559
|
+
max: config.get("DATABASE_POOL_MAX", 10),
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
}),
|
|
563
|
+
inject: [ConfigService],
|
|
564
|
+
});
|
|
288
565
|
```
|
|
289
566
|
|
|
290
|
-
|
|
567
|
+
### Multiple Databases
|
|
291
568
|
|
|
292
569
|
```typescript
|
|
293
|
-
import { APP_FILTER } from "@nestjs/core";
|
|
294
|
-
import { DatabaseExceptionFilter } from "@ciscode/database-kit";
|
|
295
|
-
|
|
296
570
|
@Module({
|
|
297
|
-
|
|
571
|
+
imports: [
|
|
572
|
+
// Primary database
|
|
573
|
+
DatabaseKitModule.forRoot({
|
|
574
|
+
config: { type: "mongo", connectionString: process.env.MONGO_URI! },
|
|
575
|
+
}),
|
|
576
|
+
// Analytics database (PostgreSQL)
|
|
577
|
+
DatabaseKitModule.forFeature("ANALYTICS_DB", {
|
|
578
|
+
type: "postgres",
|
|
579
|
+
connectionString: process.env.ANALYTICS_DB_URL!,
|
|
580
|
+
}),
|
|
581
|
+
],
|
|
298
582
|
})
|
|
299
583
|
export class AppModule {}
|
|
584
|
+
|
|
585
|
+
// Usage
|
|
586
|
+
@Injectable()
|
|
587
|
+
export class AnalyticsService {
|
|
588
|
+
constructor(
|
|
589
|
+
@InjectDatabaseByToken("ANALYTICS_DB")
|
|
590
|
+
private readonly analyticsDb: DatabaseService,
|
|
591
|
+
) {}
|
|
592
|
+
}
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
|
|
597
|
+
## π‘οΈ Error Handling
|
|
598
|
+
|
|
599
|
+
### Global Exception Filter
|
|
600
|
+
|
|
601
|
+
```typescript
|
|
602
|
+
// main.ts
|
|
603
|
+
import { DatabaseExceptionFilter } from "@ciscode/database-kit";
|
|
604
|
+
|
|
605
|
+
app.useGlobalFilters(new DatabaseExceptionFilter());
|
|
300
606
|
```
|
|
301
607
|
|
|
302
608
|
### Error Response Format
|
|
@@ -306,7 +612,7 @@ export class AppModule {}
|
|
|
306
612
|
"statusCode": 409,
|
|
307
613
|
"message": "A record with this value already exists",
|
|
308
614
|
"error": "DuplicateKeyError",
|
|
309
|
-
"timestamp": "2026-
|
|
615
|
+
"timestamp": "2026-02-01T12:00:00.000Z",
|
|
310
616
|
"path": "/api/users"
|
|
311
617
|
}
|
|
312
618
|
```
|
|
@@ -322,10 +628,15 @@ import {
|
|
|
322
628
|
normalizePaginationOptions,
|
|
323
629
|
parseSortString,
|
|
324
630
|
calculateOffset,
|
|
631
|
+
createPageResult,
|
|
325
632
|
} from "@ciscode/database-kit";
|
|
326
633
|
|
|
327
634
|
const normalized = normalizePaginationOptions({ page: 1 });
|
|
635
|
+
// { page: 1, limit: 10, filter: {}, sort: undefined }
|
|
636
|
+
|
|
328
637
|
const sortObj = parseSortString("-createdAt,name");
|
|
638
|
+
// { createdAt: -1, name: 1 }
|
|
639
|
+
|
|
329
640
|
const offset = calculateOffset(2, 10); // 10
|
|
330
641
|
```
|
|
331
642
|
|
|
@@ -337,6 +648,7 @@ import {
|
|
|
337
648
|
isValidUuid,
|
|
338
649
|
sanitizeFilter,
|
|
339
650
|
pickFields,
|
|
651
|
+
omitFields,
|
|
340
652
|
} from "@ciscode/database-kit";
|
|
341
653
|
|
|
342
654
|
isValidMongoId("507f1f77bcf86cd799439011"); // true
|
|
@@ -345,7 +657,8 @@ isValidUuid("550e8400-e29b-41d4-a716-446655440000"); // true
|
|
|
345
657
|
const clean = sanitizeFilter({ name: "John", age: undefined });
|
|
346
658
|
// { name: 'John' }
|
|
347
659
|
|
|
348
|
-
const picked = pickFields(
|
|
660
|
+
const picked = pickFields(user, ["name", "email"]);
|
|
661
|
+
const safe = omitFields(user, ["password", "secret"]);
|
|
349
662
|
```
|
|
350
663
|
|
|
351
664
|
---
|
|
@@ -358,17 +671,31 @@ npm test
|
|
|
358
671
|
|
|
359
672
|
# Run with coverage
|
|
360
673
|
npm run test:cov
|
|
674
|
+
|
|
675
|
+
# Run specific test file
|
|
676
|
+
npm test -- --testPathPattern=mongo.adapter.spec
|
|
361
677
|
```
|
|
362
678
|
|
|
363
679
|
### Mocking in Tests
|
|
364
680
|
|
|
365
681
|
```typescript
|
|
682
|
+
import { Test } from "@nestjs/testing";
|
|
683
|
+
import { DATABASE_TOKEN } from "@ciscode/database-kit";
|
|
684
|
+
|
|
685
|
+
const mockRepository = {
|
|
686
|
+
create: jest.fn().mockResolvedValue({ id: "1", name: "Test" }),
|
|
687
|
+
findById: jest.fn().mockResolvedValue({ id: "1", name: "Test" }),
|
|
688
|
+
findAll: jest.fn().mockResolvedValue([]),
|
|
689
|
+
findPage: jest
|
|
690
|
+
.fn()
|
|
691
|
+
.mockResolvedValue({ data: [], total: 0, page: 1, limit: 10, pages: 0 }),
|
|
692
|
+
updateById: jest.fn().mockResolvedValue({ id: "1", name: "Updated" }),
|
|
693
|
+
deleteById: jest.fn().mockResolvedValue(true),
|
|
694
|
+
};
|
|
695
|
+
|
|
366
696
|
const mockDb = {
|
|
367
|
-
createMongoRepository: jest.fn().mockReturnValue(
|
|
368
|
-
|
|
369
|
-
findById: jest.fn(),
|
|
370
|
-
// ...
|
|
371
|
-
}),
|
|
697
|
+
createMongoRepository: jest.fn().mockReturnValue(mockRepository),
|
|
698
|
+
createPostgresRepository: jest.fn().mockReturnValue(mockRepository),
|
|
372
699
|
};
|
|
373
700
|
|
|
374
701
|
const module = await Test.createTestingModule({
|
|
@@ -382,30 +709,42 @@ const module = await Test.createTestingModule({
|
|
|
382
709
|
|
|
383
710
|
```
|
|
384
711
|
src/
|
|
385
|
-
βββ index.ts
|
|
386
|
-
βββ database-kit.module.ts
|
|
387
|
-
βββ adapters/
|
|
388
|
-
β βββ mongo.adapter.ts
|
|
389
|
-
β βββ postgres.adapter.ts
|
|
390
|
-
βββ config/
|
|
391
|
-
β βββ database.config.ts
|
|
392
|
-
β βββ database.constants.ts
|
|
393
|
-
βββ contracts/
|
|
394
|
-
β βββ database.contracts.ts
|
|
395
|
-
βββ filters/
|
|
396
|
-
β βββ database-exception.filter.ts
|
|
397
|
-
βββ middleware/
|
|
398
|
-
β βββ database.decorators.ts
|
|
399
|
-
βββ services/
|
|
400
|
-
β βββ database.service.ts
|
|
401
|
-
β βββ logger.service.ts
|
|
402
|
-
βββ utils/
|
|
403
|
-
βββ pagination.utils.ts
|
|
404
|
-
βββ validation.utils.ts
|
|
712
|
+
βββ index.ts # Public API exports
|
|
713
|
+
βββ database-kit.module.ts # NestJS module
|
|
714
|
+
βββ adapters/
|
|
715
|
+
β βββ mongo.adapter.ts # MongoDB implementation
|
|
716
|
+
β βββ postgres.adapter.ts # PostgreSQL implementation
|
|
717
|
+
βββ config/
|
|
718
|
+
β βββ database.config.ts # Configuration helper
|
|
719
|
+
β βββ database.constants.ts # Constants
|
|
720
|
+
βββ contracts/
|
|
721
|
+
β βββ database.contracts.ts # TypeScript interfaces
|
|
722
|
+
βββ filters/
|
|
723
|
+
β βββ database-exception.filter.ts # Error handling
|
|
724
|
+
βββ middleware/
|
|
725
|
+
β βββ database.decorators.ts # DI decorators
|
|
726
|
+
βββ services/
|
|
727
|
+
β βββ database.service.ts # Main service
|
|
728
|
+
β βββ logger.service.ts # Logging
|
|
729
|
+
βββ utils/
|
|
730
|
+
βββ pagination.utils.ts # Pagination helpers
|
|
731
|
+
βββ validation.utils.ts # Validation helpers
|
|
405
732
|
```
|
|
406
733
|
|
|
407
734
|
---
|
|
408
735
|
|
|
736
|
+
## π Package Stats
|
|
737
|
+
|
|
738
|
+
| Metric | Value |
|
|
739
|
+
| ---------------- | ---------------------------- |
|
|
740
|
+
| **Version** | 1.0.0 |
|
|
741
|
+
| **Tests** | 133 passing |
|
|
742
|
+
| **Total LOC** | ~5,200 lines |
|
|
743
|
+
| **TypeScript** | 100% |
|
|
744
|
+
| **Dependencies** | Minimal (mongoose, knex, pg) |
|
|
745
|
+
|
|
746
|
+
---
|
|
747
|
+
|
|
409
748
|
## π Security
|
|
410
749
|
|
|
411
750
|
See [SECURITY.md](SECURITY.md) for:
|
|
@@ -435,12 +774,12 @@ See [CHANGELOG.md](CHANGELOG.md) for version history.
|
|
|
435
774
|
|
|
436
775
|
## π License
|
|
437
776
|
|
|
438
|
-
MIT Β© [C International Service](https://ciscode.
|
|
777
|
+
MIT Β© [C International Service](https://ciscode.co.uk)
|
|
439
778
|
|
|
440
779
|
---
|
|
441
780
|
|
|
442
781
|
## π Support
|
|
443
782
|
|
|
444
|
-
- π§ Email: info@
|
|
783
|
+
- π§ Email: info@ciscod.com
|
|
445
784
|
- π Issues: [GitHub Issues](https://github.com/CISCODE-MA/DatabaseKit/issues)
|
|
446
785
|
- π Docs: [GitHub Wiki](https://github.com/CISCODE-MA/DatabaseKit/wiki)
|