const mysql = require("mysql2/promise"); const { Client } = require("ssh2"); const { Pool } = require('pg'); const dotenv = require("dotenv"); const path = require("path"); // Helper function to setup SSH tunnel async function setupSshTunnel(sshConfig) { return new Promise((resolve, reject) => { const ssh = new Client(); ssh.on('error', (err) => { console.error('SSH connection error:', err); }); ssh.on('end', () => { console.log('SSH connection ended normally'); }); ssh.on('close', () => { console.log('SSH connection closed'); }); ssh .on("ready", () => { ssh.forwardOut( "127.0.0.1", 0, sshConfig.prodDbConfig.host, sshConfig.prodDbConfig.port, async (err, stream) => { if (err) reject(err); resolve({ ssh, stream }); } ); }) .connect(sshConfig.ssh); }); } // Helper function to setup database connections async function setupConnections(sshConfig) { const tunnel = await setupSshTunnel(sshConfig); // Setup MySQL connection for production const prodConnection = await mysql.createConnection({ ...sshConfig.prodDbConfig, stream: tunnel.stream, }); // Detect MySQL server timezone and calculate correction for the driver timezone mismatch. // The mysql2 driver is configured with timezone: '-05:00' (EST), but the MySQL server // may be in a different timezone (e.g., America/Chicago = CST/CDT). When the driver // formats a JS Date as EST and MySQL interprets it in its own timezone, DATETIME // comparisons can be off. This correction adjusts Date objects before they're passed // to MySQL queries so the formatted string matches the server's local time. const [[{ utcDiffSec }]] = await prodConnection.query( "SELECT TIMESTAMPDIFF(SECOND, NOW(), UTC_TIMESTAMP()) as utcDiffSec" ); const mysqlOffsetMs = -utcDiffSec * 1000; // MySQL UTC offset in ms (e.g., -21600000 for CST) const driverOffsetMs = -5 * 3600 * 1000; // Driver's -05:00 in ms (-18000000) const tzCorrectionMs = driverOffsetMs - mysqlOffsetMs; // CST (winter): -18000000 - (-21600000) = +3600000 (1 hour correction needed) // CDT (summer): -18000000 - (-18000000) = 0 (no correction needed) if (tzCorrectionMs !== 0) { console.log(`MySQL timezone correction: ${tzCorrectionMs / 1000}s (server offset: ${utcDiffSec}s from UTC)`); } /** * Adjusts a Date/timestamp for the mysql2 driver timezone mismatch before * passing it as a query parameter to MySQL. This ensures that the string * mysql2 generates matches the timezone that DATETIME values are stored in. */ function adjustDateForMySQL(date) { if (!date || tzCorrectionMs === 0) return date; const d = date instanceof Date ? date : new Date(date); return new Date(d.getTime() - tzCorrectionMs); } prodConnection.adjustDateForMySQL = adjustDateForMySQL; // Setup PostgreSQL connection pool for local const localPool = new Pool(sshConfig.localDbConfig); // Test the PostgreSQL connection try { const client = await localPool.connect(); await client.query('SELECT NOW()'); client.release(); console.log('PostgreSQL connection successful'); } catch (err) { console.error('PostgreSQL connection error:', err); throw err; } // Create a wrapper for the PostgreSQL pool to match MySQL interface const localConnection = { _client: null, _transactionActive: false, query: async (text, params) => { // If we're not in a transaction, use the pool directly if (!localConnection._transactionActive) { const client = await localPool.connect(); try { const result = await client.query(text, params); return [result]; } finally { client.release(); } } // If we're in a transaction, use the dedicated client if (!localConnection._client) { throw new Error('No active transaction client'); } const result = await localConnection._client.query(text, params); return [result]; }, beginTransaction: async () => { if (localConnection._transactionActive) { throw new Error('Transaction already active'); } localConnection._client = await localPool.connect(); await localConnection._client.query('BEGIN'); localConnection._transactionActive = true; }, commit: async () => { if (!localConnection._transactionActive) { throw new Error('No active transaction to commit'); } await localConnection._client.query('COMMIT'); localConnection._client.release(); localConnection._client = null; localConnection._transactionActive = false; }, rollback: async () => { if (!localConnection._transactionActive) { throw new Error('No active transaction to rollback'); } await localConnection._client.query('ROLLBACK'); localConnection._client.release(); localConnection._client = null; localConnection._transactionActive = false; }, end: async () => { if (localConnection._client) { localConnection._client.release(); localConnection._client = null; } await localPool.end(); } }; return { prodConnection, localConnection, tunnel }; } // Helper function to close connections async function closeConnections(connections) { const { ssh, prodConnection, localConnection } = connections; try { if (prodConnection) await prodConnection.end(); if (localConnection) await localConnection.end(); // Wait a bit for any pending data to be written before closing SSH await new Promise(resolve => setTimeout(resolve, 100)); if (ssh) { ssh.on('close', () => { console.log('SSH connection closed cleanly'); }); ssh.end(); } } catch (err) { console.error('Error during cleanup:', err); } } module.exports = { setupConnections, closeConnections };