Files
pikasTech-HWLAB/internal/harnessrl/registry.ts
T

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