feat(types): move Emigrate types to separate package and improve types (#41)
* feat(types): move Emigrate types to separate package Also refactor the types to use discriminating unions for easier error handling and such. Errors passed to storage plugins should now be serialized and storage plugins are expected to return already serialized errors on failed history entries. * fix(mysql): handle the new type changes * fix(storage-fs): handle the new type changes * feat(cli): better error handling and types Adapt to the new types from the @emigrate/types package, like discriminating union types and serializing and deserializing errors
This commit is contained in:
parent
afe56594c5
commit
cae6d11d53
38 changed files with 630 additions and 259 deletions
|
|
@ -44,13 +44,15 @@
|
|||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@emigrate/plugin-tools": "workspace:*",
|
||||
"@emigrate/types": "workspace:*",
|
||||
"ansis": "2.0.2",
|
||||
"cosmiconfig": "8.3.6",
|
||||
"elegant-spinner": "3.0.0",
|
||||
"figures": "6.0.1",
|
||||
"is-interactive": "2.0.0",
|
||||
"log-update": "6.0.0",
|
||||
"pretty-ms": "8.0.0"
|
||||
"pretty-ms": "8.0.0",
|
||||
"serialize-error": "11.0.3"
|
||||
},
|
||||
"volta": {
|
||||
"extends": "../../package.json"
|
||||
|
|
|
|||
|
|
@ -1,8 +1,4 @@
|
|||
import {
|
||||
type MigrationHistoryEntry,
|
||||
type MigrationMetadata,
|
||||
type MigrationMetadataFinished,
|
||||
} from '@emigrate/plugin-tools/types';
|
||||
import { type MigrationHistoryEntry, type MigrationMetadata, type MigrationMetadataFinished } from '@emigrate/types';
|
||||
import { toMigrationMetadata } from './to-migration-metadata.js';
|
||||
import { getMigrations as getMigrationsOriginal } from './get-migrations.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import process from 'node:process';
|
||||
import { getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools';
|
||||
import { BadOptionError, MissingOptionError, StorageInitError } from '../errors.js';
|
||||
import { BadOptionError, MissingOptionError, StorageInitError, toError } from '../errors.js';
|
||||
import { type Config } from '../types.js';
|
||||
import { exec } from '../exec.js';
|
||||
import { migrationRunner } from '../migration-runner.js';
|
||||
|
|
@ -12,20 +12,20 @@ const lazyDefaultReporter = async () => import('../reporters/default.js');
|
|||
|
||||
export default async function listCommand({ directory, reporter: reporterConfig, storage: storageConfig }: Config) {
|
||||
if (!directory) {
|
||||
throw new MissingOptionError('directory');
|
||||
throw MissingOptionError.fromOption('directory');
|
||||
}
|
||||
|
||||
const cwd = process.cwd();
|
||||
const storagePlugin = await getOrLoadStorage([storageConfig]);
|
||||
|
||||
if (!storagePlugin) {
|
||||
throw new BadOptionError('storage', 'No storage found, please specify a storage using the storage option');
|
||||
throw BadOptionError.fromOption('storage', 'No storage found, please specify a storage using the storage option');
|
||||
}
|
||||
|
||||
const reporter = await getOrLoadReporter([reporterConfig ?? lazyDefaultReporter]);
|
||||
|
||||
if (!reporter) {
|
||||
throw new BadOptionError(
|
||||
throw BadOptionError.fromOption(
|
||||
'reporter',
|
||||
'No reporter found, please specify an existing reporter using the reporter option',
|
||||
);
|
||||
|
|
@ -36,25 +36,33 @@ export default async function listCommand({ directory, reporter: reporterConfig,
|
|||
const [storage, storageError] = await exec(async () => storagePlugin.initializeStorage());
|
||||
|
||||
if (storageError) {
|
||||
await reporter.onFinished?.([], new StorageInitError('Could not initialize storage', { cause: storageError }));
|
||||
await reporter.onFinished?.([], StorageInitError.fromError(storageError));
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
const collectedMigrations = collectMigrations(cwd, directory, storage.getHistory());
|
||||
try {
|
||||
const collectedMigrations = collectMigrations(cwd, directory, storage.getHistory());
|
||||
|
||||
const error = await migrationRunner({
|
||||
dry: true,
|
||||
reporter,
|
||||
storage,
|
||||
migrations: await arrayFromAsync(collectedMigrations),
|
||||
async validate() {
|
||||
// No-op
|
||||
},
|
||||
async execute() {
|
||||
throw new Error('Unexpected execute call');
|
||||
},
|
||||
});
|
||||
const error = await migrationRunner({
|
||||
dry: true,
|
||||
reporter,
|
||||
storage,
|
||||
migrations: await arrayFromAsync(collectedMigrations),
|
||||
async validate() {
|
||||
// No-op
|
||||
},
|
||||
async execute() {
|
||||
throw new Error('Unexpected execute call');
|
||||
},
|
||||
});
|
||||
|
||||
return error ? 1 : 0;
|
||||
return error ? 1 : 0;
|
||||
} catch (error) {
|
||||
await reporter.onFinished?.([], toError(error));
|
||||
|
||||
return 1;
|
||||
} finally {
|
||||
await storage.end();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,19 @@ import process from 'node:process';
|
|||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { getTimestampPrefix, sanitizeMigrationName, getOrLoadPlugin, getOrLoadReporter } from '@emigrate/plugin-tools';
|
||||
import { type MigrationMetadata } from '@emigrate/plugin-tools/types';
|
||||
import { BadOptionError, MissingArgumentsError, MissingOptionError, UnexpectedError } from '../errors.js';
|
||||
import { type MigrationMetadataFinished, type MigrationMetadata, isFailedMigration } from '@emigrate/types';
|
||||
import {
|
||||
BadOptionError,
|
||||
EmigrateError,
|
||||
MissingArgumentsError,
|
||||
MissingOptionError,
|
||||
UnexpectedError,
|
||||
toError,
|
||||
} from '../errors.js';
|
||||
import { type Config } from '../types.js';
|
||||
import { withLeadingPeriod } from '../with-leading-period.js';
|
||||
import { version } from '../get-package-info.js';
|
||||
import { getDuration } from '../get-duration.js';
|
||||
|
||||
const lazyDefaultReporter = async () => import('../reporters/default.js');
|
||||
|
||||
|
|
@ -15,15 +23,15 @@ export default async function newCommand(
|
|||
name: string,
|
||||
) {
|
||||
if (!directory) {
|
||||
throw new MissingOptionError('directory');
|
||||
throw MissingOptionError.fromOption('directory');
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
throw new MissingArgumentsError('name');
|
||||
throw MissingArgumentsError.fromArgument('name');
|
||||
}
|
||||
|
||||
if (!extension && !template && plugins.length === 0) {
|
||||
throw new MissingOptionError(['extension', 'template', 'plugin']);
|
||||
throw MissingOptionError.fromOption(['extension', 'template', 'plugin']);
|
||||
}
|
||||
|
||||
const cwd = process.cwd();
|
||||
|
|
@ -31,7 +39,7 @@ export default async function newCommand(
|
|||
const reporter = await getOrLoadReporter([reporterConfig ?? lazyDefaultReporter]);
|
||||
|
||||
if (!reporter) {
|
||||
throw new BadOptionError(
|
||||
throw BadOptionError.fromOption(
|
||||
'reporter',
|
||||
'No reporter found, please specify an existing reporter using the reporter option',
|
||||
);
|
||||
|
|
@ -39,6 +47,8 @@ export default async function newCommand(
|
|||
|
||||
await reporter.onInit?.({ command: 'new', version, cwd, dry: false, directory });
|
||||
|
||||
const start = process.hrtime();
|
||||
|
||||
let filename: string | undefined;
|
||||
let content: string | undefined;
|
||||
|
||||
|
|
@ -82,7 +92,7 @@ export default async function newCommand(
|
|||
}
|
||||
|
||||
if (!filename || content === undefined) {
|
||||
throw new BadOptionError(
|
||||
throw BadOptionError.fromOption(
|
||||
'plugin',
|
||||
'No generator plugin found, please specify a generator plugin using the plugin option',
|
||||
);
|
||||
|
|
@ -102,19 +112,31 @@ export default async function newCommand(
|
|||
|
||||
await reporter.onNewMigration?.(migration, content);
|
||||
|
||||
let saveError: Error | undefined;
|
||||
const finishedMigrations: MigrationMetadataFinished[] = [];
|
||||
|
||||
try {
|
||||
await createDirectory(directoryPath);
|
||||
await saveFile(filePath, content);
|
||||
const duration = getDuration(start);
|
||||
finishedMigrations.push({ ...migration, status: 'done', duration });
|
||||
} catch (error) {
|
||||
saveError = error instanceof Error ? error : new Error(String(error));
|
||||
const duration = getDuration(start);
|
||||
const errorInstance = toError(error);
|
||||
finishedMigrations.push({ ...migration, status: 'failed', duration, error: errorInstance });
|
||||
}
|
||||
|
||||
await reporter.onFinished?.(
|
||||
[{ ...migration, status: saveError ? 'failed' : 'done', error: saveError, duration: 0 }],
|
||||
saveError,
|
||||
);
|
||||
// eslint-disable-next-line unicorn/no-array-callback-reference
|
||||
const firstFailed = finishedMigrations.find(isFailedMigration);
|
||||
const firstError =
|
||||
firstFailed?.error instanceof EmigrateError
|
||||
? firstFailed.error
|
||||
: firstFailed
|
||||
? new UnexpectedError(`Failed to create migration file: ${firstFailed.relativeFilePath}`, {
|
||||
cause: firstFailed?.error,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
await reporter.onFinished?.(finishedMigrations, firstError);
|
||||
}
|
||||
|
||||
async function createDirectory(directoryPath: string) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import process from 'node:process';
|
||||
import { getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools';
|
||||
import { type MigrationHistoryEntry, type MigrationMetadataFinished } from '@emigrate/plugin-tools/types';
|
||||
import { type MigrationHistoryEntry, type MigrationMetadataFinished } from '@emigrate/types';
|
||||
import {
|
||||
BadOptionError,
|
||||
MigrationNotRunError,
|
||||
|
|
@ -26,24 +26,24 @@ export default async function removeCommand(
|
|||
name: string,
|
||||
) {
|
||||
if (!directory) {
|
||||
throw new MissingOptionError('directory');
|
||||
throw MissingOptionError.fromOption('directory');
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
throw new MissingArgumentsError('name');
|
||||
throw MissingArgumentsError.fromArgument('name');
|
||||
}
|
||||
|
||||
const cwd = process.cwd();
|
||||
const storagePlugin = await getOrLoadStorage([storageConfig]);
|
||||
|
||||
if (!storagePlugin) {
|
||||
throw new BadOptionError('storage', 'No storage found, please specify a storage using the storage option');
|
||||
throw BadOptionError.fromOption('storage', 'No storage found, please specify a storage using the storage option');
|
||||
}
|
||||
|
||||
const reporter = await getOrLoadReporter([reporterConfig ?? lazyDefaultReporter]);
|
||||
|
||||
if (!reporter) {
|
||||
throw new BadOptionError(
|
||||
throw BadOptionError.fromOption(
|
||||
'reporter',
|
||||
'No reporter found, please specify an existing reporter using the reporter option',
|
||||
);
|
||||
|
|
@ -52,14 +52,22 @@ export default async function removeCommand(
|
|||
const [storage, storageError] = await exec(async () => storagePlugin.initializeStorage());
|
||||
|
||||
if (storageError) {
|
||||
await reporter.onFinished?.([], new StorageInitError('Could not initialize storage', { cause: storageError }));
|
||||
await reporter.onFinished?.([], StorageInitError.fromError(storageError));
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
await reporter.onInit?.({ command: 'remove', version, cwd, dry: false, directory });
|
||||
|
||||
const migrationFile = await getMigration(cwd, directory, name, !force);
|
||||
const [migrationFile, fileError] = await exec(async () => getMigration(cwd, directory, name, !force));
|
||||
|
||||
if (fileError) {
|
||||
await reporter.onFinished?.([], fileError);
|
||||
|
||||
await storage.end();
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
const finishedMigrations: MigrationMetadataFinished[] = [];
|
||||
let historyEntry: MigrationHistoryEntry | undefined;
|
||||
|
|
@ -71,7 +79,7 @@ export default async function removeCommand(
|
|||
}
|
||||
|
||||
if (migrationHistoryEntry.status === 'done' && !force) {
|
||||
removalError = new OptionNeededError(
|
||||
removalError = OptionNeededError.fromOption(
|
||||
'force',
|
||||
`The migration "${migrationFile.name}" is not in a failed state. Use the "force" option to force its removal`,
|
||||
);
|
||||
|
|
@ -98,10 +106,7 @@ export default async function removeCommand(
|
|||
removalError = error instanceof Error ? error : new Error(String(error));
|
||||
}
|
||||
} else if (!removalError) {
|
||||
removalError = new MigrationNotRunError(
|
||||
`Migration "${migrationFile.name}" is not in the migration history`,
|
||||
migrationFile,
|
||||
);
|
||||
removalError = MigrationNotRunError.fromMetadata(migrationFile);
|
||||
}
|
||||
|
||||
if (removalError) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { describe, it, mock, type Mock } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import path from 'node:path';
|
||||
import { serializeError } from '@emigrate/plugin-tools';
|
||||
import {
|
||||
type EmigrateReporter,
|
||||
type MigrationHistoryEntry,
|
||||
|
|
@ -9,7 +8,10 @@ import {
|
|||
type Storage,
|
||||
type Plugin,
|
||||
type SerializedError,
|
||||
} from '@emigrate/plugin-tools/types';
|
||||
type FailedMigrationHistoryEntry,
|
||||
type NonFailedMigrationHistoryEntry,
|
||||
} from '@emigrate/types';
|
||||
import { deserializeError } from 'serialize-error';
|
||||
import { version } from '../get-package-info.js';
|
||||
import upCommand from './up.js';
|
||||
|
||||
|
|
@ -117,7 +119,10 @@ describe('up', () => {
|
|||
assert.strictEqual(reporter.onMigrationStart.mock.calls.length, 0);
|
||||
assert.strictEqual(reporter.onMigrationSuccess.mock.calls.length, 0);
|
||||
assert.strictEqual(reporter.onMigrationError.mock.calls.length, 1);
|
||||
assert.strictEqual(getErrorCause(reporter.onMigrationError.mock.calls[0]?.arguments[1]), failedEntry.error);
|
||||
assert.deepStrictEqual(
|
||||
getErrorCause(reporter.onMigrationError.mock.calls[0]?.arguments[1]),
|
||||
deserializeError(failedEntry.error),
|
||||
);
|
||||
assert.strictEqual(reporter.onMigrationSkip.mock.calls.length, 1);
|
||||
assert.strictEqual(reporter.onFinished.mock.calls.length, 1);
|
||||
const [entries, error] = reporter.onFinished.mock.calls[0]?.arguments ?? [];
|
||||
|
|
@ -125,7 +130,7 @@ describe('up', () => {
|
|||
error?.message,
|
||||
`Migration ${failedEntry.name} is in a failed state, it should be fixed and removed`,
|
||||
);
|
||||
assert.strictEqual(getErrorCause(error), failedEntry.error);
|
||||
assert.deepStrictEqual(getErrorCause(error), deserializeError(failedEntry.error));
|
||||
assert.strictEqual(entries?.length, 2);
|
||||
assert.deepStrictEqual(
|
||||
entries.map((entry) => `${entry.name} (${entry.status})`),
|
||||
|
|
@ -155,7 +160,10 @@ describe('up', () => {
|
|||
assert.strictEqual(reporter.onMigrationStart.mock.calls.length, 0);
|
||||
assert.strictEqual(reporter.onMigrationSuccess.mock.calls.length, 0);
|
||||
assert.strictEqual(reporter.onMigrationError.mock.calls.length, 1);
|
||||
assert.strictEqual(getErrorCause(reporter.onMigrationError.mock.calls[0]?.arguments[1]), failedEntry.error);
|
||||
assert.deepStrictEqual(
|
||||
getErrorCause(reporter.onMigrationError.mock.calls[0]?.arguments[1]),
|
||||
deserializeError(failedEntry.error),
|
||||
);
|
||||
assert.strictEqual(reporter.onMigrationSkip.mock.calls.length, 1);
|
||||
assert.strictEqual(reporter.onFinished.mock.calls.length, 1);
|
||||
const [entries, error] = reporter.onFinished.mock.calls[0]?.arguments ?? [];
|
||||
|
|
@ -163,7 +171,7 @@ describe('up', () => {
|
|||
error?.message,
|
||||
`Migration ${failedEntry.name} is in a failed state, it should be fixed and removed`,
|
||||
);
|
||||
assert.strictEqual(getErrorCause(error), failedEntry.error);
|
||||
assert.deepStrictEqual(getErrorCause(error), deserializeError(failedEntry.error));
|
||||
assert.strictEqual(entries?.length, 2);
|
||||
assert.deepStrictEqual(
|
||||
entries.map((entry) => `${entry.name} (${entry.status})`),
|
||||
|
|
@ -354,27 +362,38 @@ function toMigrations(cwd: string, directory: string, names: string[]): Migratio
|
|||
return names.map((name) => toMigration(cwd, directory, name));
|
||||
}
|
||||
|
||||
function toEntry(
|
||||
name: string | MigrationHistoryEntry,
|
||||
status: MigrationHistoryEntry['status'] = 'done',
|
||||
): MigrationHistoryEntry {
|
||||
if (typeof name === 'string') {
|
||||
function toEntry(name: MigrationHistoryEntry): MigrationHistoryEntry;
|
||||
function toEntry<S extends MigrationHistoryEntry['status']>(
|
||||
name: string,
|
||||
status?: S,
|
||||
): S extends 'failed' ? FailedMigrationHistoryEntry : NonFailedMigrationHistoryEntry;
|
||||
|
||||
function toEntry(name: string | MigrationHistoryEntry, status?: 'done' | 'failed'): MigrationHistoryEntry {
|
||||
if (typeof name !== 'string') {
|
||||
return name.status === 'failed' ? name : name;
|
||||
}
|
||||
|
||||
if (status === 'failed') {
|
||||
return {
|
||||
name,
|
||||
status,
|
||||
date: new Date(),
|
||||
error: status === 'failed' ? serializeError(new Error('Failed')) : undefined,
|
||||
error: { name: 'Error', message: 'Failed' },
|
||||
};
|
||||
}
|
||||
|
||||
return name;
|
||||
return {
|
||||
name,
|
||||
status: status ?? 'done',
|
||||
date: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
function toEntries(
|
||||
names: Array<string | MigrationHistoryEntry>,
|
||||
status: MigrationHistoryEntry['status'] = 'done',
|
||||
status?: MigrationHistoryEntry['status'],
|
||||
): MigrationHistoryEntry[] {
|
||||
return names.map((name) => toEntry(name, status));
|
||||
return names.map((name) => (typeof name === 'string' ? toEntry(name, status) : name));
|
||||
}
|
||||
|
||||
async function noop() {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import process from 'node:process';
|
||||
import { getOrLoadPlugins, getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools';
|
||||
import { isFinishedMigration, type LoaderPlugin } from '@emigrate/plugin-tools/types';
|
||||
import { BadOptionError, MigrationLoadError, MissingOptionError, StorageInitError } from '../errors.js';
|
||||
import { isFinishedMigration, type LoaderPlugin } from '@emigrate/types';
|
||||
import { BadOptionError, MigrationLoadError, MissingOptionError, StorageInitError, toError } from '../errors.js';
|
||||
import { type Config } from '../types.js';
|
||||
import { withLeadingPeriod } from '../with-leading-period.js';
|
||||
import { type GetMigrationsFunction } from '../get-migrations.js';
|
||||
|
|
@ -31,19 +31,19 @@ export default async function upCommand({
|
|||
getMigrations,
|
||||
}: Config & ExtraFlags): Promise<number> {
|
||||
if (!directory) {
|
||||
throw new MissingOptionError('directory');
|
||||
throw MissingOptionError.fromOption('directory');
|
||||
}
|
||||
|
||||
const storagePlugin = await getOrLoadStorage([storageConfig]);
|
||||
|
||||
if (!storagePlugin) {
|
||||
throw new BadOptionError('storage', 'No storage found, please specify a storage using the storage option');
|
||||
throw BadOptionError.fromOption('storage', 'No storage found, please specify a storage using the storage option');
|
||||
}
|
||||
|
||||
const reporter = await getOrLoadReporter([reporterConfig ?? lazyDefaultReporter]);
|
||||
|
||||
if (!reporter) {
|
||||
throw new BadOptionError(
|
||||
throw BadOptionError.fromOption(
|
||||
'reporter',
|
||||
'No reporter found, please specify an existing reporter using the reporter option',
|
||||
);
|
||||
|
|
@ -54,57 +54,66 @@ export default async function upCommand({
|
|||
const [storage, storageError] = await exec(async () => storagePlugin.initializeStorage());
|
||||
|
||||
if (storageError) {
|
||||
await reporter.onFinished?.([], new StorageInitError('Could not initialize storage', { cause: storageError }));
|
||||
await reporter.onFinished?.([], StorageInitError.fromError(storageError));
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
const collectedMigrations = filterAsync(
|
||||
collectMigrations(cwd, directory, storage.getHistory(), getMigrations),
|
||||
(migration) => !isFinishedMigration(migration) || migration.status === 'failed',
|
||||
);
|
||||
try {
|
||||
const collectedMigrations = filterAsync(
|
||||
collectMigrations(cwd, directory, storage.getHistory(), getMigrations),
|
||||
(migration) => !isFinishedMigration(migration) || migration.status === 'failed',
|
||||
);
|
||||
|
||||
const loaderPlugins = await getOrLoadPlugins('loader', [lazyPluginLoaderJs, ...plugins]);
|
||||
const loaderPlugins = await getOrLoadPlugins('loader', [lazyPluginLoaderJs, ...plugins]);
|
||||
|
||||
const loaderByExtension = new Map<string, LoaderPlugin | undefined>();
|
||||
const loaderByExtension = new Map<string, LoaderPlugin | undefined>();
|
||||
|
||||
const getLoaderByExtension = (extension: string) => {
|
||||
if (!loaderByExtension.has(extension)) {
|
||||
const loader = loaderPlugins.find((plugin) =>
|
||||
plugin.loadableExtensions.some((loadableExtension) => withLeadingPeriod(loadableExtension) === extension),
|
||||
);
|
||||
const getLoaderByExtension = (extension: string) => {
|
||||
if (!loaderByExtension.has(extension)) {
|
||||
const loader = loaderPlugins.find((plugin) =>
|
||||
plugin.loadableExtensions.some((loadableExtension) => withLeadingPeriod(loadableExtension) === extension),
|
||||
);
|
||||
|
||||
loaderByExtension.set(extension, loader);
|
||||
}
|
||||
|
||||
return loaderByExtension.get(extension);
|
||||
};
|
||||
|
||||
const error = await migrationRunner({
|
||||
dry,
|
||||
reporter,
|
||||
storage,
|
||||
migrations: await arrayFromAsync(collectedMigrations),
|
||||
async validate(migration) {
|
||||
const loader = getLoaderByExtension(migration.extension);
|
||||
|
||||
if (!loader) {
|
||||
throw new BadOptionError('plugin', `No loader plugin found for file extension: ${migration.extension}`);
|
||||
}
|
||||
},
|
||||
async execute(migration) {
|
||||
const loader = getLoaderByExtension(migration.extension)!;
|
||||
const [migrationFunction, loadError] = await exec(async () => loader.loadMigration(migration));
|
||||
|
||||
if (loadError) {
|
||||
throw new MigrationLoadError(`Failed to load migration file: ${migration.relativeFilePath}`, migration, {
|
||||
cause: loadError,
|
||||
});
|
||||
loaderByExtension.set(extension, loader);
|
||||
}
|
||||
|
||||
await migrationFunction();
|
||||
},
|
||||
});
|
||||
return loaderByExtension.get(extension);
|
||||
};
|
||||
|
||||
return error ? 1 : 0;
|
||||
const error = await migrationRunner({
|
||||
dry,
|
||||
reporter,
|
||||
storage,
|
||||
migrations: await arrayFromAsync(collectedMigrations),
|
||||
async validate(migration) {
|
||||
const loader = getLoaderByExtension(migration.extension);
|
||||
|
||||
if (!loader) {
|
||||
throw BadOptionError.fromOption(
|
||||
'plugin',
|
||||
`No loader plugin found for file extension: ${migration.extension}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
async execute(migration) {
|
||||
const loader = getLoaderByExtension(migration.extension)!;
|
||||
const [migrationFunction, loadError] = await exec(async () => loader.loadMigration(migration));
|
||||
|
||||
if (loadError) {
|
||||
throw MigrationLoadError.fromMetadata(migration, loadError);
|
||||
}
|
||||
|
||||
await migrationFunction();
|
||||
},
|
||||
});
|
||||
|
||||
return error ? 1 : 0;
|
||||
} catch (error) {
|
||||
await reporter.onFinished?.([], toError(error));
|
||||
|
||||
return 1;
|
||||
} finally {
|
||||
await storage.end();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,26 @@
|
|||
import { type MigrationHistoryEntry, type MigrationMetadata } from '@emigrate/plugin-tools/types';
|
||||
import {
|
||||
type SerializedError,
|
||||
type MigrationMetadata,
|
||||
type FailedMigrationMetadata,
|
||||
type FailedMigrationHistoryEntry,
|
||||
} from '@emigrate/types';
|
||||
import { serializeError, errorConstructors, deserializeError } from 'serialize-error';
|
||||
|
||||
const formatter = new Intl.ListFormat('en', { style: 'long', type: 'disjunction' });
|
||||
|
||||
export const toError = (error: unknown) => (error instanceof Error ? error : new Error(String(error)));
|
||||
|
||||
export const toSerializedError = (error: unknown) => {
|
||||
const errorInstance = toError(error);
|
||||
|
||||
return serializeError(errorInstance) as unknown as SerializedError;
|
||||
};
|
||||
|
||||
export class EmigrateError extends Error {
|
||||
constructor(
|
||||
public code: string,
|
||||
message: string,
|
||||
message: string | undefined,
|
||||
options?: ErrorOptions,
|
||||
public code?: string,
|
||||
) {
|
||||
super(message, options);
|
||||
}
|
||||
|
|
@ -17,82 +29,132 @@ export class EmigrateError extends Error {
|
|||
export class ShowUsageError extends EmigrateError {}
|
||||
|
||||
export class MissingOptionError extends ShowUsageError {
|
||||
constructor(public option: string | string[]) {
|
||||
super('ERR_MISSING_OPT', `Missing required option: ${Array.isArray(option) ? formatter.format(option) : option}`);
|
||||
static fromOption(option: string | string[]) {
|
||||
return new MissingOptionError(
|
||||
`Missing required option: ${Array.isArray(option) ? formatter.format(option) : option}`,
|
||||
undefined,
|
||||
option,
|
||||
);
|
||||
}
|
||||
|
||||
constructor(
|
||||
message: string | undefined,
|
||||
options?: ErrorOptions,
|
||||
public option: string | string[] = '',
|
||||
) {
|
||||
super(message, options, 'ERR_MISSING_OPT');
|
||||
}
|
||||
}
|
||||
|
||||
export class MissingArgumentsError extends ShowUsageError {
|
||||
constructor(public argument: string) {
|
||||
super('ERR_MISSING_ARGS', `Missing required argument: ${argument}`);
|
||||
static fromArgument(argument: string) {
|
||||
return new MissingArgumentsError(`Missing required argument: ${argument}`, undefined, argument);
|
||||
}
|
||||
|
||||
constructor(
|
||||
message: string | undefined,
|
||||
options?: ErrorOptions,
|
||||
public argument = '',
|
||||
) {
|
||||
super(message, options, 'ERR_MISSING_ARGS');
|
||||
}
|
||||
}
|
||||
|
||||
export class OptionNeededError extends ShowUsageError {
|
||||
static fromOption(option: string, message: string) {
|
||||
return new OptionNeededError(message, undefined, option);
|
||||
}
|
||||
|
||||
constructor(
|
||||
public option: string,
|
||||
message: string,
|
||||
message: string | undefined,
|
||||
options?: ErrorOptions,
|
||||
public option = '',
|
||||
) {
|
||||
super('ERR_OPT_NEEDED', message);
|
||||
super(message, options, 'ERR_OPT_NEEDED');
|
||||
}
|
||||
}
|
||||
|
||||
export class BadOptionError extends ShowUsageError {
|
||||
static fromOption(option: string, message: string) {
|
||||
return new BadOptionError(message, undefined, option);
|
||||
}
|
||||
|
||||
constructor(
|
||||
public option: string,
|
||||
message: string,
|
||||
message: string | undefined,
|
||||
options?: ErrorOptions,
|
||||
public option = '',
|
||||
) {
|
||||
super('ERR_BAD_OPT', message);
|
||||
super(message, options, 'ERR_BAD_OPT');
|
||||
}
|
||||
}
|
||||
|
||||
export class UnexpectedError extends EmigrateError {
|
||||
constructor(message: string, options?: ErrorOptions) {
|
||||
super('ERR_UNEXPECTED', message, options);
|
||||
constructor(message: string | undefined, options?: ErrorOptions) {
|
||||
super(message, options, 'ERR_UNEXPECTED');
|
||||
}
|
||||
}
|
||||
|
||||
export class MigrationHistoryError extends EmigrateError {
|
||||
constructor(
|
||||
message: string,
|
||||
public entry: MigrationHistoryEntry,
|
||||
) {
|
||||
super('ERR_MIGRATION_HISTORY', message, { cause: entry.error });
|
||||
static fromHistoryEntry(entry: FailedMigrationHistoryEntry) {
|
||||
return new MigrationHistoryError(`Migration ${entry.name} is in a failed state, it should be fixed and removed`, {
|
||||
cause: deserializeError(entry.error),
|
||||
});
|
||||
}
|
||||
|
||||
constructor(message: string | undefined, options?: ErrorOptions) {
|
||||
super(message, options, 'ERR_MIGRATION_HISTORY');
|
||||
}
|
||||
}
|
||||
|
||||
export class MigrationLoadError extends EmigrateError {
|
||||
constructor(
|
||||
message: string,
|
||||
public metadata: MigrationMetadata,
|
||||
options?: ErrorOptions,
|
||||
) {
|
||||
super('ERR_MIGRATION_LOAD', message, options);
|
||||
static fromMetadata(metadata: MigrationMetadata, cause?: Error) {
|
||||
return new MigrationLoadError(`Failed to load migration file: ${metadata.relativeFilePath}`, { cause });
|
||||
}
|
||||
|
||||
constructor(message: string | undefined, options?: ErrorOptions) {
|
||||
super(message, options, 'ERR_MIGRATION_LOAD');
|
||||
}
|
||||
}
|
||||
|
||||
export class MigrationRunError extends EmigrateError {
|
||||
constructor(
|
||||
message: string,
|
||||
public metadata: MigrationMetadata,
|
||||
options?: ErrorOptions,
|
||||
) {
|
||||
super('ERR_MIGRATION_RUN', message, options);
|
||||
static fromMetadata(metadata: FailedMigrationMetadata) {
|
||||
return new MigrationRunError(`Failed to run migration: ${metadata.relativeFilePath}`, { cause: metadata.error });
|
||||
}
|
||||
|
||||
constructor(message: string | undefined, options?: ErrorOptions) {
|
||||
super(message, options, 'ERR_MIGRATION_RUN');
|
||||
}
|
||||
}
|
||||
|
||||
export class MigrationNotRunError extends EmigrateError {
|
||||
constructor(
|
||||
message: string,
|
||||
public metadata: MigrationMetadata,
|
||||
options?: ErrorOptions,
|
||||
) {
|
||||
super('ERR_MIGRATION_NOT_RUN', message, options);
|
||||
static fromMetadata(metadata: MigrationMetadata, cause?: Error) {
|
||||
return new MigrationNotRunError(`Migration "${metadata.name}" is not in the migration history`, { cause });
|
||||
}
|
||||
|
||||
constructor(message: string | undefined, options?: ErrorOptions) {
|
||||
super(message, options, 'ERR_MIGRATION_NOT_RUN');
|
||||
}
|
||||
}
|
||||
|
||||
export class StorageInitError extends EmigrateError {
|
||||
constructor(message: string, options?: ErrorOptions) {
|
||||
super('ERR_STORAGE_INIT', message, options);
|
||||
static fromError(error: Error) {
|
||||
return new StorageInitError('Could not initialize storage', { cause: error });
|
||||
}
|
||||
|
||||
constructor(message: string | undefined, options?: ErrorOptions) {
|
||||
super(message, options, 'ERR_STORAGE_INIT');
|
||||
}
|
||||
}
|
||||
|
||||
errorConstructors.set('EmigrateError', EmigrateError as ErrorConstructor);
|
||||
errorConstructors.set('ShowUsageError', ShowUsageError as ErrorConstructor);
|
||||
errorConstructors.set('MissingOptionError', MissingOptionError as unknown as ErrorConstructor);
|
||||
errorConstructors.set('MissingArgumentsError', MissingArgumentsError as unknown as ErrorConstructor);
|
||||
errorConstructors.set('OptionNeededError', OptionNeededError as unknown as ErrorConstructor);
|
||||
errorConstructors.set('BadOptionError', BadOptionError as unknown as ErrorConstructor);
|
||||
errorConstructors.set('UnexpectedError', UnexpectedError as ErrorConstructor);
|
||||
errorConstructors.set('MigrationHistoryError', MigrationHistoryError as unknown as ErrorConstructor);
|
||||
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('StorageInitError', StorageInitError as unknown as ErrorConstructor);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import path from 'node:path';
|
||||
import fs from 'node:fs/promises';
|
||||
import { type MigrationMetadata } from '@emigrate/plugin-tools/types';
|
||||
import { type MigrationMetadata } from '@emigrate/types';
|
||||
import { withLeadingPeriod } from './with-leading-period.js';
|
||||
import { OptionNeededError } from './errors.js';
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ const checkMigrationFile = async (name: string, filePath: string) => {
|
|||
throw new Error('Not a file');
|
||||
}
|
||||
} catch {
|
||||
throw new OptionNeededError(
|
||||
throw OptionNeededError.fromOption(
|
||||
'force',
|
||||
`The given migration name "${name}" does not exist or is not a file. Use the "force" option to ignore this error`,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,20 +1,32 @@
|
|||
import path from 'node:path';
|
||||
import fs from 'node:fs/promises';
|
||||
import { type MigrationMetadata } from '@emigrate/plugin-tools/types';
|
||||
import { type Dirent } from 'node:fs';
|
||||
import { type MigrationMetadata } from '@emigrate/types';
|
||||
import { withLeadingPeriod } from './with-leading-period.js';
|
||||
import { BadOptionError } from './errors.js';
|
||||
|
||||
export type GetMigrationsFunction = typeof getMigrations;
|
||||
|
||||
const tryReadDirectory = async (directoryPath: string): Promise<Dirent[]> => {
|
||||
try {
|
||||
return await fs.readdir(directoryPath, {
|
||||
withFileTypes: true,
|
||||
});
|
||||
} catch {
|
||||
throw BadOptionError.fromOption('directory', `Couldn't read directory: ${directoryPath}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const getMigrations = async (cwd: string, directory: string): Promise<MigrationMetadata[]> => {
|
||||
const allFilesInMigrationDirectory = await fs.readdir(path.resolve(cwd, directory), {
|
||||
withFileTypes: true,
|
||||
});
|
||||
const directoryPath = path.resolve(cwd, directory);
|
||||
|
||||
const allFilesInMigrationDirectory = await tryReadDirectory(directoryPath);
|
||||
|
||||
const migrationFiles: MigrationMetadata[] = allFilesInMigrationDirectory
|
||||
.filter((file) => file.isFile() && !file.name.startsWith('.') && !file.name.startsWith('_'))
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map(({ name }) => {
|
||||
const filePath = path.resolve(cwd, directory, name);
|
||||
const filePath = path.join(directoryPath, name);
|
||||
|
||||
return {
|
||||
name,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { UnexpectedError } from './errors.js';
|
||||
|
||||
type PackageInfo = {
|
||||
version: string;
|
||||
|
|
@ -24,7 +25,7 @@ const getPackageInfo = async () => {
|
|||
// ignore
|
||||
}
|
||||
|
||||
throw new Error(`Could not read package info from: ${packageInfoPath}`);
|
||||
throw new UnexpectedError(`Could not read package info from: ${packageInfoPath}`);
|
||||
};
|
||||
|
||||
export const { version } = await getPackageInfo();
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import process from 'node:process';
|
||||
import {
|
||||
isFinishedMigration,
|
||||
isFailedMigration,
|
||||
type EmigrateReporter,
|
||||
type MigrationMetadata,
|
||||
type MigrationMetadataFinished,
|
||||
type Storage,
|
||||
} from '@emigrate/plugin-tools/types';
|
||||
import { toError, EmigrateError, MigrationRunError } from './errors.js';
|
||||
type FailedMigrationMetadata,
|
||||
type SuccessfulMigrationMetadata,
|
||||
} from '@emigrate/types';
|
||||
import { toError, EmigrateError, MigrationRunError, toSerializedError } from './errors.js';
|
||||
import { exec } from './exec.js';
|
||||
import { getDuration } from './get-duration.js';
|
||||
|
||||
|
|
@ -43,7 +46,6 @@ export const migrationRunner = async ({
|
|||
finishedMigrations.push({
|
||||
...migration,
|
||||
status: dry ? 'pending' : 'skipped',
|
||||
duration: 0,
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
|
|
@ -51,7 +53,7 @@ export const migrationRunner = async ({
|
|||
migrationsToRun.push(migration);
|
||||
} catch (error) {
|
||||
for await (const migration of migrationsToRun) {
|
||||
finishedMigrations.push({ ...migration, status: 'skipped', duration: 0 });
|
||||
finishedMigrations.push({ ...migration, status: 'skipped' });
|
||||
}
|
||||
|
||||
migrationsToRun.length = 0;
|
||||
|
|
@ -72,7 +74,7 @@ export const migrationRunner = async ({
|
|||
|
||||
if (lockError) {
|
||||
for await (const migration of migrationsToRun) {
|
||||
finishedMigrations.push({ ...migration, duration: 0, status: 'skipped' });
|
||||
finishedMigrations.push({ ...migration, status: 'skipped' });
|
||||
}
|
||||
|
||||
migrationsToRun.length = 0;
|
||||
|
|
@ -85,7 +87,7 @@ export const migrationRunner = async ({
|
|||
for await (const finishedMigration of finishedMigrations) {
|
||||
switch (finishedMigration.status) {
|
||||
case 'failed': {
|
||||
await reporter.onMigrationError?.(finishedMigration, finishedMigration.error!);
|
||||
await reporter.onMigrationError?.(finishedMigration, finishedMigration.error);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +113,6 @@ export const migrationRunner = async ({
|
|||
const finishedMigration: MigrationMetadataFinished = {
|
||||
...migration,
|
||||
status: dry ? 'pending' : 'skipped',
|
||||
duration: 0,
|
||||
};
|
||||
|
||||
await reporter.onMigrationSkip?.(finishedMigration);
|
||||
|
|
@ -127,39 +128,43 @@ export const migrationRunner = async ({
|
|||
const [, migrationError] = await exec(async () => execute(migration));
|
||||
|
||||
const duration = getDuration(start);
|
||||
const finishedMigration: MigrationMetadataFinished = {
|
||||
...migration,
|
||||
status: migrationError ? 'failed' : 'done',
|
||||
duration,
|
||||
error: migrationError,
|
||||
};
|
||||
finishedMigrations.push(finishedMigration);
|
||||
|
||||
if (migrationError) {
|
||||
await storage.onError(finishedMigration, migrationError);
|
||||
const finishedMigration: FailedMigrationMetadata = {
|
||||
...migration,
|
||||
status: 'failed',
|
||||
duration,
|
||||
error: migrationError,
|
||||
};
|
||||
await storage.onError(finishedMigration, toSerializedError(migrationError));
|
||||
await reporter.onMigrationError?.(finishedMigration, migrationError);
|
||||
finishedMigrations.push(finishedMigration);
|
||||
skip = true;
|
||||
} else {
|
||||
const finishedMigration: SuccessfulMigrationMetadata = {
|
||||
...migration,
|
||||
status: 'done',
|
||||
duration,
|
||||
};
|
||||
await storage.onSuccess(finishedMigration);
|
||||
await reporter.onMigrationSuccess?.(finishedMigration);
|
||||
finishedMigrations.push(finishedMigration);
|
||||
}
|
||||
}
|
||||
|
||||
const [, unlockError] = dry ? [] : await exec(async () => storage.unlock(lockedMigrations ?? []));
|
||||
|
||||
const firstFailed = finishedMigrations.find((migration) => migration.status === 'failed');
|
||||
// eslint-disable-next-line unicorn/no-array-callback-reference
|
||||
const firstFailed = finishedMigrations.find(isFailedMigration);
|
||||
const firstError =
|
||||
firstFailed?.error instanceof EmigrateError
|
||||
? firstFailed.error
|
||||
: firstFailed
|
||||
? new MigrationRunError(`Failed to run migration: ${firstFailed.relativeFilePath}`, firstFailed, {
|
||||
cause: firstFailed?.error,
|
||||
})
|
||||
? MigrationRunError.fromMetadata(firstFailed)
|
||||
: undefined;
|
||||
const error = unlockError ?? firstError ?? lockError;
|
||||
|
||||
await reporter.onFinished?.(finishedMigrations, error);
|
||||
await storage.end();
|
||||
|
||||
return error;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { promisify } from 'node:util';
|
||||
import { type LoaderPlugin } from '@emigrate/plugin-tools/types';
|
||||
import { type LoaderPlugin } from '@emigrate/types';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
const promisifyIfNeeded = <T extends Function>(fn: T) => {
|
||||
|
|
|
|||
|
|
@ -10,8 +10,7 @@ import {
|
|||
type EmigrateReporter,
|
||||
type ReporterInitParameters,
|
||||
type Awaitable,
|
||||
} from '@emigrate/plugin-tools/types';
|
||||
import { EmigrateError } from '../errors.js';
|
||||
} from '@emigrate/types';
|
||||
|
||||
type Status = ReturnType<typeof getMigrationStatus>;
|
||||
|
||||
|
|
@ -147,15 +146,14 @@ const getError = (error?: ErrorLike, indent = ' ') => {
|
|||
others[property] = error[property as keyof ErrorLike];
|
||||
}
|
||||
|
||||
const codeString = typeof others['code'] === 'string' ? others['code'] : undefined;
|
||||
const codeString =
|
||||
typeof others['code'] === 'string' || typeof others['code'] === 'number' ? others['code'] : undefined;
|
||||
const code = codeString ? ` [${codeString}]` : '';
|
||||
|
||||
const errorTitle = error.name
|
||||
? `${error.name}${codeString && !error.name.includes(codeString) ? code : ''}: ${error.message}`
|
||||
: error.message;
|
||||
const errorTitle = error.name ? `${error.name}${code}: ${error.message}` : error.message;
|
||||
const parts = [`${indent}${bold.red(errorTitle)}`, ...stack.map((line) => `${indent} ${dim(line.trim())}`)];
|
||||
|
||||
if (properties.length > 0 && !(error instanceof EmigrateError)) {
|
||||
if (properties.length > 0) {
|
||||
parts.push(`${indent} ${JSON.stringify(others, undefined, 2).split('\n').join(`\n${indent} `)}`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import path from 'node:path';
|
||||
import { type MigrationHistoryEntry, type MigrationMetadataFinished } from '@emigrate/plugin-tools/types';
|
||||
import { type MigrationHistoryEntry, type MigrationMetadataFinished } from '@emigrate/types';
|
||||
import { withLeadingPeriod } from './with-leading-period.js';
|
||||
import { MigrationHistoryError } from './errors.js';
|
||||
|
||||
|
|
@ -8,7 +8,22 @@ export const toMigrationMetadata = (
|
|||
{ cwd, directory }: { cwd: string; directory: string },
|
||||
): MigrationMetadataFinished => {
|
||||
const filePath = path.resolve(cwd, directory, entry.name);
|
||||
const finishedMigration: MigrationMetadataFinished = {
|
||||
|
||||
if (entry.status === 'failed') {
|
||||
return {
|
||||
name: entry.name,
|
||||
status: entry.status,
|
||||
filePath,
|
||||
relativeFilePath: path.relative(cwd, filePath),
|
||||
extension: withLeadingPeriod(path.extname(entry.name)),
|
||||
directory,
|
||||
cwd,
|
||||
duration: 0,
|
||||
error: MigrationHistoryError.fromHistoryEntry(entry),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: entry.name,
|
||||
status: entry.status,
|
||||
filePath,
|
||||
|
|
@ -18,13 +33,4 @@ export const toMigrationMetadata = (
|
|||
cwd,
|
||||
duration: 0,
|
||||
};
|
||||
|
||||
if (entry.status === 'failed') {
|
||||
finishedMigration.error = new MigrationHistoryError(
|
||||
`Migration ${entry.name} is in a failed state, it should be fixed and removed`,
|
||||
entry,
|
||||
);
|
||||
}
|
||||
|
||||
return finishedMigration;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { type EmigrateStorage, type Awaitable, type Plugin, type EmigrateReporter } from '@emigrate/plugin-tools/types';
|
||||
import { type EmigrateStorage, type Awaitable, type Plugin, type EmigrateReporter } from '@emigrate/types';
|
||||
|
||||
export type EmigratePlugin = Plugin;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue