New platform/api/ service owns students, cohorts, curriculum, progress, scenario attempts, and student/admin sessions. No k3s, no --privileged, no terminal runtime — starts instantly, unlike the old identity service which booted an unused k3s cluster just to serve roster/progress APIs. Provisioner calls (student and admin) are consolidated here too, in provisioner-client.js, so it's the only service that talks to the Provisioner directly.
61 lines
2.1 KiB
JavaScript
61 lines
2.1 KiB
JavaScript
// 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 };
|