Files
otrs-turbo/db.js
T

198 lines
7.1 KiB
JavaScript

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;