Skip to content

Commit

Permalink
refactor(cli): introduce the migration-runner helper for less code du…
Browse files Browse the repository at this point in the history
…plication and fewer return paths

Thanks to the migration-runner the "up" and "list" commands are now very similar code wise
  • Loading branch information
joakimbeng committed Dec 12, 2023
1 parent 5307e87 commit 8cc43a8
Show file tree
Hide file tree
Showing 9 changed files with 334 additions and 296 deletions.
14 changes: 14 additions & 0 deletions packages/cli/src/array-from-async.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* This is a simple polyfill for [Array.fromAsync()](https://github.com/tc39/proposal-array-from-async)
*
* It converts an async iterable to an array.
*/
export const arrayFromAsync = async <T>(iterable: AsyncIterable<T>): Promise<T[]> => {
const array: T[] = [];

for await (const item of iterable) {
array.push(item);
}

return array;
};
33 changes: 33 additions & 0 deletions packages/cli/src/collect-migrations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
type MigrationHistoryEntry,
type MigrationMetadata,
type MigrationMetadataFinished,
} from '@emigrate/plugin-tools/types';
import { toMigrationMetadata } from './to-migration-metadata.js';
import { getMigrations as getMigrationsOriginal } from './get-migrations.js';

export async function* collectMigrations(
cwd: string,
directory: string,
history: AsyncIterable<MigrationHistoryEntry>,
getMigrations = getMigrationsOriginal,
): AsyncIterable<MigrationMetadata | MigrationMetadataFinished> {
const allMigrations = await getMigrations(cwd, directory);
const seen = new Set<string>();

for await (const entry of history) {
const index = allMigrations.findIndex((migrationFile) => migrationFile.name === entry.name);

if (index === -1) {
continue;
}

yield toMigrationMetadata(entry, { cwd, directory });

seen.add(entry.name);
}

yield* allMigrations.filter((migration) => !seen.has(migration.name));

seen.clear();
}
78 changes: 20 additions & 58 deletions packages/cli/src/commands/list.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import process from 'node:process';
import path from 'node:path';
import { getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools';
import { type MigrationMetadataFinished } from '@emigrate/plugin-tools/types';
import { BadOptionError, MigrationHistoryError, MissingOptionError, StorageInitError } from '../errors.js';
import { BadOptionError, MissingOptionError, StorageInitError } from '../errors.js';
import { type Config } from '../types.js';
import { withLeadingPeriod } from '../with-leading-period.js';
import { getMigrations } from '../get-migrations.js';
import { exec } from '../exec.js';
import { migrationRunner } from '../migration-runner.js';
import { arrayFromAsync } from '../array-from-async.js';
import { collectMigrations } from '../collect-migrations.js';

const lazyDefaultReporter = async () => import('../reporters/default.js');

Expand Down Expand Up @@ -41,57 +40,20 @@ export default async function listCommand({ directory, reporter: reporterConfig,
return 1;
}

const migrationFiles = await getMigrations(cwd, directory);

let migrationHistoryError: MigrationHistoryError | undefined;

const finishedMigrations: MigrationMetadataFinished[] = [];

for await (const migrationHistoryEntry of storage.getHistory()) {
const index = migrationFiles.findIndex((migrationFile) => migrationFile.name === migrationHistoryEntry.name);

if (index === -1) {
// Only care about entries that exists in the current migration directory
continue;
}

const filePath = path.resolve(cwd, directory, migrationHistoryEntry.name);
const finishedMigration: MigrationMetadataFinished = {
name: migrationHistoryEntry.name,
status: migrationHistoryEntry.status,
filePath,
relativeFilePath: path.relative(cwd, filePath),
extension: withLeadingPeriod(path.extname(migrationHistoryEntry.name)),
directory,
cwd,
duration: 0,
};

if (migrationHistoryEntry.status === 'failed') {
migrationHistoryError = new MigrationHistoryError(
`Migration ${migrationHistoryEntry.name} is in a failed state`,
migrationHistoryEntry,
);

await reporter.onMigrationError?.(finishedMigration, migrationHistoryError);
} else {
await reporter.onMigrationSuccess?.(finishedMigration);
}

finishedMigrations.push(finishedMigration);

migrationFiles.splice(index, 1);
}

for await (const migration of migrationFiles) {
const finishedMigration: MigrationMetadataFinished = { ...migration, status: 'pending', duration: 0 };
await reporter.onMigrationSkip?.(finishedMigration);
finishedMigrations.push(finishedMigration);
}

await reporter.onFinished?.(finishedMigrations, migrationHistoryError);

await storage.end();

return migrationHistoryError ? 1 : 0;
const collectedMigrations = collectMigrations(cwd, directory, storage.getHistory());

const error = await migrationRunner({
dry: true,
reporter,
storage,
migrations: await arrayFromAsync(collectedMigrations),
async validate() {
// No-op
},
async execute() {
throw new Error('Unexpected execute call');
},
});

return error ? 1 : 0;
}
19 changes: 10 additions & 9 deletions packages/cli/src/commands/up.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, it, mock, type Mock } from 'node:test';
import assert from 'node:assert';
import path from 'node:path';
import { serializeError } from '@emigrate/plugin-tools';
import {
type EmigrateReporter,
type MigrationHistoryEntry,
Expand Down Expand Up @@ -49,8 +50,8 @@ describe('up', () => {

assert.strictEqual(exitCode, 1);
assert.strictEqual(reporter.onInit.mock.calls.length, 1);
assert.strictEqual(reporter.onCollectedMigrations.mock.calls.length, 0);
assert.strictEqual(reporter.onLockedMigrations.mock.calls.length, 0);
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, 1);
Expand All @@ -59,11 +60,11 @@ describe('up', () => {
assert.strictEqual(args?.length, 2);
const entries = args[0];
const error = args[1];
assert.strictEqual(entries.length, 2);
assert.deepStrictEqual(
entries.map((entry) => `${entry.name} (${entry.status})`),
['some_other.js (skipped)', 'some_file.sql (failed)'],
);
assert.strictEqual(entries.length, 2);
assert.strictEqual(error?.message, 'No loader plugin found for file extension: .sql');
});

Expand All @@ -74,8 +75,8 @@ describe('up', () => {

assert.strictEqual(exitCode, 1);
assert.strictEqual(reporter.onInit.mock.calls.length, 1);
assert.strictEqual(reporter.onCollectedMigrations.mock.calls.length, 0);
assert.strictEqual(reporter.onLockedMigrations.mock.calls.length, 0);
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, 1);
Expand Down Expand Up @@ -119,13 +120,13 @@ describe('up', () => {
const [entries, error] = reporter.onFinished.mock.calls[0]?.arguments ?? [];
assert.strictEqual(
error?.message,
`Migration ${failedEntry.name} is in a failed state, please fix and remove it first`,
`Migration ${failedEntry.name} is in a failed state, it should be fixed and removed`,
);
assert.strictEqual(getErrorCause(error), failedEntry.error);
assert.strictEqual(entries?.length, 2);
assert.deepStrictEqual(
entries.map((entry) => `${entry.name} (${entry.status})`),
['some_failed_migration.js (failed)', 'some_file.js (pending)'],
['some_failed_migration.js (failed)', 'some_file.js (skipped)'],
);
});

Expand Down Expand Up @@ -156,7 +157,7 @@ describe('up', () => {
const [entries, error] = reporter.onFinished.mock.calls[0]?.arguments ?? [];
assert.strictEqual(
error?.message,
`Migration ${failedEntry.name} is in a failed state, please fix and remove it first`,
`Migration ${failedEntry.name} is in a failed state, it should be fixed and removed`,
);
assert.strictEqual(getErrorCause(error), failedEntry.error);
assert.strictEqual(entries?.length, 2);
Expand Down Expand Up @@ -354,7 +355,7 @@ function toEntry(
name,
status,
date: new Date(),
error: status === 'failed' ? new Error('Failed') : undefined,
error: status === 'failed' ? serializeError(new Error('Failed')) : undefined,
};
}

Expand Down
Loading

0 comments on commit 8cc43a8

Please sign in to comment.