Migrate harness from in-memory stores to CloudNativePG
Replace all in-memory Map-backed stores (credentials, models, agents, tasks, iterations, usage) with Drizzle ORM queries against the homelab-pg PostgreSQL cluster. All store functions are now async. - Add 6 harness_* tables to @homelab/db schema - Generate and apply initial Drizzle migration - Add lazy DB connection proxy to avoid build-time errors - Wire DATABASE_URL from sealed secret into harness deployment - Update all API routes, orchestrator, executor, and boot to await async store operations
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "./db";
|
||||
import { credentials as credentialsTable } from "@homelab/db";
|
||||
|
||||
export type Provider =
|
||||
| "github" | "gitlab"
|
||||
| "anthropic" | "openai" | "openrouter" | "google" | "opencode-zen";
|
||||
@@ -10,50 +14,73 @@ export interface Credential {
|
||||
provider: Provider;
|
||||
label: string;
|
||||
token: string;
|
||||
baseUrl?: string; // for self-hosted GitLab or custom endpoints
|
||||
}
|
||||
|
||||
// In-memory store shared via globalThis to survive Next.js module re-bundling.
|
||||
const g = globalThis as unknown as { __harnessCredentials?: Map<string, Credential> };
|
||||
g.__harnessCredentials ??= new Map();
|
||||
const credentials = g.__harnessCredentials;
|
||||
|
||||
export function getAllCredentials(): Credential[] {
|
||||
return Array.from(credentials.values()).map(c => ({
|
||||
...c,
|
||||
token: maskToken(c.token),
|
||||
}));
|
||||
}
|
||||
|
||||
export function getCredentialsByKind(kind: "git" | "ai"): Credential[] {
|
||||
const providers = kind === "git" ? GIT_PROVIDERS : AI_PROVIDERS;
|
||||
return Array.from(credentials.values())
|
||||
.filter(c => providers.includes(c.provider))
|
||||
.map(c => ({ ...c, token: maskToken(c.token) }));
|
||||
}
|
||||
|
||||
export function getCredential(id: string): Credential | undefined {
|
||||
return credentials.get(id);
|
||||
}
|
||||
|
||||
export function getCredentialsByProvider(provider: Provider): Credential[] {
|
||||
return Array.from(credentials.values()).filter(c => c.provider === provider);
|
||||
}
|
||||
|
||||
export function getRawCredentialsByProvider(provider: Provider): Credential[] {
|
||||
return Array.from(credentials.values()).filter(c => c.provider === provider);
|
||||
}
|
||||
|
||||
export function upsertCredential(cred: Credential): Credential {
|
||||
credentials.set(cred.id, cred);
|
||||
return { ...cred, token: maskToken(cred.token) };
|
||||
}
|
||||
|
||||
export function deleteCredential(id: string): boolean {
|
||||
return credentials.delete(id);
|
||||
baseUrl?: string;
|
||||
}
|
||||
|
||||
function maskToken(token: string): string {
|
||||
if (token.length <= 8) return "••••••••";
|
||||
return token.slice(0, 4) + "••••" + token.slice(-4);
|
||||
}
|
||||
|
||||
function rowToCredential(row: typeof credentialsTable.$inferSelect): Credential {
|
||||
return {
|
||||
id: row.id,
|
||||
provider: row.provider as Provider,
|
||||
label: row.label,
|
||||
token: row.token,
|
||||
baseUrl: row.baseUrl ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getAllCredentials(): Promise<Credential[]> {
|
||||
const rows = await db.select().from(credentialsTable);
|
||||
return rows.map(r => ({ ...rowToCredential(r), token: maskToken(r.token) }));
|
||||
}
|
||||
|
||||
export async function getCredentialsByKind(kind: "git" | "ai"): Promise<Credential[]> {
|
||||
const providers = kind === "git" ? GIT_PROVIDERS : AI_PROVIDERS;
|
||||
const rows = await db.select().from(credentialsTable);
|
||||
return rows
|
||||
.filter(r => providers.includes(r.provider as Provider))
|
||||
.map(r => ({ ...rowToCredential(r), token: maskToken(r.token) }));
|
||||
}
|
||||
|
||||
export async function getCredential(id: string): Promise<Credential | undefined> {
|
||||
const [row] = await db.select().from(credentialsTable).where(eq(credentialsTable.id, id));
|
||||
return row ? rowToCredential(row) : undefined;
|
||||
}
|
||||
|
||||
export async function getCredentialsByProvider(provider: Provider): Promise<Credential[]> {
|
||||
const rows = await db.select().from(credentialsTable).where(eq(credentialsTable.provider, provider));
|
||||
return rows.map(r => ({ ...rowToCredential(r), token: maskToken(r.token) }));
|
||||
}
|
||||
|
||||
export async function getRawCredentialsByProvider(provider: Provider): Promise<Credential[]> {
|
||||
const rows = await db.select().from(credentialsTable).where(eq(credentialsTable.provider, provider));
|
||||
return rows.map(rowToCredential);
|
||||
}
|
||||
|
||||
export async function upsertCredential(cred: Credential): Promise<Credential> {
|
||||
await db.insert(credentialsTable).values({
|
||||
id: cred.id,
|
||||
provider: cred.provider,
|
||||
label: cred.label,
|
||||
token: cred.token,
|
||||
baseUrl: cred.baseUrl,
|
||||
}).onConflictDoUpdate({
|
||||
target: credentialsTable.id,
|
||||
set: {
|
||||
provider: cred.provider,
|
||||
label: cred.label,
|
||||
token: cred.token,
|
||||
baseUrl: cred.baseUrl,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
return { ...cred, token: maskToken(cred.token) };
|
||||
}
|
||||
|
||||
export async function deleteCredential(id: string): Promise<boolean> {
|
||||
const result = await db.delete(credentialsTable).where(eq(credentialsTable.id, id));
|
||||
return (result as unknown as { rowCount: number }).rowCount > 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user