From f515c8a8547baded645444400985979e314e9e24 Mon Sep 17 00:00:00 2001 From: Joakim Carlstein Date: Fri, 19 Jan 2024 13:13:33 +0100 Subject: [PATCH] feat(cli): add --no-execution option to the "up" command ...which can be used to log manually run migrations as successful or for baselining a database. --- .changeset/beige-bottles-tie.md | 5 ++ README.md | 3 + docs/src/content/docs/commands/up.mdx | 5 ++ packages/cli/README.md | 3 + packages/cli/src/cli.ts | 8 ++ packages/cli/src/commands/up.test.ts | 122 ++++++++++++++++++++++---- packages/cli/src/commands/up.ts | 10 +++ 7 files changed, 138 insertions(+), 18 deletions(-) create mode 100644 .changeset/beige-bottles-tie.md diff --git a/.changeset/beige-bottles-tie.md b/.changeset/beige-bottles-tie.md new file mode 100644 index 0000000..148f832 --- /dev/null +++ b/.changeset/beige-bottles-tie.md @@ -0,0 +1,5 @@ +--- +'@emigrate/cli': minor +--- + +Add support for the --no-execution option to the "up" command to be able to log migrations as successful without actually running them. Can for instance be used for baselining a database or logging manually run migrations as successful. diff --git a/README.md b/README.md index bd62417..1d1521f 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,8 @@ Options: --dry List the pending migrations that would be run without actually running them --color Force color output (this option is passed to the reporter) --no-color Disable color output (this option is passed to the reporter) + --no-execution Mark the migrations as executed and successful without actually running them, + which is useful if you want to mark migrations as successful after running them manually Examples: @@ -72,6 +74,7 @@ Examples: emigrate up -d ./migrations -s mysql --import dotenv/config emigrate up --limit 1 emigrate up --to 20231122120529381_some_migration_file.js + emigrate up --to 20231122120529381_some_migration_file.js --no-execution ``` ### Examples diff --git a/docs/src/content/docs/commands/up.mdx b/docs/src/content/docs/commands/up.mdx index 39d6151..1b6eedc 100644 --- a/docs/src/content/docs/commands/up.mdx +++ b/docs/src/content/docs/commands/up.mdx @@ -146,3 +146,8 @@ For example, if you want to use the `emigrate-reporter-somereporter` package, yo ### `--color`, `--no-color` Force enable/disable colored output, option is passed to the reporter which should respect it. + +### `--no-execution` + +Mark the migrations as executed and successful without actually running them, +which is useful if you want to mark migrations as successful after running them manually diff --git a/packages/cli/README.md b/packages/cli/README.md index 95945ef..87ee365 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -42,6 +42,8 @@ Options: --dry List the pending migrations that would be run without actually running them --color Force color output (this option is passed to the reporter) --no-color Disable color output (this option is passed to the reporter) + --no-execution Mark the migrations as executed and successful without actually running them, + which is useful if you want to mark migrations as successful after running them manually Examples: @@ -51,6 +53,7 @@ Examples: emigrate up -d ./migrations -s mysql --import dotenv/config emigrate up --limit 1 emigrate up --to 20231122120529381_some_migration_file.js + emigrate up --to 20231122120529381_some_migration_file.js --no-execution ``` ### Examples diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 1ac7520..999ef57 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -72,6 +72,9 @@ const up: Action = async (args) => { color: { type: 'boolean', }, + 'no-execution': { + type: 'boolean', + }, 'no-color': { type: 'boolean', }, @@ -100,6 +103,8 @@ Options: --dry List the pending migrations that would be run without actually running them --color Force color output (this option is passed to the reporter) --no-color Disable color output (this option is passed to the reporter) + --no-execution Mark the migrations as executed and successful without actually running them, + which is useful if you want to mark migrations as successful after running them manually Examples: @@ -109,6 +114,7 @@ Examples: emigrate up -d ./migrations -s mysql --import dotenv/config emigrate up --limit 1 emigrate up --to 20231122120529381_some_migration_file.js + emigrate up --to 20231122120529381_some_migration_file.js --no-execution `; if (values.help) { @@ -127,6 +133,7 @@ Examples: to, limit: limitString, import: imports = [], + 'no-execution': noExecution, } = values; const plugins = [...(config.plugins ?? []), ...(values.plugin ?? [])]; @@ -153,6 +160,7 @@ Examples: limit, from, to, + noExecution, color: useColors(values), }); } catch (error) { diff --git a/packages/cli/src/commands/up.test.ts b/packages/cli/src/commands/up.test.ts index dce2c1e..0ce4cde 100644 --- a/packages/cli/src/commands/up.test.ts +++ b/packages/cli/src/commands/up.test.ts @@ -28,7 +28,7 @@ describe('up', () => { const exitCode = await run(); - assert.strictEqual(exitCode, 1); + assert.strictEqual(exitCode, 1, 'Exit code'); assertPreconditionsFailed({ dry: false }, reporter, StorageInitError.fromError(new Error('No storage configured'))); }); @@ -37,7 +37,7 @@ describe('up', () => { const exitCode = await run(); - assert.strictEqual(exitCode, 0); + assert.strictEqual(exitCode, 0, 'Exit code'); assertPreconditionsFulfilled({ dry: false }, reporter, []); }); @@ -46,7 +46,7 @@ describe('up', () => { const exitCode = await run(); - assert.strictEqual(exitCode, 0); + assert.strictEqual(exitCode, 0, 'Exit code'); assertPreconditionsFulfilled({ dry: false }, reporter, []); }); @@ -55,7 +55,7 @@ describe('up', () => { const exitCode = await run(); - assert.strictEqual(exitCode, 0); + assert.strictEqual(exitCode, 0, 'Exit code'); assertPreconditionsFulfilled({ dry: false }, reporter, []); }); @@ -78,7 +78,7 @@ describe('up', () => { const exitCode = await run(); - assert.strictEqual(exitCode, 0); + assert.strictEqual(exitCode, 0, 'Exit code'); assertPreconditionsFulfilled({ dry: false }, reporter, [ { name: 'some_migration.js', status: 'done', started: true }, { name: 'some_other_migration.js', status: 'done', started: true }, @@ -106,7 +106,7 @@ describe('up', () => { const exitCode = await run(); - assert.strictEqual(exitCode, 1); + assert.strictEqual(exitCode, 1, 'Exit code'); assertPreconditionsFulfilled( { dry: false }, reporter, @@ -125,7 +125,7 @@ describe('up', () => { const exitCode = await run(); - assert.strictEqual(exitCode, 1); + assert.strictEqual(exitCode, 1, 'Exit code'); assertPreconditionsFulfilled( { dry: false }, reporter, @@ -146,7 +146,7 @@ describe('up', () => { const exitCode = await run({ dry: true }); - assert.strictEqual(exitCode, 1); + assert.strictEqual(exitCode, 1, 'Exit code'); assertPreconditionsFulfilled( { dry: true }, reporter, @@ -170,7 +170,7 @@ describe('up', () => { const exitCode = await run(); - assert.strictEqual(exitCode, 1); + assert.strictEqual(exitCode, 1, 'Exit code'); assertPreconditionsFulfilled( { dry: false }, reporter, @@ -198,7 +198,7 @@ describe('up', () => { const exitCode = await run({ dry: true }); - assert.strictEqual(exitCode, 1); + assert.strictEqual(exitCode, 1, 'Exit code'); assertPreconditionsFulfilled( { dry: true }, reporter, @@ -226,7 +226,7 @@ describe('up', () => { const exitCode = await run(); - assert.strictEqual(exitCode, 0); + assert.strictEqual(exitCode, 0, 'Exit code'); assertPreconditionsFulfilled({ dry: false }, reporter, []); }); }); @@ -250,7 +250,7 @@ describe('up', () => { const exitCode = await run({ limit: 1 }); - assert.strictEqual(exitCode, 0); + assert.strictEqual(exitCode, 0, 'Exit code'); assertPreconditionsFulfilled({ dry: false }, reporter, [ { name: 'some_migration.js', status: 'done', started: true }, { name: 'some_other_migration.js', status: 'skipped' }, @@ -267,7 +267,7 @@ describe('up', () => { const exitCode = await run({ dry: true, limit: 1 }); - assert.strictEqual(exitCode, 0); + assert.strictEqual(exitCode, 0, 'Exit code'); assertPreconditionsFulfilled({ dry: true }, reporter, [ { name: 'some_migration.js', status: 'pending' }, { name: 'some_other_migration.js', status: 'skipped' }, @@ -293,7 +293,7 @@ describe('up', () => { const exitCode = await run({ from: '3_non_existing_migration.js' }); - assert.strictEqual(exitCode, 0); + assert.strictEqual(exitCode, 0, 'Exit code'); assertPreconditionsFulfilled({ dry: false }, reporter, [ { name: '2_some_migration.js', status: 'skipped' }, { name: '4_some_other_migration.js', status: 'done', started: true }, @@ -309,7 +309,7 @@ describe('up', () => { const exitCode = await run({ dry: true, from: '3_non_existing_migration.js' }); - assert.strictEqual(exitCode, 0); + assert.strictEqual(exitCode, 0, 'Exit code'); assertPreconditionsFulfilled({ dry: true }, reporter, [ { name: '2_some_migration.js', status: 'skipped' }, { name: '4_some_other_migration.js', status: 'pending' }, @@ -335,7 +335,7 @@ describe('up', () => { const exitCode = await run({ to: '3_non_existing_migration.js' }); - assert.strictEqual(exitCode, 0); + assert.strictEqual(exitCode, 0, 'Exit code'); assertPreconditionsFulfilled({ dry: false }, reporter, [ { name: '2_some_migration.js', status: 'done', started: true }, { name: '4_some_other_migration.js', status: 'skipped' }, @@ -351,7 +351,7 @@ describe('up', () => { const exitCode = await run({ dry: true, to: '3_non_existing_migration.js' }); - assert.strictEqual(exitCode, 0); + assert.strictEqual(exitCode, 0, 'Exit code'); assertPreconditionsFulfilled({ dry: true }, reporter, [ { name: '2_some_migration.js', status: 'pending' }, { name: '4_some_other_migration.js', status: 'skipped' }, @@ -384,7 +384,7 @@ describe('up', () => { const exitCode = await run({ from: '3_another_migration.js', to: '5_yet_another_migration.js', limit: 2 }); - assert.strictEqual(exitCode, 0); + assert.strictEqual(exitCode, 0, 'Exit code'); assertPreconditionsFulfilled({ dry: false }, reporter, [ { name: '2_some_migration.js', status: 'skipped' }, { name: '3_another_migration.js', status: 'done', started: true }, @@ -395,6 +395,92 @@ describe('up', () => { assert.strictEqual(migration.mock.calls.length, 2); }); }); + + describe('marking migrations as successful without running them', () => { + it('returns 0 and finishes without an error when the pending migrations have been marked as successful without executing them', async () => { + const migration = mock.fn(async () => { + // Success + }); + const { reporter, run } = getUpCommand( + [ + '1_some_already_run_migration.js', + '2_some_migration.js', + '3_another_migration.js', + '4_some_other_migration.js', + '5_yet_another_migration.js', + '6_some_more_migration.js', + ], + getStorage(['1_some_already_run_migration.js']), + [ + { + loadableExtensions: ['.js'], + async loadMigration() { + return migration; + }, + }, + ], + ); + + const exitCode = await run({ + from: '3_another_migration.js', + to: '5_yet_another_migration.js', + limit: 2, + noExecution: true, + }); + + assert.strictEqual(exitCode, 0, 'Exit code'); + assertPreconditionsFulfilled({ dry: false }, reporter, [ + { name: '2_some_migration.js', status: 'skipped' }, + { name: '3_another_migration.js', status: 'done', started: true }, + { name: '4_some_other_migration.js', status: 'done', started: true }, + { name: '5_yet_another_migration.js', status: 'skipped' }, + { name: '6_some_more_migration.js', status: 'skipped' }, + ]); + assert.strictEqual(migration.mock.calls.length, 0); + }); + }); + + it('returns 0 and finishes without an error when the pending migrations have been marked as successful without executing them even though they have no corresponding loader', async () => { + const migration = mock.fn(async () => { + // Success + }); + const { reporter, run } = getUpCommand( + [ + '1_some_already_run_migration.js', + '2_some_migration.js', + '3_another_migration.js', + '4_some_other_migration.sql', + '5_yet_another_migration.js', + '6_some_more_migration.js', + ], + getStorage(['1_some_already_run_migration.js']), + [ + { + loadableExtensions: ['.js'], + async loadMigration() { + return migration; + }, + }, + ], + ); + + const exitCode = await run({ + from: '3_another_migration.js', + to: '5_yet_another_migration.js', + limit: 2, + noExecution: true, + }); + + assert.strictEqual(exitCode, 0, 'Exit code'); + assertPreconditionsFulfilled({ dry: false }, reporter, [ + { name: '2_some_migration.js', status: 'skipped' }, + { name: '3_another_migration.js', status: 'done', started: true }, + { name: '4_some_other_migration.sql', status: 'done', started: true }, + { name: '5_yet_another_migration.js', status: 'skipped' }, + { name: '6_some_more_migration.js', status: 'skipped' }, + ]); + assert.strictEqual(migration.mock.calls.length, 0); + }); }); function getErrorCause(error: Error | undefined): Error | SerializedError | undefined { diff --git a/packages/cli/src/commands/up.ts b/packages/cli/src/commands/up.ts index 6ffecdc..896c443 100644 --- a/packages/cli/src/commands/up.ts +++ b/packages/cli/src/commands/up.ts @@ -17,6 +17,7 @@ type ExtraFlags = { limit?: number; from?: string; to?: string; + noExecution?: boolean; getMigrations?: GetMigrationsFunction; }; @@ -31,6 +32,7 @@ export default async function upCommand({ limit, from, to, + noExecution, dry = false, plugins = [], cwd, @@ -96,6 +98,10 @@ export default async function upCommand({ storage, migrations: await arrayFromAsync(collectedMigrations), async validate(migration) { + if (noExecution) { + return; + } + const loader = getLoaderByExtension(migration.extension); if (!loader) { @@ -106,6 +112,10 @@ export default async function upCommand({ } }, async execute(migration) { + if (noExecution) { + return; + } + const loader = getLoaderByExtension(migration.extension)!; const [migrationFunction, loadError] = await exec(async () => loader.loadMigration(migration));