import pg from 'pg'; import dotenv from 'dotenv'; dotenv.config(); const { Pool } = pg; // Configuración flexible para PostgreSQL (DATABASE_URL o variables individuales) const connectionString = process.env.DATABASE_URL; let pool: pg.Pool | null = null; let isPostgresConnected = false; let inMemoryVotes: Record = {}; if (connectionString || process.env.POSTGRES_HOST || process.env.PGHOST) { const sslConfig = process.env.NODE_ENV === 'production' && process.env.DATABASE_SSL === 'true' ? { rejectUnauthorized: false } : false; pool = new Pool( connectionString ? { connectionString, ssl: sslConfig, max: 20, idleTimeoutMillis: 30000, connectionTimeoutMillis: 5000, } : { host: process.env.POSTGRES_HOST || process.env.PGHOST || 'localhost', port: parseInt(process.env.POSTGRES_PORT || process.env.PGPORT || '5432', 10), user: process.env.POSTGRES_USER || process.env.PGUSER || 'postgres', password: process.env.POSTGRES_PASSWORD || process.env.PGPASSWORD || 'postgres', database: process.env.POSTGRES_DB || process.env.PGDATABASE || 'encuesta_e3', ssl: sslConfig, max: 20, idleTimeoutMillis: 30000, connectionTimeoutMillis: 5000, } ); pool.on('error', (err) => { console.error('⚠️ Error inesperado en el pool de PostgreSQL:', err.message); }); } /** * Inicializa la tabla de votos si no existe y verifica la conexión. */ export async function initDatabase(): Promise { if (!pool) { console.warn('⚠️ No se proporcionó configuración de PostgreSQL. Usando almacenamiento en memoria.'); isPostgresConnected = false; return false; } try { const client = await pool.connect(); try { await client.query(` CREATE TABLE IF NOT EXISTS votes ( voter VARCHAR(255) PRIMARY KEY, candidate VARCHAR(255) NOT NULL, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); `); await client.query(` CREATE INDEX IF NOT EXISTS idx_votes_candidate ON votes(candidate); `); isPostgresConnected = true; console.log('✅ Conexión con PostgreSQL establecida exitosamente y schema verificado.'); return true; } finally { client.release(); } } catch (error) { console.error('❌ Error conectando a PostgreSQL durante la inicialización:', (error as Error).message); console.warn('⚠️ La aplicación continuará funcionando en modo híbrido/resiliente.'); isPostgresConnected = false; return false; } } /** * Obtiene todos los votos registrados (mapa de votante -> candidato) */ export async function getAllVotes(): Promise> { if (pool && isPostgresConnected) { try { const res = await pool.query('SELECT voter, candidate FROM votes ORDER BY updated_at ASC'); const votes: Record = {}; for (const row of res.rows) { if (row.voter && row.candidate) { votes[String(row.voter).trim().toUpperCase()] = String(row.candidate).trim().toUpperCase(); } } // Actualizar réplica en memoria inMemoryVotes = { ...votes }; return votes; } catch (err) { console.error('Error al consultar PostgreSQL getAllVotes:', (err as Error).message); return inMemoryVotes; } } return inMemoryVotes; } /** * Inserta o actualiza un voto atómicamente (UPSERT) */ export async function castVote(voter: string, candidate: string): Promise<{ voter: string; candidate: string; isNew: boolean }> { const normVoter = voter.trim().toUpperCase(); const normCandidate = candidate.trim().toUpperCase(); if (!normVoter || !normCandidate) { throw new Error('Nombre de votante y candidato son requeridos.'); } if (pool && isPostgresConnected) { try { const query = ` INSERT INTO votes (voter, candidate, updated_at) VALUES ($1, $2, CURRENT_TIMESTAMP) ON CONFLICT (voter) DO UPDATE SET candidate = EXCLUDED.candidate, updated_at = CURRENT_TIMESTAMP RETURNING (xmax = 0) AS is_new, voter, candidate; `; const res = await pool.query(query, [normVoter, normCandidate]); const row = res.rows[0]; inMemoryVotes[normVoter] = normCandidate; return { voter: row.voter, candidate: row.candidate, isNew: Boolean(row.is_new), }; } catch (err) { console.error('Error al insertar voto en PostgreSQL:', (err as Error).message); throw err; } } // Fallback memoria const isNew = !inMemoryVotes[normVoter]; inMemoryVotes[normVoter] = normCandidate; return { voter: normVoter, candidate: normCandidate, isNew }; } /** * Elimina un voto (para reseteo administrativo) */ export async function deleteVote(voter: string): Promise { const normVoter = voter.trim().toUpperCase(); if (!normVoter) { throw new Error('Nombre de votante requerido para restablecer.'); } if (pool && isPostgresConnected) { try { const res = await pool.query('DELETE FROM votes WHERE UPPER(voter) = $1', [normVoter]); delete inMemoryVotes[normVoter]; return (res.rowCount ?? 0) > 0; } catch (err) { console.error('Error al eliminar voto en PostgreSQL:', (err as Error).message); throw err; } } const existed = Boolean(inMemoryVotes[normVoter]); delete inMemoryVotes[normVoter]; return existed; } /** * Elimina todos los votos (para vaciado/reset total de pruebas) */ export async function deleteAllVotes(): Promise { if (pool && isPostgresConnected) { try { const res = await pool.query('DELETE FROM votes'); inMemoryVotes = {}; return res.rowCount ?? 0; } catch (err) { console.error('Error al vaciar votos en PostgreSQL:', (err as Error).message); throw err; } } const count = Object.keys(inMemoryVotes).length; inMemoryVotes = {}; return count; } /** * Diagnóstico de salud de la base de datos */ export async function getDatabaseHealth(): Promise<{ connected: boolean; provider: string; error?: string }> { if (!pool) { return { connected: false, provider: 'in-memory-fallback' }; } try { const res = await pool.query('SELECT NOW() as now, COUNT(*) as count FROM votes'); return { connected: true, provider: 'postgresql', }; } catch (err) { return { connected: false, provider: 'postgresql-error', error: (err as Error).message, }; } }