Skip to content

Repository files navigation

@ciscode/database-kit

A NestJS-friendly, OOP-style database library providing a unified repository API for MongoDB and PostgreSQL.

npm versionLicense: MITNode.js VersionTests


🎯 How It Works

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 │
└───────────────┘ └───────────────┘

The Repository Pattern

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

✨ Features

Core Features

  • 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

Advanced Features

  • Transactions - ACID transactions with automatic retry logic
  • Bulk Operations - insertMany, updateMany, deleteMany
  • Soft Delete - Non-destructive deletion with restore capability
  • Timestamps - Automatic createdAt/updatedAt tracking
  • Health Checks - Database monitoring and connection status
  • Connection Pool Config - Fine-tune pool settings for performance
  • Event Hooks - Lifecycle callbacks (beforeCreate, afterUpdate, etc.)

Query Features

  • 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)

📦 Installation

npm install @ciscode/database-kit

Peer Dependencies

npm install @nestjs/common @nestjs/core reflect-metadata

Database Drivers

# For MongoDB
npm install mongoose
# For PostgreSQL
npm install pg knex

🚀 Quick Start

1. Import the Module

// 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{}

2. Create a Repository and Use It

// 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']);}}

📖 Complete Repository API

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[]>;}

⚡ Advanced Features

Transactions

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',},);

Event Hooks

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');},},});

Connection Pool Configuration

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,},},});

Health Checks

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,};}}

Soft Delete

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');

Timestamps

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.000Z

🔍 Query Operators

MongoDB Queries

Standard MongoDB query syntax:

awaitrepo.findAll({age: {$gte: 18,$lt: 65},status: {$in: ['active','pending']},name: {$regex: /john/i},});

PostgreSQL Queries

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 }});

⚙️ Configuration

Environment Variables

VariableDescriptionRequired
DATABASE_TYPEmongo or postgresYes
MONGO_URIMongoDB connection stringFor MongoDB
DATABASE_URLPostgreSQL connection stringFor PostgreSQL
DATABASE_POOL_MINMin pool connectionsNo (default: 0)
DATABASE_POOL_MAXMax pool connectionsNo (default: 10)
DATABASE_TIMEOUTConnection timeout (ms)No (default: 5000)

Async Configuration (Recommended)

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],});

Multiple Databases

@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,){}}

🛡️ Error Handling

Global Exception Filter

// main.tsimport{DatabaseExceptionFilter}from'@ciscode/database-kit';app.useGlobalFilters(newDatabaseExceptionFilter());

Error Response Format

{
"statusCode": 409,
"message": "A record with this value already exists",
"error": "DuplicateKeyError",
"timestamp": "2026-02-01T12:00:00.000Z",
"path": "/api/users"
}

🔧 Utilities

Pagination Utilities

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);// 10

Validation Utilities

import{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']);

🧪 Testing

# Run tests
npm test# Run with coverage
npm run test:cov
# Run specific test file
npm test -- --testPathPattern=mongo.adapter.spec

Mocking in Tests

import{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();

📁 Project Structure

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

📊 Package Stats

MetricValue
Version1.0.0
Tests133 passing
Total LOC~5,200 lines
TypeScript100%
DependenciesMinimal (mongoose, knex, pg)

🔒 Security

See SECURITY.md for:

  • Vulnerability reporting
  • Security best practices
  • Security checklist

🤝 Contributing

See CONTRIBUTING.md for:

  • Development setup
  • Git workflow
  • Code standards
  • PR process

📝 Changelog

See CHANGELOG.md for version history.


📄 License

MIT © C International Service


🙋 Support

About

A unified npm package for ExpressJS, That allows the integration of different DBMS through one single config file

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages