Skip to content

Feature Request: Support Promise<Executor> in CLI createExecutor Type #145

Description

@vlavrynovych

Feature Request: Support Promise in CLI createExecutor Type

Target Repository: https://github.com/migration-script-runner/msr-core


Problem

The CLI adapter API expects createExecutor to return a synchronous Executor instance, but many database adapters (like Firebase) require async initialization to establish connections. This forces adapters to use unsafe type casts.

Current MSR Core CLI Type (v0.8.1):

interface IAdapterConfig<DB extends IDB, TExecutor extends MigrationScriptExecutor<DB>> {
    createExecutor: (config: Config) => TExecutor;  // ❌ Synchronous only
    // ...
}

Problem for Firebase Adapter:

// Firebase requires async connection
const handler = await FirebaseHandler.getInstance(appConfig);  // async!
const runner = new FirebaseRunner({ handler, config });

// But CLI expects synchronous factory
createExecutor: (config) => {
    return FirebaseRunner.getInstance({ config: appConfig });
    // ❌ TypeScript error: Promise<FirebaseRunner> is not assignable to FirebaseRunner
}

Current Workaround (Unsafe):

createExecutor: (config) => {
    // Lie to TypeScript with double cast
    return FirebaseRunner.getInstance({ config: appConfig }) as unknown as FirebaseRunner;
    //     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ returns Promise<FirebaseRunner>
}

This bypasses TypeScript's type safety and hides the async nature of initialization.

Impact

Database adapters requiring async initialization:

  • Firebase - Requires async connection to Realtime Database
  • MongoDB - Connection establishment is async
  • PostgreSQL - Pool creation is async
  • MySQL - Connection establishment is async
  • ✅ Any cloud database service with async SDKs

Current workarounds:

  1. Unsafe type casts (as unknown as Executor)
  2. Synchronous wrapper that returns unresolved Promise (breaks type safety)
  3. Not using the CLI framework (defeats the purpose)

Proposed Solution

Update IAdapterConfig to support both sync and async executors:

interface IAdapterConfig<DB extends IDB, TExecutor extends MigrationScriptExecutor<DB>> {
    // Support both synchronous and asynchronous executor creation
    createExecutor: (config: Config) => TExecutor | Promise<TExecutor>;

    extendCLI?: (program: Command, createExecutor: ExecutorFactory<DB, TExecutor>) => void;
    defaultConfig?: Partial<Config>;
}

Benefits:
✅ Type-safe async initialization
✅ No unsafe casts required
✅ Better developer experience
✅ Matches real-world database adapter needs
✅ Backward compatible (sync executors still work)

Implementation Details

1. Update IAdapterConfig Type

// core/src/cli/IAdapterConfig.ts
interface IAdapterConfig<DB extends IDB, TExecutor extends MigrationScriptExecutor<DB>> {
    createExecutor: (config: Config) => TExecutor | Promise<TExecutor>;
    extendCLI?: (program: Command, createExecutor: ExecutorFactory<DB, TExecutor>) => void;
    defaultConfig?: Partial<Config>;
}

2. Update CLI to Handle Promises

// core/src/cli/createCLI.ts
export function createCLI<DB extends IDB, TExecutor extends MigrationScriptExecutor<DB>>(
    adapterConfig: IAdapterConfig<DB, TExecutor>
): Command {
    // ...

    // Wrap createExecutor to handle both sync and async
    const createExecutor = async (config: Config): Promise<TExecutor> => {
        const result = adapterConfig.createExecutor(config);
        return result instanceof Promise ? result : Promise.resolve(result);
    };

    // Use await when calling createExecutor
    program
        .command('migrate')
        .action(async (options) => {
            const config = loadConfig(options);
            const executor = await createExecutor(config);  // ✅ await here
            await executor.migrate();
        });

    // ... other commands
}

3. Update ExecutorFactory Type

// core/src/cli/types.ts
export type ExecutorFactory<DB extends IDB, TExecutor extends MigrationScriptExecutor<DB>> =
    (config: Config) => TExecutor | Promise<TExecutor>;

Example Usage After Fix

Firebase Adapter (Clean, Type-Safe):

// msr-firebase/src/cli.ts
const cliConfig: IAdapterConfig<IFirebaseDB, FirebaseRunner> = {
    defaultConfig: {
        folder: './migrations',
        tableName: 'schema_version',
    },

    // ✅ Clean, type-safe async initialization
    createExecutor: async (config) => {
        const appConfig = new AppConfig();
        Object.assign(appConfig, config);
        return FirebaseRunner.getInstance({ config: appConfig });
    },

    extendCLI: (program, createExecutor) => {
        // Custom commands...
    }
};

MongoDB Adapter (Type-Safe):

createExecutor: async (config) => {
    const client = await MongoClient.connect(config.connectionString);
    return MongoRunner.getInstance({ client, config });
}

Simple Adapter (Still Works):

// Synchronous adapters still work without changes
createExecutor: (config) => {
    return new SimpleRunner({ config });
}

Backward Compatibility

Fully backward compatible - Synchronous executors continue to work:

// Old code (still works)
createExecutor: (config) => new MyRunner({ config })

// New code (now supported)
createExecutor: async (config) => await MyRunner.create(config)

The CLI framework internally wraps with Promise.resolve() so sync returns are automatically converted to Promises.

Related Issues

Testing Requirements

  • Test synchronous executor creation (backward compat)
  • Test async executor creation with Promise
  • Test CLI commands work with both sync and async executors
  • Update CLI documentation with async examples
  • Test error handling for failed async initialization

Alternative Considered

Option: Keep sync-only, force adapters to handle async internally

createExecutor: (config) => {
    // Adapter manages async internally (messy)
    let handler: FirebaseHandler | null = null;
    FirebaseHandler.getInstance(config).then(h => handler = h);
    // ... complex async state management
}

❌ This is complex, error-prone, and defeats the purpose of async/await.

Version

Suggest including in: MSR Core v0.9.0 (minor version bump for new feature)

Priority

High - This affects the DX of all adapters requiring async initialization, which is most real-world database systems.

Labels

enhancement, cli, adapters, typescript, developer-experience


Related Adapter Issues

  • msr-firebase: Currently using as unknown as FirebaseRunner cast (see cli.ts#L48)

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions