Migrating from v0.2.x to v0.3.0
Guide for upgrading from MSR v0.2.x to v0.3.0
Table of contents
- Overview
- Migration Steps
- Step 1: Update Package Version
- Step 2: Update IScripts References
- Step 3: Update Renderer Method Names
- Step 4: Update Custom Renderers
- Step 5: Update Custom Render Strategies
- Step 6: Update Service Constructors (Config Separation)
- Step 7: Update Render Strategy Implementations
- Step 8: Update MigrationResult Handling
- Understanding the New Architecture
- Common Migration Patterns
- Automated Migration
- TypeScript Compilation Errors
- Error 1: Property ‘todo’ does not exist
- Error 2: Property ‘drawTodoTable’ does not exist
- Error 3: Type is missing properties
- Error 4: Property ‘cfg’ does not exist on type ‘IDatabaseMigrationHandler’
- Error 5: Expected 2 arguments, but got 1
- Error 6: Argument of type ‘(scripts: IScripts, config: Config, limit?: number)’ is not assignable
- Testing
- Troubleshooting
- Rollback Plan
- New Features in v0.3.0
- 1. Complete Migration State Tracking
- 2. Improved Architecture
- 3. Professional Terminology
- 4. Sub-folder Support for Migration Scripts
- 5. beforeMigrate File for Database Setup
- 6. Granular Backup Control with BackupMode
- 7. Optional Backup and Flexible Rollback Strategies
- 8. Lifecycle Hooks for Custom Behavior
- 9. CompositeLogger for Multi-Destination Logging
- 10. Strategy Pattern for Flexible Output Rendering
- 11. Improved Type Safety
- FAQ
- Support
Overview
Version 0.3.0 introduces several breaking changes focused on improving code clarity and architecture:
- Renamed “todo” → “pending” throughout the codebase for better terminology
- Added
ignoredfield toIScriptsfor complete migration state tracking - Removed “Table” suffix from renderer methods for format-agnostic naming
- Extracted MigrationScanner service for better separation of concerns
- Removed deprecated methods from public interfaces
- Separated Config from IDatabaseMigrationHandler following Single Responsibility Principle (issue #63)
Breaking Changes
- ❌
IScripts.todo→IScripts.pending - ❌
getTodo()→getPending()(in MigrationScriptSelector) - ❌
drawTodoTable()→drawPending()(in IMigrationRenderer) - ❌
drawIgnoredTable()→drawIgnored()(in IMigrationRenderer) - ❌
drawExecutedTable()→drawExecuted()(in IMigrationRenderer) - ❌
renderTodo()→renderPending()(in IRenderStrategy) - ❌
IDatabaseMigrationHandler.cfgproperty removed - ❌ Constructor signatures changed: Config now passed as separate parameter
- ❌
IRenderStrategy.renderMigrated()limitparameter removed (usesconfig.displayLimitinternally) - ✅ New:
IScripts.ignoredfield 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:
ignoredfield 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 todo → pending
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:
scripts.todo→scripts.pending- Method names (see Step 3 above)
- 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
V202501220100_create_users_table.ts(users/)V202501220150_create_sessions_table.ts(auth/)V202501230200_add_user_roles.ts(users/)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.tsin your migrations folder - ✅ Executes before migration scanning (can reset/erase database)
- ✅ Uses same
IRunnableScriptinterface 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
AUTOmode - 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.backupis 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
BACKUPstrategy - existing code works unchanged - ✅ Configurable: Set
config.rollbackStrategyto 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
IMigrationHooksinterface
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
ILoggerinterface - ✅ 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
IRenderStrategyfor 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
anytypes - 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)