const { Pool, types } = require('pg'); // Override parser for TIMESTAMP WITHOUT TIME ZONE (type 1114) to return Date in UTC timezone types.setTypeParser(1114, function(stringValue) { if (!stringValue) return null; // If it already has a timezone indicator or 'Z', parse normally if (stringValue.endsWith('Z') || stringValue.includes('+') || stringValue.includes('-')) { return new Date(stringValue); } // Standard OTRS timestamps are stored as UTC without offset (YYYY-MM-DD HH:mm:ss). // Appending 'Z' tells JS engine to parse as UTC instead of local time. return new Date(stringValue.replace(' ', 'T') + 'Z'); }); const dbType = (process.env.DB_TYPE || 'postgres').toLowerCase(); let pool; if (dbType === 'mysql' || dbType === 'mariadb') { const mysql = require('mysql2/promise'); // Query translation helper for MySQL/MariaDB compatibility function translateQuery(sql, params = []) { let translatedSql = sql; let translatedParams = [...params]; // 1. Replace Postgres placeholders ($1, $2, ...) with MySQL placeholders (?) // Reorder and duplicate parameters to match the sequence of ? placeholders const placeholders = [...translatedSql.matchAll(/\$([0-9]+)/g)]; if (placeholders.length > 0) { const newParams = []; for (const match of placeholders) { const index = parseInt(match[1], 10) - 1; newParams.push(params[index]); } translatedParams = newParams; translatedSql = translatedSql.replace(/\$[0-9]+/g, '?'); } // 2. ILIKE -> LIKE (MySQL LIKE is case-insensitive by default) translatedSql = translatedSql.replace(/\bILIKE\b/gi, 'LIKE'); // 3. PostgreSQL string concatenation '||' -> CONCAT(...) in ticket number generator translatedSql = translatedSql.replace(/md5\(random\(\)::text\s*\|\|\s*clock_timestamp\(\)::text\)/gi, 'MD5(CONCAT(RAND(), NOW()))'); // 4. EXTRACT(EPOCH FROM NOW()) -> UNIX_TIMESTAMP() translatedSql = translatedSql.replace(/EXTRACT\(EPOCH\s+FROM\s+NOW\(\)\)::INTEGER/gi, 'UNIX_TIMESTAMP()'); translatedSql = translatedSql.replace(/EXTRACT\(EPOCH\s+FROM\s+NOW\(\)\)/gi, 'UNIX_TIMESTAMP()'); // 5. date_trunc('week', CURRENT_DATE) -> DATE_SUB(CURRENT_DATE, INTERVAL WEEKDAY(CURRENT_DATE) DAY) translatedSql = translatedSql.replace(/date_trunc\('week',\s*CURRENT_DATE\)/gi, 'DATE_SUB(CURRENT_DATE, INTERVAL WEEKDAY(CURRENT_DATE) DAY)'); // 6. Transaction commands if (translatedSql.trim().toUpperCase() === 'BEGIN') { translatedSql = 'START TRANSACTION'; } // 7. RETURNING clauses (MySQL doesn't support them) let returningId = false; let returningCounter = false; let returningIdTn = false; const returningMatch = translatedSql.match(/\bRETURNING\s+(.+)$/i); if (returningMatch) { const fields = returningMatch[1].trim().toLowerCase(); if (fields === 'id') { returningId = true; } else if (fields === 'counter') { returningCounter = true; } else if (fields === 'id, tn' || fields === 'id,tn') { returningIdTn = true; } translatedSql = translatedSql.replace(/\bRETURNING\s+.+$/i, ''); } // 8. Subquery replacement to avoid MySQL "target table twice" error in counter insert translatedSql = translatedSql.replace(/SELECT\s+MAX\(counter\)\s+FROM\s+ticket_number_counter/gi, 'SELECT MAX(counter) FROM (SELECT counter FROM ticket_number_counter) AS tmp_counter_val'); // 9. Remove Postgres-specific casts translatedSql = translatedSql.replace(/::text/gi, ''); translatedSql = translatedSql.replace(/::integer/gi, ''); translatedSql = translatedSql.replace(/::bigint/gi, ''); translatedSql = translatedSql.replace(/::numeric/gi, ''); return { translatedSql, translatedParams, postProcess: async (result, connection) => { const [rowsOrHeader] = result; let rows = []; let rowCount = 0; if (Array.isArray(rowsOrHeader)) { rows = rowsOrHeader; rowCount = rows.length; } else if (rowsOrHeader) { rowCount = rowsOrHeader.affectedRows || 0; if (returningId) { rows = [{ id: rowsOrHeader.insertId }]; } else if (returningIdTn) { rows = [{ id: rowsOrHeader.insertId, tn: params[0] }]; } else if (returningCounter) { const [counterResult] = await connection.query('SELECT MAX(counter) AS counter FROM ticket_number_counter'); rows = [{ counter: counterResult[0] ? counterResult[0].counter : 1 }]; } } return { rows, rowCount, }; }, }; } class CompatPool { constructor() { this.mysqlPool = mysql.createPool({ host: process.env.DB_HOST || '127.0.0.1', port: parseInt(process.env.DB_PORT, 10) || 3306, database: process.env.DB_NAME || 'otrs', user: process.env.DB_USER || 'root', password: process.env.DB_PASSWORD || '', connectionLimit: 20, idleTimeout: 30000, connectTimeout: 5000, timezone: '+00:00', // Parse dates from DB as UTC }); // Ensure the session timezone is UTC for database functions like NOW() this.mysqlPool.on('connection', (connection) => { connection.query("SET time_zone = '+00:00'", (err) => { if (err) { console.error('[DB] Errore nell\'impostazione della time_zone UTC per MariaDB/MySQL:', err); } }); }); } async query(sql, params = []) { const { translatedSql, translatedParams, postProcess } = translateQuery(sql, params); const conn = await this.mysqlPool.getConnection(); try { const res = await conn.query(translatedSql, translatedParams); return await postProcess(res, conn); } finally { conn.release(); } } async connect() { const conn = await this.mysqlPool.getConnection(); return { query: async (sql, params = []) => { const { translatedSql, translatedParams, postProcess } = translateQuery(sql, params); const res = await conn.query(translatedSql, translatedParams); return await postProcess(res, conn); }, release: () => { conn.release(); }, }; } async end() { await this.mysqlPool.end(); } } pool = new CompatPool(); } else { // Standard PostgreSQL Pool pool = new Pool({ host: process.env.DB_HOST || '127.0.0.1', port: parseInt(process.env.DB_PORT, 10) || 5432, database: process.env.DB_NAME || 'otrs', user: process.env.DB_USER || 'otrs', password: process.env.DB_PASSWORD || '', max: 20, idleTimeoutMillis: 30000, connectionTimeoutMillis: 5000, options: '-c timezone=UTC', // Ensure connection timezone is UTC }); // Ensure connection timezone is UTC via query fallback pool.on('connect', (client) => { client.query("SET TIME ZONE 'UTC'").catch(err => { console.error('[DB] Errore nell\'impostazione della timezone UTC per Postgres:', err); }); }); pool.on('error', (err) => { console.error('Unexpected error on idle client', err); }); } module.exports = pool;