/** * Runs database migrations for PostgreSQL projects that use Effect SQL. * * This module reuses the shared SQL migrator and connects it to PostgreSQL. It * exposes the common migration helpers and adds `run` and `layer` functions * that apply pending migration files with the current SQL client. When schema * dumps are requested, it uses `pg_dump` and the usual process and filesystem * services. * * @since 4.0.0 */ import * as Effect from "effect/Effect" import * as FileSystem from "effect/FileSystem" import * as Layer from "effect/Layer" import * as Path from "effect/Path" import * as Redacted from "effect/Redacted" import * as ChildProcess from "effect/unstable/process/ChildProcess" import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner" import * as Migrator from "effect/unstable/sql/Migrator" import type { SqlClient } from "effect/unstable/sql/SqlClient" import type { SqlError } from "effect/unstable/sql/SqlError" import { PgClient } from "./PgClient.ts" /** * @since 4.0.0 */ export * from "effect/unstable/sql/Migrator" /** * Runs PostgreSQL SQL migrations using the configured clients. Schema dumps use `pg_dump` and require child process, filesystem, and path services. * * @category constructors * @since 4.0.0 */ export const run: ( options: Migrator.MigratorOptions ) => Effect.Effect< ReadonlyArray, Migrator.MigrationError | SqlError, | SqlClient | PgClient | ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path | R2 > = Migrator.make({ dumpSchema(path, table) { const pgDump = (args: Array) => Effect.gen(function*() { const sql = yield* PgClient const spawner = yield* ChildProcessSpawner.ChildProcessSpawner const dump = yield* ChildProcess.make("pg_dump", [...args, "--no-owner", "--no-privileges"], { env: { PATH: (globalThis as any).process?.env.PATH, PGHOST: sql.config.host, PGPORT: sql.config.port?.toString(), PGUSER: sql.config.username, PGPASSWORD: sql.config.password ? Redacted.value(sql.config.password) : undefined, PGDATABASE: sql.config.database, PGSSLMODE: sql.config.ssl ? "require" : "prefer" } }).pipe(spawner.string) return dump.replace(/^--.*$/gm, "") .replace(/^SET .*$/gm, "") .replace(/^SELECT pg_catalog\..*$/gm, "") .replace(/\n{2,}/gm, "\n\n") .trim() }).pipe( Effect.mapError((error) => new Migrator.MigrationError({ kind: "Failed", message: error.message })) ) const pgDumpSchema = pgDump(["--schema-only"]) const pgDumpMigrations = pgDump([ "--column-inserts", "--data-only", `--table=${table}` ]) const pgDumpAll = Effect.map( Effect.all([pgDumpSchema, pgDumpMigrations], { concurrency: 2 }), ([schema, migrations]) => schema + "\n\n" + migrations ) const pgDumpFile = (path: string) => Effect.gen(function*() { const fs = yield* FileSystem.FileSystem const path_ = yield* Path.Path const dump = yield* pgDumpAll yield* fs.makeDirectory(path_.dirname(path), { recursive: true }) yield* fs.writeFileString(path, dump) }).pipe( Effect.mapError((error) => new Migrator.MigrationError({ kind: "Failed", message: error.message })) ) return pgDumpFile(path) } }) /** * Creates a layer that runs PostgreSQL migrations during layer construction, including `pg_dump`-based schema dump support when requested. * * @category layers * @since 4.0.0 */ export const layer = ( options: Migrator.MigratorOptions ): Layer.Layer< never, Migrator.MigrationError | SqlError, | SqlClient | PgClient | ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path | R > => Layer.effectDiscard(run(options))