a457b9d96f
CI / Lint, typecheck, test, build (pull_request) Failing after 10s
- 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.
49 lines
1.6 KiB
TypeScript
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);
|
|
}
|