Migrating from v0.2.x to v0.3.0

Guide for upgrading from MSR v0.2.x to v0.3.0

Table of contents

  1. Overview
    1. Breaking Changes
    2. Benefits
  2. Migration Steps
    1. Step 1: Update Package Version
    2. Step 2: Update IScripts References
      1. Change 1: Rename todopending
    3. Step 3: Update Renderer Method Names
      1. Change 2: Remove “Table” Suffix
    4. Step 4: Update Custom Renderers
    5. Step 5: Update Custom Render Strategies
    6. Step 6: Update Service Constructors (Config Separation)
      1. Change 4: Pass Config as Separate Parameter
    7. Step 7: Update Render Strategy Implementations
      1. Change 5: Remove limit Parameter from renderMigrated()
    8. Step 8: Update MigrationResult Handling
  3. Understanding the New Architecture
    1. MigrationScanner Service
  4. Common Migration Patterns
    1. Pattern 1: Accessing Pending Migrations
    2. Pattern 2: Custom Rendering with JSON
    3. Pattern 3: Variable Naming
    4. Pattern 4: Service Initialization with Config
    5. Pattern 5: Custom Handler Implementation
    6. Pattern 6: Testing with Config
  5. Automated Migration
    1. Using sed (Unix/Linux/macOS)
    2. Using PowerShell (Windows)
  6. TypeScript Compilation Errors
    1. Error 1: Property ‘todo’ does not exist
    2. Error 2: Property ‘drawTodoTable’ does not exist
    3. Error 3: Type is missing properties
    4. Error 4: Property ‘cfg’ does not exist on type ‘IDatabaseMigrationHandler’
    5. Error 5: Expected 2 arguments, but got 1
    6. Error 6: Argument of type ‘(scripts: IScripts, config: Config, limit?: number)’ is not assignable
  7. Testing
    1. Before (v0.2.x)
    2. After (v0.3.0)
  8. Troubleshooting
    1. Issue: TypeScript Errors After Upgrade
    2. Issue: Tests Failing
    3. Issue: JSON Output Changed
  9. Rollback Plan
  10. New Features in v0.3.0
    1. 1. Complete Migration State Tracking
    2. 2. Improved Architecture
    3. 3. Professional Terminology
    4. 4. Sub-folder Support for Migration Scripts
    5. 5. beforeMigrate File for Database Setup
    6. 6. Granular Backup Control with BackupMode
    7. 7. Optional Backup and Flexible Rollback Strategies
    8. 8. Lifecycle Hooks for Custom Behavior
    9. 9. CompositeLogger for Multi-Destination Logging
    10. 10. Strategy Pattern for Flexible Output Rendering
    11. 11. Improved Type Safety
  11. FAQ
  12. Support

Overview

