diff --git a/.changeset/hip-camels-learn.md b/.changeset/hip-camels-learn.md new file mode 100644 index 0000000..ca95dbf --- /dev/null +++ b/.changeset/hip-camels-learn.md @@ -0,0 +1,5 @@ +--- +'@emigrate/postgres': minor +--- + +Implement the first version of the @emigrate/postgres plugin diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 00937fe..6d0a145 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -117,6 +117,10 @@ export default defineConfig({ label: 'File System', link: '/plugins/storage/file-system/', }, + { + label: 'PostgreSQL', + link: '/plugins/storage/postgres/', + }, { label: 'MySQL', link: '/plugins/storage/mysql/', @@ -135,6 +139,10 @@ export default defineConfig({ label: 'Default Loader', link: '/plugins/loaders/default/', }, + { + label: 'PostgreSQL Loader', + link: '/plugins/loaders/postgres/', + }, { label: 'MySQL Loader', link: '/plugins/loaders/mysql/', @@ -171,6 +179,10 @@ export default defineConfig({ label: 'JavaScript Generator', link: '/plugins/generators/js/', }, + { + label: 'PostgreSQL Generator', + link: '/plugins/generators/postgres/', + }, { label: 'MySQL Generator', link: '/plugins/generators/mysql/', diff --git a/docs/src/content/docs/intro/quick-start.mdx b/docs/src/content/docs/intro/quick-start.mdx index 5017867..76cf108 100644 --- a/docs/src/content/docs/intro/quick-start.mdx +++ b/docs/src/content/docs/intro/quick-start.mdx @@ -41,22 +41,22 @@ But for now, this is the way to go. Emigrate uses a storage plugin to store the migration history. -Install the plugin you want to use, for example: +Install the plugin you want to use, for example the PostgreSQL Storage: ```bash - npm install @emigrate/mysql + npm install @emigrate/postgres ``` ```bash - pnpm add @emigrate/mysql + pnpm add @emigrate/postgres ``` ```bash - yarn add @emigrate/mysql + yarn add @emigrate/postgres ``` @@ -66,7 +66,7 @@ Install the plugin you want to use, for example: Create a new migration file in your project using: ```bash title="Create a new migration file" -npx emigrate new --plugin mysql create users table +npx emigrate new --plugin postgres create users table ``` ```txt title="Output" @@ -79,12 +79,12 @@ Emigrate new v0.10.0 /your/project/path ``` :::note -The `mysql` plugin is used here to generate a migration file with the `.sql` extension. +The `postgres` plugin is used here to generate a migration file with the `.sql` extension. Otherwise the file would have the `.js` extension by default. ::: :::tip[Did you know?] -You can avoid typing `--plugin mysql` by configuring Emigrate using an `emigrate.config.js` file. +You can avoid typing `--plugin postgres` by configuring Emigrate using an `emigrate.config.js` file. See Configuration for more information. ::: @@ -95,10 +95,9 @@ Open the migration file in your editor and fill it with your SQL query: ```sql title="migrations/20231215125421364_create_users_table.sql" {2-7} -- Migration: create users table CREATE TABLE users ( - id INT NOT NULL AUTO_INCREMENT, + id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, - email VARCHAR(255) NOT NULL, - PRIMARY KEY (id) + email VARCHAR(255) NOT NULL ); ``` @@ -111,7 +110,7 @@ There's no magic about the first line comment as when using Liquibase, it's just To show both pending and already applied migrations (or previously failed), use the `list` command: ```bash title="Show all migrations" -npx emigrate list --storage mysql +npx emigrate list --storage postgres ``` ```txt title="Example output" @@ -129,7 +128,7 @@ Emigrate list v0.10.0 /your/project/path A good way to test your configuration is to run the migrations in dry mode: ```bash title="Show pending migrations" -npx emigrate up --storage mysql --plugin mysql --dry +npx emigrate up --storage postgres --plugin postgres --dry ``` :::note @@ -142,6 +141,6 @@ Be sure to configure the connection correctly and use the `--dry` flag to test y ::: :::tip[Did you know?] -In the example above the `@emigrate/mysql` plugin is used twice, once for the `--storage` option as a Storage Plugin +In the example above the `@emigrate/postgres` plugin is used twice, once for the `--storage` option as a Storage Plugin and once for the `--plugin` option as a Loader Plugin to be able to read `.sql` files. ::: diff --git a/docs/src/content/docs/plugins/generators/index.mdx b/docs/src/content/docs/plugins/generators/index.mdx index 3d737ca..de8c0da 100644 --- a/docs/src/content/docs/plugins/generators/index.mdx +++ b/docs/src/content/docs/plugins/generators/index.mdx @@ -17,6 +17,7 @@ The generator is responsible for generating migration files in a specific format + diff --git a/docs/src/content/docs/plugins/generators/postgres.mdx b/docs/src/content/docs/plugins/generators/postgres.mdx new file mode 100644 index 0000000..ef7686d --- /dev/null +++ b/docs/src/content/docs/plugins/generators/postgres.mdx @@ -0,0 +1,36 @@ +--- +title: "PostgreSQL Generator" +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; +import Link from '@components/Link.astro'; + +The PostgreSQL generator creates new migration files with the `.sql` extension. In the same package you can find the PostgreSQL Loader and the PostgreSQL Storage. + +## Installation + + + + ```bash + npm install @emigrate/postgres + ``` + + + ```bash + pnpm add @emigrate/postgres + ``` + + + ```bash + yarn add @emigrate/postgres + ``` + + + +## Usage + +```bash +emigrate new --plugin postgres create some fancy table +``` + +For more information see the `new` command's documentation. diff --git a/docs/src/content/docs/plugins/loaders/index.mdx b/docs/src/content/docs/plugins/loaders/index.mdx index 288393a..bca094b 100644 --- a/docs/src/content/docs/plugins/loaders/index.mdx +++ b/docs/src/content/docs/plugins/loaders/index.mdx @@ -21,7 +21,7 @@ Or set it up in your configuration file, see default loader will be used for all other file types, and doesn't need to be specified. ::: @@ -29,5 +29,6 @@ The default loader will be used fo + diff --git a/docs/src/content/docs/plugins/loaders/postgres.mdx b/docs/src/content/docs/plugins/loaders/postgres.mdx new file mode 100644 index 0000000..38ec8ac --- /dev/null +++ b/docs/src/content/docs/plugins/loaders/postgres.mdx @@ -0,0 +1,86 @@ +--- +title: PostgreSQL Loader Plugin +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; +import Link from '@components/Link.astro'; + +The PostgreSQL loader plugin transforms `.sql` files into JavaScript functions that Emigrate can use to execute the migrations. In the same package you can find the PostgreSQL Generator and the PostgreSQL Storage. + +## Installation + + + + ```bash + npm install @emigrate/postgres + ``` + + + ```bash + pnpm add @emigrate/postgres + ``` + + + ```bash + yarn add @emigrate/postgres + ``` + + + +## Configuration + +The PostgreSQL loader plugin can be configured either using environment variables or by configuring the plugin directly in the `emigrate.config.js` file. + +### Configuration file + +```js title="emigrate.config.js" {1,4-8} +import { createPostgresLoader } from '@emigrate/postgres'; + +export default { + plugins: [ + createPostgresLoader({ + connection: { ... }, + }), + ], +}; +``` + +#### Options + +##### `connection` (required) + +**type:** `object | string` + +The connection options to use for connecting to the PostgreSQL database when the SQL statements from the migration files are executed. This can either be a connection URI or an object with connection options. +For a list of supported connection options, see the [postgres documentation](https://github.com/porsager/postgres#connection). + +### Environment variables + +The following environment variables are supported: + +| Variable | Description | Default | +| ------------------- | ----------------------------------------------------------------------------------------------------------- | ------------- | +| `POSTGRES_URL` | The full URI for connecting to a PostgreSQL database, e.g: `"postgres://user:pass@127.0.0.1:3306/database"` | | +| `POSTGRES_HOST` | The host on which the PostgreSQL server instance is running | `"localhost"` | +| `POSTGRES_USER` | The PostgreSQL user account to use for the authentication | | +| `POSTGRES_PASSWORD` | The PostgreSQL user password to use for the authentication | | +| `POSTGRES_PORT` | The network port on which the PostgreSQL server is listening | `5432` | +| `POSTGRES_DB` | The PostgreSQL database to use for the connection | | + +:::note +The `POSTGRES_URL` environment variable takes precedence over the other environment variables. If `POSTGRES_URL` is set, the other environment variables are ignored. +::: + +The environment variables are used when the plugin is used using the `--plugin` command line option: + +```bash +npx emigrate list --plugin postgres +``` + +Or when specifying the plugin in the `emigrate.config.js` file as a string: + +```js title="emigrate.config.js" {2} +export default { + plugins: ['postgres'], +}; +``` diff --git a/docs/src/content/docs/plugins/storage/index.mdx b/docs/src/content/docs/plugins/storage/index.mdx index 4188084..319969c 100644 --- a/docs/src/content/docs/plugins/storage/index.mdx +++ b/docs/src/content/docs/plugins/storage/index.mdx @@ -13,7 +13,7 @@ Usually you'll want to store the migration history in the same database as the o You can specify a storage plugin via the `--storage` (or `-s` for short) option: ```bash -npx emigrate list --storage mysql +npx emigrate list --storage postgres ``` Or set it up in your configuration file, see Storage configuration for more information. @@ -22,6 +22,7 @@ Or set it up in your configuration file, see + diff --git a/docs/src/content/docs/plugins/storage/postgres.mdx b/docs/src/content/docs/plugins/storage/postgres.mdx new file mode 100644 index 0000000..368e894 --- /dev/null +++ b/docs/src/content/docs/plugins/storage/postgres.mdx @@ -0,0 +1,93 @@ +--- +title: PostgreSQL Storage +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; +import Link from '@components/Link.astro'; + +The PostgreSQL storage plugin uses a PostgreSQL database to store the migration history (*duh*). In the same package you can find the PostgreSQL Loader and the PostgreSQL Generator. + +## Installation + + + + ```bash + npm install @emigrate/postgres + ``` + + + ```bash + pnpm add @emigrate/postgres + ``` + + + ```bash + yarn add @emigrate/postgres + ``` + + + +## Configuration + +The PostgreSQL storage can be configured either using environment variables or by configuring the plugin directly in the `emigrate.config.js` file. + +### Configuration file + +```js title="emigrate.config.js" {1,4-7} +import { createPostgresStorage } from '@emigrate/postgres'; + +export default { + storage: createPostgresStorage({ + table: 'migrations', + connection: { ... }, + }), +}; +``` + +#### Options + +##### `table` + +**type:** `string` +**default:** `"migrations"` + +The name of the table to use for storing the migrations. + +##### `connection` (required) + +**type:** `object | string` + +The connection options to use for connecting to the PostgreSQL database. This can either be a connection URI or an object with connection options. +For a list of supported connection options, see the [postgres documentation](https://github.com/porsager/postgres#connection). + +### Environment variables + +The following environment variables are supported: + +| Variable | Description | Default | +| ------------------- | ----------------------------------------------------------------------------------------------------------- | -------------- | +| `POSTGRES_TABLE` | The name of the table to use for storing the migrations | `"migrations"` | +| `POSTGRES_URL` | The full URI for connecting to a PostgreSQL database, e.g: `"postgres://user:pass@127.0.0.1:3306/database"` | | +| `POSTGRES_HOST` | The host on which the PostgreSQL server instance is running | `"localhost"` | +| `POSTGRES_USER` | The PostgreSQL user account to use for the authentication | | +| `POSTGRES_PASSWORD` | The PostgreSQL user password to use for the authentication | | +| `POSTGRES_PORT` | The network port on which the PostgreSQL server is listening | `5432` | +| `POSTGRES_DB` | The PostgreSQL database to use for the connection | | + +:::note +The `POSTGRES_URL` environment variable takes precedence over the other environment variables. If `POSTGRES_URL` is set, the other environment variables are ignored, except for `POSTGRES_TABLE`. +::: + +The environment variables are used when the storage plugin is used using the `--storage` command line option: + +```bash +npx emigrate list --storage postgres +``` + +Or when specifying the storage in the `emigrate.config.js` file as a string: + +```js title="emigrate.config.js" {2} +export default { + storage: 'postgres', +}; +``` diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 53494b9..0ea469c 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -56,7 +56,7 @@ Options: Examples: emigrate up --directory src/migrations -s fs - emigrate up -d ./migrations --storage @emigrate/storage-mysql + emigrate up -d ./migrations --storage @emigrate/mysql emigrate up -d src/migrations -s postgres -r json --dry `; diff --git a/packages/mysql/README.md b/packages/mysql/README.md index 4cf64f8..54abfe7 100644 --- a/packages/mysql/README.md +++ b/packages/mysql/README.md @@ -1,6 +1,6 @@ -# @emigrate/storage-mysql +# @emigrate/mysql -A MySQL plugin for Emigrate. Uses a MySQL database for storing migration history. Can load and generate .sql migration files. +A MySQL plugin for Emigrate. Uses a MySQL database for storing the migration history. Can load and generate .sql migration files. The table used for storing the migration history is compatible with the [immigration-mysql](https://github.com/joakimbeng/immigration-mysql) package, so you can use this together with the [@emigrate/cli](../cli) as a drop-in replacement for that package. diff --git a/packages/postgres/README.md b/packages/postgres/README.md new file mode 100644 index 0000000..1e397c3 --- /dev/null +++ b/packages/postgres/README.md @@ -0,0 +1,177 @@ +# @emigrate/postgres + +A PostgreSQL plugin for Emigrate. Uses a PostgreSQL database for storing the migration history. Can load and generate .sql migration files. + +The table used for storing the migration history is compatible with the [immigration-postgres](https://github.com/aboviq/immigration-postgres) package, so you can use this together with the [@emigrate/cli](../cli) as a drop-in replacement for that package. + +## Description + +This plugin is actually three different Emigrate plugins in one: + +1. A [storage plugin](#using-the-storage-plugin) for storing the migration history in a PostgreSQL database. +2. A [loader plugin](#using-the-loader-plugin) for loading .sql migration files and be able to execute them as part of the migration process. +3. A [generator plugin](#using-the-generator-plugin) for generating .sql migration files. + +## Installation + +Install the plugin in your project, alongside the Emigrate CLI: + +```bash +npm install --save-dev @emigrate/cli @emigrate/postgres +``` + +## Usage + +### Using the storage plugin + +See [Options](#options) below for the default values and how to configure the plugin using environment variables. + +Configure the storage in your `emigrate.config.js` file: + +```js +export default { + directory: 'migrations', + storage: 'postgres', // the @emigrate/ prefix is optional +}; +``` + +Or use the CLI options `--storage` (or `-s`) + +```bash +emigrate up --storage postgres # the @emigrate/ prefix is optional +``` + +#### Storage plugin with custom options + +Configure the storage in your `emigrate.config.js` file by importing the `createPostgresStorage` function (see [Options](#options) for available options). + +In this mode the plugin will _not_ use any of the environment variables for configuration. + +```js +import { createPostgresStorage } from '@emigrate/postgres'; + +export default { + directory: 'migrations', + storage: createPostgresStorage({ table: 'migrations', connection: { ... } }), // All connection options are passed to postgres() +}; +``` + +Or use the CLI option `--storage` (or `-s`) and use environment variables (see [Options](#options) for available variables). + +```bash +POSTGRES_URL=postgres://user:pass@host/db emigrate up --storage postgres # the @emigrate/ prefix is optional +``` + +### Using the loader plugin + +The loader plugin is used to transform .sql migration files into JavaScript functions that can be executed by the "up" command. + +See [Options](#options) below for the default values and how to configure the plugin using environment variables. + +Configure the loader in your `emigrate.config.js` file: + +```js +export default { + directory: 'migrations', + plugins: ['postgres'], // the @emigrate/ prefix is optional +}; +``` + +Or by importing the default export from the plugin: + +```js +import postgresPlugin from '@emigrate/postgres'; + +export default { + directory: 'migrations', + plugins: [postgresPlugin], +}; +``` + +**NOTE:** Using the root level `plugins` option will load the plugin for all commands, which means the [generator plugin](#using-the-generator-plugin) will be used by default for the "new" command as well. If you only want to use the loader plugin, use the `up.plugins` option instead: + +```js +export default { + directory: 'migrations', + up: { + plugins: ['postgres'], // the @emigrate/ prefix is optional + // or: + plugins: [import('@emigrate/postgres')], + }, +}; +``` + +The loader plugin can also be loaded using the CLI option `--plugin` (or `-p`) together with the "up" command: + +```bash +emigrate up --plugin postgres # the @emigrate/ prefix is optional +``` + +### Using the generator plugin + +The generator plugin is used to generate skeleton .sql migration files inside your migration directory. + +Configure the generator in your `emigrate.config.js` file: + +```js +export default { + directory: 'migrations', + plugins: ['postgres'], // the @emigrate/ prefix is optional +}; +``` + +Or by importing the default export from the plugin: + +```js +import postgresPlugin from '@emigrate/postgres'; + +export default { + directory: 'migrations', + plugins: [postgresPlugin], +}; +``` + +**NOTE:** Using the root level `plugins` option will load the plugin for all commands, which means the [loader plugin](#using-the-loader-plugin) will be used by default for the "up" command as well. If you only want to use the generator plugin, use the `new.plugins` option instead: + +```js +export default { + directory: 'migrations', + new: { + plugins: ['postgres'], // the @emigrate/ prefix is optional + // or: + plugins: [import('@emigrate/postgres')], + }, +}; +``` + +The generator plugin can also be loaded using the CLI option `--plugin` (or `-p`) together with the "new" command: + +```bash +emigrate new --plugin postgres My new migration file # the @emigrate/ prefix is optional +``` + +#### Loader plugin with custom options + +Configure the loader in your `emigrate.config.js` file by importing the `createPostgresLoader` function (see [Options](#options) for available options). + +In this mode the plugin will _not_ use any of the environment variables for configuration. + +```js +import { createPostgresLoader } from '@emigrate/postgres'; + +export default { + directory: 'migrations', + plugins: [ + createPostgresLoader({ connection: { ... } }), // All connection options are passed to postgres() + ], +}; +``` + +## Options + +The storage plugin accepts the following options: + +| Option | Applies to | Description | Default | Environment variable | +| ------------ | -------------------------- | -------------------------------------------------------------------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------- | +| `table` | storage plugin | The name of the table to use for storing the migrations. | `migrations` | `POSTGRES_TABLE` | +| `connection` | storage and loader plugins | The connection options to pass to [`postgres()`](https://github.com/porsager/postgres#connection). | `{}` | `POSTGRES_URL` or `POSTGRES_HOST`, `POSTGRES_PORT`, `POSTGRES_USER`, `POSTGRES_PASSWORD` and `POSTGRES_DB` | diff --git a/packages/postgres/package.json b/packages/postgres/package.json new file mode 100644 index 0000000..82a6cb8 --- /dev/null +++ b/packages/postgres/package.json @@ -0,0 +1,51 @@ +{ + "name": "@emigrate/postgres", + "version": "0.0.0", + "publishConfig": { + "access": "public" + }, + "description": "A PostgreSQL plugin for Emigrate. Uses a PostgreSQL database for storing migration history. Can load and generate .sql migration files.", + "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", + "lint": "xo --cwd=../.. $(pwd)" + }, + "keywords": [ + "emigrate", + "emigrate-storage", + "emigrate-loader", + "emigrate-plugin", + "emigrate-generator", + "migrations", + "postgres", + "postgresql" + ], + "author": "Aboviq AB (https://www.aboviq.com)", + "homepage": "https://github.com/aboviq/emigrate/tree/main/packages/postgres#readme", + "repository": "https://github.com/aboviq/emigrate/tree/main/packages/postgres", + "bugs": "https://github.com/aboviq/emigrate/issues", + "license": "MIT", + "dependencies": { + "@emigrate/plugin-tools": "workspace:*", + "@emigrate/types": "workspace:*", + "postgres": "3.4.3" + }, + "devDependencies": { + "@emigrate/tsconfig": "workspace:*" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/packages/postgres/src/index.ts b/packages/postgres/src/index.ts new file mode 100644 index 0000000..bcbe9e1 --- /dev/null +++ b/packages/postgres/src/index.ts @@ -0,0 +1,252 @@ +import process from 'node:process'; +import postgres, { type Options, type PostgresType, type Sql } from 'postgres'; +import { getTimestampPrefix, sanitizeMigrationName } from '@emigrate/plugin-tools'; +import { + type MigrationMetadata, + type EmigrateStorage, + type LoaderPlugin, + type Storage, + type MigrationMetadataFinished, + type GenerateMigrationFunction, + type GeneratorPlugin, + type SerializedError, + type MigrationHistoryEntry, +} from '@emigrate/types'; + +const defaultTable = 'migrations'; + +type ConnectionOptions = Options>; + +export type PostgresStorageOptions = { + table?: string; + /** + * @see https://github.com/porsager/postgres#connection + */ + connection: ConnectionOptions | string; +}; + +export type PostgresLoaderOptions = { + /** + * @see https://github.com/porsager/postgres#connection + */ + connection: ConnectionOptions | string; +}; + +const getPool = (connection: ConnectionOptions | string) => { + if (typeof connection === 'string') { + return postgres(connection); + } + + return postgres(connection); +}; + +const lockMigration = async (sql: Sql, table: string, migration: MigrationMetadata) => { + const result = await sql` + INSERT INTO ${sql(table)} (name, status, date) + VALUES (${migration.name}, ${'locked'}, NOW()) + ON CONFLICT (name) DO NOTHING + `; + + return result.count === 1; +}; + +const unlockMigration = async (sql: Sql, table: string, migration: MigrationMetadata) => { + const result = await sql` + DELETE FROM ${sql(table)} + WHERE + name = ${migration.name} + AND status = ${'locked'} + `; + + return result.count === 1; +}; + +const finishMigration = async ( + sql: Sql, + table: string, + migration: MigrationMetadataFinished, + _error?: SerializedError, +) => { + const result = await sql` + UPDATE + ${sql(table)} + SET + status = ${migration.status}, + date = NOW() + WHERE + name = ${migration.name} + AND status = ${'locked'} + `; + + return result.count === 1; +}; + +const deleteMigration = async (sql: Sql, table: string, migration: MigrationMetadata) => { + const result = await sql` + DELETE FROM ${sql(table)} + WHERE + name = ${migration.name} + AND status <> ${'locked'} + `; + + return result.count === 1; +}; + +const initializeTable = async (sql: Sql, table: string) => { + const [row] = await sql>` + SELECT 1 as exists + FROM + information_schema.tables + WHERE + table_schema = 'public' + AND table_name = ${table} + `; + + if (row?.exists) { + return; + } + + // This table definition is compatible with the one used by the immigration-postgres package + await sql` + CREATE TABLE ${sql(table)} ( + name varchar(255) not null primary key, + status varchar(32), + date timestamptz not null + ); + `; +}; + +export const createPostgresStorage = ({ + table = defaultTable, + connection, +}: PostgresStorageOptions): EmigrateStorage => { + return { + async initializeStorage() { + const sql = getPool(connection); + + try { + await initializeTable(sql, table); + } catch (error) { + await sql.end(); + throw error; + } + + const storage: Storage = { + async lock(migrations) { + const lockedMigrations: MigrationMetadata[] = []; + + for await (const migration of migrations) { + if (await lockMigration(sql, table, migration)) { + lockedMigrations.push(migration); + } + } + + return lockedMigrations; + }, + async unlock(migrations) { + for await (const migration of migrations) { + await unlockMigration(sql, table, migration); + } + }, + async remove(migration) { + await deleteMigration(sql, table, migration); + }, + async *getHistory() { + const query = sql>>` + SELECT + * + FROM + ${sql(table)} + WHERE + status <> ${'locked'} + ORDER BY + date ASC + `.cursor(); + + for await (const [row] of query) { + if (!row) { + continue; + } + + if (row.status === 'failed') { + yield { + ...row, + error: { name: 'Error', message: 'Unknown error' }, + }; + continue; + } + + yield row; + } + }, + async onSuccess(migration) { + await finishMigration(sql, table, migration); + }, + async onError(migration, error) { + await finishMigration(sql, table, migration, error); + }, + async end() { + await sql.end(); + }, + }; + + return storage; + }, + }; +}; + +export const { initializeStorage } = createPostgresStorage({ + table: process.env['POSTGRES_TABLE'], + connection: process.env['POSTGRES_URL'] ?? { + host: process.env['POSTGRES_HOST'], + port: process.env['POSTGRES_PORT'] ? Number.parseInt(process.env['POSTGRES_PORT'], 10) : undefined, + user: process.env['POSTGRES_USER'], + password: process.env['POSTGRES_PASSWORD'], + database: process.env['POSTGRES_DB'], + }, +}); + +export const createPostgresLoader = ({ connection }: PostgresLoaderOptions): LoaderPlugin => { + return { + loadableExtensions: ['.sql'], + async loadMigration(migration) { + return async () => { + const sql = getPool(connection); + + try { + // @ts-expect-error The "simple" option is not documented, but it exists + await sql.file(migration.filePath, { simple: true }); + } finally { + await sql.end(); + } + }; + }, + }; +}; + +export const { loadableExtensions, loadMigration } = createPostgresLoader({ + connection: process.env['POSTGRES_URL'] ?? { + host: process.env['POSTGRES_HOST'], + port: process.env['POSTGRES_PORT'] ? Number.parseInt(process.env['POSTGRES_PORT'], 10) : undefined, + user: process.env['POSTGRES_USER'], + password: process.env['POSTGRES_PASSWORD'], + database: process.env['POSTGRES_DB'], + }, +}); + +export const generateMigration: GenerateMigrationFunction = async (name) => { + return { + filename: `${getTimestampPrefix()}_${sanitizeMigrationName(name)}.sql`, + content: `-- Migration: ${name} +`, + }; +}; + +const defaultExport: EmigrateStorage & LoaderPlugin & GeneratorPlugin = { + initializeStorage, + loadableExtensions, + loadMigration, + generateMigration, +}; + +export default defaultExport; diff --git a/packages/postgres/tsconfig.json b/packages/postgres/tsconfig.json new file mode 100644 index 0000000..1cfcebb --- /dev/null +++ b/packages/postgres/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 1c78edd..9951810 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -148,6 +148,22 @@ importers: specifier: workspace:* version: link:../tsconfig + packages/postgres: + dependencies: + '@emigrate/plugin-tools': + specifier: workspace:* + version: link:../plugin-tools + '@emigrate/types': + specifier: workspace:* + version: link:../types + postgres: + specifier: 3.4.3 + version: 3.4.3 + devDependencies: + '@emigrate/tsconfig': + specifier: workspace:* + version: link:../tsconfig + packages/reporter-pino: dependencies: '@emigrate/types': @@ -7030,6 +7046,11 @@ packages: source-map-js: 1.0.2 dev: false + /postgres@3.4.3: + resolution: {integrity: sha512-iHJn4+M9vbTdHSdDzNkC0crHq+1CUdFhx+YqCE+SqWxPjm+Zu63jq7yZborOBF64c8pc58O5uMudyL1FQcHacA==} + engines: {node: '>=12'} + dev: false + /prebuild-install@7.1.1: resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==} engines: {node: '>=10'}