import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { pool } from "./pool.ts"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const MIGRATIONS_DIR = path.join(__dirname, "migrations"); /** * Aplica en orden alfabético los .sql que aún no estén en schema_migrations. * Cada archivo corre dentro de su propia transacción: si falla a la mitad, no * queda registrado y la siguiente corrida lo reintenta entero. */ export async function runMigrations(): Promise { await pool.query(` CREATE TABLE IF NOT EXISTS schema_migrations ( filename text PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT now() ) `); const files = fs .readdirSync(MIGRATIONS_DIR) .filter((f) => f.endsWith(".sql")) .sort(); const { rows } = await pool.query<{ filename: string }>( `SELECT filename FROM schema_migrations` ); const applied = new Set(rows.map((r) => r.filename)); const ran: string[] = []; for (const file of files) { if (applied.has(file)) continue; const sql = fs.readFileSync(path.join(MIGRATIONS_DIR, file), "utf8"); const client = await pool.connect(); try { await client.query("BEGIN"); await client.query(sql); await client.query(`INSERT INTO schema_migrations (filename) VALUES ($1)`, [file]); await client.query("COMMIT"); ran.push(file); console.log(`[migrate] aplicada ${file}`); } catch (e) { await client.query("ROLLBACK"); throw new Error(`Migración ${file} falló: ${(e as Error).message}`); } finally { client.release(); } } return ran; } // Permite `node scripts/run-tsx.mjs platform/db/migrate.ts` desde la línea de comandos. if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) { runMigrations() .then((ran) => { console.log(ran.length ? `[migrate] ${ran.length} aplicadas` : "[migrate] al día"); return pool.end(); }) .catch((e) => { console.error(e.message); process.exit(1); }); }