feat(plugin-tools): first version of the package with some nice plugin utilities

This commit is contained in:
Joakim Carlstein 2023-11-09 09:26:48 +01:00
parent cccdfb817d
commit cdafd05c20
5 changed files with 193 additions and 0 deletions

View file

@ -0,0 +1,56 @@
import { type GeneratorPlugin, type StoragePlugin } from './types.js';
export const createStoragePlugin = (initialize: StoragePlugin['initialize']): StoragePlugin => {
return {
type: 'storage',
initialize,
};
};
export const createGeneratorPlugin = (generate: GeneratorPlugin['generate']): GeneratorPlugin => {
return {
type: 'generator',
generate,
};
};
export const isGeneratorPlugin = (plugin: any): plugin is GeneratorPlugin => {
if (!plugin || typeof plugin !== 'object') {
return false;
}
if (plugin.type === 'generator') {
return typeof plugin.generate === 'function';
}
return false;
};
export const isStoragePlugin = (plugin: any): plugin is StoragePlugin => {
if (!plugin || typeof plugin !== 'object') {
return false;
}
if (plugin.type === 'storage') {
return typeof plugin.initialize === 'function';
}
return false;
};
/**
* Get a timestamp string in the format YYYYMMDDHHmmssmmm based on the current time (UTC)
*
* Can be used to prefix migration filenames so that they are executed in the correct order
*
* @returns A timestamp string in the format YYYYMMDDHHmmssmmm
*/
export const getTimestampPrefix = () => new Date().toISOString().replaceAll(/[-:ZT.]/g, '');
/**
* A utility function to sanitize a migration name so that it can be used as a filename
*
* @param name A migration name to sanitize
* @returns A sanitized migration name that can be used as a filename
*/
export const sanitizeMigrationName = (name: string) => name.replaceAll(/[\W/\\:|*?'"<>]/g, '_').trim();

View file

@ -0,0 +1,88 @@
export type MigrationStatus = 'failed' | 'done';
export type MigrationHistoryEntry = {
name: string;
status: MigrationStatus;
date: Date;
error?: unknown;
};
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: string[]): Promise<string[]>;
/**
* 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: string[]): 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: string): 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: string, error: Error): Promise<void>;
};
export type StoragePlugin = {
type: 'storage';
initialize(): Promise<Storage>;
};
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 = {
type: 'generator';
/**
* 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.
*/
generate(name: string): Promise<MigrationFile>;
};
export type Plugin = StoragePlugin | GeneratorPlugin;