// Postgres connection + migration runner — same pattern as platform/provisioner/src/db.js // (check-before-alter idempotency, tracked in schema_migrations). Same database // (rootnode_platform) as the Provisioner, different table set: this service owns // students/cohorts/enrollments/curriculum_assignments/progress/scenario_attempts/ // exam_sessions; the Provisioner owns lab_hosts/environments/environment_events. Both connect // to the same Postgres instance — scenario_attempts.environment_id is a real FK across that // shared boundary, which Postgres allows regardless of which service's migrations created // which table. const fs = require('fs'); const path = require('path'); const { Pool } = require('pg'); const MIGRATIONS_DIR = path.join(__dirname, '../migrations'); let _pool = null; function getPool() { if (_pool) return _pool; _pool = new Pool({ connectionString: process.env.PLATFORM_DB_URL || 'postgres://postgres:postgres@localhost:5433/rootnode_platform', }); return _pool; } async function runMigrations() { const pool = getPool(); await pool.query(` CREATE TABLE IF NOT EXISTS schema_migrations ( version TEXT PRIMARY KEY, applied_at TIMESTAMPTZ NOT NULL DEFAULT now() ) `); const applied = new Set( (await pool.query('SELECT version FROM schema_migrations')).rows.map((r) => r.version) ); const files = fs.readdirSync(MIGRATIONS_DIR).filter((f) => f.endsWith('.sql')).sort(); const ranNow = []; 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 (version) VALUES ($1)', [file]); await client.query('COMMIT'); ranNow.push(file); } catch (e) { await client.query('ROLLBACK'); throw new Error(`Migration ${file} failed: ${e.message}`); } finally { client.release(); } } return ranNow; } module.exports = { getPool, runMigrations };