feat(postgres): implement the first version of the PostgreSQL plugin

This commit is contained in:
Joakim Carlstein 2023-12-19 13:18:56 +01:00 committed by Joakim Carlstein
parent 3d34b8ba13
commit 17c4723bb8
16 changed files with 761 additions and 18 deletions

View file

@ -0,0 +1,5 @@
---
'@emigrate/postgres': minor
---
Implement the first version of the @emigrate/postgres plugin

View file

@ -117,6 +117,10 @@ export default defineConfig({
label: 'File System', label: 'File System',
link: '/plugins/storage/file-system/', link: '/plugins/storage/file-system/',
}, },
{
label: 'PostgreSQL',
link: '/plugins/storage/postgres/',
},
{ {
label: 'MySQL', label: 'MySQL',
link: '/plugins/storage/mysql/', link: '/plugins/storage/mysql/',
@ -135,6 +139,10 @@ export default defineConfig({
label: 'Default Loader', label: 'Default Loader',
link: '/plugins/loaders/default/', link: '/plugins/loaders/default/',
}, },
{
label: 'PostgreSQL Loader',
link: '/plugins/loaders/postgres/',
},
{ {
label: 'MySQL Loader', label: 'MySQL Loader',
link: '/plugins/loaders/mysql/', link: '/plugins/loaders/mysql/',
@ -171,6 +179,10 @@ export default defineConfig({
label: 'JavaScript Generator', label: 'JavaScript Generator',
link: '/plugins/generators/js/', link: '/plugins/generators/js/',
}, },
{
label: 'PostgreSQL Generator',
link: '/plugins/generators/postgres/',
},
{ {
label: 'MySQL Generator', label: 'MySQL Generator',
link: '/plugins/generators/mysql/', link: '/plugins/generators/mysql/',

View file

@ -41,22 +41,22 @@ But for now, this is the way to go.
Emigrate uses a <Link href="/plugins/storage/">storage plugin</Link> to store the migration history. Emigrate uses a <Link href="/plugins/storage/">storage plugin</Link> to store the migration history.
Install the plugin you want to use, for example: Install the plugin you want to use, for example the <Link href="/plugins/storage/postgres/">PostgreSQL Storage</Link>:
<Tabs> <Tabs>
<TabItem label="npm"> <TabItem label="npm">
```bash ```bash
npm install @emigrate/mysql npm install @emigrate/postgres
``` ```
</TabItem> </TabItem>
<TabItem label="pnpm"> <TabItem label="pnpm">
```bash ```bash
pnpm add @emigrate/mysql pnpm add @emigrate/postgres
``` ```
</TabItem> </TabItem>
<TabItem label="yarn"> <TabItem label="yarn">
```bash ```bash
yarn add @emigrate/mysql yarn add @emigrate/postgres
``` ```
</TabItem> </TabItem>
</Tabs> </Tabs>
@ -66,7 +66,7 @@ Install the plugin you want to use, for example:
Create a new migration file in your project using: Create a new migration file in your project using:
```bash title="Create a new migration file" ```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" ```txt title="Output"
@ -79,12 +79,12 @@ Emigrate new v0.10.0 /your/project/path
``` ```
:::note :::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. Otherwise the file would have the `.js` extension by default.
::: :::
:::tip[Did you know?] :::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 <Link href="/reference/configuration/">Configuration</Link> for more information. See <Link href="/reference/configuration/">Configuration</Link> 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} ```sql title="migrations/20231215125421364_create_users_table.sql" {2-7}
-- Migration: create users table -- Migration: create users table
CREATE TABLE users ( CREATE TABLE users (
id INT NOT NULL AUTO_INCREMENT, id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL
PRIMARY KEY (id)
); );
``` ```
@ -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: To show both pending and already applied migrations (or previously failed), use the `list` command:
```bash title="Show all migrations" ```bash title="Show all migrations"
npx emigrate list --storage mysql npx emigrate list --storage postgres
``` ```
```txt title="Example output" ```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: A good way to test your configuration is to run the migrations in dry mode:
```bash title="Show pending migrations" ```bash title="Show pending migrations"
npx emigrate up --storage mysql --plugin mysql --dry npx emigrate up --storage postgres --plugin postgres --dry
``` ```
:::note :::note
@ -142,6 +141,6 @@ Be sure to configure the connection correctly and use the `--dry` flag to test y
::: :::
:::tip[Did you know?] :::tip[Did you know?]
In the example above the `@emigrate/mysql` plugin is used twice, once for the `--storage` option as a <Link href="/plugins/storage/">Storage Plugin</Link> In the example above the `@emigrate/postgres` plugin is used twice, once for the `--storage` option as a <Link href="/plugins/storage/">Storage Plugin</Link>
and once for the `--plugin` option as a <Link href="/plugins/loaders/">Loader Plugin</Link> to be able to read `.sql` files. and once for the `--plugin` option as a <Link href="/plugins/loaders/">Loader Plugin</Link> to be able to read `.sql` files.
::: :::

View file

@ -17,6 +17,7 @@ The generator is responsible for generating migration files in a specific format
<CardGrid> <CardGrid>
<LinkCard title="JavaScript generator" href="js/" description="A generator that generates .js migration files (using ESM and default export)" /> <LinkCard title="JavaScript generator" href="js/" description="A generator that generates .js migration files (using ESM and default export)" />
<LinkCard title="PostgreSQL generator" href="postgres/" description="A generator that generates .sql migration files" />
<LinkCard title="MySQL generator" href="mysql/" description="A generator that generates .sql migration files" /> <LinkCard title="MySQL generator" href="mysql/" description="A generator that generates .sql migration files" />
</CardGrid> </CardGrid>

View file

@ -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 <Link href="/plugins/loaders/postgres/">PostgreSQL Loader</Link> and the <Link href="/plugins/storage/postgres/">PostgreSQL Storage</Link>.
## Installation
<Tabs>
<TabItem label="npm">
```bash
npm install @emigrate/postgres
```
</TabItem>
<TabItem label="pnpm">
```bash
pnpm add @emigrate/postgres
```
</TabItem>
<TabItem label="yarn">
```bash
yarn add @emigrate/postgres
```
</TabItem>
</Tabs>
## Usage
```bash
emigrate new --plugin postgres create some fancy table
```
For more information see <Link href="/commands/new/">the `new` command</Link>'s documentation.

View file

@ -21,7 +21,7 @@ Or set it up in your configuration file, see <Link href="/reference/configuratio
:::tip[Did you know?] :::tip[Did you know?]
You can specify multiple loader plugins at the same time, which is needed when you mix file types in your migrations folder. You can specify multiple loader plugins at the same time, which is needed when you mix file types in your migrations folder.
For example, you can use the `mysql` loader for `.sql` files and the `typescript` loader for `.ts` files. For example, you can use the `postgres` or `mysql` loader for `.sql` files and the `typescript` loader for `.ts` files.
The <Link href="/plugins/loaders/default/">default loader</Link> will be used for all other file types, and doesn't need to be specified. The <Link href="/plugins/loaders/default/">default loader</Link> will be used for all other file types, and doesn't need to be specified.
::: :::
@ -29,5 +29,6 @@ The <Link href="/plugins/loaders/default/">default loader</Link> will be used fo
<CardGrid> <CardGrid>
<LinkCard title="Default Loader" href="default/" description="The loader responsible for loading .js, .cjs and .mjs files" /> <LinkCard title="Default Loader" href="default/" description="The loader responsible for loading .js, .cjs and .mjs files" />
<LinkCard title="PostgreSQL Loader" href="postgres/" description="Can load and execute .sql files against a PostgreSQL database" />
<LinkCard title="MySQL Loader" href="mysql/" description="Can load and execute .sql files against a MySQL database" /> <LinkCard title="MySQL Loader" href="mysql/" description="Can load and execute .sql files against a MySQL database" />
</CardGrid> </CardGrid>

View file

@ -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 <Link href="/plugins/generators/postgres/">PostgreSQL Generator</Link> and the <Link href="/plugins/storage/postgres/">PostgreSQL Storage</Link>.
## Installation
<Tabs>
<TabItem label="npm">
```bash
npm install @emigrate/postgres
```
</TabItem>
<TabItem label="pnpm">
```bash
pnpm add @emigrate/postgres
```
</TabItem>
<TabItem label="yarn">
```bash
yarn add @emigrate/postgres
```
</TabItem>
</Tabs>
## Configuration
The PostgreSQL loader plugin can be configured either using environment variables or by configuring the plugin directly in the <Link href="/reference/configuration/">`emigrate.config.js` file</Link>.
### 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 <Link href="/reference/configuration/">`emigrate.config.js` file</Link> as a string:
```js title="emigrate.config.js" {2}
export default {
plugins: ['postgres'],
};
```

View file

@ -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: You can specify a storage plugin via the `--storage` (or `-s` for short) option:
```bash ```bash
npx emigrate list --storage mysql npx emigrate list --storage postgres
``` ```
Or set it up in your configuration file, see <Link href="/reference/configuration/#storage">Storage configuration</Link> for more information. Or set it up in your configuration file, see <Link href="/reference/configuration/#storage">Storage configuration</Link> for more information.
@ -22,6 +22,7 @@ Or set it up in your configuration file, see <Link href="/reference/configuratio
<CardGrid> <CardGrid>
<LinkCard title="File System" href="file-system/" description="The most basic storage plugin - for simple setups" /> <LinkCard title="File System" href="file-system/" description="The most basic storage plugin - for simple setups" />
<LinkCard title="PostgreSQL" href="postgres/" description="A storage plugin that uses a PostgreSQL database for storing the migration history state" />
<LinkCard title="MySQL" href="mysql/" description="A storage plugin that uses a MySQL database for storing the migration history state" /> <LinkCard title="MySQL" href="mysql/" description="A storage plugin that uses a MySQL database for storing the migration history state" />
</CardGrid> </CardGrid>

View file

@ -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 <Link href="/plugins/loaders/postgres/">PostgreSQL Loader</Link> and the <Link href="/plugins/generators/postgres/">PostgreSQL Generator</Link>.
## Installation
<Tabs>
<TabItem label="npm">
```bash
npm install @emigrate/postgres
```
</TabItem>
<TabItem label="pnpm">
```bash
pnpm add @emigrate/postgres
```
</TabItem>
<TabItem label="yarn">
```bash
yarn add @emigrate/postgres
```
</TabItem>
</Tabs>
## Configuration
The PostgreSQL storage can be configured either using environment variables or by configuring the plugin directly in the <Link href="/reference/configuration/">`emigrate.config.js` file</Link>.
### 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 <Link href="/reference/configuration/">`emigrate.config.js` file</Link> as a string:
```js title="emigrate.config.js" {2}
export default {
storage: 'postgres',
};
```

View file

@ -56,7 +56,7 @@ Options:
Examples: Examples:
emigrate up --directory src/migrations -s fs 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 emigrate up -d src/migrations -s postgres -r json --dry
`; `;

View file

@ -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. 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.

177
packages/postgres/README.md Normal file
View file

@ -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` |

View file

@ -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 <dev@aboviq.com> (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"
}
}

View file

@ -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<Record<string, PostgresType>>;
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<Array<{ exists: 1 }>>`
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<Array<Exclude<MigrationHistoryEntry, 'error'>>>`
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;

View file

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

21
pnpm-lock.yaml generated
View file

@ -148,6 +148,22 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../tsconfig 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: packages/reporter-pino:
dependencies: dependencies:
'@emigrate/types': '@emigrate/types':
@ -7030,6 +7046,11 @@ packages:
source-map-js: 1.0.2 source-map-js: 1.0.2
dev: false 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: /prebuild-install@7.1.1:
resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==} resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==}
engines: {node: '>=10'} engines: {node: '>=10'}