DEV Community

Cover image for A Unified, Type-Safe Database Adapter System for Multi-DB Applications
bilel salem
bilel salem

Posted on

A Unified, Type-Safe Database Adapter System for Multi-DB Applications

Database Adapters System

A flexible, database-agnostic interface that allows your application to work with multiple database types through a unified API.

Table of Contents

Overview

The Database Adapters System provides a generic interface that abstracts database-specific operations, allowing you to:

  • Switch between databases without changing business logic
  • Support multiple databases in the same application
  • Maintain type safety across all database operations
  • Preserve backward compatibility with existing MongoDB code
  • Extend easily to support new database types

Architecture

┌─────────────────────┐
│   Business Logic    │
├─────────────────────┤
│ IDatabaseService    │  ← Generic Interface
├─────────────────────┤
│ DatabaseService     │  ← Implementation
├─────────────────────┤
│   Database Adapters │
├─────────────────────┤
│ MongoDB │ PostgreSQL│  ← Database-specific
│  MySQL  │  SQLite   │     adapters
└─────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Core Components

  1. Generic Types (src/common/types/common.types.ts)
- `DatabaseOperator` - Standardized query operators
- `DatabaseCondition` - Generic condition structure
- `DatabaseQuery` - Complete query specification
- `DatabaseAdapter` - Adapter interface contract
Enter fullscreen mode Exit fullscreen mode
  1. Database Service (src/common/database/services/database.service.ts)
- Unified API for all database operations
- Adapter management and switching
- Backward compatibility layer
Enter fullscreen mode Exit fullscreen mode
  1. Adapters (src/common/database/adapters/)
- `MongoDBAdapter` - MongoDB translation
- `PostgreSQLAdapter` - PostgreSQL SQL generation
- Extensible for additional databases
Enter fullscreen mode Exit fullscreen mode
  1. Factory (src/common/database/factories/database-adapter.factory.ts)
    • Centralized adapter creation and management
    • Runtime adapter switching support

Supported Databases

Database Status Adapter Class Notes
MongoDB ✅ Full MongoDBAdapter Default, backward compatible
PostgreSQL ✅ Full PostgreSQLAdapter SQL generation with parameters
MySQL 🚧 Planned MySQLAdapter Coming soon
SQLite 🚧 Planned SQLiteAdapter Coming soon
Redis 🚧 Planned RedisAdapter For caching/sessions

Quick Start

1. Basic Usage (MongoDB - Default)

import { DatabaseService } from 'src/common/database/services/database.service';

@Injectable()
export class UserService {
    constructor(private readonly databaseService: DatabaseService) {}

