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
.changeset/thin-pillows-obey.md
Normal file
5
.changeset/thin-pillows-obey.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@emigrate/cli': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Completely rework how the "remove" command is run, this is to make it more similar to the "up" and "list" command as now it will also use the `onMigrationStart`, `onMigrationSuccess` and `onMigrationError` reporter methods when reporting the command progress. It's also in preparation for adding `--from` and `--to` CLI options for the "remove" command, similar to how the same options work for the "up" command.
|
||||||
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
|
For example if you want to use Dotenv to load environment variables
|
||||||
-r, --reporter <name> The reporter to use for reporting the removal process
|
-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)
|
-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
|
-f, --force Force removal of the migration history entry even if the migration is not in a failed state
|
||||||
or it's in a non-failed state
|
|
||||||
--color Force color output (this option is passed to the reporter)
|
--color Force color output (this option is passed to the reporter)
|
||||||
--no-color Disable 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() {
|
async execute() {
|
||||||
throw new Error('Unexpected execute call');
|
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;
|
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 { getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools';
|
||||||
import { type MigrationHistoryEntry, type MigrationMetadataFinished } from '@emigrate/types';
|
import { type MigrationMetadata, isFinishedMigration } from '@emigrate/types';
|
||||||
import {
|
import {
|
||||||
BadOptionError,
|
BadOptionError,
|
||||||
MigrationNotRunError,
|
MigrationNotRunError,
|
||||||
|
MigrationRemovalError,
|
||||||
MissingArgumentsError,
|
MissingArgumentsError,
|
||||||
MissingOptionError,
|
MissingOptionError,
|
||||||
OptionNeededError,
|
OptionNeededError,
|
||||||
StorageInitError,
|
StorageInitError,
|
||||||
|
toError,
|
||||||
} from '../errors.js';
|
} from '../errors.js';
|
||||||
import { type Config } from '../types.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 { exec } from '../exec.js';
|
||||||
import { version } from '../get-package-info.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 = {
|
type ExtraFlags = {
|
||||||
cwd: string;
|
cwd: string;
|
||||||
force?: boolean;
|
force?: boolean;
|
||||||
|
getMigrations?: GetMigrationsFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type RemovableMigrationMetadata = MigrationMetadata & { originalStatus?: 'done' | 'failed' };
|
||||||
|
|
||||||
const lazyDefaultReporter = async () => import('../reporters/default.js');
|
const lazyDefaultReporter = async () => import('../reporters/default.js');
|
||||||
|
|
||||||
export default async function removeCommand(
|
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,
|
name: string,
|
||||||
) {
|
) {
|
||||||
if (!directory) {
|
if (!directory) {
|
||||||
|
|
@ -59,71 +74,79 @@ export default async function removeCommand(
|
||||||
return 1;
|
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) {
|
return removableMigration;
|
||||||
await reporter.onFinished?.([], fileError);
|
}
|
||||||
|
|
||||||
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;
|
return 1;
|
||||||
}
|
} finally {
|
||||||
|
|
||||||
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();
|
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 assert from 'node:assert';
|
||||||
import {
|
import {
|
||||||
type EmigrateReporter,
|
type EmigrateReporter,
|
||||||
type MigrationHistoryEntry,
|
|
||||||
type Storage,
|
type Storage,
|
||||||
type Plugin,
|
type Plugin,
|
||||||
type SerializedError,
|
type SerializedError,
|
||||||
|
|
@ -19,14 +18,16 @@ import {
|
||||||
StorageInitError,
|
StorageInitError,
|
||||||
toSerializedError,
|
toSerializedError,
|
||||||
} from '../errors.js';
|
} 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';
|
import upCommand from './up.js';
|
||||||
|
|
||||||
type Mocked<T> = {
|
|
||||||
// @ts-expect-error - This is a mock
|
|
||||||
[K in keyof T]: Mock<T[K]>;
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('up', () => {
|
describe('up', () => {
|
||||||
it("returns 1 and finishes with an error when the storage couldn't be initialized", async () => {
|
it("returns 1 and finishes with an error when the storage couldn't be initialized", async () => {
|
||||||
const { reporter, run } = getUpCommand(['some_migration.js']);
|
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 () => {
|
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 { reporter, run } = getUpCommand([], storage);
|
||||||
|
|
||||||
const exitCode = await run();
|
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 () => {
|
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 { reporter, run } = getUpCommand(['my_migration.js'], storage);
|
||||||
|
|
||||||
const exitCode = await run();
|
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 () => {
|
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 { reporter, run } = getUpCommand(['my_migration.js'], storage);
|
||||||
|
|
||||||
const exitCode = await run();
|
const exitCode = await run();
|
||||||
|
|
@ -71,7 +72,7 @@ describe('up', () => {
|
||||||
const migration = mock.fn(async () => {
|
const migration = mock.fn(async () => {
|
||||||
// Success
|
// Success
|
||||||
});
|
});
|
||||||
const storage = getStorage(['some_already_run_migration.js']);
|
const storage = getMockedStorage(['some_already_run_migration.js']);
|
||||||
const { reporter, run } = getUpCommand(
|
const { reporter, run } = getUpCommand(
|
||||||
['some_already_run_migration.js', 'some_migration.js', 'some_other_migration.js'],
|
['some_already_run_migration.js', 'some_migration.js', 'some_other_migration.js'],
|
||||||
storage,
|
storage,
|
||||||
|
|
@ -96,7 +97,7 @@ describe('up', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns 1 and finishes with an error when a pending migration throw when run', async () => {
|
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(
|
const { reporter, run } = getUpCommand(
|
||||||
['some_already_run_migration.js', 'some_migration.js', 'fail.js', 'some_other_migration.js'],
|
['some_already_run_migration.js', 'some_migration.js', 'fail.js', 'some_other_migration.js'],
|
||||||
storage,
|
storage,
|
||||||
|
|
@ -132,7 +133,7 @@ describe('up', () => {
|
||||||
|
|
||||||
describe('each migration file extension needs a corresponding loader plugin', () => {
|
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 () => {
|
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 { reporter, run } = getUpCommand(['some_other.js', 'some_file.sql'], storage);
|
||||||
|
|
||||||
const exitCode = await run();
|
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 () => {
|
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 { reporter, run } = getUpCommand(['some_other.js', 'some_file.sql'], storage);
|
||||||
|
|
||||||
const exitCode = await run({ dry: true });
|
const exitCode = await run({ dry: true });
|
||||||
|
|
@ -181,7 +182,7 @@ describe('up', () => {
|
||||||
describe('failed migrations in the history are blocking', () => {
|
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 () => {
|
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 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 { reporter, run } = getUpCommand([failedEntry.name, 'some_file.js'], storage);
|
||||||
|
|
||||||
const exitCode = await run();
|
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 () => {
|
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 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 { reporter, run } = getUpCommand([failedEntry.name, 'some_file.js'], storage);
|
||||||
|
|
||||||
const exitCode = await run({ dry: true });
|
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 () => {
|
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 failedEntry = toEntry('some_failed_migration.js', 'failed');
|
||||||
const storage = getStorage([failedEntry]);
|
const storage = getMockedStorage([failedEntry]);
|
||||||
const { reporter, run } = getUpCommand([], storage);
|
const { reporter, run } = getUpCommand([], storage);
|
||||||
|
|
||||||
const exitCode = await run();
|
const exitCode = await run();
|
||||||
|
|
@ -255,7 +256,7 @@ describe('up', () => {
|
||||||
const migration = mock.fn(async () => {
|
const migration = mock.fn(async () => {
|
||||||
// Success
|
// Success
|
||||||
});
|
});
|
||||||
const storage = getStorage(['some_already_run_migration.js']);
|
const storage = getMockedStorage(['some_already_run_migration.js']);
|
||||||
const { reporter, run } = getUpCommand(
|
const { reporter, run } = getUpCommand(
|
||||||
['some_already_run_migration.js', 'some_migration.js', 'some_other_migration.js'],
|
['some_already_run_migration.js', 'some_migration.js', 'some_other_migration.js'],
|
||||||
storage,
|
storage,
|
||||||
|
|
@ -281,7 +282,7 @@ describe('up', () => {
|
||||||
|
|
||||||
describe('limiting which pending migrations to run', () => {
|
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 () => {
|
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(
|
const { reporter, run } = getUpCommand(
|
||||||
['some_already_run_migration.js', 'some_migration.js', 'some_other_migration.js'],
|
['some_already_run_migration.js', 'some_migration.js', 'some_other_migration.js'],
|
||||||
storage,
|
storage,
|
||||||
|
|
@ -300,7 +301,7 @@ describe('up', () => {
|
||||||
const migration = mock.fn(async () => {
|
const migration = mock.fn(async () => {
|
||||||
// Success
|
// Success
|
||||||
});
|
});
|
||||||
const storage = getStorage(['1_some_already_run_migration.js']);
|
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
||||||
const { reporter, run } = getUpCommand(
|
const { reporter, run } = getUpCommand(
|
||||||
[
|
[
|
||||||
'1_some_already_run_migration.js',
|
'1_some_already_run_migration.js',
|
||||||
|
|
@ -334,7 +335,7 @@ describe('up', () => {
|
||||||
const migration = mock.fn(async () => {
|
const migration = mock.fn(async () => {
|
||||||
// Success
|
// Success
|
||||||
});
|
});
|
||||||
const storage = getStorage(['1_some_already_run_migration.js']);
|
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
||||||
const { reporter, run } = getUpCommand(
|
const { reporter, run } = getUpCommand(
|
||||||
[
|
[
|
||||||
'1_some_already_run_migration.js',
|
'1_some_already_run_migration.js',
|
||||||
|
|
@ -368,7 +369,7 @@ describe('up', () => {
|
||||||
const migration = mock.fn(async () => {
|
const migration = mock.fn(async () => {
|
||||||
// Success
|
// Success
|
||||||
});
|
});
|
||||||
const storage = getStorage(['1_some_already_run_migration.js']);
|
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
||||||
const { reporter, run } = getUpCommand(
|
const { reporter, run } = getUpCommand(
|
||||||
['1_some_already_run_migration.js', '2_some_migration.js', '4_some_other_migration.js'],
|
['1_some_already_run_migration.js', '2_some_migration.js', '4_some_other_migration.js'],
|
||||||
storage,
|
storage,
|
||||||
|
|
@ -396,7 +397,7 @@ describe('up', () => {
|
||||||
const migration = mock.fn(async () => {
|
const migration = mock.fn(async () => {
|
||||||
// Success
|
// Success
|
||||||
});
|
});
|
||||||
const storage = getStorage(['1_some_already_run_migration.js']);
|
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
||||||
const { reporter, run } = getUpCommand(
|
const { reporter, run } = getUpCommand(
|
||||||
['1_some_already_run_migration.js', '2_some_migration.js', '4_some_other_migration.js'],
|
['1_some_already_run_migration.js', '2_some_migration.js', '4_some_other_migration.js'],
|
||||||
storage,
|
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 () => {
|
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(
|
const { reporter, run } = getUpCommand(
|
||||||
['1_some_already_run_migration.js', '2_some_migration.js', '3_some_other_migration.js'],
|
['1_some_already_run_migration.js', '2_some_migration.js', '3_some_other_migration.js'],
|
||||||
storage,
|
storage,
|
||||||
|
|
@ -449,7 +450,7 @@ describe('up', () => {
|
||||||
const migration = mock.fn(async () => {
|
const migration = mock.fn(async () => {
|
||||||
// Success
|
// Success
|
||||||
});
|
});
|
||||||
const storage = getStorage(['1_some_already_run_migration.js']);
|
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
||||||
const { reporter, run } = getUpCommand(
|
const { reporter, run } = getUpCommand(
|
||||||
[
|
[
|
||||||
'1_some_already_run_migration.js',
|
'1_some_already_run_migration.js',
|
||||||
|
|
@ -483,7 +484,7 @@ describe('up', () => {
|
||||||
const migration = mock.fn(async () => {
|
const migration = mock.fn(async () => {
|
||||||
// Success
|
// Success
|
||||||
});
|
});
|
||||||
const storage = getStorage(['1_some_already_run_migration.js']);
|
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
||||||
const { reporter, run } = getUpCommand(
|
const { reporter, run } = getUpCommand(
|
||||||
['1_some_already_run_migration.js', '2_some_migration.js', '4_some_other_migration.js'],
|
['1_some_already_run_migration.js', '2_some_migration.js', '4_some_other_migration.js'],
|
||||||
storage,
|
storage,
|
||||||
|
|
@ -517,7 +518,7 @@ describe('up', () => {
|
||||||
const migration = mock.fn(async () => {
|
const migration = mock.fn(async () => {
|
||||||
// Success
|
// Success
|
||||||
});
|
});
|
||||||
const storage = getStorage(['1_some_already_run_migration.js']);
|
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
||||||
const { reporter, run } = getUpCommand(
|
const { reporter, run } = getUpCommand(
|
||||||
['1_some_already_run_migration.js', '2_some_migration.js', '4_some_other_migration.js'],
|
['1_some_already_run_migration.js', '2_some_migration.js', '4_some_other_migration.js'],
|
||||||
storage,
|
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 () => {
|
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(
|
const { reporter, run } = getUpCommand(
|
||||||
[
|
[
|
||||||
'1_some_already_run_migration.js',
|
'1_some_already_run_migration.js',
|
||||||
|
|
@ -567,7 +568,7 @@ describe('up', () => {
|
||||||
const migration = mock.fn(async () => {
|
const migration = mock.fn(async () => {
|
||||||
// Success
|
// Success
|
||||||
});
|
});
|
||||||
const storage = getStorage(['1_some_already_run_migration.js']);
|
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
||||||
const { reporter, run } = getUpCommand(
|
const { reporter, run } = getUpCommand(
|
||||||
[
|
[
|
||||||
'1_some_already_run_migration.js',
|
'1_some_already_run_migration.js',
|
||||||
|
|
@ -607,7 +608,7 @@ describe('up', () => {
|
||||||
const migration = mock.fn(async () => {
|
const migration = mock.fn(async () => {
|
||||||
// Success
|
// Success
|
||||||
});
|
});
|
||||||
const storage = getStorage(['1_some_already_run_migration.js']);
|
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
||||||
const { reporter, run } = getUpCommand(
|
const { reporter, run } = getUpCommand(
|
||||||
[
|
[
|
||||||
'1_some_already_run_migration.js',
|
'1_some_already_run_migration.js',
|
||||||
|
|
@ -651,7 +652,7 @@ describe('up', () => {
|
||||||
const migration = mock.fn(async () => {
|
const migration = mock.fn(async () => {
|
||||||
// Success
|
// Success
|
||||||
});
|
});
|
||||||
const storage = getStorage(['1_some_already_run_migration.js']);
|
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
||||||
const { reporter, run } = getUpCommand(
|
const { reporter, run } = getUpCommand(
|
||||||
[
|
[
|
||||||
'1_some_already_run_migration.js',
|
'1_some_already_run_migration.js',
|
||||||
|
|
@ -703,7 +704,7 @@ describe('up', () => {
|
||||||
},
|
},
|
||||||
{ times: 1 },
|
{ times: 1 },
|
||||||
);
|
);
|
||||||
const storage = getStorage(['1_some_already_run_migration.js']);
|
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
||||||
const { reporter, run } = getUpCommand(
|
const { reporter, run } = getUpCommand(
|
||||||
[
|
[
|
||||||
'1_some_already_run_migration.js',
|
'1_some_already_run_migration.js',
|
||||||
|
|
@ -762,7 +763,7 @@ describe('up', () => {
|
||||||
},
|
},
|
||||||
{ times: 1 },
|
{ times: 1 },
|
||||||
);
|
);
|
||||||
const storage = getStorage(['1_some_already_run_migration.js']);
|
const storage = getMockedStorage(['1_some_already_run_migration.js']);
|
||||||
const { reporter, run } = getUpCommand(
|
const { reporter, run } = getUpCommand(
|
||||||
[
|
[
|
||||||
'1_some_already_run_migration.js',
|
'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[]) {
|
function getUpCommand(migrationFiles: string[], storage?: Mocked<Storage>, plugins?: Plugin[]) {
|
||||||
const reporter: Mocked<Required<EmigrateReporter>> = {
|
const reporter = getMockedReporter();
|
||||||
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 run = async (
|
const run = async (
|
||||||
options?: Omit<
|
options?: Omit<
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { getOrLoadPlugins, getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools';
|
import { getOrLoadPlugins, getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools';
|
||||||
import { isFinishedMigration, type LoaderPlugin } from '@emigrate/types';
|
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 { type Config } from '../types.js';
|
||||||
import { withLeadingPeriod } from '../with-leading-period.js';
|
import { withLeadingPeriod } from '../with-leading-period.js';
|
||||||
import { type GetMigrationsFunction } from '../get-migrations.js';
|
import { type GetMigrationsFunction } from '../get-migrations.js';
|
||||||
|
|
@ -138,6 +145,12 @@ export default async function upCommand({
|
||||||
|
|
||||||
await migrationFunction();
|
await migrationFunction();
|
||||||
},
|
},
|
||||||
|
async onSuccess(migration) {
|
||||||
|
await storage.onSuccess(migration);
|
||||||
|
},
|
||||||
|
async onError(migration, error) {
|
||||||
|
await storage.onError(migration, toSerializedError(error));
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return error ? 1 : 0;
|
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 {
|
export class StorageInitError extends EmigrateError {
|
||||||
static fromError(error: Error) {
|
static fromError(error: Error) {
|
||||||
return new StorageInitError('Could not initialize storage', { cause: 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('MigrationLoadError', MigrationLoadError as unknown as ErrorConstructor);
|
||||||
errorConstructors.set('MigrationRunError', MigrationRunError as unknown as ErrorConstructor);
|
errorConstructors.set('MigrationRunError', MigrationRunError as unknown as ErrorConstructor);
|
||||||
errorConstructors.set('MigrationNotRunError', MigrationNotRunError 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('StorageInitError', StorageInitError as unknown as ErrorConstructor);
|
||||||
errorConstructors.set('CommandAbortError', CommandAbortError as unknown as ErrorConstructor);
|
errorConstructors.set('CommandAbortError', CommandAbortError as unknown as ErrorConstructor);
|
||||||
errorConstructors.set('ExecutionDesertedError', ExecutionDesertedError as unknown as ErrorConstructor);
|
errorConstructors.set('ExecutionDesertedError', ExecutionDesertedError as unknown as ErrorConstructor);
|
||||||
|
|
|
||||||
|
|
@ -9,28 +9,34 @@ import {
|
||||||
type FailedMigrationMetadata,
|
type FailedMigrationMetadata,
|
||||||
type SuccessfulMigrationMetadata,
|
type SuccessfulMigrationMetadata,
|
||||||
} from '@emigrate/types';
|
} 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 { exec } from './exec.js';
|
||||||
import { getDuration } from './get-duration.js';
|
import { getDuration } from './get-duration.js';
|
||||||
|
|
||||||
type MigrationRunnerParameters = {
|
type MigrationRunnerParameters<T extends MigrationMetadata | MigrationMetadataFinished> = {
|
||||||
dry: boolean;
|
dry: boolean;
|
||||||
|
lock?: boolean;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
name?: string;
|
||||||
from?: string;
|
from?: string;
|
||||||
to?: string;
|
to?: string;
|
||||||
abortSignal?: AbortSignal;
|
abortSignal?: AbortSignal;
|
||||||
abortRespite?: number;
|
abortRespite?: number;
|
||||||
reporter: EmigrateReporter;
|
reporter: EmigrateReporter;
|
||||||
storage: Storage;
|
storage: Storage;
|
||||||
migrations: AsyncIterable<MigrationMetadata | MigrationMetadataFinished>;
|
migrations: AsyncIterable<T>;
|
||||||
migrationFilter?: (migration: MigrationMetadata | MigrationMetadataFinished) => boolean;
|
migrationFilter?: (migration: T) => boolean;
|
||||||
validate: (migration: MigrationMetadata) => Promise<void>;
|
validate: (migration: T) => Promise<void>;
|
||||||
execute: (migration: MigrationMetadata) => 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,
|
dry,
|
||||||
|
lock = true,
|
||||||
limit,
|
limit,
|
||||||
|
name,
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
abortSignal,
|
abortSignal,
|
||||||
|
|
@ -40,8 +46,10 @@ export const migrationRunner = async ({
|
||||||
migrations,
|
migrations,
|
||||||
validate,
|
validate,
|
||||||
execute,
|
execute,
|
||||||
|
onSuccess,
|
||||||
|
onError,
|
||||||
migrationFilter = () => true,
|
migrationFilter = () => true,
|
||||||
}: MigrationRunnerParameters): Promise<Error | undefined> => {
|
}: MigrationRunnerParameters<T>): Promise<Error | undefined> => {
|
||||||
const collectedMigrations: Array<MigrationMetadata | MigrationMetadataFinished> = [];
|
const collectedMigrations: Array<MigrationMetadata | MigrationMetadataFinished> = [];
|
||||||
const validatedMigrations: Array<MigrationMetadata | MigrationMetadataFinished> = [];
|
const validatedMigrations: Array<MigrationMetadata | MigrationMetadataFinished> = [];
|
||||||
const migrationsToLock: MigrationMetadata[] = [];
|
const migrationsToLock: MigrationMetadata[] = [];
|
||||||
|
|
@ -64,10 +72,15 @@ export const migrationRunner = async ({
|
||||||
{ once: true },
|
{ once: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let nameFound = false;
|
||||||
let fromFound = false;
|
let fromFound = false;
|
||||||
let toFound = false;
|
let toFound = false;
|
||||||
|
|
||||||
for await (const migration of migrations) {
|
for await (const migration of migrations) {
|
||||||
|
if (name && migration.relativeFilePath === name) {
|
||||||
|
nameFound = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (from && migration.relativeFilePath === from) {
|
if (from && migration.relativeFilePath === from) {
|
||||||
fromFound = true;
|
fromFound = true;
|
||||||
}
|
}
|
||||||
|
|
@ -129,7 +142,9 @@ export const migrationRunner = async ({
|
||||||
|
|
||||||
let optionError: Error | undefined;
|
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`);
|
optionError = BadOptionError.fromOption('from', `The "from" migration: "${from}" was not found`);
|
||||||
} else if (to && !toFound) {
|
} else if (to && !toFound) {
|
||||||
optionError = BadOptionError.fromOption('to', `The "to" migration: "${to}" was not found`);
|
optionError = BadOptionError.fromOption('to', `The "to" migration: "${to}" was not found`);
|
||||||
|
|
@ -151,7 +166,8 @@ export const migrationRunner = async ({
|
||||||
migrationsToLock.length = 0;
|
migrationsToLock.length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [lockedMigrations, lockError] = dry
|
const [lockedMigrations, lockError] =
|
||||||
|
dry || !lock
|
||||||
? [migrationsToLock]
|
? [migrationsToLock]
|
||||||
: await exec(async () => storage.lock(migrationsToLock), { abortSignal, abortRespite });
|
: await exec(async () => storage.lock(migrationsToLock), { abortSignal, abortRespite });
|
||||||
|
|
||||||
|
|
@ -168,7 +184,7 @@ export const migrationRunner = async ({
|
||||||
migrationsToLock.length = 0;
|
migrationsToLock.length = 0;
|
||||||
|
|
||||||
skip = true;
|
skip = true;
|
||||||
} else {
|
} else if (lock) {
|
||||||
for (const migration of migrationsToLock) {
|
for (const migration of migrationsToLock) {
|
||||||
const isLocked = lockedMigrations.some((lockedMigration) => lockedMigration.name === migration.name);
|
const isLocked = lockedMigrations.some((lockedMigration) => lockedMigration.name === migration.name);
|
||||||
|
|
||||||
|
|
@ -231,7 +247,7 @@ export const migrationRunner = async ({
|
||||||
|
|
||||||
const start = hrtime();
|
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);
|
const duration = getDuration(start);
|
||||||
|
|
||||||
|
|
@ -242,7 +258,7 @@ export const migrationRunner = async ({
|
||||||
duration,
|
duration,
|
||||||
error: migrationError,
|
error: migrationError,
|
||||||
};
|
};
|
||||||
await storage.onError(finishedMigration, toSerializedError(migrationError));
|
await onError(finishedMigration, migrationError);
|
||||||
await reporter.onMigrationError?.(finishedMigration, migrationError);
|
await reporter.onMigrationError?.(finishedMigration, migrationError);
|
||||||
finishedMigrations.push(finishedMigration);
|
finishedMigrations.push(finishedMigration);
|
||||||
skip = true;
|
skip = true;
|
||||||
|
|
@ -252,15 +268,14 @@ export const migrationRunner = async ({
|
||||||
status: 'done',
|
status: 'done',
|
||||||
duration,
|
duration,
|
||||||
};
|
};
|
||||||
await storage.onSuccess(finishedMigration);
|
await onSuccess(finishedMigration);
|
||||||
await reporter.onMigrationSuccess?.(finishedMigration);
|
await reporter.onMigrationSuccess?.(finishedMigration);
|
||||||
finishedMigrations.push(finishedMigration);
|
finishedMigrations.push(finishedMigration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [, unlockError] = dry
|
const [, unlockError] =
|
||||||
? []
|
dry || !lock ? [] : await exec(async () => storage.unlock(lockedMigrations ?? []), { abortSignal, abortRespite });
|
||||||
: await exec(async () => storage.unlock(lockedMigrations ?? []), { abortSignal, abortRespite });
|
|
||||||
|
|
||||||
// eslint-disable-next-line unicorn/no-array-callback-reference
|
// eslint-disable-next-line unicorn/no-array-callback-reference
|
||||||
const firstFailed = finishedMigrations.find(isFailedMigration);
|
const firstFailed = finishedMigrations.find(isFailedMigration);
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
} from '@emigrate/types';
|
} from '@emigrate/types';
|
||||||
|
|
||||||
type Status = ReturnType<typeof getMigrationStatus>;
|
type Status = ReturnType<typeof getMigrationStatus>;
|
||||||
|
type Command = ReporterInitParameters['command'];
|
||||||
|
|
||||||
const interactive = isInteractive();
|
const interactive = isInteractive();
|
||||||
const spinner = interactive ? elegantSpinner() : () => figures.pointerSmall;
|
const spinner = interactive ? elegantSpinner() : () => figures.pointerSmall;
|
||||||
|
|
@ -30,11 +31,16 @@ const getTitle = ({ command, version, dry, cwd }: ReporterInitParameters) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMigrationStatus = (
|
const getMigrationStatus = (
|
||||||
|
command: Command,
|
||||||
migration: MigrationMetadata | MigrationMetadataFinished,
|
migration: MigrationMetadata | MigrationMetadataFinished,
|
||||||
activeMigration?: MigrationMetadata,
|
activeMigration?: MigrationMetadata,
|
||||||
) => {
|
) => {
|
||||||
if ('status' in migration) {
|
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';
|
return migration.name === activeMigration?.name ? 'running' : 'pending';
|
||||||
|
|
@ -42,6 +48,10 @@ const getMigrationStatus = (
|
||||||
|
|
||||||
const getIcon = (status: Status) => {
|
const getIcon = (status: Status) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
|
case 'removing': {
|
||||||
|
return cyan(spinner());
|
||||||
|
}
|
||||||
|
|
||||||
case 'running': {
|
case 'running': {
|
||||||
return cyan(spinner());
|
return cyan(spinner());
|
||||||
}
|
}
|
||||||
|
|
@ -50,6 +60,10 @@ const getIcon = (status: Status) => {
|
||||||
return gray(figures.pointerSmall);
|
return gray(figures.pointerSmall);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'removed': {
|
||||||
|
return green(figures.tick);
|
||||||
|
}
|
||||||
|
|
||||||
case 'done': {
|
case 'done': {
|
||||||
return green(figures.tick);
|
return green(figures.tick);
|
||||||
}
|
}
|
||||||
|
|
@ -89,20 +103,19 @@ const getName = (name: string, status?: Status) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMigrationText = (
|
const getMigrationText = (
|
||||||
|
command: Command,
|
||||||
migration: MigrationMetadata | MigrationMetadataFinished,
|
migration: MigrationMetadata | MigrationMetadataFinished,
|
||||||
activeMigration?: MigrationMetadata,
|
activeMigration?: MigrationMetadata,
|
||||||
) => {
|
) => {
|
||||||
const pathWithoutName = migration.relativeFilePath.slice(0, -migration.name.length);
|
const pathWithoutName = migration.relativeFilePath.slice(0, -migration.name.length);
|
||||||
const nameWithoutExtension = migration.name.slice(0, -migration.extension.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)];
|
const parts = [' ', getIcon(status)];
|
||||||
|
|
||||||
parts.push(`${dim(pathWithoutName)}${getName(nameWithoutExtension, status)}${dim(migration.extension)}`);
|
parts.push(`${dim(pathWithoutName)}${getName(nameWithoutExtension, status)}${dim(migration.extension)}`);
|
||||||
|
|
||||||
if ('status' in migration) {
|
if ('status' in migration || migration.name === activeMigration?.name) {
|
||||||
parts.push(gray`(${migration.status})`);
|
parts.push(gray`(${status})`);
|
||||||
} else if (migration.name === activeMigration?.name) {
|
|
||||||
parts.push(gray`(running)`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('duration' in migration && migration.duration) {
|
if ('duration' in migration && migration.duration) {
|
||||||
|
|
@ -319,19 +332,6 @@ class DefaultFancyReporter implements Required<EmigrateReporter> {
|
||||||
this.#migrations = [migration];
|
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> {
|
onMigrationStart(migration: MigrationMetadata): void | PromiseLike<void> {
|
||||||
this.#activeMigration = migration;
|
this.#activeMigration = migration;
|
||||||
}
|
}
|
||||||
|
|
@ -376,7 +376,9 @@ class DefaultFancyReporter implements Required<EmigrateReporter> {
|
||||||
const parts = [
|
const parts = [
|
||||||
getTitle(this.#parameters),
|
getTitle(this.#parameters),
|
||||||
getHeaderMessage(this.#parameters.command, this.#migrations, this.#lockedMigrations),
|
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),
|
getAbortMessage(this.#abortReason),
|
||||||
getSummary(this.#parameters.command, this.#migrations),
|
getSummary(this.#parameters.command, this.#migrations),
|
||||||
getError(this.#error),
|
getError(this.#error),
|
||||||
|
|
@ -441,35 +443,23 @@ class DefaultReporter implements Required<EmigrateReporter> {
|
||||||
}
|
}
|
||||||
|
|
||||||
onNewMigration(migration: MigrationMetadata, _content: string): Awaitable<void> {
|
onNewMigration(migration: MigrationMetadata, _content: string): Awaitable<void> {
|
||||||
console.log(getMigrationText(migration));
|
console.log(getMigrationText(this.#parameters.command, 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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMigrationStart(migration: MigrationMetadata): void | PromiseLike<void> {
|
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> {
|
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> {
|
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> {
|
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> {
|
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 path from 'node:path';
|
||||||
import {
|
import {
|
||||||
|
type SerializedError,
|
||||||
|
type EmigrateReporter,
|
||||||
type FailedMigrationHistoryEntry,
|
type FailedMigrationHistoryEntry,
|
||||||
type MigrationHistoryEntry,
|
type MigrationHistoryEntry,
|
||||||
type MigrationMetadata,
|
type MigrationMetadata,
|
||||||
type NonFailedMigrationHistoryEntry,
|
type NonFailedMigrationHistoryEntry,
|
||||||
|
type Storage,
|
||||||
} from '@emigrate/types';
|
} 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 {
|
export function toMigration(cwd: string, directory: string, name: string): MigrationMetadata {
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue