A NestJS-friendly, OOP-style database library providing a unified repository API for MongoDB and PostgreSQL.
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:
┌─────────────────────────────────────────────────────────────────────┐
│ Your NestJS Application │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ inject ┌─────────────────────────────────┐ │
│ │ Service │ ──────────── │ DatabaseService │ │
│ └─────────────┘ │ ├── createMongoRepository() │ │
│ │ └── createPostgresRepository() │ │
│ └───────────────┬─────────────────┘ │
│ │ │
│ ┌─────────────────────┴─────────────────┐ │
│ │ │ │
│ ┌─────────▼─────────┐ ┌─────────────▼─┐│
│ │ MongoAdapter │ │ PostgresAdapter│
│ │ (Mongoose) │ │ (Knex.js) ││
│ └─────────┬─────────┘ └───────┬───────┘│
│ │ │ │
└──────────────────────────┼─────────────────────────────────┼────────┘
│ │
┌───────▼───────┐ ┌───────▼───────┐
│ MongoDB │ │ PostgreSQL │
└───────────────┘ └───────────────┘
Every repository (MongoDB or PostgreSQL) implements the same interface:
constuser=awaitrepo.create({name: 'John'});// Works on both!constfound=awaitrepo.findById('123');// Works on both!constpage=awaitrepo.findPage({page: 1});// Works on both!This means you can:
- Switch databases without changing your service code
- Test with MongoDB and deploy with PostgreSQL (or vice versa)
- Use the same mental model regardless of database
- ✅ Unified Repository API - Same interface for MongoDB and PostgreSQL
- ✅ NestJS Integration - First-class support with
DatabaseKitModule - ✅ TypeScript First - Full type safety and IntelliSense
- ✅ Pagination Built-in - Consistent pagination across databases
- ✅ Transactions - ACID transactions with automatic retry logic
- ✅ Bulk Operations -
insertMany,updateMany,deleteMany - ✅ Soft Delete - Non-destructive deletion with restore capability
- ✅ Timestamps - Automatic
createdAt/updatedAttracking - ✅ Health Checks - Database monitoring and connection status
- ✅ Connection Pool Config - Fine-tune pool settings for performance
- ✅ Event Hooks - Lifecycle callbacks (beforeCreate, afterUpdate, etc.)
- ✅ findOne - Find single record by filter
- ✅ upsert - Update or insert in one operation
- ✅ distinct - Get unique values for a field
- ✅ select - Field projection (return only specific fields)
npm install @ciscode/database-kitnpm install @nestjs/common @nestjs/core reflect-metadata# For MongoDB
npm install mongoose
# For PostgreSQL
npm install pg knex// app.module.tsimport{Module}from'@nestjs/common';import{DatabaseKitModule}from'@ciscode/database-kit';
@Module({imports: [DatabaseKitModule.forRoot({config: {type: 'mongo',// or 'postgres'connectionString: process.env.MONGO_URI!,},}),],})exportclassAppModule{}// users.service.tsimport{Injectable}from'@nestjs/common';import{InjectDatabase,DatabaseService,Repository,}from'@ciscode/database-kit';import{UserModel}from'./user.model';interfaceUser{_id: string;name: string;email: string;createdAt: Date;}
@Injectable()exportclassUsersService{privatereadonlyusersRepo: Repository<User>;constructor(@InjectDatabase()privatereadonlydb: DatabaseService){// For MongoDBthis.usersRepo=db.createMongoRepository<User>({model: UserModel,timestamps: true,// Auto createdAt/updatedAtsoftDelete: true,// Enable soft deletehooks: {// Lifecycle hooksbeforeCreate: (ctx)=>{console.log('Creating user:',ctx.data);returnctx.data;// Can modify data},afterCreate: (user)=>{console.log('User created:',user._id);},},});}// CREATEasynccreateUser(data: Partial<User>): Promise<User>{returnthis.usersRepo.create(data);}// READasyncgetUser(id: string): Promise<User|null>{returnthis.usersRepo.findById(id);}asyncgetUserByEmail(email: string): Promise<User|null>{returnthis.usersRepo.findOne({ email });}asynclistUsers(page=1,limit=10){returnthis.usersRepo.findPage({
page,
limit,sort: '-createdAt',});}// UPDATEasyncupdateUser(id: string,data: Partial<User>): Promise<User|null>{returnthis.usersRepo.updateById(id,data);}// UPSERT (update or create)asyncupsertByEmail(email: string,data: Partial<User>): Promise<User>{returnthis.usersRepo.upsert({ email },data);}// DELETE (soft delete if enabled)asyncdeleteUser(id: string): Promise<boolean>{returnthis.usersRepo.deleteById(id);}// RESTORE (only with soft delete)asyncrestoreUser(id: string): Promise<User|null>{returnthis.usersRepo.restore!(id);}// BULK OPERATIONSasynccreateManyUsers(users: Partial<User>[]): Promise<User[]>{returnthis.usersRepo.insertMany(users);}// DISTINCT VALUESasyncgetUniqueEmails(): Promise<string[]>{returnthis.usersRepo.distinct('email');}// SELECT SPECIFIC FIELDSasyncgetUserNames(): Promise<Pick<User,'name'|'email'>[]>{returnthis.usersRepo.select({},['name','email']);}}interfaceRepository<T>{// ─────────────────────────────────────────────────────────────// CRUD Operations// ─────────────────────────────────────────────────────────────create(data: Partial<T>): Promise<T>;findById(id: string|number): Promise<T|null>;findOne(filter: Filter): Promise<T|null>;findAll(filter?: Filter): Promise<T[]>;findPage(options?: PageOptions): Promise<PageResult<T>>;updateById(id: string|number,update: Partial<T>): Promise<T|null>;deleteById(id: string|number): Promise<boolean>;count(filter?: Filter): Promise<number>;exists(filter?: Filter): Promise<boolean>;// ─────────────────────────────────────────────────────────────// Bulk Operations// ─────────────────────────────────────────────────────────────insertMany(data: Partial<T>[]): Promise<T[]>;updateMany(filter: Filter,update: Partial<T>): Promise<number>;deleteMany(filter: Filter): Promise<number>;// ─────────────────────────────────────────────────────────────// Advanced Queries// ─────────────────────────────────────────────────────────────upsert(filter: Filter,data: Partial<T>): Promise<T>;distinct<KextendskeyofT>(field: K,filter?: Filter): Promise<T[K][]>;select<KextendskeyofT>(filter: Filter,fields: K[]): Promise<Pick<T,K>[]>;// ─────────────────────────────────────────────────────────────// Soft Delete (when enabled)// ─────────────────────────────────────────────────────────────softDelete?(id: string|number): Promise<boolean>;softDeleteMany?(filter: Filter): Promise<number>;restore?(id: string|number): Promise<T|null>;restoreMany?(filter: Filter): Promise<number>;findWithDeleted?(filter?: Filter): Promise<T[]>;}Execute multiple operations atomically:
// MongoDB Transactionconstresult=awaitdb.getMongoAdapter().withTransaction(async(ctx)=>{constuserRepo=ctx.createRepository<User>({model: UserModel});constorderRepo=ctx.createRepository<Order>({model: OrderModel});constuser=awaituserRepo.create({name: 'John'});constorder=awaitorderRepo.create({userId: user._id,total: 99.99});return{ user, order };},{maxRetries: 3,// Retry on transient errorsretryDelayMs: 100,},);// PostgreSQL Transactionconstresult=awaitdb.getPostgresAdapter().withTransaction(async(ctx)=>{constuserRepo=ctx.createRepository<User>({table: 'users'});constorderRepo=ctx.createRepository<Order>({table: 'orders'});constuser=awaituserRepo.create({name: 'John'});constorder=awaitorderRepo.create({user_id: user.id,total: 99.99});return{ user, order };},{isolationLevel: 'serializable',},);React to repository lifecycle events:
constrepo=db.createMongoRepository<User>({model: UserModel,hooks: {// Before create - can modify databeforeCreate: (context)=>{console.log('Creating:',context.data);return{
...context.data,normalizedEmail: context.data.email?.toLowerCase(),};},// After create - for side effectsafterCreate: (user)=>{sendWelcomeEmail(user.email);},// Before update - can modify databeforeUpdate: (context)=>{return{ ...context.data,updatedBy: 'system'};},// After updateafterUpdate: (user)=>{if(user)invalidateCache(user._id);},// Before delete - for validationbeforeDelete: (id)=>{console.log('Deleting user:',id);},// After deleteafterDelete: (success)=>{if(success)console.log('User deleted');},},});Fine-tune database connection pooling:
// MongoDBDatabaseKitModule.forRoot({config: {type: 'mongo',connectionString: process.env.MONGO_URI!,pool: {min: 5,max: 50,idleTimeoutMs: 30000,acquireTimeoutMs: 60000,},// MongoDB-specificserverSelectionTimeoutMS: 5000,socketTimeoutMS: 45000,},});// PostgreSQLDatabaseKitModule.forRoot({config: {type: 'postgres',connectionString: process.env.DATABASE_URL!,pool: {min: 2,max: 20,idleTimeoutMs: 30000,acquireTimeoutMs: 60000,},},});Monitor database health in production:
@Controller('health')exportclassHealthController{constructor(@InjectDatabase()privatereadonlydb: DatabaseService){}
@Get()asynccheck(){constmongoHealth=awaitthis.db.getMongoAdapter().healthCheck();// Returns:// {// healthy: true,// responseTimeMs: 12,// type: 'mongo',// details: {// version: 'MongoDB 6.0',// activeConnections: 5,// poolSize: 10,// }// }return{status: mongoHealth.healthy ? 'healthy' : 'unhealthy',database: mongoHealth,};}}Non-destructive deletion with restore capability:
constrepo=db.createMongoRepository<User>({model: UserModel,softDelete: true,// Enable soft deletesoftDeleteField: 'deletedAt',// Default field name});// "Delete" - sets deletedAt timestampawaitrepo.deleteById('123');// Regular queries exclude deleted recordsawaitrepo.findAll();// Only non-deleted users// Include deleted recordsawaitrepo.findWithDeleted!();// All users including deleted// Restore a deleted recordawaitrepo.restore!('123');Automatic created/updated tracking:
constrepo=db.createMongoRepository<User>({model: UserModel,timestamps: true,// Enable timestampscreatedAtField: 'createdAt',// DefaultupdatedAtField: 'updatedAt',// Default});// create() automatically sets createdAtconstuser=awaitrepo.create({name: 'John'});// user.createdAt = 2026-02-01T12:00:00.000Z// updateById() automatically sets updatedAtawaitrepo.updateById(user._id,{name: 'Johnny'});// user.updatedAt = 2026-02-01T12:01:00.000ZStandard MongoDB query syntax:
awaitrepo.findAll({age: {$gte: 18,$lt: 65},status: {$in: ['active','pending']},name: {$regex: /john/i},});Structured query operators:
// Comparisonawaitrepo.findAll({price: {gt: 100,lte: 500},// > 100 AND <= 500status: {ne: 'cancelled'},// != 'cancelled'});// IN / NOT INawaitrepo.findAll({category: {in: ['electronics','books']},brand: {nin: ['unknown']},});// LIKE (case-insensitive)awaitrepo.findAll({name: {like: '%widget%'},});// NULL checksawaitrepo.findAll({deleted_at: {isNull: true},email: {isNotNull: true},});// Sortingawaitrepo.findPage({sort: '-created_at,name',// DESC created_at, ASC name// or: { created_at: -1, name: 1 }});| Variable | Description | Required |
|---|---|---|
DATABASE_TYPE | mongo or postgres | Yes |
MONGO_URI | MongoDB connection string | For MongoDB |
DATABASE_URL | PostgreSQL connection string | For PostgreSQL |
DATABASE_POOL_MIN | Min pool connections | No (default: 0) |
DATABASE_POOL_MAX | Max pool connections | No (default: 10) |
DATABASE_TIMEOUT | Connection timeout (ms) | No (default: 5000) |
import{ConfigModule,ConfigService}from'@nestjs/config';DatabaseKitModule.forRootAsync({imports: [ConfigModule],useFactory: (config: ConfigService)=>({config: {type: config.get('DATABASE_TYPE')as'mongo'|'postgres',connectionString: config.get('DATABASE_URL')!,pool: {min: config.get('DATABASE_POOL_MIN',0),max: config.get('DATABASE_POOL_MAX',10),},},}),inject: [ConfigService],});@Module({imports: [// Primary databaseDatabaseKitModule.forRoot({config: {type: 'mongo',connectionString: process.env.MONGO_URI!},}),// Analytics database (PostgreSQL)DatabaseKitModule.forFeature('ANALYTICS_DB',{type: 'postgres',connectionString: process.env.ANALYTICS_DB_URL!,}),],})exportclassAppModule{}// Usage
@Injectable()exportclassAnalyticsService{constructor(
@InjectDatabaseByToken('ANALYTICS_DB')privatereadonlyanalyticsDb: DatabaseService,){}}// main.tsimport{DatabaseExceptionFilter}from'@ciscode/database-kit';app.useGlobalFilters(newDatabaseExceptionFilter());{
"statusCode": 409,
"message": "A record with this value already exists",
"error": "DuplicateKeyError",
"timestamp": "2026-02-01T12:00:00.000Z",
"path": "/api/users"
}import{normalizePaginationOptions,parseSortString,calculateOffset,createPageResult,}from'@ciscode/database-kit';constnormalized=normalizePaginationOptions({page: 1});// { page: 1, limit: 10, filter: {}, sort: undefined }constsortObj=parseSortString('-createdAt,name');// { createdAt: -1, name: 1 }constoffset=calculateOffset(2,10);// 10import{isValidMongoId,isValidUuid,sanitizeFilter,pickFields,omitFields,}from'@ciscode/database-kit';isValidMongoId('507f1f77bcf86cd799439011');// trueisValidUuid('550e8400-e29b-41d4-a716-446655440000');// trueconstclean=sanitizeFilter({name: 'John',age: undefined});// { name: 'John' }constpicked=pickFields(user,['name','email']);constsafe=omitFields(user,['password','secret']);# Run tests
npm test# Run with coverage
npm run test:cov
# Run specific test file
npm test -- --testPathPattern=mongo.adapter.specimport{Test}from'@nestjs/testing';import{DATABASE_TOKEN}from'@ciscode/database-kit';constmockRepository={create: jest.fn().mockResolvedValue({id: '1',name: 'Test'}),findById: jest.fn().mockResolvedValue({id: '1',name: 'Test'}),findAll: jest.fn().mockResolvedValue([]),findPage: jest.fn().mockResolvedValue({data: [],total: 0,page: 1,limit: 10,pages: 0}),updateById: jest.fn().mockResolvedValue({id: '1',name: 'Updated'}),deleteById: jest.fn().mockResolvedValue(true),};constmockDb={createMongoRepository: jest.fn().mockReturnValue(mockRepository),createPostgresRepository: jest.fn().mockReturnValue(mockRepository),};constmodule=awaitTest.createTestingModule({providers: [UsersService,{provide: DATABASE_TOKEN,useValue: mockDb}],}).compile();src/
├── index.ts # Public API exports
├── database-kit.module.ts # NestJS module
├── adapters/
│ ├── mongo.adapter.ts # MongoDB implementation
│ └── postgres.adapter.ts # PostgreSQL implementation
├── config/
│ ├── database.config.ts # Configuration helper
│ └── database.constants.ts # Constants
├── contracts/
│ └── database.contracts.ts # TypeScript interfaces
├── filters/
│ └── database-exception.filter.ts # Error handling
├── middleware/
│ └── database.decorators.ts # DI decorators
├── services/
│ ├── database.service.ts # Main service
│ └── logger.service.ts # Logging
└── utils/
├── pagination.utils.ts # Pagination helpers
└── validation.utils.ts # Validation helpers
| Metric | Value |
|---|---|
| Version | 1.0.0 |
| Tests | 133 passing |
| Total LOC | ~5,200 lines |
| TypeScript | 100% |
| Dependencies | Minimal (mongoose, knex, pg) |
See SECURITY.md for:
- Vulnerability reporting
- Security best practices
- Security checklist
See CONTRIBUTING.md for:
- Development setup
- Git workflow
- Code standards
- PR process
See CHANGELOG.md for version history.
MIT © C International Service
- 📧 Email: info@ciscod.com
- 🐛 Issues: GitHub Issues
- 📖 Docs: GitHub Wiki