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:
Joakim Carlstein 2023-12-15 13:03:35 +01:00 committed by GitHub
parent afe56594c5
commit cae6d11d53
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 630 additions and 259 deletions

View file

@ -7,21 +7,7 @@ import {
type EmigrateStorage,
type LoaderPlugin,
type StringOrModule,
type SerializedError,
} from './types.js';
export const serializeError = (error: Error): SerializedError => {
const properties: Record<string, unknown> = {
name: error.name,
};
for (const key of Object.getOwnPropertyNames(error)) {
const value = error[key as keyof Error];
properties[key] = value instanceof Error ? serializeError(value) : value;
}
return properties as SerializedError;
};
} from '@emigrate/types';
export const isGeneratorPlugin = (plugin: any): plugin is GeneratorPlugin => {
if (!plugin || typeof plugin !== 'object') {

View file

@ -1,315 +0,0 @@
export type Awaitable<T> = T | PromiseLike<T>;
export type StringOrModule<T> = string | T | (() => Awaitable<T>) | (() => Awaitable<{ default: T }>);
export type MigrationStatus = 'failed' | 'done' | 'pending';
export type SerializedError = {
[key: string]: unknown;
name?: string;
message: string;
stack?: string;
cause?: unknown;
};
export type MigrationHistoryEntry = {
name: string;
status: MigrationStatus;
date: Date;
error?: SerializedError;
};
export type Storage = {
/**
* Acquire a lock on the given migrations.
*
* To best support concurrent migrations (e.g. when multiple services are deployed at the same time and want to migrate the same database)
* the plugin should try to lock all migrations at once (i.e. in a transaction) and ignore migrations that are already locked (or done).
* The successfully locked migrations should be returned and are the migrations that will be executed.
*
* If one of the migrations to lock is in a failed state, the plugin should throw an error to abort the migration attempt.
*
* @returns The migrations that were successfully locked.
*/
lock(migrations: MigrationMetadata[]): Promise<MigrationMetadata[]>;
/**
* The unlock method is called after all migrations have been executed or when the process is interrupted (e.g. by a SIGTERM or SIGINT signal).
*
* Depending on the plugin implementation, the unlock method is usually a no-op for already succeeded or failed migrations.
*
* @param migrations The previously successfully locked migrations that should now be unlocked.
*/
unlock(migrations: MigrationMetadata[]): Promise<void>;
/**
* Remove a migration from the history.
*
* This is used to remove a migration from the history which is needed for failed migrations to be re-executed.
*
* @param migration The migration that should be removed from the history.
*/
remove(migration: MigrationMetadata): Promise<void>;
/**
* Get the history of previously executed migrations.
*
* For failed migrations, the error property should be set.
* Emigrate will not sort the history entries, so the plugin should return the entries in the order they were executed.
* The order doesn't affect the execution of migrations, but it does affect the order in which the history is displayed in the CLI.
* Migrations that have not yet been executed will always be run in alphabetical order.
*
* The history has two purposes:
* 1. To determine which migrations have already been executed.
* 2. To list the migration history in the CLI.
*/
getHistory(): AsyncIterable<MigrationHistoryEntry>;
/**
* Called when a migration has been successfully executed.
*
* @param migration The name of the migration that should be marked as done.
*/
onSuccess(migration: MigrationMetadataFinished): Promise<void>;
/**
* Called when a migration has failed.
*
* @param migration The name of the migration that should be marked as failed.
* @param error The error that caused the migration to fail.
*/
onError(migration: MigrationMetadataFinished, error: Error): Promise<void>;
/**
* Called when the command is finished or aborted (e.g. by a SIGTERM or SIGINT signal).
*
* Use this to clean up any resources like database connections or file handles.
*/
end(): Promise<void>;
};
export type EmigrateStorage = {
initializeStorage(): Promise<Storage>;
};
export type InitializeStorageFunction = EmigrateStorage['initializeStorage'];
export type MigrationFile = {
/**
* The complete filename of the migration file, including the extension.
*
* Migrations that have not yet been executed will be run in alphabetical order, so preferably prefix the filename with a timestamp (and avoid unix timestamp and prefer something more human readable).
*/
filename: string;
/**
* The content of the migration file.
*/
content: string;
};
export type GeneratorPlugin = {
/**
* Used to generate a new migration file.
*
* @param name The name of the migration that should be generated (provided as arguments to the CLI)
* @returns The generated migration file.
*/
generateMigration(name: string): Promise<MigrationFile>;
};
export type GenerateMigrationFunction = GeneratorPlugin['generateMigration'];
export type MigrationFunction = () => Awaitable<void>;
export type MigrationMetadata = {
/**
* The name of the migration file
*
* @example 20210901123456000_create_users_table.js
*/
name: string;
/**
* The directory where the migration file is located, relative to the current working directory
*
* @example migrations
*/
directory: string;
/**
* The full absolute path to the migration file
*
* @example /home/user/project/migrations/20210901123456000_create_users_table.js
*/
filePath: string;
/**
* The relative path to the migration file, relative to the current working directory
*
* @example migrations/20210901123456000_create_users_table.js
*/
relativeFilePath: string;
/**
* The current working directory (the same as process.cwd())
*/
cwd: string;
/**
* The extension of the migration file, with a leading period
*
* @example .js
*/
extension: string;
};
export type MigrationMetadataFinished = MigrationMetadata & {
status: MigrationStatus | 'skipped';
duration: number;
error?: Error;
};
export const isFinishedMigration = (
migration: MigrationMetadata | MigrationMetadataFinished,
): migration is MigrationMetadataFinished => {
return 'status' in migration;
};
export type LoaderPlugin = {
/**
* The file extensions that this plugin can load.
*/
loadableExtensions: string[];
/**
* Used to load a migration file, i.e. transform it into a function that can be executed.
*
* @param migration Some metadata about the migration file that should be loaded.
* @returns A function that will execute the migration.
*/
loadMigration(migration: MigrationMetadata): Awaitable<MigrationFunction>;
};
export type ReporterInitParameters = {
/**
* The version of the emigrate CLI that is being used
*
* @example 1.0.0
*/
version: string;
/**
* The command that is being executed
*/
command: 'up' | 'new' | 'list' | 'remove';
/**
* The directory where the migration files are located
*/
directory: string;
/**
* The current working directory (the same as process.cwd())
*/
cwd: string;
/**
* Specifies whether the migration process is a dry run or not.
*
* Will only be true when the command is 'up' and the --dry option is specified.
*/
dry: boolean;
};
export type EmigrateReporter = Partial<{
/**
* Called when the reporter is initialized, which is the first method that is called when a command is executed.
*/
onInit(parameters: ReporterInitParameters): Awaitable<void>;
/**
* Called when all pending migrations that should be executed have been collected.
*
* @param migrations The pending migrations that will be executed.
*/
onCollectedMigrations(migrations: MigrationMetadata[]): Awaitable<void>;
/**
* Called when the migrations have been successfully locked.
*
* Usually the migrations passed to this method are the same as the migrations passed to the onCollectedMigrations method,
* but in case of a concurrent migration attempt, some or all migrations might already be locked by another process.
*
* @param migrations The migrations that have been successfully locked so they can be executed.
*/
onLockedMigrations(migrations: MigrationMetadata[]): Awaitable<void>;
/**
* Called when a new migration file has been generated.
*
* This is only called when the command is 'new'.
*/
onNewMigration(migration: MigrationMetadata, content: string): Awaitable<void>;
/**
* Called when a migration is about to be removed from the migration history.
*
* This is only called when the command is 'remove'.
*/
onMigrationRemoveStart(migration: MigrationMetadata): Awaitable<void>;
/**
* Called when a migration is successfully removed from the migration history.
*
* This is only called when the command is 'remove'.
*/
onMigrationRemoveSuccess(migration: MigrationMetadataFinished): Awaitable<void>;
/**
* Called when a migration couldn't be removed from the migration history.
*
* This is only called when the command is 'remove'.
*/
onMigrationRemoveError(migration: MigrationMetadataFinished, error: Error): Awaitable<void>;
/**
* Called when a migration is about to be executed.
*
* Will only be called for each migration when the command is "up".
*
* @param migration Information about the migration that is about to be executed.
*/
onMigrationStart(migration: MigrationMetadata): Awaitable<void>;
/**
* Called when a migration has been successfully executed.
*
* Will be called after a successful migration when the command is "up"
* or for each successful migration from the history when the command is "list".
*
* @param migration Information about the migration that was executed.
*/
onMigrationSuccess(migration: MigrationMetadataFinished): Awaitable<void>;
/**
* Called when a migration has failed.
*
* Will be called after a failed migration when the command is "up"
* or for each failed migration from the history when the command is "list" (will be at most one in this case).
*
* @param migration Information about the migration that failed.
* @param error The error that caused the migration to fail.
*/
onMigrationError(migration: MigrationMetadataFinished, error: Error): Awaitable<void>;
/**
* Called when a migration is skipped
*
* Will be called when a migration is skipped because a previous migration failed,
* it couldn't be successfully locked, or in case of a dry run when the command is "up".
* When the command is "list" this will be called for each pending migration (i.e. those that have not run yet).
* When the command is "remove" this will be called when the removal of some migrations are skipped
* because the removal of a previous migration failed.
*
* @param migration Information about the migration that was skipped.
*/
onMigrationSkip(migration: MigrationMetadataFinished): Awaitable<void>;
/**
* Called as a final step after all migrations have been executed, listed or removed.
*
* This is called either after all migrations have been listed successfully for the "list" command
* or for the "up" command when they are executed successfully, at the end of a dry run, or when a migration has failed.
* It is also called after migrations have been removed from the history with the "remove" command.
* It is also called after a migration file has been generated with the "new" command.
*
* @param migrations Information about all migrations that were executed or listed, their status and any error that occurred.
* @param error If the migration process failed, this will be the error that caused the failure.
*/
onFinished(migrations: MigrationMetadataFinished[], error?: Error): Awaitable<void>;
}>;
export type Plugin = GeneratorPlugin | LoaderPlugin;
type PluginTypeMap = {
generator: GeneratorPlugin;
loader: LoaderPlugin;
};
export type PluginType = keyof PluginTypeMap;
export type PluginFromType<T extends PluginType> = PluginTypeMap[T];