feat(remove): rework the "remove" command to be more similar to "up" and "list"
The old reporter methods related to the "remove" command is not used anymore and instead the shared `onMigrationStart`, `onMigrationSuccess` and `onMigrationError` methods are used. Some preparation has also been made to support for removing multiple migrations at once in the future, similar to how the `--from` and `--to` CLI options work for the "up" command.
This commit is contained in:
parent
86e0d52e5c
commit
1f139fd975
12 changed files with 593 additions and 209 deletions
5
packages/cli/src/array-map-async.ts
Normal file
5
packages/cli/src/array-map-async.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export async function* arrayMapAsync<T, U>(iterable: AsyncIterable<T>, mapper: (item: T) => U): AsyncIterable<U> {
|
||||
for await (const item of iterable) {
|
||||
yield mapper(item);
|
||||
}
|
||||
}
|
||||
|
|
@ -457,8 +457,7 @@ Options:
|
|||
For example if you want to use Dotenv to load environment variables
|
||||
-r, --reporter <name> The reporter to use for reporting the removal process
|
||||
-s, --storage <name> The storage to use to get the migration history (required)
|
||||
-f, --force Force removal of the migration history entry even if the migration file does not exist
|
||||
or it's in a non-failed state
|
||||
-f, --force Force removal of the migration history entry even if the migration is not in a failed state
|
||||
--color Force color output (this option is passed to the reporter)
|
||||
--no-color Disable color output (this option is passed to the reporter)
|
||||
|
||||
|
|
|
|||
|
|
@ -62,6 +62,12 @@ export default async function listCommand({
|
|||
async execute() {
|
||||
throw new Error('Unexpected execute call');
|
||||
},
|
||||
async onSuccess() {
|
||||
throw new Error('Unexpected onSuccess call');
|
||||
},
|
||||
async onError() {
|
||||
throw new Error('Unexpected onError call');
|
||||
},
|
||||
});
|
||||
|
||||
return error ? 1 : 0;
|
||||
|
|
|
|||
306
packages/cli/src/commands/remove.test.ts
Normal file
306
packages/cli/src/commands/remove.test.ts
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import { type EmigrateReporter, type Storage, type Plugin, type MigrationMetadataFinished } from '@emigrate/types';
|
||||
import { deserializeError } from 'serialize-error';
|
||||
import { version } from '../get-package-info.js';
|
||||
import {
|
||||
BadOptionError,
|
||||
MigrationNotRunError,
|
||||
MigrationRemovalError,
|
||||
OptionNeededError,
|
||||
StorageInitError,
|
||||
} from '../errors.js';
|
||||
import {
|
||||
getErrorCause,
|
||||
getMockedReporter,
|
||||
getMockedStorage,
|
||||
toEntry,
|
||||
toMigrations,
|
||||
type Mocked,
|
||||
} from '../test-utils.js';
|
||||
import removeCommand from './remove.js';
|
||||
|
||||
describe('remove', () => {
|
||||
it("returns 1 and finishes with an error when the storage couldn't be initialized", async () => {
|
||||
const { reporter, run } = getRemoveCommand([]);
|
||||
|
||||
const exitCode = await run('some_migration.js');
|
||||
|
||||
assert.strictEqual(exitCode, 1, 'Exit code');
|
||||
assertPreconditionsFailed(reporter, StorageInitError.fromError(new Error('No storage configured')));
|
||||
});
|
||||
|
||||
it('returns 1 and finishes with an error when the given migration has not been executed', async () => {
|
||||
const storage = getMockedStorage(['some_other_migration.js']);
|
||||
const { reporter, run } = getRemoveCommand(['some_migration.js'], storage);
|
||||
|
||||
const exitCode = await run('some_migration.js');
|
||||
|
||||
assert.strictEqual(exitCode, 1, 'Exit code');
|
||||
assertPreconditionsFulfilled(
|
||||
reporter,
|
||||
storage,
|
||||
[
|
||||
{
|
||||
name: 'some_migration.js',
|
||||
status: 'failed',
|
||||
error: new MigrationNotRunError('Migration "some_migration.js" is not in the migration history'),
|
||||
},
|
||||
],
|
||||
new MigrationNotRunError('Migration "some_migration.js" is not in the migration history'),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns 1 and finishes with an error when the given migration is not in a failed state in the history', async () => {
|
||||
const storage = getMockedStorage(['1_old_migration.js', '2_some_migration.js', '3_new_migration.js']);
|
||||
const { reporter, run } = getRemoveCommand(['2_some_migration.js'], storage);
|
||||
|
||||
const exitCode = await run('2_some_migration.js');
|
||||
|
||||
assert.strictEqual(exitCode, 1, 'Exit code');
|
||||
assertPreconditionsFulfilled(
|
||||
reporter,
|
||||
storage,
|
||||
[
|
||||
{
|
||||
name: '2_some_migration.js',
|
||||
status: 'failed',
|
||||
error: OptionNeededError.fromOption(
|
||||
'force',
|
||||
'The migration "2_some_migration.js" is not in a failed state. Use the "force" option to force its removal',
|
||||
),
|
||||
},
|
||||
],
|
||||
OptionNeededError.fromOption(
|
||||
'force',
|
||||
'The migration "2_some_migration.js" is not in a failed state. Use the "force" option to force its removal',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns 1 and finishes with an error when the given migration does not exist at all', async () => {
|
||||
const storage = getMockedStorage(['some_migration.js']);
|
||||
const { reporter, run } = getRemoveCommand(['some_migration.js'], storage);
|
||||
|
||||
const exitCode = await run('some_other_migration.js');
|
||||
|
||||
assert.strictEqual(exitCode, 1, 'Exit code');
|
||||
assertPreconditionsFulfilled(
|
||||
reporter,
|
||||
storage,
|
||||
[],
|
||||
BadOptionError.fromOption('name', 'The migration: "migrations/some_other_migration.js" was not found'),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns 0, removes the migration from the history and finishes without an error when the given migration is in a failed state', async () => {
|
||||
const storage = getMockedStorage([toEntry('some_migration.js', 'failed')]);
|
||||
const { reporter, run } = getRemoveCommand(['some_migration.js'], storage);
|
||||
|
||||
const exitCode = await run('some_migration.js');
|
||||
|
||||
assert.strictEqual(exitCode, 0, 'Exit code');
|
||||
assertPreconditionsFulfilled(reporter, storage, [{ name: 'some_migration.js', status: 'done', started: true }]);
|
||||
});
|
||||
|
||||
it('returns 0, removes the migration from the history and finishes without an error when the given migration is not in a failed state but "force" is true', async () => {
|
||||
const storage = getMockedStorage(['1_old_migration.js', '2_some_migration.js', '3_new_migration.js']);
|
||||
const { reporter, run } = getRemoveCommand(['2_some_migration.js'], storage);
|
||||
|
||||
const exitCode = await run('2_some_migration.js', { force: true });
|
||||
|
||||
assert.strictEqual(exitCode, 0, 'Exit code');
|
||||
assertPreconditionsFulfilled(reporter, storage, [{ name: '2_some_migration.js', status: 'done', started: true }]);
|
||||
});
|
||||
|
||||
it('returns 1 and finishes with an error when the removal of the migration crashes', async () => {
|
||||
const storage = getMockedStorage([toEntry('some_migration.js', 'failed')]);
|
||||
storage.remove.mock.mockImplementation(async () => {
|
||||
throw new Error('Some error');
|
||||
});
|
||||
const { reporter, run } = getRemoveCommand(['some_migration.js'], storage);
|
||||
|
||||
const exitCode = await run('some_migration.js');
|
||||
|
||||
assert.strictEqual(exitCode, 1, 'Exit code');
|
||||
assertPreconditionsFulfilled(
|
||||
reporter,
|
||||
storage,
|
||||
[
|
||||
{
|
||||
name: 'some_migration.js',
|
||||
status: 'failed',
|
||||
error: new Error('Some error'),
|
||||
started: true,
|
||||
},
|
||||
],
|
||||
new MigrationRemovalError('Failed to remove migration: migrations/some_migration.js', {
|
||||
cause: new Error('Some error'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function getRemoveCommand(migrationFiles: string[], storage?: Mocked<Storage>, plugins?: Plugin[]) {
|
||||
const reporter = getMockedReporter();
|
||||
|
||||
const run = async (
|
||||
name: string,
|
||||
options?: Omit<Parameters<typeof removeCommand>[0], 'cwd' | 'directory' | 'storage' | 'reporter' | 'plugins'>,
|
||||
) => {
|
||||
return removeCommand(
|
||||
{
|
||||
cwd: '/emigrate',
|
||||
directory: 'migrations',
|
||||
storage: {
|
||||
async initializeStorage() {
|
||||
if (!storage) {
|
||||
throw new Error('No storage configured');
|
||||
}
|
||||
|
||||
return storage;
|
||||
},
|
||||
},
|
||||
reporter,
|
||||
plugins: plugins ?? [],
|
||||
async getMigrations(cwd, directory) {
|
||||
return toMigrations(cwd, directory, migrationFiles);
|
||||
},
|
||||
...options,
|
||||
},
|
||||
name,
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
reporter,
|
||||
storage,
|
||||
run,
|
||||
};
|
||||
}
|
||||
|
||||
function assertPreconditionsFailed(reporter: Mocked<Required<EmigrateReporter>>, finishedError?: Error) {
|
||||
assert.strictEqual(reporter.onInit.mock.calls.length, 1);
|
||||
assert.deepStrictEqual(reporter.onInit.mock.calls[0]?.arguments, [
|
||||
{
|
||||
command: 'remove',
|
||||
cwd: '/emigrate',
|
||||
version,
|
||||
dry: false,
|
||||
color: undefined,
|
||||
directory: 'migrations',
|
||||
},
|
||||
]);
|
||||
assert.strictEqual(reporter.onCollectedMigrations.mock.calls.length, 0, 'Collected call');
|
||||
assert.strictEqual(reporter.onLockedMigrations.mock.calls.length, 0, 'Locked call');
|
||||
assert.strictEqual(reporter.onMigrationStart.mock.calls.length, 0, 'Started migrations');
|
||||
assert.strictEqual(reporter.onMigrationSuccess.mock.calls.length, 0, 'Successful migrations');
|
||||
assert.strictEqual(reporter.onMigrationError.mock.calls.length, 0, 'Failed migrations');
|
||||
assert.strictEqual(reporter.onMigrationSkip.mock.calls.length, 0, 'Total pending and skipped');
|
||||
assert.strictEqual(reporter.onFinished.mock.calls.length, 1, 'Finished called once');
|
||||
const [entries, error] = reporter.onFinished.mock.calls[0]?.arguments ?? [];
|
||||
assert.deepStrictEqual(error, finishedError, 'Finished error');
|
||||
const cause = getErrorCause(error);
|
||||
const expectedCause = finishedError?.cause;
|
||||
assert.deepStrictEqual(
|
||||
cause,
|
||||
expectedCause ? deserializeError(expectedCause) : expectedCause,
|
||||
'Finished error cause',
|
||||
);
|
||||
assert.strictEqual(entries?.length, 0, 'Finished entries length');
|
||||
}
|
||||
|
||||
function assertPreconditionsFulfilled(
|
||||
reporter: Mocked<Required<EmigrateReporter>>,
|
||||
storage: Mocked<Storage>,
|
||||
expected: Array<{ name: string; status: MigrationMetadataFinished['status']; started?: boolean; error?: Error }>,
|
||||
finishedError?: Error,
|
||||
) {
|
||||
assert.strictEqual(reporter.onInit.mock.calls.length, 1);
|
||||
assert.deepStrictEqual(reporter.onInit.mock.calls[0]?.arguments, [
|
||||
{
|
||||
command: 'remove',
|
||||
cwd: '/emigrate',
|
||||
version,
|
||||
dry: false,
|
||||
color: undefined,
|
||||
directory: 'migrations',
|
||||
},
|
||||
]);
|
||||
|
||||
let started = 0;
|
||||
let done = 0;
|
||||
let failed = 0;
|
||||
let skipped = 0;
|
||||
let pending = 0;
|
||||
let failedAndStarted = 0;
|
||||
const failedEntries: typeof expected = [];
|
||||
const successfulEntries: typeof expected = [];
|
||||
|
||||
for (const entry of expected) {
|
||||
if (entry.started) {
|
||||
started++;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line default-case
|
||||
switch (entry.status) {
|
||||
case 'done': {
|
||||
done++;
|
||||
|
||||
if (entry.started) {
|
||||
successfulEntries.push(entry);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'failed': {
|
||||
failed++;
|
||||
failedEntries.push(entry);
|
||||
|
||||
if (entry.started) {
|
||||
failedAndStarted++;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'skipped': {
|
||||
skipped++;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'pending': {
|
||||
pending++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.strictEqual(reporter.onCollectedMigrations.mock.calls.length, 1, 'Collected call');
|
||||
assert.strictEqual(storage.lock.mock.calls.length, 0, 'Storage lock never called');
|
||||
assert.strictEqual(storage.unlock.mock.calls.length, 0, 'Storage unlock never called');
|
||||
assert.strictEqual(reporter.onLockedMigrations.mock.calls.length, 0, 'Locked call');
|
||||
assert.strictEqual(reporter.onMigrationStart.mock.calls.length, started, 'Started migrations');
|
||||
assert.strictEqual(reporter.onMigrationSuccess.mock.calls.length, successfulEntries.length, 'Successful migrations');
|
||||
assert.strictEqual(storage.remove.mock.calls.length, started, 'Storage remove called');
|
||||
assert.strictEqual(reporter.onMigrationError.mock.calls.length, failedEntries.length, 'Failed migrations');
|
||||
assert.strictEqual(reporter.onMigrationSkip.mock.calls.length, 0, 'Total pending and skipped');
|
||||
assert.strictEqual(reporter.onFinished.mock.calls.length, 1, 'Finished called once');
|
||||
const [entries, error] = reporter.onFinished.mock.calls[0]?.arguments ?? [];
|
||||
assert.deepStrictEqual(error, finishedError, 'Finished error');
|
||||
const cause = getErrorCause(error);
|
||||
const expectedCause = finishedError?.cause;
|
||||
assert.deepStrictEqual(
|
||||
cause,
|
||||
expectedCause ? deserializeError(expectedCause) : expectedCause,
|
||||
'Finished error cause',
|
||||
);
|
||||
assert.strictEqual(entries?.length, expected.length, 'Finished entries length');
|
||||
assert.deepStrictEqual(
|
||||
entries.map((entry) => `${entry.name} (${entry.status})`),
|
||||
expected.map((entry) => `${entry.name} (${entry.status})`),
|
||||
'Finished entries',
|
||||
);
|
||||
assert.strictEqual(storage.end.mock.calls.length, 1, 'Storage end called once');
|
||||
}
|
||||
|
|
@ -1,29 +1,44 @@
|
|||
import process from 'node:process';
|
||||
import path from 'node:path';
|
||||
import { getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools';
|
||||
import { type MigrationHistoryEntry, type MigrationMetadataFinished } from '@emigrate/types';
|
||||
import { type MigrationMetadata, isFinishedMigration } from '@emigrate/types';
|
||||
import {
|
||||
BadOptionError,
|
||||
MigrationNotRunError,
|
||||
MigrationRemovalError,
|
||||
MissingArgumentsError,
|
||||
MissingOptionError,
|
||||
OptionNeededError,
|
||||
StorageInitError,
|
||||
toError,
|
||||
} from '../errors.js';
|
||||
import { type Config } from '../types.js';
|
||||
import { getMigration } from '../get-migration.js';
|
||||
import { getDuration } from '../get-duration.js';
|
||||
import { exec } from '../exec.js';
|
||||
import { version } from '../get-package-info.js';
|
||||
import { collectMigrations } from '../collect-migrations.js';
|
||||
import { migrationRunner } from '../migration-runner.js';
|
||||
import { arrayMapAsync } from '../array-map-async.js';
|
||||
import { type GetMigrationsFunction } from '../get-migrations.js';
|
||||
|
||||
type ExtraFlags = {
|
||||
cwd: string;
|
||||
force?: boolean;
|
||||
getMigrations?: GetMigrationsFunction;
|
||||
};
|
||||
|
||||
type RemovableMigrationMetadata = MigrationMetadata & { originalStatus?: 'done' | 'failed' };
|
||||
|
||||
const lazyDefaultReporter = async () => import('../reporters/default.js');
|
||||
|
||||
export default async function removeCommand(
|
||||
{ directory, reporter: reporterConfig, storage: storageConfig, color, cwd, force = false }: Config & ExtraFlags,
|
||||
{
|
||||
directory,
|
||||
reporter: reporterConfig,
|
||||
storage: storageConfig,
|
||||
color,
|
||||
cwd,
|
||||
force = false,
|
||||
getMigrations,
|
||||
}: Config & ExtraFlags,
|
||||
name: string,
|
||||
) {
|
||||
if (!directory) {
|
||||
|
|
@ -59,71 +74,79 @@ export default async function removeCommand(
|
|||
return 1;
|
||||
}
|
||||
|
||||
const [migrationFile, fileError] = await exec(async () => getMigration(cwd, directory, name, !force));
|
||||
try {
|
||||
const collectedMigrations = arrayMapAsync(
|
||||
collectMigrations(cwd, directory, storage.getHistory(), getMigrations),
|
||||
(migration) => {
|
||||
if (isFinishedMigration(migration)) {
|
||||
if (migration.status === 'failed') {
|
||||
const { status, duration, error, ...pendingMigration } = migration;
|
||||
const removableMigration: RemovableMigrationMetadata = { ...pendingMigration, originalStatus: status };
|
||||
|
||||
if (fileError) {
|
||||
await reporter.onFinished?.([], fileError);
|
||||
return removableMigration;
|
||||
}
|
||||
|
||||
await storage.end();
|
||||
if (migration.status === 'done') {
|
||||
const { status, duration, ...pendingMigration } = migration;
|
||||
const removableMigration: RemovableMigrationMetadata = { ...pendingMigration, originalStatus: status };
|
||||
|
||||
return removableMigration;
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected migration status: ${migration.status}`);
|
||||
}
|
||||
|
||||
return migration as RemovableMigrationMetadata;
|
||||
},
|
||||
);
|
||||
|
||||
if (!name.includes(path.sep)) {
|
||||
name = path.join(directory, name);
|
||||
}
|
||||
|
||||
const error = await migrationRunner({
|
||||
dry: false,
|
||||
lock: false,
|
||||
name,
|
||||
reporter,
|
||||
storage,
|
||||
migrations: collectedMigrations,
|
||||
migrationFilter(migration) {
|
||||
return migration.relativeFilePath === name;
|
||||
},
|
||||
async validate(migration) {
|
||||
if (migration.originalStatus === 'done' && !force) {
|
||||
throw OptionNeededError.fromOption(
|
||||
'force',
|
||||
`The migration "${migration.name}" is not in a failed state. Use the "force" option to force its removal`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!migration.originalStatus) {
|
||||
throw MigrationNotRunError.fromMetadata(migration);
|
||||
}
|
||||
},
|
||||
async execute(migration) {
|
||||
try {
|
||||
await storage.remove(migration);
|
||||
} catch (error) {
|
||||
throw MigrationRemovalError.fromMetadata(migration, toError(error));
|
||||
}
|
||||
},
|
||||
async onSuccess() {
|
||||
// No-op
|
||||
},
|
||||
async onError() {
|
||||
// No-op
|
||||
},
|
||||
});
|
||||
|
||||
return error ? 1 : 0;
|
||||
} catch (error) {
|
||||
await reporter.onFinished?.([], toError(error));
|
||||
|
||||
return 1;
|
||||
} finally {
|
||||
await storage.end();
|
||||
}
|
||||
|
||||
const finishedMigrations: MigrationMetadataFinished[] = [];
|
||||
let historyEntry: MigrationHistoryEntry | undefined;
|
||||
let removalError: Error | undefined;
|
||||
|
||||
for await (const migrationHistoryEntry of storage.getHistory()) {
|
||||
if (migrationHistoryEntry.name !== migrationFile.name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (migrationHistoryEntry.status === 'done' && !force) {
|
||||
removalError = OptionNeededError.fromOption(
|
||||
'force',
|
||||
`The migration "${migrationFile.name}" is not in a failed state. Use the "force" option to force its removal`,
|
||||
);
|
||||
} else {
|
||||
historyEntry = migrationHistoryEntry;
|
||||
}
|
||||
}
|
||||
|
||||
await reporter.onMigrationRemoveStart?.(migrationFile);
|
||||
|
||||
const start = process.hrtime();
|
||||
|
||||
if (historyEntry) {
|
||||
try {
|
||||
await storage.remove(migrationFile);
|
||||
|
||||
const duration = getDuration(start);
|
||||
const finishedMigration: MigrationMetadataFinished = { ...migrationFile, status: 'done', duration };
|
||||
|
||||
await reporter.onMigrationRemoveSuccess?.(finishedMigration);
|
||||
|
||||
finishedMigrations.push(finishedMigration);
|
||||
} catch (error) {
|
||||
removalError = error instanceof Error ? error : new Error(String(error));
|
||||
}
|
||||
} else if (!removalError) {
|
||||
removalError = MigrationNotRunError.fromMetadata(migrationFile);
|
||||
}
|
||||
|
||||
if (removalError) {
|
||||
const duration = getDuration(start);
|
||||
const finishedMigration: MigrationMetadataFinished = {
|
||||
...migrationFile,
|
||||
status: 'failed',
|
||||
error: removalError,
|
||||
duration,
|
||||
};
|
||||
await reporter.onMigrationRemoveError?.(finishedMigration, removalError);
|
||||
finishedMigrations.push(finishedMigration);
|
||||
}
|
||||
|
||||
await reporter.onFinished?.(finishedMigrations, removalError);
|
||||
|
||||
await storage.end();
|
||||
|
||||
return removalError ? 1 : 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import { describe, it, mock, type Mock } from 'node:test';
|
||||
import { describe, it, mock } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import {
|
||||
type EmigrateReporter,
|
||||
type MigrationHistoryEntry,
|
||||
type Storage,
|
||||
type Plugin,
|
||||
type SerializedError,
|
||||
|
|
@ -19,14 +18,16 @@ import {
|
|||
StorageInitError,
|
||||
toSerializedError,
|
||||
} from '../errors.js';
|
||||
import { toEntries, toEntry, toMigrations } from '../test-utils.js';
|
||||
import {
|
||||
type Mocked,
|
||||
toEntry,
|
||||
toMigrations,
|
||||
getMockedReporter,
|
||||
getMockedStorage,
|
||||
getErrorCause,
|
||||
} from '../test-utils.js';
|
||||
import upCommand from './up.js';
|
||||
|
||||
type Mocked<T> = {
|
||||
// @ts-expect-error - This is a mock
|
||||
[K in keyof T]: Mock<T[K]>;
|
||||
};
|
||||
|
||||
describe('up', () => {
|
||||
it("returns 1 and finishes with an error when the storage couldn't be initialized", async () => {
|
||||
const { reporter, run } = getUpCommand(['some_migration.js']);
|
||||
|
|
@ -38,7 +39,7 @@ describe('up', () => {
|
|||
});
|
||||
|
||||
it('returns 0 and finishes without an error when there are no migrations to run', async () => {
|
||||
const storage = getStorage([]);
|
||||
const storage = getMockedStorage([]);
|
||||
const { reporter, run } = getUpCommand([], storage);
|
||||
|
||||
const exitCode = await run();
|
||||
|
|
@ -48,7 +49,7 @@ describe('up', () => {
|
|||
});
|
||||
|
||||
it('returns 0 and finishes without an error when all migrations have already been run', async () => {
|
||||
const storage = getStorage(['my_migration.js']);
|
||||
const storage = getMockedStorage(['my_migration.js']);
|
||||
const { reporter, run } = getUpCommand(['my_migration.js'], storage);
|
||||
|
||||
const exitCode = await run();
|
||||
|
|
@ -58,7 +59,7 @@ describe('up', () => {
|
|||
});
|
||||
|
||||
it('returns 0 and finishes without an error when all migrations have already been run even when the history responds without file extensions', async () => {
|
||||
const storage = getStorage(['my_migration']);
|
||||
const storage = getMockedStorage(['my_migration']);
|
||||
const { reporter, run } = getUpCommand(['my_migration.js'], storage);
|
||||
|
||||
const exitCode = await run();
|
||||
|
|
@ -71,7 +72,7 @@ describe('up', () => {
|
|||
const migration = mock.fn(async () => {
|
||||
// Success
|
||||
});
|
||||
const storage = getStorage(['some_already_run_migration.js']);
|
||||
const storage = getMockedStorage(['some_already_run_migration.js']);
|
||||
const { reporter, run } = getUpCommand(
|
||||
['some_already_run_migration.js', 'some_migration.js', 'some_other_migration.js'],
|
||||
storage,
|
||||
|
|
@ -96,7 +97,7 @@ describe('up', () => {
|
|||
});
|
||||
|
||||
it('returns 1 and finishes with an error when a pending migration throw when run', async () => {
|
||||
const storage = getStorage(['some_already_run_migration.js']);
|
||||
const storage = getMockedStorage(['some_already_run_migration.js']);
|
||||
const { reporter, run } = getUpCommand(
|
||||
['some_already_run_migration.js', 'some_migration.js', 'fail.js', 'some_other_migration.js'],
|
||||
storage,
|
||||
|
|
@ -132,7 +133,7 @@ describe('up', () => {
|
|||
|
||||
describe('each migration file extension needs a corresponding loader plugin', () => {
|
||||
it('returns 1 and finishes with an error when there are migration file extensions without a corresponding loader plugin', async () => {
|
||||
const storage = getStorage([]);
|
||||
const storage = getMockedStorage([]);
|
||||
const { reporter, run } = getUpCommand(['some_other.js', 'some_file.sql'], storage);
|
||||
|
||||
const exitCode = await run();
|
||||
|
|
@ -155,7 +156,7 @@ describe('up', () => {
|
|||
});
|
||||
|
||||
it('returns 1 and finishes with an error when there are migration file extensions without a corresponding loader plugin in dry-run mode as well', async () => {
|
||||
const storage = getStorage([]);
|
||||
const storage = getMockedStorage([]);
|
||||
const { reporter, run } = getUpCommand(['some_other.js', 'some_file.sql'], storage);
|
||||
|
||||
const exitCode = await run({ dry: true });
|
||||
|
|
@ -181,7 +182,7 @@ describe('up', () => {
|
|||
describe('failed migrations in the history are blocking', () => {
|
||||
it('returns 1 and finishes with an error when there are failed migrations in the history', async () => {
|
||||
const failedEntry = toEntry('some_failed_migration.js', 'failed');
|
||||
const storage = getStorage([failedEntry]);
|
||||
const storage = getMockedStorage([failedEntry]);
|
||||
const { reporter, run } = getUpCommand([failedEntry.name, 'some_file.js'], storage);
|
||||
|
||||
const exitCode = await run();
|
||||
|
|
@ -211,7 +212,7 @@ describe('up', () => {
|
|||
|
||||
it('returns 1 and finishes with an error when there are failed migrations in the history in dry-run mode as well', async () => {
|
||||
const failedEntry = toEntry('some_failed_migration.js', 'failed');
|
||||
const storage = getStorage([failedEntry]);
|
||||
const storage = getMockedStorage([failedEntry]);
|
||||
const { reporter, run } = getUpCommand([failedEntry.name, 'some_file.js'], storage);
|
||||
|
||||
const exitCode = await run({ dry: true });
|
||||
|
|
@ -241,7 +242,7 @@ describe('up', () => {
|
|||
|
||||
it('returns 0 and finishes without an error when the failed migrations in the history are not part of the current set of migrations', async () => {
|
||||
const failedEntry = toEntry('some_failed_migration.js', 'failed');
|
||||
const storage = getStorage([failedEntry]);
|
||||
const storage = getMockedStorage([failedEntry]);
|
||||
const { reporter, run } = getUpCommand([], storage);
|
||||
|
||||
const exitCode = await run();
|
||||
|
|
@ -255,7 +256,7 @@ describe('up', () => {
|
|||
const migration = mock.fn(async () => {
|
||||
// Success
|
||||
});
|
||||
const storage = getStorage(['some_already_run_migration.js']);
|
||||
const storage = getMockedStorage(['some_already_run_migration.js']);
|
||||
const { reporter, run } = getUpCommand(
|
||||
['some_already_run_migration.js', 'some_migration.js', 'some_other_migration.js'],
|
||||
storage,
|
||||
|
|
@ -281,7 +282,7 @@ describe('up', () => {
|
|||
|
||||
describe('limiting which pending migrations to run', () => {
|
||||
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 storage = getStorage(['some_already_run_migration.js']);
|
||||
const storage = getMockedStorage(['some_already_run_migration.js']);
|
||||
const { reporter, run } = getUpCommand(
|
||||
['some_already_run_migration.js', 'some_migration.js', 'some_other_migration.js'],
|
||||
storage,
|
||||
|
|
@ -300,7 +301,7 @@ describe('up', () => {
|
|||
const migration = mock.fn(async () => {
|
||||
// Success
|
||||
});
|
||||
const storage = getStorage(['1_some_already_run_migration.js']);
|
||||
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
||||
const { reporter, run } = getUpCommand(
|
||||
[
|
||||
'1_some_already_run_migration.js',
|
||||
|
|
@ -334,7 +335,7 @@ describe('up', () => {
|
|||
const migration = mock.fn(async () => {
|
||||
// Success
|
||||
});
|
||||
const storage = getStorage(['1_some_already_run_migration.js']);
|
||||
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
||||
const { reporter, run } = getUpCommand(
|
||||
[
|
||||
'1_some_already_run_migration.js',
|
||||
|
|
@ -368,7 +369,7 @@ describe('up', () => {
|
|||
const migration = mock.fn(async () => {
|
||||
// Success
|
||||
});
|
||||
const storage = getStorage(['1_some_already_run_migration.js']);
|
||||
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
||||
const { reporter, run } = getUpCommand(
|
||||
['1_some_already_run_migration.js', '2_some_migration.js', '4_some_other_migration.js'],
|
||||
storage,
|
||||
|
|
@ -396,7 +397,7 @@ describe('up', () => {
|
|||
const migration = mock.fn(async () => {
|
||||
// Success
|
||||
});
|
||||
const storage = getStorage(['1_some_already_run_migration.js']);
|
||||
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
||||
const { reporter, run } = getUpCommand(
|
||||
['1_some_already_run_migration.js', '2_some_migration.js', '4_some_other_migration.js'],
|
||||
storage,
|
||||
|
|
@ -430,7 +431,7 @@ describe('up', () => {
|
|||
});
|
||||
|
||||
it('returns 0 and finishes without an error when pending migrations after given "from" parameter are validated and listed successfully in dry-mode', async () => {
|
||||
const storage = getStorage(['1_some_already_run_migration.js']);
|
||||
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
||||
const { reporter, run } = getUpCommand(
|
||||
['1_some_already_run_migration.js', '2_some_migration.js', '3_some_other_migration.js'],
|
||||
storage,
|
||||
|
|
@ -449,7 +450,7 @@ describe('up', () => {
|
|||
const migration = mock.fn(async () => {
|
||||
// Success
|
||||
});
|
||||
const storage = getStorage(['1_some_already_run_migration.js']);
|
||||
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
||||
const { reporter, run } = getUpCommand(
|
||||
[
|
||||
'1_some_already_run_migration.js',
|
||||
|
|
@ -483,7 +484,7 @@ describe('up', () => {
|
|||
const migration = mock.fn(async () => {
|
||||
// Success
|
||||
});
|
||||
const storage = getStorage(['1_some_already_run_migration.js']);
|
||||
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
||||
const { reporter, run } = getUpCommand(
|
||||
['1_some_already_run_migration.js', '2_some_migration.js', '4_some_other_migration.js'],
|
||||
storage,
|
||||
|
|
@ -517,7 +518,7 @@ describe('up', () => {
|
|||
const migration = mock.fn(async () => {
|
||||
// Success
|
||||
});
|
||||
const storage = getStorage(['1_some_already_run_migration.js']);
|
||||
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
||||
const { reporter, run } = getUpCommand(
|
||||
['1_some_already_run_migration.js', '2_some_migration.js', '4_some_other_migration.js'],
|
||||
storage,
|
||||
|
|
@ -542,7 +543,7 @@ describe('up', () => {
|
|||
});
|
||||
|
||||
it('returns 0 and finishes without an error when pending migrations after given "to" parameter are validated and listed successfully in dry-mode', async () => {
|
||||
const storage = getStorage(['1_some_already_run_migration.js']);
|
||||
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
||||
const { reporter, run } = getUpCommand(
|
||||
[
|
||||
'1_some_already_run_migration.js',
|
||||
|
|
@ -567,7 +568,7 @@ describe('up', () => {
|
|||
const migration = mock.fn(async () => {
|
||||
// Success
|
||||
});
|
||||
const storage = getStorage(['1_some_already_run_migration.js']);
|
||||
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
||||
const { reporter, run } = getUpCommand(
|
||||
[
|
||||
'1_some_already_run_migration.js',
|
||||
|
|
@ -607,7 +608,7 @@ describe('up', () => {
|
|||
const migration = mock.fn(async () => {
|
||||
// Success
|
||||
});
|
||||
const storage = getStorage(['1_some_already_run_migration.js']);
|
||||
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
||||
const { reporter, run } = getUpCommand(
|
||||
[
|
||||
'1_some_already_run_migration.js',
|
||||
|
|
@ -651,7 +652,7 @@ describe('up', () => {
|
|||
const migration = mock.fn(async () => {
|
||||
// Success
|
||||
});
|
||||
const storage = getStorage(['1_some_already_run_migration.js']);
|
||||
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
||||
const { reporter, run } = getUpCommand(
|
||||
[
|
||||
'1_some_already_run_migration.js',
|
||||
|
|
@ -703,7 +704,7 @@ describe('up', () => {
|
|||
},
|
||||
{ times: 1 },
|
||||
);
|
||||
const storage = getStorage(['1_some_already_run_migration.js']);
|
||||
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
||||
const { reporter, run } = getUpCommand(
|
||||
[
|
||||
'1_some_already_run_migration.js',
|
||||
|
|
@ -762,7 +763,7 @@ describe('up', () => {
|
|||
},
|
||||
{ times: 1 },
|
||||
);
|
||||
const storage = getStorage(['1_some_already_run_migration.js']);
|
||||
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
||||
const { reporter, run } = getUpCommand(
|
||||
[
|
||||
'1_some_already_run_migration.js',
|
||||
|
|
@ -812,56 +813,8 @@ describe('up', () => {
|
|||
});
|
||||
});
|
||||
|
||||
function getErrorCause(error: Error | undefined): Error | SerializedError | undefined {
|
||||
if (error?.cause instanceof Error) {
|
||||
return error.cause;
|
||||
}
|
||||
|
||||
if (typeof error?.cause === 'object' && error.cause !== null) {
|
||||
return error.cause as unknown as SerializedError;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function noop() {
|
||||
// noop
|
||||
}
|
||||
|
||||
function getStorage(historyEntries: Array<string | MigrationHistoryEntry>) {
|
||||
const storage: Mocked<Storage> = {
|
||||
lock: mock.fn(async (migrations) => migrations),
|
||||
unlock: mock.fn(async () => {
|
||||
// void
|
||||
}),
|
||||
getHistory: mock.fn(async function* () {
|
||||
yield* toEntries(historyEntries);
|
||||
}),
|
||||
remove: mock.fn(),
|
||||
onSuccess: mock.fn(),
|
||||
onError: mock.fn(),
|
||||
end: mock.fn(),
|
||||
};
|
||||
|
||||
return storage;
|
||||
}
|
||||
|
||||
function getUpCommand(migrationFiles: string[], storage?: Mocked<Storage>, plugins?: Plugin[]) {
|
||||
const reporter: Mocked<Required<EmigrateReporter>> = {
|
||||
onFinished: mock.fn(noop),
|
||||
onInit: mock.fn(noop),
|
||||
onAbort: mock.fn(noop),
|
||||
onCollectedMigrations: mock.fn(noop),
|
||||
onLockedMigrations: mock.fn(noop),
|
||||
onNewMigration: mock.fn(noop),
|
||||
onMigrationRemoveStart: mock.fn(noop),
|
||||
onMigrationRemoveSuccess: mock.fn(noop),
|
||||
onMigrationRemoveError: mock.fn(noop),
|
||||
onMigrationStart: mock.fn(noop),
|
||||
onMigrationSuccess: mock.fn(noop),
|
||||
onMigrationError: mock.fn(noop),
|
||||
onMigrationSkip: mock.fn(noop),
|
||||
};
|
||||
const reporter = getMockedReporter();
|
||||
|
||||
const run = async (
|
||||
options?: Omit<
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
import path from 'node:path';
|
||||
import { getOrLoadPlugins, getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools';
|
||||
import { isFinishedMigration, type LoaderPlugin } from '@emigrate/types';
|
||||
import { BadOptionError, MigrationLoadError, MissingOptionError, StorageInitError, toError } from '../errors.js';
|
||||
import {
|
||||
BadOptionError,
|
||||
MigrationLoadError,
|
||||
MissingOptionError,
|
||||
StorageInitError,
|
||||
toError,
|
||||
toSerializedError,
|
||||
} from '../errors.js';
|
||||
import { type Config } from '../types.js';
|
||||
import { withLeadingPeriod } from '../with-leading-period.js';
|
||||
import { type GetMigrationsFunction } from '../get-migrations.js';
|
||||
|
|
@ -138,6 +145,12 @@ export default async function upCommand({
|
|||
|
||||
await migrationFunction();
|
||||
},
|
||||
async onSuccess(migration) {
|
||||
await storage.onSuccess(migration);
|
||||
},
|
||||
async onError(migration, error) {
|
||||
await storage.onError(migration, toSerializedError(error));
|
||||
},
|
||||
});
|
||||
|
||||
return error ? 1 : 0;
|
||||
|
|
|
|||
|
|
@ -137,6 +137,16 @@ export class MigrationNotRunError extends EmigrateError {
|
|||
}
|
||||
}
|
||||
|
||||
export class MigrationRemovalError extends EmigrateError {
|
||||
static fromMetadata(metadata: MigrationMetadata, cause?: Error) {
|
||||
return new MigrationRemovalError(`Failed to remove migration: ${metadata.relativeFilePath}`, { cause });
|
||||
}
|
||||
|
||||
constructor(message: string | undefined, options?: ErrorOptions) {
|
||||
super(message, options, 'ERR_MIGRATION_REMOVE');
|
||||
}
|
||||
}
|
||||
|
||||
export class StorageInitError extends EmigrateError {
|
||||
static fromError(error: Error) {
|
||||
return new StorageInitError('Could not initialize storage', { cause: error });
|
||||
|
|
@ -182,6 +192,7 @@ errorConstructors.set('MigrationHistoryError', MigrationHistoryError as unknown
|
|||
errorConstructors.set('MigrationLoadError', MigrationLoadError as unknown as ErrorConstructor);
|
||||
errorConstructors.set('MigrationRunError', MigrationRunError as unknown as ErrorConstructor);
|
||||
errorConstructors.set('MigrationNotRunError', MigrationNotRunError as unknown as ErrorConstructor);
|
||||
errorConstructors.set('MigrationRemovalError', MigrationRemovalError as unknown as ErrorConstructor);
|
||||
errorConstructors.set('StorageInitError', StorageInitError as unknown as ErrorConstructor);
|
||||
errorConstructors.set('CommandAbortError', CommandAbortError as unknown as ErrorConstructor);
|
||||
errorConstructors.set('ExecutionDesertedError', ExecutionDesertedError as unknown as ErrorConstructor);
|
||||
|
|
|
|||
|
|
@ -9,28 +9,34 @@ import {
|
|||
type FailedMigrationMetadata,
|
||||
type SuccessfulMigrationMetadata,
|
||||
} from '@emigrate/types';
|
||||
import { toError, EmigrateError, MigrationRunError, toSerializedError, BadOptionError } from './errors.js';
|
||||
import { toError, EmigrateError, MigrationRunError, BadOptionError } from './errors.js';
|
||||
import { exec } from './exec.js';
|
||||
import { getDuration } from './get-duration.js';
|
||||
|
||||
type MigrationRunnerParameters = {
|
||||
type MigrationRunnerParameters<T extends MigrationMetadata | MigrationMetadataFinished> = {
|
||||
dry: boolean;
|
||||
lock?: boolean;
|
||||
limit?: number;
|
||||
name?: string;
|
||||
from?: string;
|
||||
to?: string;
|
||||
abortSignal?: AbortSignal;
|
||||
abortRespite?: number;
|
||||
reporter: EmigrateReporter;
|
||||
storage: Storage;
|
||||
migrations: AsyncIterable<MigrationMetadata | MigrationMetadataFinished>;
|
||||
migrationFilter?: (migration: MigrationMetadata | MigrationMetadataFinished) => boolean;
|
||||
validate: (migration: MigrationMetadata) => Promise<void>;
|
||||
execute: (migration: MigrationMetadata) => Promise<void>;
|
||||
migrations: AsyncIterable<T>;
|
||||
migrationFilter?: (migration: T) => boolean;
|
||||
validate: (migration: T) => Promise<void>;
|
||||
execute: (migration: T) => Promise<void>;
|
||||
onSuccess: (migration: SuccessfulMigrationMetadata) => Promise<void>;
|
||||
onError: (migration: FailedMigrationMetadata, error: Error) => Promise<void>;
|
||||
};
|
||||
|
||||
export const migrationRunner = async ({
|
||||
export const migrationRunner = async <T extends MigrationMetadata | MigrationMetadataFinished>({
|
||||
dry,
|
||||
lock = true,
|
||||
limit,
|
||||
name,
|
||||
from,
|
||||
to,
|
||||
abortSignal,
|
||||
|
|
@ -40,8 +46,10 @@ export const migrationRunner = async ({
|
|||
migrations,
|
||||
validate,
|
||||
execute,
|
||||
onSuccess,
|
||||
onError,
|
||||
migrationFilter = () => true,
|
||||
}: MigrationRunnerParameters): Promise<Error | undefined> => {
|
||||
}: MigrationRunnerParameters<T>): Promise<Error | undefined> => {
|
||||
const collectedMigrations: Array<MigrationMetadata | MigrationMetadataFinished> = [];
|
||||
const validatedMigrations: Array<MigrationMetadata | MigrationMetadataFinished> = [];
|
||||
const migrationsToLock: MigrationMetadata[] = [];
|
||||
|
|
@ -64,10 +72,15 @@ export const migrationRunner = async ({
|
|||
{ once: true },
|
||||
);
|
||||
|
||||
let nameFound = false;
|
||||
let fromFound = false;
|
||||
let toFound = false;
|
||||
|
||||
for await (const migration of migrations) {
|
||||
if (name && migration.relativeFilePath === name) {
|
||||
nameFound = true;
|
||||
}
|
||||
|
||||
if (from && migration.relativeFilePath === from) {
|
||||
fromFound = true;
|
||||
}
|
||||
|
|
@ -129,7 +142,9 @@ export const migrationRunner = async ({
|
|||
|
||||
let optionError: Error | undefined;
|
||||
|
||||
if (from && !fromFound) {
|
||||
if (name && !nameFound) {
|
||||
optionError = BadOptionError.fromOption('name', `The migration: "${name}" was not found`);
|
||||
} else if (from && !fromFound) {
|
||||
optionError = BadOptionError.fromOption('from', `The "from" migration: "${from}" was not found`);
|
||||
} else if (to && !toFound) {
|
||||
optionError = BadOptionError.fromOption('to', `The "to" migration: "${to}" was not found`);
|
||||
|
|
@ -151,9 +166,10 @@ export const migrationRunner = async ({
|
|||
migrationsToLock.length = 0;
|
||||
}
|
||||
|
||||
const [lockedMigrations, lockError] = dry
|
||||
? [migrationsToLock]
|
||||
: await exec(async () => storage.lock(migrationsToLock), { abortSignal, abortRespite });
|
||||
const [lockedMigrations, lockError] =
|
||||
dry || !lock
|
||||
? [migrationsToLock]
|
||||
: await exec(async () => storage.lock(migrationsToLock), { abortSignal, abortRespite });
|
||||
|
||||
if (lockError) {
|
||||
for (const migration of migrationsToLock) {
|
||||
|
|
@ -168,7 +184,7 @@ export const migrationRunner = async ({
|
|||
migrationsToLock.length = 0;
|
||||
|
||||
skip = true;
|
||||
} else {
|
||||
} else if (lock) {
|
||||
for (const migration of migrationsToLock) {
|
||||
const isLocked = lockedMigrations.some((lockedMigration) => lockedMigration.name === migration.name);
|
||||
|
||||
|
|
@ -231,7 +247,7 @@ export const migrationRunner = async ({
|
|||
|
||||
const start = hrtime();
|
||||
|
||||
const [, migrationError] = await exec(async () => execute(migration), { abortSignal, abortRespite });
|
||||
const [, migrationError] = await exec(async () => execute(migration as T), { abortSignal, abortRespite });
|
||||
|
||||
const duration = getDuration(start);
|
||||
|
||||
|
|
@ -242,7 +258,7 @@ export const migrationRunner = async ({
|
|||
duration,
|
||||
error: migrationError,
|
||||
};
|
||||
await storage.onError(finishedMigration, toSerializedError(migrationError));
|
||||
await onError(finishedMigration, migrationError);
|
||||
await reporter.onMigrationError?.(finishedMigration, migrationError);
|
||||
finishedMigrations.push(finishedMigration);
|
||||
skip = true;
|
||||
|
|
@ -252,15 +268,14 @@ export const migrationRunner = async ({
|
|||
status: 'done',
|
||||
duration,
|
||||
};
|
||||
await storage.onSuccess(finishedMigration);
|
||||
await onSuccess(finishedMigration);
|
||||
await reporter.onMigrationSuccess?.(finishedMigration);
|
||||
finishedMigrations.push(finishedMigration);
|
||||
}
|
||||
}
|
||||
|
||||
const [, unlockError] = dry
|
||||
? []
|
||||
: await exec(async () => storage.unlock(lockedMigrations ?? []), { abortSignal, abortRespite });
|
||||
const [, unlockError] =
|
||||
dry || !lock ? [] : await exec(async () => storage.unlock(lockedMigrations ?? []), { abortSignal, abortRespite });
|
||||
|
||||
// eslint-disable-next-line unicorn/no-array-callback-reference
|
||||
const firstFailed = finishedMigrations.find(isFailedMigration);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
} from '@emigrate/types';
|
||||
|
||||
type Status = ReturnType<typeof getMigrationStatus>;
|
||||
type Command = ReporterInitParameters['command'];
|
||||
|
||||
const interactive = isInteractive();
|
||||
const spinner = interactive ? elegantSpinner() : () => figures.pointerSmall;
|
||||
|
|
@ -30,11 +31,16 @@ const getTitle = ({ command, version, dry, cwd }: ReporterInitParameters) => {
|
|||
};
|
||||
|
||||
const getMigrationStatus = (
|
||||
command: Command,
|
||||
migration: MigrationMetadata | MigrationMetadataFinished,
|
||||
activeMigration?: MigrationMetadata,
|
||||
) => {
|
||||
if ('status' in migration) {
|
||||
return migration.status;
|
||||
return command === 'remove' && migration.status === 'done' ? 'removed' : migration.status;
|
||||
}
|
||||
|
||||
if (command === 'remove' && migration.name === activeMigration?.name) {
|
||||
return 'removing';
|
||||
}
|
||||
|
||||
return migration.name === activeMigration?.name ? 'running' : 'pending';
|
||||
|
|
@ -42,6 +48,10 @@ const getMigrationStatus = (
|
|||
|
||||
const getIcon = (status: Status) => {
|
||||
switch (status) {
|
||||
case 'removing': {
|
||||
return cyan(spinner());
|
||||
}
|
||||
|
||||
case 'running': {
|
||||
return cyan(spinner());
|
||||
}
|
||||
|
|
@ -50,6 +60,10 @@ const getIcon = (status: Status) => {
|
|||
return gray(figures.pointerSmall);
|
||||
}
|
||||
|
||||
case 'removed': {
|
||||
return green(figures.tick);
|
||||
}
|
||||
|
||||
case 'done': {
|
||||
return green(figures.tick);
|
||||
}
|
||||
|
|
@ -89,20 +103,19 @@ const getName = (name: string, status?: Status) => {
|
|||
};
|
||||
|
||||
const getMigrationText = (
|
||||
command: Command,
|
||||
migration: MigrationMetadata | MigrationMetadataFinished,
|
||||
activeMigration?: MigrationMetadata,
|
||||
) => {
|
||||
const pathWithoutName = migration.relativeFilePath.slice(0, -migration.name.length);
|
||||
const nameWithoutExtension = migration.name.slice(0, -migration.extension.length);
|
||||
const status = getMigrationStatus(migration, activeMigration);
|
||||
const status = getMigrationStatus(command, migration, activeMigration);
|
||||
const parts = [' ', getIcon(status)];
|
||||
|
||||
parts.push(`${dim(pathWithoutName)}${getName(nameWithoutExtension, status)}${dim(migration.extension)}`);
|
||||
|
||||
if ('status' in migration) {
|
||||
parts.push(gray`(${migration.status})`);
|
||||
} else if (migration.name === activeMigration?.name) {
|
||||
parts.push(gray`(running)`);
|
||||
if ('status' in migration || migration.name === activeMigration?.name) {
|
||||
parts.push(gray`(${status})`);
|
||||
}
|
||||
|
||||
if ('duration' in migration && migration.duration) {
|
||||
|
|
@ -319,19 +332,6 @@ class DefaultFancyReporter implements Required<EmigrateReporter> {
|
|||
this.#migrations = [migration];
|
||||
}
|
||||
|
||||
onMigrationRemoveStart(migration: MigrationMetadata): Awaitable<void> {
|
||||
this.#migrations = [migration];
|
||||
this.#activeMigration = migration;
|
||||
}
|
||||
|
||||
onMigrationRemoveSuccess(migration: MigrationMetadataFinished): Awaitable<void> {
|
||||
this.#finishMigration(migration);
|
||||
}
|
||||
|
||||
onMigrationRemoveError(migration: MigrationMetadataFinished, _error: Error): Awaitable<void> {
|
||||
this.#finishMigration(migration);
|
||||
}
|
||||
|
||||
onMigrationStart(migration: MigrationMetadata): void | PromiseLike<void> {
|
||||
this.#activeMigration = migration;
|
||||
}
|
||||
|
|
@ -376,7 +376,9 @@ class DefaultFancyReporter implements Required<EmigrateReporter> {
|
|||
const parts = [
|
||||
getTitle(this.#parameters),
|
||||
getHeaderMessage(this.#parameters.command, this.#migrations, this.#lockedMigrations),
|
||||
this.#migrations?.map((migration) => getMigrationText(migration, this.#activeMigration)).join('\n') ?? '',
|
||||
this.#migrations
|
||||
?.map((migration) => getMigrationText(this.#parameters.command, migration, this.#activeMigration))
|
||||
.join('\n') ?? '',
|
||||
getAbortMessage(this.#abortReason),
|
||||
getSummary(this.#parameters.command, this.#migrations),
|
||||
getError(this.#error),
|
||||
|
|
@ -441,35 +443,23 @@ class DefaultReporter implements Required<EmigrateReporter> {
|
|||
}
|
||||
|
||||
onNewMigration(migration: MigrationMetadata, _content: string): Awaitable<void> {
|
||||
console.log(getMigrationText(migration));
|
||||
}
|
||||
|
||||
onMigrationRemoveStart(migration: MigrationMetadata): Awaitable<void> {
|
||||
console.log(getMigrationText(migration));
|
||||
}
|
||||
|
||||
onMigrationRemoveSuccess(migration: MigrationMetadataFinished): Awaitable<void> {
|
||||
console.log(getMigrationText(migration));
|
||||
}
|
||||
|
||||
onMigrationRemoveError(migration: MigrationMetadataFinished, _error: Error): Awaitable<void> {
|
||||
console.error(getMigrationText(migration));
|
||||
console.log(getMigrationText(this.#parameters.command, migration));
|
||||
}
|
||||
|
||||
onMigrationStart(migration: MigrationMetadata): void | PromiseLike<void> {
|
||||
console.log(getMigrationText(migration, migration));
|
||||
console.log(getMigrationText(this.#parameters.command, migration, migration));
|
||||
}
|
||||
|
||||
onMigrationSuccess(migration: MigrationMetadataFinished): void | PromiseLike<void> {
|
||||
console.log(getMigrationText(migration));
|
||||
console.log(getMigrationText(this.#parameters.command, migration));
|
||||
}
|
||||
|
||||
onMigrationError(migration: MigrationMetadataFinished, _error: Error): void | PromiseLike<void> {
|
||||
console.error(getMigrationText(migration));
|
||||
console.error(getMigrationText(this.#parameters.command, migration));
|
||||
}
|
||||
|
||||
onMigrationSkip(migration: MigrationMetadataFinished): void | PromiseLike<void> {
|
||||
console.log(getMigrationText(migration));
|
||||
console.log(getMigrationText(this.#parameters.command, migration));
|
||||
}
|
||||
|
||||
onFinished(migrations: MigrationMetadataFinished[], error?: Error | undefined): void | PromiseLike<void> {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,69 @@
|
|||
import { mock, type Mock } from 'node:test';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
type SerializedError,
|
||||
type EmigrateReporter,
|
||||
type FailedMigrationHistoryEntry,
|
||||
type MigrationHistoryEntry,
|
||||
type MigrationMetadata,
|
||||
type NonFailedMigrationHistoryEntry,
|
||||
type Storage,
|
||||
} from '@emigrate/types';
|
||||
|
||||
export type Mocked<T> = {
|
||||
// @ts-expect-error - This is a mock
|
||||
[K in keyof T]: Mock<T[K]>;
|
||||
};
|
||||
|
||||
export async function noop() {
|
||||
// noop
|
||||
}
|
||||
|
||||
export function getErrorCause(error: Error | undefined): Error | SerializedError | undefined {
|
||||
if (error?.cause instanceof Error) {
|
||||
return error.cause;
|
||||
}
|
||||
|
||||
if (typeof error?.cause === 'object' && error.cause !== null) {
|
||||
return error.cause as unknown as SerializedError;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getMockedStorage(historyEntries: Array<string | MigrationHistoryEntry>) {
|
||||
const storage: Mocked<Storage> = {
|
||||
lock: mock.fn(async (migrations) => migrations),
|
||||
unlock: mock.fn(async () => {
|
||||
// void
|
||||
}),
|
||||
getHistory: mock.fn(async function* () {
|
||||
yield* toEntries(historyEntries);
|
||||
}),
|
||||
remove: mock.fn(),
|
||||
onSuccess: mock.fn(),
|
||||
onError: mock.fn(),
|
||||
end: mock.fn(),
|
||||
};
|
||||
|
||||
return storage;
|
||||
}
|
||||
|
||||
export function getMockedReporter(): Mocked<Required<EmigrateReporter>> {
|
||||
return {
|
||||
onFinished: mock.fn(noop),
|
||||
onInit: mock.fn(noop),
|
||||
onAbort: mock.fn(noop),
|
||||
onCollectedMigrations: mock.fn(noop),
|
||||
onLockedMigrations: mock.fn(noop),
|
||||
onNewMigration: mock.fn(noop),
|
||||
onMigrationStart: mock.fn(noop),
|
||||
onMigrationSuccess: mock.fn(noop),
|
||||
onMigrationError: mock.fn(noop),
|
||||
onMigrationSkip: mock.fn(noop),
|
||||
};
|
||||
}
|
||||
|
||||
export function toMigration(cwd: string, directory: string, name: string): MigrationMetadata {
|
||||
return {
|
||||
name,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue