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:
const user = await repo.create({ name: "John" }); // Works on both!
const found = await repo.findById("123"); // Works on both!
const page = await repo.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.ts
import { Module } from "@nestjs/common";
import { DatabaseKitModule } from "@ciscode/database-kit";
@Module({
imports: [
DatabaseKitModule.forRoot({
config: {
type: "mongo", // or 'postgres'
connectionString: process.env.MONGO_URI!,
},
}),
],
})
export class AppModule {}// users.service.ts
import { Injectable } from "@nestjs/common";
import {
InjectDatabase,
DatabaseService,
Repository,
} from "@ciscode/database-kit";
import { UserModel } from "./user.model";
interface User {
_id: string;
name: string;
email: string;
createdAt: Date;
}
@Injectable()
export class UsersService {
private readonly usersRepo: Repository<User>;
constructor(@InjectDatabase() private readonly db: DatabaseService) {
// For MongoDB
this.usersRepo = db.createMongoRepository<User>({
model: UserModel,
timestamps: true, // Auto createdAt/updatedAt
softDelete: true, // Enable soft delete
hooks: {
// Lifecycle hooks
beforeCreate: (ctx) => {
console.log("Creating user:", ctx.data);
return ctx.data; // Can modify data
},
afterCreate: (user) => {
console.log("User created:", user._id);
},
},
});
}
// CREATE
async createUser(data: Partial<User>): Promise<User> {
return this.usersRepo.create(data);
}
// READ
async getUser(id: string): Promise<User | null> {
return this.usersRepo.findById(id);
}
async getUserByEmail(email: string): Promise<User | null> {
return this.usersRepo.findOne({ email });
}
async listUsers(page = 1, limit = 10) {
return this.usersRepo.findPage({
page,
limit,
sort: "-createdAt",
});
}
// UPDATE
async updateUser(id: string, data: Partial<User>): Promise<User | null> {
return this.usersRepo.updateById(id, data);
}
// UPSERT (update or create)
async upsertByEmail(email: string, data: Partial<User>): Promise<User> {
return this.usersRepo.upsert({ email }, data);
}
// DELETE (soft delete if enabled)
async deleteUser(id: string): Promise<boolean> {
return this.usersRepo.deleteById(id);
}
// RESTORE (only with soft delete)
async restoreUser(id: string): Promise<User | null> {
return this.usersRepo.restore!(id);
}
// BULK OPERATIONS
async createManyUsers(users: Partial<User>[]): Promise<User[]> {
return this.usersRepo.insertMany(users);
}
// DISTINCT VALUES
async getUniqueEmails(): Promise<string[]> {
return this.usersRepo.distinct("email");
}
// SELECT SPECIFIC FIELDS
async getUserNames(): Promise<Pick<User, "name" | "email">[]> {
return this.usersRepo.select({}, ["name", "email"]);
}
}interface Repository<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<K extends keyof T>(field: K, filter?: Filter): Promise<T[K][]>;
select<K extends keyof T>(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 Transaction
const result = await db.getMongoAdapter().withTransaction(
async (ctx) => {
const userRepo = ctx.createRepository<User>({ model: UserModel });
const orderRepo = ctx.createRepository<Order>({ model: OrderModel });
const user = await userRepo.create({ name: "John" });
const order = await orderRepo.create({ userId: user._id, total: 99.99 });
return { user, order };
},
{
maxRetries: 3, // Retry on transient errors
retryDelayMs: 100,
},
);
// PostgreSQL Transaction
const result = await db.getPostgresAdapter().withTransaction(
async (ctx) => {
const userRepo = ctx.createRepository<User>({ table: "users" });
const orderRepo = ctx.createRepository<Order>({ table: "orders" });
const user = await userRepo.create({ name: "John" });
const order = await orderRepo.create({ user_id: user.id, total: 99.99 });
return { user, order };
},
{
isolationLevel: "serializable",
},
);React to repository lifecycle events:
const repo = db.createMongoRepository<User>({
model: UserModel,
hooks: {
// Before create - can modify data
beforeCreate: (context) => {
console.log("Creating:", context.data);
return {
...context.data,
normalizedEmail: context.data.email?.toLowerCase(),
};
},
// After create - for side effects
afterCreate: (user) => {
sendWelcomeEmail(user.email);
},
// Before update - can modify data
beforeUpdate: (context) => {
return { ...context.data, updatedBy: "system" };
},
// After update
afterUpdate: (user) => {
if (user) invalidateCache(user._id);
},
// Before delete - for validation
beforeDelete: (id) => {
console.log("Deleting user:", id);
},
// After delete
afterDelete: (success) => {
if (success) console.log("User deleted");
},
},
});Fine-tune database connection pooling:
// MongoDB
DatabaseKitModule.forRoot({
config: {
type: "mongo",
connectionString: process.env.MONGO_URI!,
pool: {
min: 5,
max: 50,
idleTimeoutMs: 30000,
acquireTimeoutMs: 60000,
},
// MongoDB-specific
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
},
});
// PostgreSQL
DatabaseKitModule.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")
export class HealthController {
constructor(@InjectDatabase() private readonly db: DatabaseService) {}
@Get()
async check() {
const mongoHealth = await this.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:
const repo = db.createMongoRepository<User>({
model: UserModel,
softDelete: true, // Enable soft delete
softDeleteField: "deletedAt", // Default field name
});
// "Delete" - sets deletedAt timestamp
await repo.deleteById("123");
// Regular queries exclude deleted records
await repo.findAll(); // Only non-deleted users
// Include deleted records
await repo.findWithDeleted!(); // All users including deleted
// Restore a deleted record
await repo.restore!("123");Automatic created/updated tracking:
const repo = db.createMongoRepository<User>({
model: UserModel,
timestamps: true, // Enable timestamps
createdAtField: "createdAt", // Default
updatedAtField: "updatedAt", // Default
});
// create() automatically sets createdAt
const user = await repo.create({ name: "John" });
// user.createdAt = 2026-02-01T12:00:00.000Z
// updateById() automatically sets updatedAt
await repo.updateById(user._id, { name: "Johnny" });
// user.updatedAt = 2026-02-01T12:01:00.000ZStandard MongoDB query syntax:
await repo.findAll({
age: { $gte: 18, $lt: 65 },
status: { $in: ["active", "pending"] },
name: { $regex: /john/i },
});Structured query operators:
// Comparison
await repo.findAll({
price: { gt: 100, lte: 500 }, // > 100 AND <= 500
status: { ne: "cancelled" }, // != 'cancelled'
});
// IN / NOT IN
await repo.findAll({
category: { in: ["electronics", "books"] },
brand: { nin: ["unknown"] },
});
// LIKE (case-insensitive)
await repo.findAll({
name: { like: "%widget%" },
});
// NULL checks
await repo.findAll({
deleted_at: { isNull: true },
email: { isNotNull: true },
});
// Sorting
await repo.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 database
DatabaseKitModule.forRoot({
config: { type: "mongo", connectionString: process.env.MONGO_URI! },
}),
// Analytics database (PostgreSQL)
DatabaseKitModule.forFeature("ANALYTICS_DB", {
type: "postgres",
connectionString: process.env.ANALYTICS_DB_URL!,
}),
],
})
export class AppModule {}
// Usage
@Injectable()
export class AnalyticsService {
constructor(
@InjectDatabaseByToken("ANALYTICS_DB")
private readonly analyticsDb: DatabaseService,
) {}
}// main.ts
import { DatabaseExceptionFilter } from "@ciscode/database-kit";
app.useGlobalFilters(new DatabaseExceptionFilter());{
"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";
const normalized = normalizePaginationOptions({ page: 1 });
// { page: 1, limit: 10, filter: {}, sort: undefined }
const sortObj = parseSortString("-createdAt,name");
// { createdAt: -1, name: 1 }
const offset = calculateOffset(2, 10); // 10import {
isValidMongoId,
isValidUuid,
sanitizeFilter,
pickFields,
omitFields,
} from "@ciscode/database-kit";
isValidMongoId("507f1f77bcf86cd799439011"); // true
isValidUuid("550e8400-e29b-41d4-a716-446655440000"); // true
const clean = sanitizeFilter({ name: "John", age: undefined });
// { name: 'John' }
const picked = pickFields(user, ["name", "email"]);
const safe = 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";
const mockRepository = {
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),
};
const mockDb = {
createMongoRepository: jest.fn().mockReturnValue(mockRepository),
createPostgresRepository: jest.fn().mockReturnValue(mockRepository),
};
const module = await Test.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