Version 0.3.0 introduces several breaking changes focused on improving code clarity and architecture:

  1. Renamed “todo” → “pending” throughout the codebase for better terminology
  2. Added ignored field to IScripts for complete migration state tracking
  3. Removed “Table” suffix from renderer methods for format-agnostic naming
  4. Extracted MigrationScanner service for better separation of concerns
  5. Removed deprecated methods from public interfaces
  6. Separated Config from IDatabaseMigrationHandler following Single Responsibility Principle (issue #63)

Breaking Changes

  • IScripts.todoIScripts.pending
  • getTodo()getPending() (in MigrationScriptSelector)
  • drawTodoTable()drawPending() (in IMigrationRenderer)
  • drawIgnoredTable()drawIgnored() (in IMigrationRenderer)
  • drawExecutedTable()drawExecuted() (in IMigrationRenderer)
  • renderTodo()renderPending() (in IRenderStrategy)
  • IDatabaseMigrationHandler.cfg property removed
  • ❌ Constructor signatures changed: Config now passed as separate parameter
  • IRenderStrategy.renderMigrated() limit parameter removed (uses config.displayLimit internally)
  • New: IScripts.ignored field added

Benefits

  • ✅ Professional terminology: “pending” aligns with industry standards (Rails, Flyway, Liquibase)
  • ✅ Format-agnostic method names: supports ASCII, JSON, and custom formats
  • ✅ Complete migration state: ignored field provides full visibility
  • ✅ Better architecture: Extracted MigrationScanner follows Single Responsibility Principle
  • ✅ Cleaner interfaces: Removed deprecated methods
  • ✅ Improved separation of concerns: Config separated from database handler
  • ✅ Enhanced testability: Config can be managed independently in tests

Migration Steps

Step 1: Update Package Version

npm install @migration-script-runner/core@^0.3.0

Or with yarn:

yarn add @migration-script-runner/core@^0.3.0

Step 2: Update IScripts References

Change 1: Rename todopending

Before (v0.2.x):

interface IScripts {
    all: MigrationScript[]
    migrated: MigrationScript[]
    todo: MigrationScript[]     // ❌ REMOVED
    executed: MigrationScript[]
}

const scripts = await getScripts();
console.log(`Pending migrations: ${scripts.todo.length}`);

After (v0.3.0):

interface IScripts {
    all: MigrationScript[]
    migrated: MigrationScript[]
    pending: MigrationScript[]   // ✅ NEW
    ignored: MigrationScript[]   // ✅ NEW
    executed: MigrationScript[]
}

const scripts = await getScripts();
console.log(`Pending migrations: ${scripts.pending.length}`);
console.log(`Ignored migrations: ${scripts.ignored.length}`);

Search & Replace:

# Find all occurrences
grep -r "scripts\.todo" your-project/

# Replace pattern
scripts.todo → scripts.pending

Step 3: Update Renderer Method Names

Change 2: Remove “Table” Suffix

If you’re calling renderer methods directly:

Before (v0.2.x):

renderer.drawTodoTable(pendingScripts);
renderer.drawIgnoredTable(ignoredScripts);
renderer.drawExecutedTable(executedScripts);

After (v0.3.0):

renderer.drawPending(pendingScripts);
renderer.drawIgnored(ignoredScripts);
renderer.drawExecuted(executedScripts);

Search & Replace:

drawTodoTable     → drawPending
drawIgnoredTable  → drawIgnored
drawExecutedTable → drawExecuted

Step 4: Update Custom Renderers

If you implemented a custom renderer, update the interface methods:

Before (v0.2.x):

export class MyCustomRenderer implements IMigrationRenderer {
    drawFiglet(): void { /* ... */ }
    drawMigrated(scripts: IScripts, number?: number): void { /* ... */ }
    drawTodoTable(scripts: MigrationScript[]): void { /* ... */ }  // ❌ OLD
    drawIgnoredTable(scripts: MigrationScript[]): void { /* ... */ }  // ❌ OLD
    drawExecutedTable(scripts: IMigrationInfo[]): void { /* ... */ }  // ❌ OLD
}

After (v0.3.0):

export class MyCustomRenderer implements IMigrationRenderer {
    drawFiglet(): void { /* ... */ }
    drawMigrated(scripts: IScripts, number?: number): void { /* ... */ }
    drawPending(scripts: MigrationScript[]): void { /* ... */ }  // ✅ NEW
    drawIgnored(scripts: MigrationScript[]): void { /* ... */ }  // ✅ NEW
    drawExecuted(scripts: IMigrationInfo[]): void { /* ... */ }  // ✅ NEW
}

Step 5: Update Custom Render Strategies

If you implemented a custom render strategy:

Before (v0.2.x):

export class MyRenderStrategy implements IRenderStrategy {
    renderBanner(version: string, handlerName: string): void { /* ... */ }
    renderMigrated(scripts: IScripts, handler: IDatabaseMigrationHandler, limit?: number): void { /* ... */ }
    renderTodo(scripts: MigrationScript[]): void { /* ... */ }  // ❌ OLD
    renderExecuted(scripts: IMigrationInfo[]): void { /* ... */ }
    renderIgnored(scripts: MigrationScript[]): void { /* ... */ }
}

After (v0.3.0):

export class MyRenderStrategy implements IRenderStrategy {
    renderBanner(version: string, handlerName: string): void { /* ... */ }
    renderMigrated(scripts: IScripts, handler: IDatabaseMigrationHandler, limit?: number): void { /* ... */ }
    renderPending(scripts: MigrationScript[]): void { /* ... */ }  // ✅ NEW
    renderExecuted(scripts: IMigrationInfo[]): void { /* ... */ }
    renderIgnored(scripts: MigrationScript[]): void { /* ... */ }
}

Step 6: Update Service Constructors (Config Separation)

Change 4: Pass Config as Separate Parameter

In v0.3.0, Config has been separated from IDatabaseMigrationHandler following the Single Responsibility Principle. Services now accept config as a separate constructor parameter.

Before (v0.2.x):

// Config was accessed from handler
handler.cfg = new Config();

// Services only needed handler
const executor = new MigrationScriptExecutor(handler);
const backupService = new BackupService(handler);
const renderer = new MigrationRenderer(handler);

After (v0.3.0):

// Config is now separate
const config = new Config();

// Services accept config as second parameter
const executor = new MigrationScriptExecutor(handler, config);
const backupService = new BackupService(handler, config);
const renderer = new MigrationRenderer(handler, config);

IDatabaseMigrationHandler Interface Change:

// Before (v0.2.x)
interface IDatabaseMigrationHandler {
    getName(): string
    db: IDB
    schemaVersion: ISchemaVersion
    backup: IBackup
    cfg: Config  // ❌ REMOVED
}

// After (v0.3.0)
interface IDatabaseMigrationHandler {
    getName(): string
    db: IDB
    schemaVersion: ISchemaVersion
    backup: IBackup
    // cfg property removed - pass separately to constructors
}

Updated Constructor Signatures:

Service Old Signature New Signature
MigrationScriptExecutor (handler) (handler, config, dependencies?)
BackupService (handler, logger?) (handler, config, logger?)
MigrationRenderer (handler, strategy?) (handler, config, strategy?)
MigrationScanner (...) (..., handler, config)

If you have custom handler implementations:

// Before (v0.2.x)
class MyDatabaseHandler implements IDatabaseMigrationHandler {
    cfg: Config;

    constructor(config: Config) {
        this.cfg = config;
    }
}

// After (v0.3.0)
class MyDatabaseHandler implements IDatabaseMigrationHandler {
    // No cfg property

    constructor(config: Config) {
        // Config is managed externally, passed to services
    }
}

Step 7: Update Render Strategy Implementations

Change 5: Remove limit Parameter from renderMigrated()

Render strategies now use config.displayLimit internally instead of receiving it as a parameter.

Before (v0.2.x):

class MyRenderStrategy implements IRenderStrategy {
    renderMigrated(scripts: IScripts, config: Config, limit?: number): void {
        const displayLimit = limit || config.displayLimit;
        // Use displayLimit...
    }
}

After (v0.3.0):

class MyRenderStrategy implements IRenderStrategy {
    renderMigrated(scripts: IScripts, config: Config): void {
        // Use config.displayLimit directly
        const displayLimit = config.displayLimit;
        // ...
    }
}

Why this change?

  • Eliminates redundancy - config already has the limit
  • Simplifies the interface
  • More consistent - other render methods don’t have extra parameters

Step 8: Update MigrationResult Handling

The IMigrationResult.ignored field is now properly populated:

Before (v0.2.x):

const result = await executor.migrate();

console.log(`Executed: ${result.executed.length}`);
console.log(`Migrated: ${result.migrated.length}`);
// result.ignored was always empty

After (v0.3.0):

const result = await executor.migrate();

console.log(`Executed: ${result.executed.length}`);
console.log(`Migrated: ${result.migrated.length}`);
console.log(`Ignored: ${result.ignored.length}`);  // ✅ Now populated!

// Show which migrations were skipped
if (result.ignored.length > 0) {
    console.warn('Ignored migrations (older than last executed):');
    result.ignored.forEach(script => {
        console.warn(`  - ${script.name} (timestamp: ${script.timestamp})`);
    });
}

Understanding the New Architecture

MigrationScanner Service

v0.3.0 extracts migration state gathering into a dedicated MigrationScanner service:

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

// The scanner gathers complete migration state
const scanner = new MigrationScanner(
    migrationService,
    schemaVersionService,
    selector,
    handler
);

const scripts = await scanner.scan();
// Returns: { all, migrated, pending, ignored, executed }

Benefits:

  • Single Responsibility: Separation of concerns - gathering state vs. executing migrations
  • Testability: Easy to test scanning logic independently
  • Reusability: Can use scanner in other contexts (reporting, analytics)
  • Performance: Parallel execution of database and filesystem queries

Common Migration Patterns

Pattern 1: Accessing Pending Migrations

Before (v0.2.x):

const result = await executor.migrate();
const pendingCount = result.executed.length; // Only shows what was executed

After (v0.3.0):

const result = await executor.migrate();
const pendingCount = result.executed.length;
const ignoredCount = result.ignored.length;

console.log(`Executed: ${pendingCount}, Ignored: ${ignoredCount}`);

Pattern 2: Custom Rendering with JSON

Before (v0.2.x):

const output = {
    todo: scripts.todo.map(s => ({ name: s.name, timestamp: s.timestamp }))
};

After (v0.3.0):

const output = {
    pending: scripts.pending.map(s => ({ name: s.name, timestamp: s.timestamp })),
    ignored: scripts.ignored.map(s => ({ name: s.name, timestamp: s.timestamp }))
};

Pattern 3: Variable Naming

Before (v0.2.x):

const todo = scripts.todo;
todo.forEach(script => {
    console.log(`Will execute: ${script.name}`);
});

After (v0.3.0):

const pending = scripts.pending;
pending.forEach(script => {
    console.log(`Will execute: ${script.name}`);
});

Pattern 4: Service Initialization with Config

Before (v0.2.x):

// Handler contained config
handler.cfg = new Config();
handler.cfg.folder = './migrations';
handler.cfg.displayLimit = 10;

// Services accessed config from handler
const executor = new MigrationScriptExecutor(handler);
const backupService = new BackupService(handler);
const renderer = new MigrationRenderer(handler);

After (v0.3.0):

// Config is separate
const config = new Config();
config.folder = './migrations';
config.displayLimit = 10;

// Services accept config as parameter
const executor = new MigrationScriptExecutor(handler, config);
const backupService = new BackupService(handler, config);
const renderer = new MigrationRenderer(handler, config);

Pattern 5: Custom Handler Implementation

Before (v0.2.x):

class PostgresHandler implements IDatabaseMigrationHandler {
    cfg: Config;
    db: IDB;
    schemaVersion: ISchemaVersion;
    backup: IBackup;

    constructor(connectionString: string, config: Config) {
        this.cfg = config;
        this.db = new PostgresDB(connectionString);
        // ...
    }

    getName(): string {
        return 'PostgreSQL';
    }
}

// Usage
const handler = new PostgresHandler(connStr, config);
const executor = new MigrationScriptExecutor(handler);

After (v0.3.0):

class PostgresHandler implements IDatabaseMigrationHandler {
    // No cfg property
    db: IDB;
    schemaVersion: ISchemaVersion;
    backup: IBackup;

    constructor(connectionString: string) {
        this.db = new PostgresDB(connectionString);
        // ...
    }

    getName(): string {
        return 'PostgreSQL';
    }
}

// Usage - config passed separately
const config = new Config();
const handler = new PostgresHandler(connStr);
const executor = new MigrationScriptExecutor(handler, config);

Pattern 6: Testing with Config

Before (v0.2.x):

it('should execute migrations', async () => {
    const handler = createMockHandler();
    handler.cfg = new Config();
    handler.cfg.folder = './test-migrations';

    const executor = new MigrationScriptExecutor(handler);
    await executor.migrate();
});

After (v0.3.0):

it('should execute migrations', async () => {
    const handler = createMockHandler();
    const config = new Config();
    config.folder = './test-migrations';

    const executor = new MigrationScriptExecutor(handler, config);
    await executor.migrate();
});

Automated Migration

Using sed (Unix/Linux/macOS)

# Navigate to your project
cd your-project/

# Backup first!
git add .
git commit -m "Before MSR v0.4 upgrade"

# Replace scripts.todo → scripts.pending
find . -type f \( -name "*.ts" -o -name "*.js" \) -exec sed -i '' 's/scripts\.todo/scripts.pending/g' {} +

# Replace method names
find . -type f \( -name "*.ts" -o -name "*.js" \) -exec sed -i '' 's/drawTodoTable/drawPending/g' {} +
find . -type f \( -name "*.ts" -o -name "*.js" \) -exec sed -i '' 's/drawIgnoredTable/drawIgnored/g' {} +
find . -type f \( -name "*.ts" -o -name "*.js" \) -exec sed -i '' 's/drawExecutedTable/drawExecuted/g' {} +
find . -type f \( -name "*.ts" -o -name "*.js" \) -exec sed -i '' 's/renderTodo/renderPending/g' {} +

# Verify changes
git diff

Using PowerShell (Windows)

# Backup first!
git add .
git commit -m "Before MSR v0.4 upgrade"

# Replace patterns
Get-ChildItem -Recurse -Include *.ts,*.js | ForEach-Object {
    (Get-Content $_) | ForEach-Object {
        $_ -replace 'scripts\.todo', 'scripts.pending' `
           -replace 'drawTodoTable', 'drawPending' `
           -replace 'drawIgnoredTable', 'drawIgnored' `
           -replace 'drawExecutedTable', 'drawExecuted' `
           -replace 'renderTodo', 'renderPending'
    } | Set-Content $_
}

TypeScript Compilation Errors

After upgrading, you may see TypeScript errors. Here’s how to fix them:

Error 1: Property ‘todo’ does not exist

Property 'todo' does not exist on type 'IScripts'. Did you mean 'pending'?

Fix: Replace scripts.todo with scripts.pending

Error 2: Property ‘drawTodoTable’ does not exist

Property 'drawTodoTable' does not exist on type 'IMigrationRenderer'.

Fix: Replace drawTodoTable with drawPending

Error 3: Type is missing properties

Type 'MyRenderer' is missing properties 'drawPending', 'drawIgnored', 'drawExecuted'

Fix: Implement the new method names in your custom renderer

Error 4: Property ‘cfg’ does not exist on type ‘IDatabaseMigrationHandler’

Property 'cfg' does not exist on type 'IDatabaseMigrationHandler'.

Fix: Remove handler.cfg references and pass config as a separate parameter to service constructors

Before:

const config = handler.cfg;
const executor = new MigrationScriptExecutor(handler);

After:

const config = new Config();
const executor = new MigrationScriptExecutor(handler, config);

Error 5: Expected 2 arguments, but got 1

Expected 2 arguments, but got 1.

Context: Occurs when instantiating services like MigrationScriptExecutor, BackupService, MigrationRenderer

Fix: Add config as the second parameter

Before:

new MigrationScriptExecutor(handler)
new BackupService(handler, logger)
new MigrationRenderer(handler, strategy)

After:

new MigrationScriptExecutor(handler, config)
new BackupService(handler, config, logger)
new MigrationRenderer(handler, config, strategy)

Error 6: Argument of type ‘(scripts: IScripts, config: Config, limit?: number)’ is not assignable

Types of parameters 'limit' and 'limit' are incompatible.
Type 'undefined' is not assignable to type 'number | undefined'.

Context: Occurs in custom render strategy implementations

Fix: Remove the limit parameter from renderMigrated() method

Before:

renderMigrated(scripts: IScripts, config: Config, limit?: number): void {
    const displayLimit = limit || config.displayLimit;
}

After:

renderMigrated(scripts: IScripts, config: Config): void {
    const displayLimit = config.displayLimit;
}

Testing

Before (v0.2.x)

it('should get todo scripts', () => {
    const todo = selector.getTodo(migrated, all);
    expect(todo).to.have.lengthOf(3);
});

After (v0.3.0)

it('should get pending scripts', () => {
    const pending = selector.getPending(migrated, all);
    expect(pending).to.have.lengthOf(3);
});

Troubleshooting

Issue: TypeScript Errors After Upgrade

Problem: Getting compilation errors about missing properties.

Solution: Make sure you’ve updated all references:

  1. scripts.todoscripts.pending
  2. Method names (see Step 3 above)
  3. Custom renderer implementations

Issue: Tests Failing

Problem: Tests fail with “property ‘todo’ does not exist”

Solution: Update test code to use new naming:

# Search for test files
grep -r "todo" test/

# Update them using the patterns above

Issue: JSON Output Changed

Problem: JSON output format changed from {todo: [...]} to {pending: [...]}

Solution: This is expected. Update any code that parses the JSON output:

// Before
const data = JSON.parse(output);
data.todo.forEach(...);

// After
const data = JSON.parse(output);
data.pending.forEach(...);

Rollback Plan

If you need to rollback to v0.2.x:

npm install @migration-script-runner/core@^0.2.0

Then restore your code using git:

git checkout HEAD -- .

New Features in v0.3.0

In addition to the breaking changes, v0.3.0 includes:

1. Complete Migration State Tracking

The ignored field now provides complete visibility:

const result = await executor.migrate();

// Full visibility into migration state
console.log('Total scripts:', result.migrated.length + result.executed.length);
console.log('Newly executed:', result.executed.length);
console.log('Skipped (too old):', result.ignored.length);

2. Improved Architecture

  • MigrationScanner service extracts state gathering logic
  • Better testability and separation of concerns
  • Parallel execution of database and filesystem queries for performance

3. Professional Terminology

  • Aligns with industry standards (Rails, Flyway, Liquibase use “pending”)
  • More intuitive for developers familiar with migration tools

4. Sub-folder Support for Migration Scripts

v0.3.0 introduces recursive sub-folder scanning, allowing you to organize migrations by feature, module, or version while maintaining timestamp-based execution order.

Key Features:

  • ✅ Organize migrations in sub-folders by feature, module, or version
  • ✅ Migrations always execute in timestamp order regardless of folder location
  • ✅ Enabled by default with config.recursive = true
  • ✅ Hidden files and folders (starting with .) are automatically excluded
  • ✅ Supports deep nesting for complex project structures

Example Structure:

migrations/
├── users/
│   ├── V202501220100_create_users_table.ts
│   └── V202501230200_add_user_roles.ts
├── auth/
│   └── V202501220150_create_sessions_table.ts
└── products/
    └── V202501240100_create_products_table.ts

Execution Order: Always by timestamp

  1. V202501220100_create_users_table.ts (users/)
  2. V202501220150_create_sessions_table.ts (auth/)
  3. V202501230200_add_user_roles.ts (users/)
  4. V202501240100_create_products_table.ts (products/)

Configuration:

const config = new Config();

// Recursive mode (default) - scan all sub-folders
config.recursive = true;

// Single-folder mode - scan only root folder
config.recursive = false;

No Migration Required: Sub-folder support is opt-in via configuration. Existing flat structures continue to work without changes.

5. beforeMigrate File for Database Setup

v0.3.0 introduces support for a special beforeMigrate.ts (or .js) file that executes before MSR scans for pending migrations. This file-based approach is similar to Flyway’s beforeMigrate.sql.

Key Features:

  • ✅ File-based: Create beforeMigrate.ts in your migrations folder
  • ✅ Executes before migration scanning (can reset/erase database)
  • ✅ Uses same IRunnableScript interface as regular migrations
  • ✅ NOT saved to schema version table
  • ✅ Configurable filename via config.beforeMigrateName
  • ✅ Can be disabled by setting config.beforeMigrateName = null
  • ✅ Completely optional - maintains backward compatibility

File Location:

migrations/
├── beforeMigrate.ts          # ← Special setup script
├── V202501010001_init.ts
└── V202501020001_users.ts

Example: beforeMigrate.ts

// migrations/beforeMigrate.ts
import fs from 'fs';
import {IRunnableScript, IMigrationInfo, IDatabaseMigrationHandler, IDB} from 'migration-script-runner';

export default class BeforeMigrate implements IRunnableScript {
  async up(
    db: IDB,
    info: IMigrationInfo,
    handler: IDatabaseMigrationHandler
  ): Promise<string> {
    // Data seeding - load production snapshot for development/testing
    if (process.env.NODE_ENV === 'development') {
      const snapshot = fs.readFileSync('./snapshots/prod_snapshot.sql', 'utf8');
      console.log('Loading production snapshot...');
      await (db as any).query(snapshot);
      console.log('✅ Production snapshot loaded');
    }

    // Fresh database setup - create extensions
    const tables = await (db as any).query(`
      SELECT COUNT(*) as count
      FROM information_schema.tables
      WHERE table_schema = 'public'
    `);

    if (tables[0].count === 0) {
      await (db as any).query('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"');
      await (db as any).query('CREATE EXTENSION IF NOT EXISTS "pg_trgm"');
    }

    // Environment-specific setup
    if (process.env.NODE_ENV === 'test') {
      await (db as any).query('SET statement_timeout = 0');
    }

    return 'beforeMigrate setup completed';
  }
}

Configuration:

import { Config } from 'migration-script-runner';

const config = new Config();

// Default: looks for beforeMigrate.ts or beforeMigrate.js
config.beforeMigrateName = 'beforeMigrate';

// Custom name: looks for setup.ts or setup.js
config.beforeMigrateName = 'setup';

// Disable feature entirely
config.beforeMigrateName = null;

When to Use:

  • Loading production snapshots or test data before migrations
  • Completely resetting/erasing the database (runs before scan)
  • Creating database extensions on fresh setups
  • Setting environment-specific parameters
  • Validating database version or prerequisites

Execution Timing:

1. Create backup
2. Initialize schema version table
3. Execute beforeMigrate.ts  ← Runs BEFORE scan
4. Scan for pending migrations
5. Execute pending migrations
6. Delete backup

No Migration Required: beforeMigrate.ts is optional. Projects without it continue to work without changes.

6. Granular Backup Control with BackupMode

v0.3.0 adds fine-grained control over backup operations through the new BackupMode enum and public backup/restore methods.

Key Features:

  • BackupMode enum: Control when backups are created (AUTO, BACKUP_ONLY, SKIP)
  • Public backup methods: backupOnly(), restoreOnly() for manual backup operations
  • Backward Compatible: Defaults to AUTO mode - existing behavior unchanged
  • Use Cases: Manual backups, testing, backup-as-a-service

BackupMode Options:

Mode Description Use Case
AUTO (default) Backup created automatically before migrations Normal migration runs
BACKUP_ONLY Create backup without running migrations Manual backup before risky operations
SKIP Skip backup creation entirely When using DOWN strategy or external backups

Configuration:

import { Config, BackupMode } from '@migration-script-runner/core';

const config = new Config();

// Automatic backup (default)
config.backup.mode = BackupMode.AUTO;

// Backup only, don't run migrations
config.backup.mode = BackupMode.BACKUP_ONLY;

// Skip backup entirely
config.backup.mode = BackupMode.SKIP;

Manual Backup Operations:

const executor = new MigrationScriptExecutor(handler, config);

// Create backup without running migrations
await executor.backupOnly();

// Later, restore from backup
await executor.restoreOnly('/path/to/backup.sql');

No Migration Required: Defaults to AUTO mode for backward compatibility.

7. Optional Backup and Flexible Rollback Strategies

v0.3.0 makes backup/restore optional and introduces flexible rollback strategies, giving you control over how migration failures are handled.

Key Features:

  • Optional Backup: IDatabaseMigrationHandler.backup is now optional
  • Four Rollback Strategies: Choose between backup, down() methods, both, or none
  • Down() Migrations: Support for reversible migrations via down() methods
  • Backward Compatible: Defaults to BACKUP strategy - existing code works unchanged
  • Configurable: Set config.rollbackStrategy to control behavior

Rollback Strategies:

Strategy Description When to Use Requires
BACKUP (default) Create backup before migrations, restore on failure Production, safest option handler.backup implementation
DOWN Call down() methods to reverse failed migrations Development, when backups are slow down() methods in migration scripts
BOTH Try down() first, fallback to backup if down() fails Maximum safety, hybrid approach Both handler.backup and down() methods
NONE No automatic rollback Test environments, use with caution Nothing

Interface Changes:

// Before v0.3.0
interface IDatabaseMigrationHandler {
    backup: IBackup;  // Required
}

// After v0.3.0
interface IDatabaseMigrationHandler {
    backup?: IBackup;  // Optional
}

Configuration:

import { Config, RollbackStrategy } from '@migration-script-runner/core';

const config = new Config();

// Use backup/restore (default)
config.rollbackStrategy = RollbackStrategy.BACKUP;

// Use down() methods for rollback
config.rollbackStrategy = RollbackStrategy.DOWN;

// Use both strategies (safest)
config.rollbackStrategy = RollbackStrategy.BOTH;

// No automatic rollback (use with caution)
config.rollbackStrategy = RollbackStrategy.NONE;

Writing Reversible Migrations:

// migrations/V202501260001_add_users_table.ts
import { IRunnableScript, IDB, IMigrationInfo, IDatabaseMigrationHandler } from '@migration-script-runner/core';

export default class AddUsersTable implements IRunnableScript {
    // Forward migration
    async up(db: IDB, info: IMigrationInfo, handler: IDatabaseMigrationHandler): Promise<string> {
        await db.query(`
            CREATE TABLE users (
                id INT PRIMARY KEY,
                name VARCHAR(255),
                email VARCHAR(255)
            )
        `);
        return 'Users table created';
    }

    // Reverse migration (optional)
    async down(db: IDB, info: IMigrationInfo, handler: IDatabaseMigrationHandler): Promise<string> {
        await db.query('DROP TABLE users');
        return 'Users table dropped';
    }
}

Example Use Cases:

Production Environment (backup strategy):

const config = new Config();
config.rollbackStrategy = RollbackStrategy.BACKUP;
// Slow but guaranteed recovery

Development Environment (down strategy):

const config = new Config();
config.rollbackStrategy = RollbackStrategy.DOWN;
// Fast rollback using down() methods

Maximum Safety (both strategies):

const config = new Config();
config.rollbackStrategy = RollbackStrategy.BOTH;
// Try down() first, fallback to backup if down() fails

Test Environment (no rollback):

const config = new Config();
config.rollbackStrategy = RollbackStrategy.NONE;
// No automatic rollback - database left in failed state for inspection

No Migration Required: Existing handlers with required backup continue to work. Default strategy is BACKUP for backward compatibility.

8. Lifecycle Hooks for Custom Behavior

v0.3.0 introduces lifecycle hooks that allow you to inject custom behavior at key points in the migration process.

Key Features:

  • beforeMigrate - Execute code before any migrations run
  • afterMigrate - Execute code after all migrations complete
  • onError - Execute code when a migration fails
  • CompositeHooks - Combine multiple hook implementations
  • Type-safe - Full TypeScript support with IMigrationHooks interface

Hook Interface:

interface IMigrationHooks {
    beforeMigrate?(scripts: IScripts, handler: IDatabaseMigrationHandler): Promise<void>;
    afterMigrate?(result: IMigrationResult, handler: IDatabaseMigrationHandler): Promise<void>;
    onError?(error: Error, handler: IDatabaseMigrationHandler): Promise<void>;
}

Example: Logging Hooks

import { IMigrationHooks, IScripts, IMigrationResult, IDatabaseMigrationHandler } from '@migration-script-runner/core';

class LoggingHooks implements IMigrationHooks {
    async beforeMigrate(scripts: IScripts, handler: IDatabaseMigrationHandler): Promise<void> {
        console.log(`Starting migration with ${scripts.pending.length} pending scripts`);
    }

    async afterMigrate(result: IMigrationResult, handler: IDatabaseMigrationHandler): Promise<void> {
        console.log(`Migration completed: ${result.executed.length} scripts executed`);
    }

    async onError(error: Error, handler: IDatabaseMigrationHandler): Promise<void> {
        console.error('Migration failed:', error.message);
        // Send to error tracking service
    }
}

// Usage
const hooks = new LoggingHooks();
const executor = new MigrationScriptExecutor(handler, config, { hooks });

Example: Notification Hooks

class NotificationHooks implements IMigrationHooks {
    async afterMigrate(result: IMigrationResult): Promise<void> {
        if (result.success) {
            await sendSlackNotification(`✅ ${result.executed.length} migrations deployed`);
        }
    }

    async onError(error: Error): Promise<void> {
        await sendSlackNotification(`❌ Migration failed: ${error.message}`);
    }
}

Combining Multiple Hooks:

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

const hooks = new CompositeHooks([
    new LoggingHooks(),
    new NotificationHooks(),
    new MetricsHooks()
]);

const executor = new MigrationScriptExecutor(handler, config, { hooks });

No Migration Required: Hooks are optional. Existing code continues to work without them.

9. CompositeLogger for Multi-Destination Logging

v0.3.0 adds CompositeLogger to send log output to multiple destinations simultaneously.

Key Features:

  • Multi-destination - Log to console, file, and custom destinations at once
  • Type-safe - Implements ILogger interface
  • Flexible - Combine any number of loggers
  • Use Cases - Production logging (console + file), development (console + debug), testing (silent + file)

Example: Console + File Logging

import { CompositeLogger, ConsoleLogger, FileLogger } from '@migration-script-runner/core';

const logger = new CompositeLogger([
    new ConsoleLogger(),
    new FileLogger('/var/log/migrations.log')
]);

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

Example: Environment-based Logging

const loggers = [new ConsoleLogger()];

if (process.env.NODE_ENV === 'production') {
    loggers.push(new FileLogger('/var/log/migrations.log'));
    loggers.push(new SentryLogger()); // Custom logger
}

const logger = new CompositeLogger(loggers);

No Migration Required: Default logger behavior unchanged.

10. Strategy Pattern for Flexible Output Rendering

v0.3.0 refactored the rendering system using the Strategy Pattern, enabling multiple output formats.

Key Features:

  • Multiple formats - ASCII tables (default), JSON, Silent
  • Custom strategies - Implement IRenderStrategy for custom formats
  • Type-safe - Full TypeScript support
  • Backward compatible - ASCII table format is default

Built-in Strategies:

Strategy Description Use Case
AsciiRenderStrategy (default) Colored ASCII tables Human-readable console output
JsonRenderStrategy JSON format CI/CD pipelines, logging, automation
SilentRenderStrategy No output Testing, when using custom loggers

Configuration:

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

const config = new Config();

// JSON output for CI/CD
config.renderStrategy = new JsonRenderStrategy();

// Silent mode for testing
config.renderStrategy = new SilentRenderStrategy();

Custom Render Strategy:

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

class CustomRenderStrategy implements IRenderStrategy {
    renderBanner(version: string, handlerName: string): void {
        console.log(`=== ${handlerName} - MSR v${version} ===`);
    }

    renderPending(scripts: IMigrationScript[]): void {
        console.log(`Pending: ${scripts.length} migrations`);
    }

    // Implement other methods...
}

const config = new Config();
config.renderStrategy = new CustomRenderStrategy();

No Migration Required: Defaults to ASCII table format for backward compatibility.

11. Improved Type Safety

v0.3.0 eliminates all any types and defines proper interfaces throughout the codebase.

Key Improvements:

  • No any types - All types properly defined
  • IDB interface - Type-safe database interface
  • Stricter TypeScript - Better IDE support and compile-time checks
  • Better documentation - Types serve as inline documentation

No Migration Required: Type improvements are transparent to existing code.


FAQ

Q: Is this a breaking change? A: Yes. Multiple breaking changes to interfaces and field names.

Q: Will my migration scripts break? A: No. Migration scripts themselves are not affected. Only the code that uses MSR’s API needs updating.

Q: Can I use v0.2.x and v0.3.0 side by side? A: No. Choose one version for your project.

Q: How long will v0.2.x be supported? A: v0.2.x will receive bug fixes for 6 months after v0.3.0 release. New features will only be added to v0.3.0 and later.

Q: Do I need to update my database? A: No. The database schema and migration scripts are unchanged.

Q: What about the ignored migrations? A: They were always being calculated, just not exposed. Now you can see them in result.ignored.

Q: Why was Config separated from IDatabaseMigrationHandler? A: Following the Single Responsibility Principle - the handler should manage database operations, not application configuration. This improves testability, reduces coupling, and makes the architecture cleaner.

Q: Do I need to update my handler implementation? A: Yes, if your handler implements IDatabaseMigrationHandler, remove the cfg property. Config is now passed separately to services that need it.

Q: Will this affect my tests? A: Yes, you’ll need to update test code to pass config as a separate parameter to service constructors. This actually makes tests easier to write since config can be managed independently.

Q: Can I still access config in services? A: Yes, services that need config accept it as a constructor parameter and store it internally. The change is only about where config lives, not how it’s used.


Support

If you encounter issues during migration:


Released: v0.3.0 (TBD)