/** * MCP Outline PostgreSQL - PostgreSQL Client * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 */ import { Pool, PoolConfig, QueryResult, QueryResultRow } from 'pg'; import { DatabaseConfig } from './config/database.js'; import { logger } from './utils/logger.js'; export class PgClient { private pool: Pool; private isConnected: boolean = false; constructor(config: DatabaseConfig) { const poolConfig: PoolConfig = config.connectionString ? { connectionString: config.connectionString, max: config.max, idleTimeoutMillis: config.idleTimeoutMillis, connectionTimeoutMillis: config.connectionTimeoutMillis } : { host: config.host, port: config.port, user: config.user, password: config.password, database: config.database, ssl: config.ssl ? { rejectUnauthorized: false } : false, max: config.max, idleTimeoutMillis: config.idleTimeoutMillis, connectionTimeoutMillis: config.connectionTimeoutMillis }; this.pool = new Pool(poolConfig); // Handle pool errors this.pool.on('error', (err) => { logger.error('Unexpected PostgreSQL pool error', { error: err.message }); }); } /** * Get the underlying pool for direct access */ getPool(): Pool { return this.pool; } /** * Test database connection */ async testConnection(): Promise { let client = null; try { client = await this.pool.connect(); await client.query('SELECT 1'); this.isConnected = true; logger.info('PostgreSQL connection successful'); return true; } catch (error) { logger.error('PostgreSQL connection failed', { error: error instanceof Error ? error.message : String(error) }); this.isConnected = false; return false; } finally { if (client) { client.release(); } } } /** * Execute a query with parameters */ async query(sql: string, params?: any[]): Promise { const start = Date.now(); try { const result = await this.pool.query(sql, params); const duration = Date.now() - start; logger.debug('Query executed', { sql: sql.substring(0, 100), duration, rowCount: result.rowCount }); return result.rows; } catch (error) { const duration = Date.now() - start; logger.error('Query failed', { sql: sql.substring(0, 100), duration, error: error instanceof Error ? error.message : String(error) }); throw error; } } /** * Execute a query and return the full result */ async queryRaw(sql: string, params?: any[]): Promise> { return this.pool.query(sql, params); } /** * Execute a query and return a single row */ async queryOne(sql: string, params?: any[]): Promise { const rows = await this.query(sql, params); return rows.length > 0 ? rows[0] : null; } /** * Execute multiple queries in a transaction */ async transaction(callback: (client: any) => Promise): Promise { const client = await this.pool.connect(); try { await client.query('BEGIN'); const result = await callback(client); await client.query('COMMIT'); return result; } catch (error) { try { await client.query('ROLLBACK'); } catch (rollbackError) { logger.error('Rollback failed', { error: rollbackError instanceof Error ? rollbackError.message : String(rollbackError), }); } throw error; } finally { client.release(); } } /** * Close the pool */ async close(): Promise { await this.pool.end(); this.isConnected = false; logger.info('PostgreSQL pool closed'); } /** * Check if connected */ isPoolConnected(): boolean { return this.isConnected; } } // Export a singleton factory let instance: PgClient | null = null; export function createPgClient(config: DatabaseConfig): PgClient { if (!instance) { instance = new PgClient(config); } return instance; } export function getPgClient(): PgClient | null { return instance; }