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,5 @@
---
'@emigrate/plugin-tools': minor
---
First version of the @emigrate/plugin-tools package which contains some nice to have utilities when building and using Emigrate plugins

View file

@ -0,0 +1,36 @@
{
"name": "@emigrate/plugin-tools",
"version": "0.0.0",
"publishConfig": {
"access": "public"
},
"description": "",
"main": "dist/index.js",
"types": "dist/index.d.js",
"type": "module",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./types": {
"import": "./dist/types.js",
"types": "./dist/types.d.ts"
}
},
"scripts": {
"build": "tsc --pretty",
"build:watch": "tsc --pretty --watch"
},
"keywords": [
"emigrate",
"plugin",
"migrations",
"types"
],
"author": "Aboviq AB <dev@aboviq.com> (https://www.aboviq.com)",
"license": "MIT",
"devDependencies": {
"@emigrate/tsconfig": "workspace:*"
}
}

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;

View file

@ -0,0 +1,8 @@
{
"extends": "@emigrate/tsconfig/build.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}