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:
48
control-plane/src/db/migrate.ts
Normal file
48
control-plane/src/db/migrate.ts
Normal 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, [])
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user