    async findActiveUsers() {
        // Create a simple filter - works with any database
        const filter = this.databaseService.filterEqual('status', 'active');
        return filter; // MongoDB: { status: { $eq: 'active' } }
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Switching to PostgreSQL

import { DatabaseService } from 'src/common/database/services/database.service';
import { DatabaseAdapterFactory } from 'src/common/database/factories/database-adapter.factory';

@Injectable()
export class UserService {
    constructor(
        private readonly databaseService: DatabaseService,
        private readonly adapterFactory: DatabaseAdapterFactory
    ) {
        // Switch to PostgreSQL
        const pgAdapter = this.adapterFactory.createAdapter('postgresql');
        this.databaseService.setAdapter(pgAdapter);
    }

    async findActiveUsers() {
        const filter = this.databaseService.filterEqual('status', 'active');
        return filter; // PostgreSQL: { whereClause: 'WHERE status = $1', parameters: ['active'] }
    }
}
Enter fullscreen mode Exit fullscreen mode

Usage Examples

Building Complex Queries

import { DatabaseOperator } from 'src/common/types/common.types';

// Create individual conditions
const conditions = [
    this.databaseService.createCondition(
        'age',
        DatabaseOperator.GREATER_THAN,
        18
    ),
    this.databaseService.createCondition(
        'email',
        DatabaseOperator.CONTAINS,
        '@company.com'
    ),
    this.databaseService.createCondition('status', DatabaseOperator.IN, [
        'active',
        'premium',
    ]),
];

// Build complex query
const query = this.databaseService.buildQuery(conditions, 'AND', {
    limit: 10,
    offset: 0,
    sort: [{ field: 'createdAt', direction: 'DESC' }],
});

// Execute with current adapter
const result = this.databaseService.executeQuery(query);
Enter fullscreen mode Exit fullscreen mode

Common Filter Operations

// Text search
const nameFilter = this.databaseService.filterContain('name', 'John', {
    caseSensitive: false,
    fullWord: false,
});

// Date range
const dateFilter = this.databaseService.filterDateBetween(
    'createdAt',
    'updatedAt',
    new Date('2023-01-01'),
    new Date('2023-12-31')
);

// Array operations
const categoryFilter = this.databaseService.filterIn('category', [
    'tech',
    'business',
]);
const excludeFilter = this.databaseService.filterNin('status', [
    'banned',
    'deleted',
]);

// Existence checks
const profileFilter = this.databaseService.filterExists('profile', true);

// Numeric comparisons
const scoreFilter = this.databaseService.filterGreaterThan('score', 100);
Enter fullscreen mode Exit fullscreen mode

Runtime Adapter Switching

@Injectable()
export class DatabaseManager {
    constructor(
        private readonly databaseService: DatabaseService,
        private readonly adapterFactory: DatabaseAdapterFactory
    ) {}

    async switchDatabase(dbType: string) {
        if (this.adapterFactory.isSupported(dbType)) {
            const adapter = this.adapterFactory.createAdapter(dbType);
            this.databaseService.setAdapter(adapter);
            console.log(`Switched to ${dbType} adapter`);
        } else {
            throw new Error(`Unsupported database type: ${dbType}`);
        }
    }

    getSupportedDatabases(): string[] {
        return this.adapterFactory.getSupportedTypes();
    }
}
Enter fullscreen mode Exit fullscreen mode

Creating Custom Adapters

1. Implement the DatabaseAdapter Interface

import { Injectable } from '@nestjs/common';
import {
    DatabaseAdapter,
    DatabaseQuery,
    DatabaseCondition,
    DatabaseSort,
    DatabaseOperator,
} from 'src/common/types/common.types';

@Injectable()
export class MySQLAdapter implements DatabaseAdapter {
    readonly name = 'mysql';

    translateQuery(query: DatabaseQuery): { sql: string; parameters: any[] } {
        // Implement MySQL-specific query translation
        const conditions = query.conditions.map(condition =>
            this.translateCondition(condition)
        );

        const whereClause =
            conditions.length > 0
                ? `WHERE ${conditions.map(c => c.condition).join(` ${query.logic || 'AND'} `)}`
                : '';

        const parameters = conditions.flatMap(c => c.parameters);

        return {
            sql: whereClause,
            parameters,
        };
    }

    translateCondition(condition: DatabaseCondition): {
        condition: string;
        parameters: any[];
    } {
        const { field, operator, value } = condition;

        switch (operator) {
            case DatabaseOperator.EQUAL:
                return { condition: `${field} = ?`, parameters: [value] };

            case DatabaseOperator.GREATER_THAN:
                return { condition: `${field} > ?`, parameters: [value] };

            case DatabaseOperator.CONTAINS:
                return {
                    condition: `${field} LIKE ?`,
                    parameters: [`%${value}%`],
                };

            // Add more operators as needed
            default:
                throw new Error(`Unsupported operator: ${operator}`);
        }
    }

    translateSort(sort: DatabaseSort[]): string {
        if (sort.length === 0) return '';

        const orderClauses = sort.map(s => `${s.field} ${s.direction}`);
        return `ORDER BY ${orderClauses.join(', ')}`;
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Register the Adapter

// In your module or service
import { MySQLAdapter } from './adapters/mysql.adapter';

@Injectable()
export class DatabaseSetupService {
    constructor(private readonly adapterFactory: DatabaseAdapterFactory) {
        // Register custom adapter
        this.adapterFactory.registerAdapter('mysql', () => new MySQLAdapter());
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Use the Custom Adapter

const mysqlAdapter = this.adapterFactory.createAdapter('mysql');
this.databaseService.setAdapter(mysqlAdapter);

// Now all operations use MySQL adapter
const filter = this.databaseService.filterEqual('id', 123);
// Output: { sql: 'WHERE id = ?', parameters: [123] }
Enter fullscreen mode Exit fullscreen mode

Migration Guide

From MongoDB-Specific to Generic

Before (MongoDB-specific)

// Old way - MongoDB only
const mongoFilter = {
    status: { $eq: 'active' },
    age: { $gt: 18 },
    email: { $regex: /.*@company\.com/i },
};
Enter fullscreen mode Exit fullscreen mode

After (Database-agnostic)

// New way - works with any database
const conditions = [
    this.databaseService.createCondition(
        'status',
        DatabaseOperator.EQUAL,
        'active'
    ),
    this.databaseService.createCondition(
        'age',
        DatabaseOperator.GREATER_THAN,
        18
    ),
    this.databaseService.createCondition(
        'email',
        DatabaseOperator.CONTAINS,
        '@company.com'
    ),
];

const query = this.databaseService.buildQuery(conditions, 'AND');
const filter = this.databaseService.executeQuery(query);
Enter fullscreen mode Exit fullscreen mode

Backward Compatibility

All existing MongoDB code continues to work:

// These still work unchanged
const legacyFilter = this.databaseService.filterEqual('status', 'active');
const legacySearch = this.databaseService.filterContain('name', 'John');
const legacyDate = this.databaseService.filterDateBetween(
    'start',
    'end',
    date1,
    date2
);
Enter fullscreen mode Exit fullscreen mode

API Reference

DatabaseOperator Enum

enum DatabaseOperator {
    EQUAL = 'eq',
    NOT_EQUAL = 'ne',
    GREATER_THAN = 'gt',
    GREATER_THAN_OR_EQUAL = 'gte',
    LESS_THAN = 'lt',
    LESS_THAN_OR_EQUAL = 'lte',
    IN = 'in',
    NOT_IN = 'nin',
    CONTAINS = 'contains',
    STARTS_WITH = 'startsWith',
    ENDS_WITH = 'endsWith',
    REGEX = 'regex',
    EXISTS = 'exists',
    IS_NULL = 'isNull',
    BETWEEN = 'between',
}
Enter fullscreen mode Exit fullscreen mode

IDatabaseService Interface

Generic Query Methods

  • createCondition<T>(field, operator, value, options?) - Create a condition
  • buildQuery(conditions, logic?, options?) - Build complex query
  • executeQuery(query) - Execute query with current adapter

Convenience Methods

  • filterEqual<T>(field, value) - Equal filter
  • filterNotEqual<T>(field, value) - Not equal filter
  • filterContain(field, value, options?) - Text contains filter
  • filterIn<T>(field, values) - IN filter
  • filterNin<T>(field, values) - NOT IN filter
  • filterDateBetween(start, end, startDate, endDate) - Date range filter
  • filterGreaterThan<T>(field, value) - Greater than filter
  • filterLessThan<T>(field, value) - Less than filter
  • filterExists(field, exists) - Existence filter

Adapter Management

  • setAdapter(adapter) - Set database adapter
  • getAdapter() - Get current adapter
  • getSupportedDatabases() - List supported databases

DatabaseAdapterFactory

  • createAdapter(type) - Create adapter by type
  • getSupportedTypes() - List all supported types
  • isSupported(type) - Check if type is supported
  • registerAdapter(type, factory) - Register custom adapter

Best Practices

1. Adapter Selection Strategy

// Environment-based adapter selection
const dbType = process.env.DATABASE_TYPE || 'mongodb';
const adapter = this.adapterFactory.createAdapter(dbType);
this.databaseService.setAdapter(adapter);
Enter fullscreen mode Exit fullscreen mode

2. Error Handling

try {
    const adapter = this.adapterFactory.createAdapter(dbType);
    this.databaseService.setAdapter(adapter);
} catch (error) {
    console.error(`Failed to initialize ${dbType} adapter:`, error.message);
    // Fallback to default
    const defaultAdapter = this.adapterFactory.createAdapter('mongodb');
    this.databaseService.setAdapter(defaultAdapter);
}
Enter fullscreen mode Exit fullscreen mode

3. Type Safety

// Use specific types for better type safety
interface UserFilter {
    status: 'active' | 'inactive';
    age: number;
    email: string;
}

const userCondition = this.databaseService.createCondition<
    UserFilter['status']
>('status', DatabaseOperator.EQUAL, 'active');
Enter fullscreen mode Exit fullscreen mode

4. Performance Considerations

// Cache adapters for frequently used databases
const adapterCache = new Map<string, DatabaseAdapter>();

function getCachedAdapter(type: string): DatabaseAdapter {
    if (!adapterCache.has(type)) {
        adapterCache.set(type, this.adapterFactory.createAdapter(type));
    }
    return adapterCache.get(type)!;
}
Enter fullscreen mode Exit fullscreen mode

5. Testing Different Adapters

describe('Database Operations', () => {
    const testCases = ['mongodb', 'postgresql'];

    testCases.forEach(dbType => {
        describe(`with ${dbType}`, () => {
            beforeEach(() => {
                const adapter = adapterFactory.createAdapter(dbType);
                databaseService.setAdapter(adapter);
            });

            it('should filter active users', () => {
                const filter = databaseService.filterEqual('status', 'active');
                expect(filter).toBeDefined();
                // Add adapter-specific assertions
            });
        });
    });
});
Enter fullscreen mode Exit fullscreen mode

Open Source & Contributing

This Database Adapter System is being released as an open-source project on GitHub to encourage collaboration and community-driven improvement.

🔗 GitHub Repository: [Coming Soon / https://github.com/bilelsalemdev/universal-db-adapter/tree/main]

Everyone is welcome to contribute — whether it's:

  • Writing unit and integration tests
  • Completing or improving existing adapters (e.g., MySQL, SQLite, Redis)
  • Suggesting enhancements or new features
  • Reporting issues or edge cases
  • Improving documentation and usage examples

If you're interested in helping shape a flexible, database-agnostic abstraction layer for modern applications, feel free to fork the repo, submit PRs, or start a discussion.

Let’s build it together! 💪

Top comments (0)