Files
vibn-frontend/lib/db-postgres.ts

318 lines
9.3 KiB
TypeScript

import { Pool, QueryResult } from 'pg';
// ==================================================
// Coolify PostgreSQL Connection
// ==================================================
const DATABASE_URL = process.env.DATABASE_URL ||
process.env.POSTGRES_URL ||
'postgresql://vibn_user:password@vibn-postgres:5432/vibn';
let pool: Pool | null = null;
// Internal Docker network connections (Coolify) don't use SSL.
// Only enable SSL for external/RDS/cloud DB connections.
function getSslConfig() {
const url = DATABASE_URL;
if (!url) return undefined;
// Internal Docker hostnames never use SSL
if (url.includes('localhost') || url.includes('127.0.0.1') ||
/postgresql:\/\/[^@]+@[a-z0-9_-]+:\d+\//.test(url)) {
return undefined;
}
// External cloud DBs (RDS, AlloyDB, etc.) need SSL
if (process.env.DB_SSL === 'true') {
return { rejectUnauthorized: false };
}
return undefined;
}
export function getPool() {
if (!pool) {
pool = new Pool({
connectionString: DATABASE_URL,
ssl: getSslConfig(),
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
pool.on('error', (err) => {
console.error('Unexpected error on idle client', err);
});
}
return pool;
}
export async function query<T = any>(text: string, params?: any[]): Promise<T[]> {
const pool = getPool();
const result = await pool.query(text, params);
return result.rows;
}
export async function queryOne<T = any>(text: string, params?: any[]): Promise<T | null> {
const rows = await query<T>(text, params);
return rows[0] || null;
}
// ==================================================
// User operations (replaces Firebase auth.ts)
// ==================================================
export interface User {
id: number;
uid: string;
email: string;
display_name?: string;
photo_url?: string;
workspace: string;
created_at: Date;
updated_at: Date;
}
export async function createUser(data: {
email: string;
password_hash?: string;
display_name?: string;
photo_url?: string;
workspace: string;
google_id?: string;
github_id?: string;
}): Promise<User> {
const uid = `user-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const result = await query<User>(`
INSERT INTO users (uid, email, password_hash, display_name, photo_url, workspace, google_id, github_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *
`, [uid, data.email, data.password_hash, data.display_name, data.photo_url, data.workspace, data.google_id, data.github_id]);
return result[0];
}
export async function getUserByEmail(email: string): Promise<User | null> {
return await queryOne<User>('SELECT * FROM users WHERE email = $1', [email]);
}
export async function getUserByUid(uid: string): Promise<User | null> {
return await queryOne<User>('SELECT * FROM users WHERE uid = $1', [uid]);
}
export async function getUserById(id: number): Promise<User | null> {
return await queryOne<User>('SELECT * FROM users WHERE id = $1', [id]);
}
export async function updateUser(id: number, data: Partial<User>): Promise<User | null> {
const fields = Object.keys(data).filter(k => k !== 'id' && k !== 'uid' && k !== 'created_at');
const values = fields.map((_, i) => `$${i + 2}`);
const setClause = fields.map((f, i) => `${f} = ${values[i]}`).join(', ');
const result = await query<User>(`
UPDATE users
SET ${setClause}, updated_at = NOW()
WHERE id = $1
RETURNING *
`, [id, ...fields.map(f => (data as any)[f])]);
return result[0] || null;
}
// ==================================================
// Project operations (replaces Firebase collections.ts)
// ==================================================
export interface Project {
id: number;
firebase_id?: string;
client_id?: number;
user_id: number;
name: string;
slug: string;
workspace: string;
product_name: string;
product_vision?: string;
status: string;
current_phase: string;
phase_status: string;
github_repo?: string;
chatgpt_project_id?: string;
gitea_repo_url?: string;
coolify_app_id?: string;
deployment_url?: string;
phase_data: any;
created_at: Date;
updated_at: Date;
}
export async function createProject(data: Omit<Project, 'id' | 'created_at' | 'updated_at'>): Promise<Project> {
const result = await query<Project>(`
INSERT INTO projects (
user_id, name, slug, workspace, product_name, product_vision,
status, current_phase, phase_status, phase_data
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING *
`, [
data.user_id, data.name, data.slug, data.workspace, data.product_name,
data.product_vision, data.status || 'active', data.current_phase || 'collection',
data.phase_status || 'not_started', JSON.stringify(data.phase_data || {})
]);
return result[0];
}
export async function getProject(id: number): Promise<Project | null> {
return await queryOne<Project>('SELECT * FROM projects WHERE id = $1', [id]);
}
export async function getUserProjects(userId: number): Promise<Project[]> {
return await query<Project>('SELECT * FROM projects WHERE user_id = $1 ORDER BY updated_at DESC', [userId]);
}
export async function updateProject(id: number, data: Partial<Project>): Promise<Project | null> {
const fields = Object.keys(data).filter(k => !['id', 'created_at', 'updated_at'].includes(k));
const values = fields.map((_, i) => `$${i + 2}`);
const setClause = fields.map((f, i) => `${f} = ${values[i]}`).join(', ');
const fieldValues = fields.map(f => {
const val = (data as any)[f];
return (f === 'phase_data' && typeof val === 'object') ? JSON.stringify(val) : val;
});
const result = await query<Project>(`
UPDATE projects
SET ${setClause}, updated_at = NOW()
WHERE id = $1
RETURNING *
`, [id, ...fieldValues]);
return result[0] || null;
}
// ==================================================
// Session operations (replaces Firebase + Railway logging)
// ==================================================
export interface Session {
id: number;
session_id: string;
project_id?: number;
user_id: number;
started_at: Date;
ended_at?: Date;
duration_minutes: number;
workspace_path?: string;
workspace_name?: string;
conversation: any[];
total_tokens: number;
estimated_cost_usd: number;
model: string;
summary?: string;
}
export async function createSession(data: {
session_id: string;
project_id?: number;
user_id: number;
workspace_path?: string;
workspace_name?: string;
model?: string;
}): Promise<Session> {
const result = await query<Session>(`
INSERT INTO sessions (session_id, project_id, user_id, workspace_path, workspace_name, model)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *
`, [data.session_id, data.project_id, data.user_id, data.workspace_path, data.workspace_name, data.model || 'unknown']);
return result[0];
}
export async function getSession(sessionId: string): Promise<Session | null> {
return await queryOne<Session>('SELECT * FROM sessions WHERE session_id = $1', [sessionId]);
}
export async function getProjectSessions(projectId: number): Promise<Session[]> {
return await query<Session>(
'SELECT * FROM sessions WHERE project_id = $1 ORDER BY started_at DESC',
[projectId]
);
}
export async function updateSession(sessionId: string, data: {
conversation?: any[];
total_tokens?: number;
estimated_cost_usd?: number;
ended_at?: Date;
duration_minutes?: number;
summary?: string;
}): Promise<Session | null> {
const fields = Object.keys(data);
const values = fields.map((_, i) => `$${i + 2}`);
const setClause = fields.map((f, i) => `${f} = ${values[i]}`).join(', ');
const fieldValues = fields.map(f => {
const val = (data as any)[f];
return (f === 'conversation' && typeof val === 'object') ? JSON.stringify(val) : val;
});
const result = await query<Session>(`
UPDATE sessions
SET ${setClause}, last_updated = NOW()
WHERE session_id = $1
RETURNING *
`, [sessionId, ...fieldValues]);
return result[0] || null;
}
// ==================================================
// Analysis operations
// ==================================================
export interface Analysis {
id: number;
project_id: number;
type: string;
summary: string;
tech_stack: string[];
features: string[];
raw_data: any;
created_at: Date;
}
export async function createAnalysis(data: Omit<Analysis, 'id' | 'created_at'>): Promise<Analysis> {
const result = await query<Analysis>(`
INSERT INTO analyses (project_id, type, summary, tech_stack, features, raw_data)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *
`, [
data.project_id, data.type, data.summary,
JSON.stringify(data.tech_stack),
JSON.stringify(data.features),
JSON.stringify(data.raw_data)
]);
return result[0];
}
export async function getProjectAnalyses(projectId: number): Promise<Analysis[]> {
return await query<Analysis>(
'SELECT * FROM analyses WHERE project_id = $1 ORDER BY created_at DESC',
[projectId]
);
}
// ==================================================
// Health check
// ==================================================
export async function checkConnection(): Promise<boolean> {
try {
await query('SELECT 1');
return true;
} catch (error) {
console.error('Database connection failed:', error);
return false;
}
}