197 lines
6.4 KiB
JavaScript
197 lines
6.4 KiB
JavaScript
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)
|
|
// Driver offset from the actual config (e.g. '-06:00' in winter, '-05:00'
|
|
// in summer — set dynamically in import-from-prod.js).
|
|
const tzMatch = /^([+-])(\d{2}):(\d{2})$/.exec(sshConfig.prodDbConfig.timezone || '');
|
|
const driverOffsetMs = tzMatch
|
|
? (tzMatch[1] === '-' ? -1 : 1) * (Number(tzMatch[2]) * 3600 + Number(tzMatch[3]) * 60) * 1000
|
|
: -5 * 3600 * 1000;
|
|
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.
|
|
// Pin the session timezone to business time (see docs/TIME.md) so import
|
|
// writes and any CURRENT_DATE/NOW()::date logic mean Chicago dates.
|
|
const localPool = new Pool({
|
|
options: '-c TimeZone=America/Chicago',
|
|
...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
|
|
};
|