Design Patterns

Architectural patterns and design decisions in MSR.

Table of contents

  1. Class Diagram
  2. Dependency Injection
    1. Default vs Custom Dependencies
    2. Dependency Graph
  3. Layer Responsibilities
    1. Layer 1: Orchestration
    2. Layer 2: Business Logic
    3. Layer 3: Services
    4. Layer 4: Models
    5. Layer 5: Interfaces
    6. Layer 6: Database Handlers
  4. Extension Points
    1. Custom Logger
    2. Custom Backup
    3. Custom Render Strategy

Class Diagram

This UML class diagram shows the main classes, their properties, methods, and relationships:

classDiagram
    class MigrationScriptExecutor {
        -handler: IDatabaseMigrationHandler
        -backupService: IBackup<IDB>Service
        -schemaVersionService: ISchemaVersionService<IDB>
        -migrationService: IMigrationService
        -migrationRenderer: IMigrationRenderer
        -migrationScanner: IMigrationScanner
        -selector: MigrationScriptSelector
        -runner: MigrationRunner
        -rollbackService: RollbackService
        -logger: ILogger
        +migrate() Promise~IMigrationResult~
        +migrateTo(version) Promise~IMigrationResult~
        +downTo(version) Promise~IMigrationResult~
        +list(number) Promise~void~
    }

    class MigrationScanner {
        -migrationService: IMigrationService
        -schemaVersionService: ISchemaVersionService<IDB>
        -selector: MigrationScriptSelector
        -handler: IDatabaseMigrationHandler
        +scan() Promise~IScripts~
    }

    class MigrationScriptSelector {
        <<stateless>>
        +getPending(migrated, all) MigrationScript[]
        +getIgnored(migrated, all) MigrationScript[]
        +getPendingUpTo(migrated, all, version) MigrationScript[]
        +getMigratedDownTo(migrated, version) MigrationScript[]
    }

    class MigrationRunner {
        -handler: IDatabaseMigrationHandler
        -schemaVersionService: ISchemaVersionService<IDB>
        -logger: ILogger
        +execute(scripts) Promise~MigrationScript[]~
        +executeOne(script) Promise~void~
    }

    class BackupService {
        -handler: IDatabaseMigrationHandler
        -config: Config
        -logger: ILogger
        +backup() Promise~string~
        +restore(backupPath) Promise~void~
        +deleteBackup(backupPath) Promise~void~
    }

    class SchemaVersionService {
        -handler: IDatabaseMigrationHandler
        -logger: ILogger
        +init() Promise~void~
        +save(script) Promise~void~
        +getAllMigrated() Promise~MigrationScript[]~
        +remove(script) Promise~void~
    }

    class MigrationService {
        -logger: ILogger
        +readMigrationScripts(config) Promise~MigrationScript[]~
        +parseFilename(filename) ParsedFilename
    }

    class RollbackService {
        -handler: IDatabaseMigrationHandler
        -config: Config
        -backupService: BackupService
        -logger: ILogger
        +rollback(scripts, backupPath) Promise~void~
        +shouldCreateBackup() boolean
    }

    class MigrationRenderer {
        -strategy: IRenderStrategy
        -logger: ILogger
        -config: Config
        +render(result) void
        +renderMigrated(scripts) void
    }

    MigrationScriptExecutor --> MigrationScanner : uses
    MigrationScriptExecutor --> MigrationScriptSelector : uses
    MigrationScriptExecutor --> MigrationRunner : uses
    MigrationScriptExecutor --> BackupService : uses
    MigrationScriptExecutor --> SchemaVersionService : uses
    MigrationScriptExecutor --> RollbackService : uses
    MigrationScriptExecutor --> MigrationRenderer : uses

    MigrationScanner --> MigrationService : uses
    MigrationScanner --> SchemaVersionService : uses
    MigrationScanner --> MigrationScriptSelector : uses

    MigrationRunner --> SchemaVersionService : uses

    RollbackService --> BackupService : uses

Dependency Injection

MSR supports optional dependency injection for all services, enabling:

  • Testing - Mock services for unit tests
  • Customization - Replace default implementations
  • Extension - Add new functionality

Default vs Custom Dependencies

// Default (uses built-in dependencies)
const config = new Config();
const executor = new MigrationScriptExecutor({ handler }, config);
// Uses: ConsoleLogger, BackupService, SchemaVersionService, etc.

