feat(up): add --limit option to limit the number of migrations to run

This commit is contained in:
Joakim Carlstein 2024-01-18 19:01:23 +01:00 committed by Joakim Carlstein
parent bf4d596980
commit 02c142e39a
7 changed files with 176 additions and 5 deletions

View file

@ -48,6 +48,10 @@ const up: Action = async (args) => {
type: 'string',
short: 's',
},
limit: {
type: 'string',
short: 'l',
},
dry: {
type: 'boolean',
},
@ -80,6 +84,7 @@ Options:
-s, --storage <name> The storage to use for where to store the migration history (required)
-p, --plugin <name> The plugin(s) to use (can be specified multiple times)
-r, --reporter <name> The reporter to use for reporting the migration progress
-l, --limit <count> Limit the number of migrations to run
--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)
@ -104,15 +109,34 @@ Examples:
storage = config.storage,
reporter = config.reporter,
dry,
limit: limitString,
import: imports = [],
} = values;
const plugins = [...(config.plugins ?? []), ...(values.plugin ?? [])];
const limit = limitString === undefined ? undefined : Number.parseInt(limitString, 10);
if (Number.isNaN(limit)) {
console.error('Invalid limit value, expected an integer but was:', limitString);
console.log(usage);
process.exitCode = 1;
return;
}
await importAll(cwd, imports);
try {
const { default: upCommand } = await import('./commands/up.js');
process.exitCode = await upCommand({ storage, reporter, directory, plugins, cwd, dry, color: useColors(values) });
process.exitCode = await upCommand({
storage,
reporter,
directory,
plugins,
cwd,
dry,
limit,
color: useColors(values),
});
} catch (error) {
if (error instanceof ShowUsageError) {
console.error(error.message, '\n');

View file

@ -198,7 +198,7 @@ describe('up', () => {
const failedEntry = toEntry('some_failed_migration.js', 'failed');
const { reporter, run } = getUpCommand([failedEntry.name, 'some_file.js'], getStorage([failedEntry]));
const exitCode = await run(true);
const exitCode = await run({ dry: true });
assert.strictEqual(exitCode, 1);
assert.strictEqual(reporter.onInit.mock.calls.length, 1);
@ -344,6 +344,88 @@ describe('up', () => {
);
});
it('returns 0 and finishes without an error when the given number of pending migrations are run successfully', async () => {
const { reporter, run } = getUpCommand(
['some_already_run_migration.js', 'some_migration.js', 'some_other_migration.js'],
getStorage(['some_already_run_migration.js']),
[
{
loadableExtensions: ['.js'],
async loadMigration() {
return async () => {
// Success
};
},
},
],
);
const exitCode = await run({ limit: 1 });
assert.strictEqual(exitCode, 0);
assert.strictEqual(reporter.onInit.mock.calls.length, 1);
assert.deepStrictEqual(reporter.onInit.mock.calls[0]?.arguments, [
{
command: 'up',
cwd: '/emigrate',
version,
dry: false,
color: undefined,
directory: 'migrations',
},
]);
assert.strictEqual(reporter.onCollectedMigrations.mock.calls.length, 1);
assert.strictEqual(reporter.onLockedMigrations.mock.calls.length, 1);
assert.strictEqual(reporter.onMigrationStart.mock.calls.length, 1);
assert.strictEqual(reporter.onMigrationSuccess.mock.calls.length, 1);
assert.strictEqual(reporter.onMigrationError.mock.calls.length, 0);
assert.strictEqual(reporter.onMigrationSkip.mock.calls.length, 1);
assert.strictEqual(reporter.onFinished.mock.calls.length, 1);
const [entries, error] = reporter.onFinished.mock.calls[0]?.arguments ?? [];
assert.strictEqual(error, undefined);
assert.strictEqual(entries?.length, 2);
assert.deepStrictEqual(
entries.map((entry) => `${entry.name} (${entry.status})`),
['some_migration.js (done)', 'some_other_migration.js (skipped)'],
);
});
it('returns 0 and finishes without an error with the given number of pending migrations are validated and listed successfully in dry-mode', async () => {
const { reporter, run } = getUpCommand(
['some_already_run_migration.js', 'some_migration.js', 'some_other_migration.js'],
getStorage(['some_already_run_migration.js']),
);
const exitCode = await run({ dry: true, limit: 1 });
assert.strictEqual(exitCode, 0);
assert.strictEqual(reporter.onInit.mock.calls.length, 1);
assert.deepStrictEqual(reporter.onInit.mock.calls[0]?.arguments, [
{
command: 'up',
cwd: '/emigrate',
version,
dry: true,
color: undefined,
directory: 'migrations',
},
]);
assert.strictEqual(reporter.onCollectedMigrations.mock.calls.length, 1);
assert.strictEqual(reporter.onLockedMigrations.mock.calls.length, 1);
assert.strictEqual(reporter.onMigrationStart.mock.calls.length, 0);
assert.strictEqual(reporter.onMigrationSuccess.mock.calls.length, 0);
assert.strictEqual(reporter.onMigrationError.mock.calls.length, 0);
assert.strictEqual(reporter.onMigrationSkip.mock.calls.length, 2);
assert.strictEqual(reporter.onFinished.mock.calls.length, 1);
const [entries, error] = reporter.onFinished.mock.calls[0]?.arguments ?? [];
assert.strictEqual(error, undefined);
assert.strictEqual(entries?.length, 2);
assert.deepStrictEqual(
entries.map((entry) => `${entry.name} (${entry.status})`),
['some_migration.js (pending)', 'some_other_migration.js (skipped)'],
);
});
it('returns 1 and finishes with an error when a pending migration throw when run', async () => {
const { reporter, run } = getUpCommand(
['some_already_run_migration.js', 'some_migration.js', 'fail.js', 'some_other_migration.js'],
@ -495,7 +577,12 @@ function getUpCommand(migrationFiles: string[], storage?: Mocked<Storage>, plugi
onMigrationSkip: mock.fn(noop),
};
const run = async (dry = false) => {
const run = async (
options?: Omit<
Parameters<typeof upCommand>[0],
'cwd' | 'directory' | 'storage' | 'reporter' | 'plugins' | 'getMigrations'
>,
) => {
return upCommand({
cwd: '/emigrate',
directory: 'migrations',
@ -509,11 +596,11 @@ function getUpCommand(migrationFiles: string[], storage?: Mocked<Storage>, plugi
},
},
reporter,
dry,
plugins: plugins ?? [],
async getMigrations(cwd, directory) {
return toMigrations(cwd, directory, migrationFiles);
},
...options,
});
};

View file

@ -14,6 +14,7 @@ import { version } from '../get-package-info.js';
type ExtraFlags = {
cwd: string;
dry?: boolean;
limit?: number;
getMigrations?: GetMigrationsFunction;
};
@ -25,6 +26,7 @@ export default async function upCommand({
reporter: reporterConfig,
directory,
color,
limit,
dry = false,
plugins = [],
cwd,
@ -83,6 +85,7 @@ export default async function upCommand({
const error = await migrationRunner({
dry,
limit,
reporter,
storage,
migrations: await arrayFromAsync(collectedMigrations),

View file

@ -15,6 +15,7 @@ import { getDuration } from './get-duration.js';
type MigrationRunnerParameters = {
dry: boolean;
limit?: number;
reporter: EmigrateReporter;
storage: Storage;
migrations: Array<MigrationMetadata | MigrationMetadataFinished>;
@ -24,6 +25,7 @@ type MigrationRunnerParameters = {
export const migrationRunner = async ({
dry,
limit,
reporter,
storage,
migrations,
@ -70,7 +72,12 @@ export const migrationRunner = async ({
}
}
const [lockedMigrations, lockError] = dry ? [migrationsToRun] : await exec(async () => storage.lock(migrationsToRun));
const migrationsToLock = limit ? migrationsToRun.slice(0, limit) : migrationsToRun;
const migrationsToSkip = limit ? migrationsToRun.slice(limit) : [];
const [lockedMigrations, lockError] = dry
? [migrationsToLock]
: await exec(async () => storage.lock(migrationsToLock));
if (lockError) {
for await (const migration of migrationsToRun) {
@ -152,6 +159,17 @@ export const migrationRunner = async ({
}
}
for await (const migration of migrationsToSkip) {
const finishedMigration: MigrationMetadataFinished = {
...migration,
status: 'skipped',
};
await reporter.onMigrationSkip?.(finishedMigration);
finishedMigrations.push(finishedMigration);
}
const [, unlockError] = dry ? [] : await exec(async () => storage.unlock(lockedMigrations ?? []));
// eslint-disable-next-line unicorn/no-array-callback-reference