Files
nuc-portal/src/lib/coolify-db.ts
2026-02-04 01:14:46 +01:00

141 lines
4.3 KiB
TypeScript

import pg from 'pg';
import { serverConfig } from './config';
import type { Deployment, DeploymentStatus } from './deployments';
const { Pool } = pg;
let pool: pg.Pool | null = null;
function parseDbUrl(url: string): pg.PoolConfig {
// Manual parsing to handle special chars in password (/, =, etc.)
const match = url.match(/^postgres(?:ql)?:\/\/([^:]+):(.+)@([^:]+):(\d+)\/(.+)$/);
if (match) {
return { user: match[1], password: match[2], host: match[3], port: parseInt(match[4]), database: match[5] };
}
return { connectionString: url };
}
function getPool(): pg.Pool {
if (!pool) {
pool = new Pool({
...parseDbUrl(serverConfig.coolifyDbUrl),
max: 3,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
});
pool.on('error', (err) => {
console.error('Coolify DB pool error:', err);
});
}
return pool;
}
function mapStatus(status: string): DeploymentStatus {
if (status === 'failed') return 'error';
if (status === 'cancelled-by-user') return 'cancelled';
return status as DeploymentStatus;
}
function rowToDeployment(row: Record<string, unknown>): Deployment {
const createdAt = row.created_at as string;
const updatedAt = row.updated_at as string;
let duration: number | undefined;
if (createdAt && updatedAt) {
const start = new Date(createdAt).getTime();
const end = new Date(updatedAt).getTime();
duration = Math.max(0, Math.floor((end - start) / 1000));
}
return {
deployment_uuid: row.deployment_uuid as string,
application_uuid: (row.application_uuid as string) || 'unknown',
application_name: (row.application_name as string) || 'Unknown App',
application_fqdn: (row.application_fqdn as string) || undefined,
status: mapStatus(row.status as string),
created_at: createdAt,
updated_at: updatedAt,
git_branch: (row.git_branch as string) || 'main',
git_commit_sha: (row.commit as string) || undefined,
commit_message: (row.commit_message as string) || undefined,
is_webhook: row.is_webhook as boolean | undefined,
is_api: row.is_api as boolean | undefined,
logs: (row.logs as string) || undefined,
duration,
};
}
export async function fetchDeployments(limit = 50): Promise<Deployment[]> {
const db = getPool();
const { rows } = await db.query(`
SELECT
q.deployment_uuid,
a.uuid AS application_uuid,
COALESCE(q.application_name, a.name) AS application_name,
a.fqdn AS application_fqdn,
q.status,
q.created_at,
q.updated_at,
a.git_branch,
q.commit,
q.commit_message,
q.is_webhook,
q.is_api
FROM application_deployment_queues q
LEFT JOIN applications a ON a.id = q.application_id::bigint
ORDER BY q.created_at DESC
LIMIT $1
`, [limit]);
const deployments = rows.map(rowToDeployment);
// Mark latest finished deployment per app as current
const latestByApp = new Map<string, string>();
for (const d of deployments) {
if (d.status === 'finished' && !latestByApp.has(d.application_uuid)) {
latestByApp.set(d.application_uuid, d.deployment_uuid);
}
}
for (const d of deployments) {
d.is_current = latestByApp.get(d.application_uuid) === d.deployment_uuid;
}
return deployments;
}
export async function fetchDeploymentDetail(uuid: string): Promise<Deployment | null> {
const db = getPool();
const { rows } = await db.query(`
SELECT
q.deployment_uuid,
a.uuid AS application_uuid,
COALESCE(q.application_name, a.name) AS application_name,
a.fqdn AS application_fqdn,
q.status,
q.created_at,
q.updated_at,
a.git_branch,
q.commit,
q.commit_message,
q.is_webhook,
q.is_api,
q.logs
FROM application_deployment_queues q
LEFT JOIN applications a ON a.id = q.application_id::bigint
WHERE q.deployment_uuid = $1
`, [uuid]);
if (rows.length === 0) return null;
return rowToDeployment(rows[0]);
}
export async function fetchActiveDeploymentLogs(): Promise<Array<{ uuid: string; logs: string; status: string }>> {
const db = getPool();
const { rows } = await db.query(`
SELECT deployment_uuid AS uuid, logs, status
FROM application_deployment_queues
WHERE status IN ('in_progress', 'queued')
ORDER BY created_at DESC
`);
return rows as Array<{ uuid: string; logs: string; status: string }>;
}