// Custom dependencies
const executor2 = new MigrationScriptExecutor({ handler, 
    logger: new SilentLogger(),          // Custom logger
    backupService: new S3BackupService(), // Custom backup
    renderStrategy: new JsonRenderStrategy()  // Custom render strategy
});

Dependency Graph

This diagram shows the dependency injection hierarchy and how services are composed:

graph TD
    Executor[MigrationScriptExecutor]

    Executor --> Logger[ILogger<br/>default: ConsoleLogger]

    Executor --> Backup[IBackupService<br/>default: BackupService]
    Backup --> Logger2[ILogger]

    Executor --> Schema[ISchemaVersionService<br/>default: SchemaVersionService]
    Schema --> DAO[ISchemaVersionDAO<br/>from handler]

    Executor --> Migration[IMigrationService<br/>default: MigrationService]
    Migration --> Logger3[ILogger]

    Executor --> Renderer[IMigrationRenderer<br/>default: MigrationRenderer]
    Renderer --> Strategy[IRenderStrategy<br/>default: AsciiTableRenderStrategy]
    Strategy --> Logger4[ILogger]

    Executor --> Scanner[IMigrationScanner<br/>default: MigrationScanner]
    Scanner --> Migration2[IMigrationService]
    Scanner --> Schema2[ISchemaVersionService]
    Scanner --> Selector[MigrationScriptSelector]
    Scanner --> Handler[IDatabaseMigrationHandler]

    Executor --> Selector2[MigrationScriptSelector<br/>always created]
    Selector2 --> Stateless[stateless, no dependencies]

    Executor --> Runner[MigrationRunner<br/>always created]
    Runner --> Handler2[IDatabaseMigrationHandler]
    Runner --> Schema3[ISchemaVersionService]
    Runner --> Logger5[ILogger]

    Executor --> Rollback[RollbackService<br/>always created]
    Rollback --> Backup2[BackupService]

    style Executor fill:#fff3cd
    style Logger fill:#d4edda
    style Backup fill:#d4edda
    style Schema fill:#d4edda
    style Migration fill:#d4edda
    style Renderer fill:#d4edda
    style Scanner fill:#d4edda
    style Selector2 fill:#d4edda
    style Runner fill:#d4edda
    style Rollback fill:#d4edda
    style Strategy fill:#e8e8e8
    style Handler fill:#f8d7da

Layer Responsibilities

Layer 1: Orchestration

Class: MigrationScriptExecutor Role: Coordinates workflow, handles errors, manages lifecycle

Layer 2: Business Logic

Classes: MigrationScriptSelector, MigrationRunner Role: Core migration logic - filtering and execution

Layer 3: Services

Classes: BackupService, SchemaVersionService, MigrationService, MigrationRenderer Role: Specialized operations - backup, tracking, discovery, display

Layer 4: Models

Classes: MigrationScript, Config, BackupConfig Role: Data structures and configuration

Layer 5: Interfaces

Interfaces: IDB, IDatabaseMigrationHandler, ILogger, etc. Role: Contracts and abstraction boundaries

Layer 6: Database Handlers

User-provided: PostgreSQL, MySQL, MongoDB handlers Role: Database-specific implementations


Extension Points

Custom Logger

import { ILogger } from '@migration-script-runner/core';

class CloudLogger implements ILogger {
    log(message: string) {
        sendToCloudWatch(message);
    }
    // ... implement other methods
}

const executor = new MigrationScriptExecutor({ handler, 
    logger: new CloudLogger()
});

Custom Backup

import { IBackupService } from '@migration-script-runner/core';

class S3BackupService implements IBackupService {
    async backup() {
        const dump = await createDump();
        await s3.upload(dump);
    }
    // ... implement restore, deleteBackup
}

const executor = new MigrationScriptExecutor({ handler, 
    backupService: new S3BackupService()
});

Custom Render Strategy

import { IRenderStrategy, JsonRenderStrategy } from '@migration-script-runner/core';

// Use built-in JSON render strategy
const executor = new MigrationScriptExecutor({ handler, 
    renderStrategy: new JsonRenderStrategy(true)  // pretty-printed JSON
});

// Or create a custom render strategy
class CustomRenderStrategy implements IRenderStrategy {
    renderMigrated(scripts, handler, limit) {
        console.log('Custom output:', scripts);
    }
    // ... implement other methods
}

const executor = new MigrationScriptExecutor({ handler, 
    renderStrategy: new CustomRenderStrategy()
});