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:
- Unsafe type casts (
as unknown as Executor)
- Synchronous wrapper that returns unresolved Promise (breaks type safety)
- 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
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)
Feature Request: Support Promise in CLI createExecutor Type
Target Repository: https://github.com/migration-script-runner/msr-core
Problem
The CLI adapter API expects
createExecutorto return a synchronousExecutorinstance, 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):
Problem for Firebase Adapter:
Current Workaround (Unsafe):
This bypasses TypeScript's type safety and hides the async nature of initialization.
Impact
Database adapters requiring async initialization:
Current workarounds:
as unknown as Executor)Proposed Solution
Update
IAdapterConfigto support both sync and async executors: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
2. Update CLI to Handle Promises
3. Update ExecutorFactory Type
Example Usage After Fix
Firebase Adapter (Clean, Type-Safe):
MongoDB Adapter (Type-Safe):
Simple Adapter (Still Works):
Backward Compatibility
✅ Fully backward compatible - Synchronous executors continue to work:
The CLI framework internally wraps with
Promise.resolve()so sync returns are automatically converted to Promises.Related Issues
Testing Requirements
Alternative Considered
Option: Keep sync-only, force adapters to handle async internally
❌ 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-experienceRelated Adapter Issues
as unknown as FirebaseRunnercast (see cli.ts#L48)