diff --git a/.changeset/olive-phones-heal.md b/.changeset/olive-phones-heal.md new file mode 100644 index 0000000..0c4474c --- /dev/null +++ b/.changeset/olive-phones-heal.md @@ -0,0 +1,5 @@ +--- +'@emigrate/plugin-storage-fs': minor +--- + +Implement a first version of the File System Storage plugin for simple migration setups diff --git a/packages/plugin-storage-fs/README.md b/packages/plugin-storage-fs/README.md new file mode 100644 index 0000000..ac3e9f3 --- /dev/null +++ b/packages/plugin-storage-fs/README.md @@ -0,0 +1,24 @@ +# @emigrate/plugin-storage-fs + +A file system storage plugin for Emigrate, suitable for simple migration setups. To support containerized environments, it is recommended to use a database storage plugin instead. + +## Installation + +Install the plugin in your project, alongside the Emigrate CLI: + +```bash +npm install --save-dev @emigrate/cli @emigrate/plugin-storage-fs +``` + +## Usage + +Configure the plugin in your `emigrate.config.js` file: + +```js +import storageFs from '@emigrate/plugin-storage-fs'; + +export default { + directory: 'migrations', + plugins: [storageFs({ filename: '.migrated.json' })], +}; +``` diff --git a/packages/plugin-storage-fs/package.json b/packages/plugin-storage-fs/package.json new file mode 100644 index 0000000..d1bfa52 --- /dev/null +++ b/packages/plugin-storage-fs/package.json @@ -0,0 +1,45 @@ +{ + "name": "@emigrate/plugin-storage-fs", + "version": "0.0.0", + "publishConfig": { + "access": "public" + }, + "description": "A storage plugin for Emigrate for storing the migration history in a file", + "main": "dist/index.js", + "types": "dist/index.d.js", + "type": "module", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc --pretty", + "build:watch": "tsc --pretty --watch" + }, + "keywords": [ + "emigrate", + "emigrate-plugin", + "plugin", + "migrations", + "storage" + ], + "author": "Aboviq AB (https://www.aboviq.com)", + "homepage": "https://github.com/aboviq/emigrate/tree/main/packages/plugin-storage-fs#readme", + "repository": "https://github.com/aboviq/emigrate/tree/main/packages/plugin-storage-fs", + "bugs": "https://github.com/aboviq/emigrate/issues", + "license": "MIT", + "dependencies": { + "@emigrate/plugin-tools": "workspace:*" + }, + "devDependencies": { + "@emigrate/tsconfig": "workspace:*" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/packages/plugin-storage-fs/src/index.ts b/packages/plugin-storage-fs/src/index.ts new file mode 100644 index 0000000..3790258 --- /dev/null +++ b/packages/plugin-storage-fs/src/index.ts @@ -0,0 +1,90 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import process from 'node:process'; +import { type StoragePlugin, type MigrationStatus } from '@emigrate/plugin-tools/types'; + +export type StorageFsOptions = { + filename: string; +}; + +type SerializedError = { + name: string; + message: string; + stack?: string; +}; + +export default function storageFs({ filename }: StorageFsOptions): StoragePlugin { + const filePath = path.resolve(process.cwd(), filename); + const lockFilePath = `${filePath}.lock`; + + const read = async (): Promise< + Record + > => { + try { + const contents = await fs.readFile(filePath, 'utf8'); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return JSON.parse(contents); + } catch { + return {}; + } + }; + + let lastUpdate: Promise = Promise.resolve(); + + const update = async (migration: string, status: MigrationStatus, error?: Error) => { + lastUpdate = lastUpdate.then(async () => { + const history = await read(); + + const newHistory = { + ...history, + [migration]: { + status, + date: new Date().toISOString(), + error: error ? { name: error.name, message: error.message, stack: error.stack } : undefined, + }, + }; + + await fs.writeFile(filePath, JSON.stringify(newHistory, undefined, 2)); + }); + + return lastUpdate; + }; + + return { + async initializeStorage() { + return { + async lock(migrations) { + const fd = await fs.open(lockFilePath, 'wx'); + + await fd.close(); + + return migrations; + }, + async unlock() { + try { + await fs.unlink(lockFilePath); + } catch { + // Ignore + } + }, + async *getHistory() { + const history = await read(); + + yield* Object.entries(history).map(([name, { status, date, error }]) => ({ + name, + status, + error, + date: new Date(date), + })); + }, + async onSuccess(migration) { + await update(migration, 'done'); + }, + async onError(migration, error) { + await update(migration, 'failed', error); + }, + }; + }, + }; +} diff --git a/packages/plugin-storage-fs/tsconfig.json b/packages/plugin-storage-fs/tsconfig.json new file mode 100644 index 0000000..1cfcebb --- /dev/null +++ b/packages/plugin-storage-fs/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@emigrate/tsconfig/build.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 17745ce..faad354 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,6 +77,16 @@ importers: specifier: workspace:* version: link:../tsconfig + packages/plugin-storage-fs: + dependencies: + '@emigrate/plugin-tools': + specifier: workspace:* + version: link:../plugin-tools + devDependencies: + '@emigrate/tsconfig': + specifier: workspace:* + version: link:../tsconfig + packages/plugin-tools: dependencies: import-from-esm: