161 lines
6.3 KiB
TypeScript
161 lines
6.3 KiB
TypeScript
import pg from "pg";
|
|
import { readFile } from "node:fs/promises";
|
|
|
|
import { buildPostgresPoolConfig, postgresSslModeFromConnectionString } from "../db/runtime-store-core.ts";
|
|
import type { ActivityResult, CaseRunEvent, CaseRunRecord, CaseRunStatus, HarnessRLRegistry, ListCaseRunsInput, StartCaseRunInput } from "./contracts.ts";
|
|
import { terminalStatus } from "./contracts.ts";
|
|
|
|
const { Pool } = pg;
|
|
|
|
const migrationUrl = new URL("./migrations/0001_harnessrl_registry.sql", import.meta.url);
|
|
|
|
export class PostgresHarnessRLRegistry implements HarnessRLRegistry {
|
|
private readonly pool: pg.Pool;
|
|
private ready?: Promise<void>;
|
|
|
|
constructor(databaseUrl: string) {
|
|
if (!databaseUrl) throw codedError("harnessrl_database_url_required", "HARNESSRL_DATABASE_URL is required");
|
|
this.pool = new Pool(buildPostgresPoolConfig({
|
|
dbUrl: databaseUrl,
|
|
sslMode: postgresSslModeFromConnectionString(databaseUrl) ?? "disable",
|
|
poolMax: 8
|
|
}));
|
|
}
|
|
|
|
ensureSchema() {
|
|
this.ready ??= this.migrate();
|
|
return this.ready;
|
|
}
|
|
|
|
private async migrate() {
|
|
const client = await this.pool.connect();
|
|
try {
|
|
await client.query("BEGIN");
|
|
await client.query(await readFile(migrationUrl, "utf8"));
|
|
await client.query("COMMIT");
|
|
} catch (error) {
|
|
await client.query("ROLLBACK");
|
|
throw error;
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
|
|
async createRun(input: StartCaseRunInput) {
|
|
await this.ensureSchema();
|
|
const result = await this.pool.query(
|
|
`INSERT INTO harnessrl_runs (run_id,case_id,workflow_id,status,stage,case_repo,run_dir,runtime_api_url)
|
|
VALUES ($1,$2,$3,'queued','queued',$4,$5,$6)
|
|
ON CONFLICT (run_id) DO UPDATE SET run_id=EXCLUDED.run_id
|
|
RETURNING *`,
|
|
[input.runId, input.caseId, input.workflowId, input.caseRepo, input.runDir, input.runtimeApiUrl]
|
|
);
|
|
return runRow(result.rows[0]);
|
|
}
|
|
|
|
async getRun(runId: string) {
|
|
await this.ensureSchema();
|
|
const result = await this.pool.query("SELECT * FROM harnessrl_runs WHERE run_id=$1", [runId]);
|
|
return result.rows[0] ? runRow(result.rows[0]) : null;
|
|
}
|
|
|
|
async listRuns(input: ListCaseRunsInput) {
|
|
await this.ensureSchema();
|
|
const values: unknown[] = [];
|
|
const where: string[] = [];
|
|
if (input.status) {
|
|
values.push(input.status);
|
|
where.push(`status=$${values.length}`);
|
|
}
|
|
if (input.before) {
|
|
values.push(input.before.createdAt, input.before.runId);
|
|
where.push(`(created_at,run_id)<($${values.length - 1}::timestamptz,$${values.length})`);
|
|
}
|
|
values.push(input.limit);
|
|
const result = await this.pool.query(
|
|
`SELECT * FROM harnessrl_runs${where.length ? ` WHERE ${where.join(" AND ")}` : ""}
|
|
ORDER BY created_at DESC,run_id DESC LIMIT $${values.length}`,
|
|
values
|
|
);
|
|
return result.rows.map(runRow);
|
|
}
|
|
|
|
async updateRun(runId: string, patch: Partial<Pick<CaseRunRecord, "workflowRunId" | "status" | "stage" | "result" | "error" | "blocker">>) {
|
|
await this.ensureSchema();
|
|
const current = await this.getRun(runId);
|
|
if (!current) throw codedError("caserun_run_not_found", "CaseRun run was not found");
|
|
const result = await this.pool.query(
|
|
`UPDATE harnessrl_runs SET workflow_run_id=$2,status=$3,stage=$4,result=$5,error=$6,blocker=$7,updated_at=now()
|
|
WHERE run_id=$1 RETURNING *`,
|
|
[runId, patch.workflowRunId === undefined ? current.workflowRunId : patch.workflowRunId,
|
|
patch.status ?? current.status, patch.stage ?? current.stage,
|
|
patch.result === undefined ? current.result : patch.result,
|
|
patch.error === undefined ? current.error : patch.error,
|
|
patch.blocker === undefined ? current.blocker : patch.blocker]
|
|
);
|
|
return runRow(result.rows[0]);
|
|
}
|
|
|
|
async appendEvent(runId: string, status: CaseRunStatus, stage: string, payload: Record<string, unknown> = {}) {
|
|
await this.ensureSchema();
|
|
const result = await this.pool.query(
|
|
"INSERT INTO harnessrl_run_events (run_id,status,stage,payload) VALUES ($1,$2,$3,$4) RETURNING *",
|
|
[runId, status, stage, payload]
|
|
);
|
|
return eventRow(result.rows[0]);
|
|
}
|
|
|
|
async listEvents(runId: string) {
|
|
await this.ensureSchema();
|
|
const result = await this.pool.query("SELECT * FROM harnessrl_run_events WHERE run_id=$1 ORDER BY id", [runId]);
|
|
return result.rows.map(eventRow);
|
|
}
|
|
|
|
async getActivityResult(runId: string, activityName: string, identity: string) {
|
|
await this.ensureSchema();
|
|
const result = await this.pool.query(
|
|
"SELECT * FROM harnessrl_activity_results WHERE run_id=$1 AND activity_name=$2 AND identity=$3",
|
|
[runId, activityName, identity]
|
|
);
|
|
return result.rows[0] ? activityRow(result.rows[0]) : null;
|
|
}
|
|
|
|
async saveActivityResult(result: ActivityResult) {
|
|
await this.ensureSchema();
|
|
const saved = await this.pool.query(
|
|
`INSERT INTO harnessrl_activity_results (run_id,activity_name,identity,output) VALUES ($1,$2,$3,$4)
|
|
ON CONFLICT (run_id,activity_name,identity) DO UPDATE SET output=EXCLUDED.output RETURNING *`,
|
|
[result.runId, result.activityName, result.identity, result.output]
|
|
);
|
|
return activityRow(saved.rows[0]);
|
|
}
|
|
|
|
async health() {
|
|
await this.ensureSchema();
|
|
await this.pool.query("SELECT 1");
|
|
return { ok: true as const };
|
|
}
|
|
|
|
async close() { await this.pool.end(); }
|
|
}
|
|
|
|
function runRow(row: any): CaseRunRecord {
|
|
const status = row.status as CaseRunStatus;
|
|
return {
|
|
runId: row.run_id, caseId: row.case_id, workflowId: row.workflow_id, workflowRunId: row.workflow_run_id,
|
|
status, stage: row.stage, terminal: terminalStatus(status), caseRepo: row.case_repo, runDir: row.run_dir,
|
|
runtimeApiUrl: row.runtime_api_url, result: row.result, error: row.error, blocker: row.blocker,
|
|
createdAt: new Date(row.created_at).toISOString(), updatedAt: new Date(row.updated_at).toISOString()
|
|
};
|
|
}
|
|
|
|
function eventRow(row: any): CaseRunEvent {
|
|
return { id: Number(row.id), runId: row.run_id, status: row.status, stage: row.stage, payload: row.payload ?? {}, createdAt: new Date(row.created_at).toISOString() };
|
|
}
|
|
|
|
function activityRow(row: any): ActivityResult {
|
|
return { runId: row.run_id, activityName: row.activity_name, identity: row.identity, output: row.output ?? {} };
|
|
}
|
|
|
|
function codedError(code: string, message: string) { return Object.assign(new Error(message), { code }); }
|