import { promises as fs } from "node:fs"; import path from "node:path"; import type { Pool } from "pg"; export type AppliedMigration = { id: string; }; type MigrationFile = { id: string; sql: string; }; async function loadMigrationFiles(migrationsDir: string): Promise { const files = (await fs.readdir(migrationsDir)).filter((file) => file.endsWith(".sql")).sort(); return Promise.all( files.map(async (file) => ({ id: file, sql: await fs.readFile(path.join(migrationsDir, file), "utf8"), })) ); } export async function runMigrations(pool: Pool, migrationsDir = path.resolve(process.cwd(), "migrations")): Promise { const migrations = await loadMigrationFiles(migrationsDir); const client = await pool.connect(); try { await client.query("BEGIN"); await client.query(` CREATE TABLE IF NOT EXISTS schema_migrations ( id text PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT now() ) `); const appliedRows = await client.query<{ id: string }>("SELECT id FROM schema_migrations"); const appliedIds = new Set(appliedRows.rows.map((row) => row.id)); const newlyApplied: AppliedMigration[] = []; for (const migration of migrations) { if (appliedIds.has(migration.id)) { continue; } await client.query(migration.sql); await client.query("INSERT INTO schema_migrations(id) VALUES ($1)", [migration.id]); newlyApplied.push({ id: migration.id }); } await client.query("COMMIT"); return newlyApplied; } catch (error) { await client.query("ROLLBACK"); throw error; } finally { client.release(); } }