feat(relay): Phase1 foundation — P3 Postgres store + async capability verifier + compose

RELAY-PHASE1 Wave A (2/3) + E1 infra:
- A1: createPgStores() Postgres adapter for all 9 P3 store ports + runMigrations();
  writable-CTE atomic INV8 status swaps; 0002_routes.sql. 23/23 pg tests, 96.72% cov.
- A3: real capability verifier delegating to relay-auth verifyCapabilityToken; sync->async
  seam across authz/provision/main. Full CP suite 15 files/101 pass, tsc clean.
- E1: deploy/docker-compose.yml (Postgres16+Redis7, loopback-only) + .env.example.
- docs: PLAN_RELAY_PHASE1.md file-level execution spec; PROGRESS_LOG RELAY-PHASE1 section.
This commit is contained in:
Yaojia Wang
2026-07-06 14:51:37 +02:00
parent 242a4e0dc1
commit 95b9cccf07
17 changed files with 1488 additions and 16 deletions

View File

@@ -0,0 +1,48 @@
/**
* A1 — migration runner. Reads every `db/migrations/*.sql` in filename order and executes it
* over the parameterized `query` wrapper (db/pool.ts). All migrations are `IF NOT EXISTS`, so
* `runMigrations` is idempotent and safe to run at every boot / test setup.
*
* `createQuery` ALWAYS routes through the extended (parameterized) protocol — even with `[]`
* params — which rejects multi-statement command strings. So each file is split into its
* top-level statements (on `;`, after stripping `-- line comments`) and each statement is run
* as its own single-command `query(stmt, [])`. Our migrations are plain IF-NOT-EXISTS DDL with
* no `;` inside string literals and no dollar-quoted bodies, so this split is safe.
*/
import { readdir, readFile } from 'node:fs/promises'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import type { QueryFn } from './pool.js'
/** Absolute path to `control-plane/db/migrations`, resolved relative to this source file. */
function migrationsDir(): string {
const here = dirname(fileURLToPath(import.meta.url)) // .../control-plane/src/db
return join(here, '..', '..', 'db', 'migrations') // .../control-plane/db/migrations
}
/** Strip `-- line comments`, then split into non-empty top-level statements on `;`. */
export function splitStatements(sql: string): readonly string[] {
const withoutComments = sql
.split('\n')
.map((line) => {
const idx = line.indexOf('--')
return idx >= 0 ? line.slice(0, idx) : line
})
.join('\n')
return withoutComments
.split(';')
.map((stmt) => stmt.trim())
.filter((stmt) => stmt.length > 0)
}
/** Apply all `*.sql` migrations in filename order. Idempotent (every migration is IF NOT EXISTS). */
export async function runMigrations(query: QueryFn): Promise<void> {
const dir = migrationsDir()
const files = (await readdir(dir)).filter((f) => f.endsWith('.sql')).sort()
for (const file of files) {
const sql = await readFile(join(dir, file), 'utf8')
for (const stmt of splitStatements(sql)) {
await query(stmt, [])
}
}
}