Files
KSP-MissionControl/packages/db/src/migrate.ts
T
Mavis a457b9d96f
CI / Lint, typecheck, test, build (pull_request) Failing after 10s
Phase 1: data pipeline (Postgres+Redis with in-memory fallback, mock telemetry publisher, WebSocket fan-out, hub /debug page)
- packages/db: postgres.js + ioredis wrapper, StateStore interface with
  InMemory + Postgres implementations, schema migration runner
- apps/api: refactored to use @kerbal-rt/db; live WebSocket hub subscribes
  to store changes and broadcasts to all clients
- apps/tools/mock-telemetry: Node script that generates realistic KSP
  state and POSTs to /api/v1/ingest at 1Hz (uses same keplerian math
  as the live-map renderer)
- apps/hub: new /debug page that connects to /api/v1/live and shows
  the live state
- ksp/README.md: documents Phase 1c (real kRPC bridge) and the two
  implementation options
- Tests: 17 total (5 kepler math, 5 db store, 5 API health/state,
  2 API WebSocket fan-out)

End-to-end verified: mock publisher → API → hub /debug page,
11 snapshots/5s over WebSocket, vessels advancing in mean anomaly.
2026-06-02 18:54:46 +00:00

49 lines
1.6 KiB
TypeScript

/**
* 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);
}