318 lines
9.3 KiB
TypeScript
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;
|
|
}
|
|
}
|