From cdafd05c20c607e0c473c5ffc3f3800a0af0f1b1 Mon Sep 17 00:00:00 2001 From: Joakim Carlstein Date: Thu, 9 Nov 2023 09:26:48 +0100 Subject: [PATCH] feat(plugin-tools): first version of the package with some nice plugin utilities --- .changeset/proud-ducks-refuse.md | 5 ++ packages/plugin-tools/package.json | 36 ++++++++++++ packages/plugin-tools/src/index.ts | 56 ++++++++++++++++++ packages/plugin-tools/src/types.ts | 88 +++++++++++++++++++++++++++++ packages/plugin-tools/tsconfig.json | 8 +++ 5 files changed, 193 insertions(+) create mode 100644 .changeset/proud-ducks-refuse.md create mode 100644 packages/plugin-tools/package.json create mode 100644 packages/plugin-tools/src/index.ts create mode 100644 packages/plugin-tools/src/types.ts create mode 100644 packages/plugin-tools/tsconfig.json diff --git a/.changeset/proud-ducks-refuse.md b/.changeset/proud-ducks-refuse.md new file mode 100644 index 0000000..180281e --- /dev/null +++ b/.changeset/proud-ducks-refuse.md @@ -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 diff --git a/packages/plugin-tools/package.json b/packages/plugin-tools/package.json new file mode 100644 index 0000000..5cad51c --- /dev/null +++ b/packages/plugin-tools/package.json @@ -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 (https://www.aboviq.com)", + "license": "MIT", + "devDependencies": { + "@emigrate/tsconfig": "workspace:*" + } +} diff --git a/packages/plugin-tools/src/index.ts b/packages/plugin-tools/src/index.ts new file mode 100644 index 0000000..bd296cf --- /dev/null +++ b/packages/plugin-tools/src/index.ts @@ -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(); diff --git a/packages/plugin-tools/src/types.ts b/packages/plugin-tools/src/types.ts new file mode 100644 index 0000000..63e8087 --- /dev/null +++ b/packages/plugin-tools/src/types.ts @@ -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; + /** + * 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; + /** + * 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; + /** + * 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; + /** + * 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; +}; + +export type StoragePlugin = { + type: 'storage'; + initialize(): Promise; +}; + +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; +}; + +export type Plugin = StoragePlugin | GeneratorPlugin; diff --git a/packages/plugin-tools/tsconfig.json b/packages/plugin-tools/tsconfig.json new file mode 100644 index 0000000..1cfcebb --- /dev/null +++ b/packages/plugin-tools/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@emigrate/tsconfig/build.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +}