Skip to content

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

License

Notifications You must be signed in to change notification settings

CISCODE-MA/DatabaseKit

@ciscode/database-kit

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

npm version License: MIT Node.js Version Tests


🎯 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:

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

✨ 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.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 {}

2. Create a Repository and Use It

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

πŸ“– Complete Repository API

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

⚑ Advanced Features

Transactions

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

Event Hooks

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

Connection Pool Configuration

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

Health Checks

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

Soft Delete

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

Timestamps

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

πŸ” Query Operators

MongoDB Queries

Standard MongoDB query syntax:

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

PostgreSQL Queries

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

βš™οΈ Configuration

Environment Variables

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)

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

πŸ›‘οΈ Error Handling

Global Exception Filter

// main.ts
import { DatabaseExceptionFilter } from "@ciscode/database-kit";

app.useGlobalFilters(new DatabaseExceptionFilter());

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";

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

Validation Utilities

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

πŸ§ͺ 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";

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

πŸ“ 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

Metric Value
Version 1.0.0
Tests 133 passing
Total LOC ~5,200 lines
TypeScript 100%
Dependencies Minimal (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

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Contributors 2

  •  
  •