/** * Apply the schema migrations to a Postgres database. * * Reads the SQL files from infra/init-sql/ and runs them in lexical * order. Idempotent — safe to run on every boot. * * In production you'd use a proper migration tool (drizzle-kit, * prisma, knex, sqlx, etc.). For Phase 1 we just want a simple, * zero-deps way to get the schema into a fresh database. */ import { readFile, readdir } from 'node:fs/promises'; import { join, dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import type { Sql } from 'postgres'; const INIT_SQL_DEFAULT = '../../../infra/init-sql'; export async function runMigrations( sql: Sql, initDir: string = INIT_SQL_DEFAULT, ): Promise<{ applied: string[] }> { const dir = resolve(dirname(fileURLToPath(import.meta.url)), initDir); const entries = (await readdir(dir)).filter((f) => f.endsWith('.sql')).sort(); const applied: string[] = []; for (const file of entries) { const path = join(dir, file); const content = await readFile(path, 'utf8'); try { await sql.unsafe(content); applied.push(file); } catch (err) { // Some errors are non-fatal (e.g. "extension already exists" race), // but anything else should fail loudly. if (isBenignError(err)) { applied.push(`${file} (skipped: ${(err as Error).message.split('\n')[0]})`); continue; } throw new Error(`Migration ${file} failed: ${(err as Error).message}`); } } return { applied }; } function isBenignError(err: unknown): boolean { const msg = String((err as Error).message ?? err); return /already exists/i.test(msg) || /duplicate key/i.test(msg); }