Ajustes 2306

This commit is contained in:
2026-06-23 12:42:14 +00:00
parent e7aa5f5333
commit cf720c37a1
18 changed files with 471 additions and 850 deletions
+153 -508
View File
@@ -5,13 +5,11 @@
*
* 1) CONFIGURAÇÃO — aliases, drivers, conexões estáticas e customizadas
* 2) DRIVER FIREBIRD
* 3) DRIVER POSTGRES (pool + tradutor de SQL + schema)
* 4) DISPATCHER — API pública usada por todo o sistema
* 3) DISPATCHER — API pública usada por todo o sistema
*
* Suporta múltiplos drivers ('postgres' padrão, 'firebird' legado). Cada
* conexão declara o campo `driver`. Conexões adicionadas em runtime (via API
* /api/databases) são persistidas em databases_custom.json (ao lado deste
* arquivo) — é um arquivo de DADOS, não de código.
* Suporte ao banco Firebird. Cada conexão declara o campo `driver`.
* Conexões adicionadas em runtime (via API /api/databases) são persistidas
* em databases_custom.json (ao lado deste arquivo).
*/
const path = require('path');
const fs = require('fs');
@@ -31,22 +29,11 @@ try {
console.error('[Databases] Erro ao carregar databases_custom.json:', e.message);
}
const DRIVERS = ['postgres', 'firebird'];
const DEFAULT_DRIVER = (process.env.DB_DRIVER || 'postgres').toLowerCase();
const DRIVERS = ['firebird'];
const DEFAULT_DRIVER = 'firebird';
// Valores padrão por driver
// Valores padrão para Firebird
const DRIVER_DEFAULTS = {
postgres: {
host: '127.0.0.1',
port: 5432,
user: 'postgres',
password: 'postgres',
schema: 'public',
ssl: false,
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 10000,
},
firebird: {
host: 'localhost',
port: 3050,
@@ -61,22 +48,10 @@ const DRIVER_DEFAULTS = {
/**
* Conexões estáticas.
* - `novo_local` → PostgreSQL (conexão principal)
* - `firebird_local`→ Firebird (informe o CAMINHO do arquivo .FDB em `database`)
* Sobrescrevíveis pelo .env (PG_* para Postgres, DB_* para Firebird).
* - `novo_local` → Firebird (informe o CAMINHO do arquivo .FDB em `database`)
* Sobrescrevível pelo .env (DB_*).
*/
const databases = {
novo: {
driver: 'postgres',
host: process.env.PG_HOST || 'db.assantos.app.br',
port: parseInt(process.env.PG_PORT, 10) || 443,
user: process.env.PG_USER || 'atendimento_user',
password: process.env.PG_PASSWORD || 'PSSQDfPHilIZF1mbGzuR2RrEh0cTEGaF',
database: process.env.PG_DATABASE || 'atendimento',
schema: process.env.PG_SCHEMA || 'atendi',
ssl: process.env.PG_SSL === 'true' ? true : process.env.PG_SSL === 'false' ? false : (parseInt(process.env.PG_PORT, 10) === 443 ? { rejectUnauthorized: false } : false),
},
novo_local: {
driver: 'firebird',
host: process.env.DB_HOST || 'localhost',
@@ -105,30 +80,10 @@ function salvarCustomDatabases() {
/** Normaliza uma config bruta aplicando o driver e seus defaults. */
function normalize(raw) {
const driver = DRIVERS.includes((raw.driver || '').toLowerCase())
? raw.driver.toLowerCase()
: DEFAULT_DRIVER;
const d = DRIVER_DEFAULTS[driver];
const d = DRIVER_DEFAULTS['firebird'];
if (driver === 'postgres') {
return {
driver,
host: raw.host || d.host,
port: raw.port || d.port,
database: raw.database,
user: raw.user || d.user,
password: raw.password || d.password,
schema: (raw.schema || d.schema || 'public'),
ssl: raw.ssl !== undefined ? raw.ssl : d.ssl,
max: raw.max || d.max,
idleTimeoutMillis: raw.idleTimeoutMillis || d.idleTimeoutMillis,
connectionTimeoutMillis: raw.connectionTimeoutMillis || d.connectionTimeoutMillis,
};
}
// firebird
return {
driver,
driver: 'firebird',
host: raw.host || d.host,
port: raw.port || d.port,
database: raw.database,
@@ -178,14 +133,10 @@ function addDatabase(alias, config) {
throw new Error('Alias e database são obrigatórios.');
}
var aliasLower = alias.toLowerCase().replace(/[^a-z0-9_]/g, '_');
var driver = DRIVERS.includes((config.driver || '').toLowerCase())
? config.driver.toLowerCase()
: DEFAULT_DRIVER;
var stored = { driver: driver };
['host', 'port', 'database', 'schema', 'user', 'password', 'ssl',
'encoding', 'lowercase_keys', 'role', 'pageSize', 'wireCrypt',
'max', 'idleTimeoutMillis', 'connectionTimeoutMillis']
var stored = { driver: 'firebird' };
['host', 'port', 'database', 'user', 'password',
'encoding', 'lowercase_keys', 'role', 'pageSize', 'wireCrypt']
.forEach(function (k) {
if (config[k] !== undefined && config[k] !== null && config[k] !== '') {
stored[k] = config[k];
@@ -208,478 +159,172 @@ function removeDatabase(alias) {
// ============================================================
// 2) DRIVER FIREBIRD
// ============================================================
const firebirdDriver = (() => {
const Firebird = require('node-firebird');
const Firebird = require('node-firebird');
function readBlob(blobFunc) {
return new Promise(function (resolve, reject) {
if (typeof blobFunc !== 'function') return resolve(blobFunc);
blobFunc(function (err, name, emitter) {
if (err) return reject(err);
if (!emitter || typeof emitter.on !== 'function') return resolve(null);
var chunks = [];
var total = 0;
emitter.on('data', function (c) { chunks.push(c); total += c.length; });
emitter.on('end', function () { resolve(Buffer.concat(chunks, total)); });
emitter.on('error', reject);
});
function readBlob(blobFunc) {
return new Promise(function (resolve, reject) {
if (typeof blobFunc !== 'function') return resolve(blobFunc);
blobFunc(function (err, name, emitter) {
if (err) return reject(err);
if (!emitter || typeof emitter.on !== 'function') return resolve(null);
var chunks = [];
var total = 0;
emitter.on('data', function (c) { chunks.push(c); total += c.length; });
emitter.on('end', function () { resolve(Buffer.concat(chunks, total)); });
emitter.on('error', reject);
});
}
});
}
function query(config, sql, params = []) {
return new Promise((resolve, reject) => {
Firebird.attach(config, (err, db) => {
if (err) return reject(new Error(`Erro ao conectar (firebird): ${err.message}`));
var allRows = [];
db.sequentially(sql, params, function (row) {
var keys = Object.keys(row);
var blobPromises = keys.map(function (key) {
var val = row[key];
if (typeof val === 'function') {
return readBlob(val).then(function (data) { row[key] = data; });
}
return Promise.resolve();
});
return Promise.all(blobPromises).then(function () { allRows.push(row); });
}, function (queryErr) {
db.detach();
if (queryErr) return reject(new Error(`Erro na consulta (firebird): ${queryErr.message}`));
resolve(allRows);
});
});
});
}
function execute(config, sql, params = []) {
return new Promise((resolve, reject) => {
Firebird.attach(config, (err, db) => {
if (err) return reject(new Error(`Erro ao conectar (firebird): ${err.message}`));
db.transaction(Firebird.ISOLATION_READ_COMMITTED, (transErr, transaction) => {
if (transErr) {
db.detach();
return reject(new Error(`Erro ao iniciar transação (firebird): ${transErr.message}`));
function firebirdQuery(config, sql, params = []) {
return new Promise((resolve, reject) => {
Firebird.attach(config, (err, db) => {
if (err) return reject(new Error(`Erro ao conectar (firebird): ${err.message}`));
var allRows = [];
db.sequentially(sql, params, function (row) {
var keys = Object.keys(row);
var blobPromises = keys.map(function (key) {
var val = row[key];
if (typeof val === 'function') {
return readBlob(val).then(function (data) { row[key] = data; });
}
transaction.query(sql, params, (queryErr, result) => {
if (queryErr) {
transaction.rollback();
db.detach();
return reject(new Error(`Erro na execução (firebird): ${queryErr.message}`));
}
transaction.commit((commitErr) => {
db.detach();
if (commitErr) return reject(new Error(`Erro ao commitar (firebird): ${commitErr.message}`));
resolve({ affectedRows: result ? result.length : 0, result });
});
return Promise.resolve();
});
return Promise.all(blobPromises).then(function () { allRows.push(row); });
}, function (queryErr) {
db.detach();
if (queryErr) return reject(new Error(`Erro na consulta (firebird): ${queryErr.message}`));
resolve(allRows);
});
});
});
}
function firebirdExecute(config, sql, params = []) {
return new Promise((resolve, reject) => {
Firebird.attach(config, (err, db) => {
if (err) return reject(new Error(`Erro ao conectar (firebird): ${err.message}`));
db.transaction(Firebird.ISOLATION_READ_COMMITTED, (transErr, transaction) => {
if (transErr) {
db.detach();
return reject(new Error(`Erro ao iniciar transação (firebird): ${transErr.message}`));
}
transaction.query(sql, params, (queryErr, result) => {
if (queryErr) {
transaction.rollback();
db.detach();
return reject(new Error(`Erro na execução (firebird): ${queryErr.message}`));
}
transaction.commit((commitErr) => {
db.detach();
if (commitErr) return reject(new Error(`Erro ao commitar (firebird): ${commitErr.message}`));
resolve({ affectedRows: result ? result.length : 0, result });
});
});
});
});
}
});
}
async function testConnection(config) {
await query(config, 'SELECT 1 FROM RDB$DATABASE');
return true;
}
async function firebirdTestConnection(config) {
await firebirdQuery(config, 'SELECT 1 FROM RDB$DATABASE');
return true;
}
async function listTables(config) {
const rows = await query(config, `
SELECT TRIM(RDB$RELATION_NAME) AS TABLE_NAME
FROM RDB$RELATIONS
WHERE RDB$SYSTEM_FLAG = 0 AND RDB$RELATION_TYPE = 0
ORDER BY RDB$RELATION_NAME
`);
return rows.map((r) => (r.TABLE_NAME || '').trim()).filter(Boolean);
}
async function firebirdListTables(config) {
const rows = await firebirdQuery(config, `
SELECT TRIM(RDB$RELATION_NAME) AS TABLE_NAME
FROM RDB$RELATIONS
WHERE RDB$SYSTEM_FLAG = 0 AND RDB$RELATION_TYPE = 0
ORDER BY RDB$RELATION_NAME
`);
return rows.map((r) => (r.TABLE_NAME || '').trim()).filter(Boolean);
}
async function tableInfo(config, tableName) {
const rows = await query(config, `
SELECT
rf.RDB$FIELD_NAME AS COLUMN_NAME,
rf.RDB$FIELD_POSITION AS ORDINAL_POSITION,
CASE f.RDB$FIELD_TYPE
WHEN 7 THEN 'SMALLINT' WHEN 8 THEN 'INTEGER' WHEN 16 THEN 'BIGINT'
WHEN 9 THEN 'QUAD' WHEN 10 THEN 'FLOAT' WHEN 27 THEN 'DOUBLE PRECISION'
WHEN 12 THEN 'DATE' WHEN 13 THEN 'TIME' WHEN 35 THEN 'TIMESTAMP'
WHEN 37 THEN 'VARCHAR' WHEN 40 THEN 'CSTRING' WHEN 45 THEN 'BLOB_ID'
WHEN 261 THEN 'BLOB' WHEN 14 THEN 'CHAR' WHEN 41 THEN 'NUMERIC'
ELSE 'UNKNOWN'
END AS DATA_TYPE,
f.RDB$FIELD_LENGTH AS FIELD_LENGTH,
f.RDB$FIELD_SCALE AS FIELD_SCALE,
f.RDB$FIELD_PRECISION AS FIELD_PRECISION,
rf.RDB$NULL_FLAG AS NULL_FLAG
FROM RDB$RELATION_FIELDS rf
INNER JOIN RDB$FIELDS f ON rf.RDB$FIELD_SOURCE = f.RDB$FIELD_NAME
WHERE rf.RDB$RELATION_NAME = ?
ORDER BY rf.RDB$FIELD_POSITION
`, [String(tableName).toUpperCase()]);
async function firebirdTableInfo(config, tableName) {
const rows = await firebirdQuery(config, `
SELECT
rf.RDB$FIELD_NAME AS COLUMN_NAME,
rf.RDB$FIELD_POSITION AS ORDINAL_POSITION,
CASE f.RDB$FIELD_TYPE
WHEN 7 THEN 'SMALLINT' WHEN 8 THEN 'INTEGER' WHEN 16 THEN 'BIGINT'
WHEN 9 THEN 'QUAD' WHEN 10 THEN 'FLOAT' WHEN 27 THEN 'DOUBLE PRECISION'
WHEN 12 THEN 'DATE' WHEN 13 THEN 'TIME' WHEN 35 THEN 'TIMESTAMP'
WHEN 37 THEN 'VARCHAR' WHEN 40 THEN 'CSTRING' WHEN 45 THEN 'BLOB_ID'
WHEN 261 THEN 'BLOB' WHEN 14 THEN 'CHAR' WHEN 41 THEN 'NUMERIC'
ELSE 'UNKNOWN'
END AS DATA_TYPE,
f.RDB$FIELD_LENGTH AS FIELD_LENGTH,
f.RDB$FIELD_SCALE AS FIELD_SCALE,
f.RDB$FIELD_PRECISION AS FIELD_PRECISION,
rf.RDB$NULL_FLAG AS NULL_FLAG
FROM RDB$RELATION_FIELDS rf
INNER JOIN RDB$FIELDS f ON rf.RDB$FIELD_SOURCE = f.RDB$FIELD_NAME
WHERE rf.RDB$RELATION_NAME = ?
ORDER BY rf.RDB$FIELD_POSITION
`, [String(tableName).toUpperCase()]);
return rows.map((row) => ({
name: (row.COLUMN_NAME || '').trim(),
position: row.ORDINAL_POSITION,
type: (row.DATA_TYPE || '').trim(),
length: row.FIELD_LENGTH,
precision: row.FIELD_PRECISION,
scale: row.FIELD_SCALE,
nullable: row.NULL_FLAG !== 1,
}));
}
return rows.map((row) => ({
name: (row.COLUMN_NAME || '').trim(),
position: row.ORDINAL_POSITION,
type: (row.DATA_TYPE || '').trim(),
length: row.FIELD_LENGTH,
precision: row.FIELD_PRECISION,
scale: row.FIELD_SCALE,
nullable: row.NULL_FLAG !== 1,
}));
}
async function tableExists(config, tableName) {
const r = await query(config,
"SELECT COUNT(*) AS CT FROM RDB$RELATIONS WHERE RDB$RELATION_NAME = ?",
[String(tableName).toUpperCase()]);
return (r[0] && r[0].CT > 0) || false;
}
async function firebirdTableExists(config, tableName) {
const r = await firebirdQuery(config,
"SELECT COUNT(*) AS CT FROM RDB$RELATIONS WHERE RDB$RELATION_NAME = ?",
[String(tableName).toUpperCase()]);
return (r[0] && r[0].CT > 0) || false;
}
async function columnExists(config, tableName, columnName) {
const r = await query(config,
"SELECT COUNT(*) AS CT FROM RDB$RELATION_FIELDS WHERE RDB$RELATION_NAME = ? AND RDB$FIELD_NAME = ?",
[String(tableName).toUpperCase(), String(columnName).toUpperCase()]);
return (r[0] && r[0].CT > 0) || false;
}
async function firebirdColumnExists(config, tableName, columnName) {
const r = await firebirdQuery(config,
"SELECT COUNT(*) AS CT FROM RDB$RELATION_FIELDS WHERE RDB$RELATION_NAME = ? AND RDB$FIELD_NAME = ?",
[String(tableName).toUpperCase(), String(columnName).toUpperCase()]);
return (r[0] && r[0].CT > 0) || false;
}
async function close() { /* Firebird abre conexão por consulta; nada a encerrar */ }
return { query, execute, testConnection, listTables, tableInfo, tableExists, columnExists, close };
})();
async function firebirdClose() { /* Firebird abre conexão por consulta; nada a encerrar */ }
// ============================================================
// 3) DRIVER POSTGRES (pool + tradutor de SQL Firebird->PG + schema)
// 3) DISPATCHER — API pública
// ============================================================
const postgresDriver = (() => {
const pg = require('pg');
const driver = {
query: firebirdQuery,
execute: firebirdExecute,
testConnection: firebirdTestConnection,
listTables: firebirdListTables,
tableInfo: firebirdTableInfo,
tableExists: firebirdTableExists,
columnExists: firebirdColumnExists,
close: firebirdClose,
};
// COUNT()/bigint chegam como string no pg; o código histórico usa números.
pg.types.setTypeParser(20, (v) => (v === null ? null : parseInt(v, 10))); // int8 / bigint
pg.types.setTypeParser(1700, (v) => (v === null ? null : parseFloat(v))); // numeric / decimal
const pools = new Map();
function safeSchema(schema) {
const s = String(schema || 'public').trim();
return /^[A-Za-z_][A-Za-z0-9_$]*$/.test(s) ? s : 'public';
}
function poolKey(config) {
return [config.host, config.port, config.database, config.user, safeSchema(config.schema)].join('|');
}
// Schemas que o pool enxerga (configurado + public como fallback)
function schemasFor(config) {
const schema = safeSchema(config.schema);
return schema === 'public' ? ['public'] : [schema, 'public'];
}
function getEntry(config) {
const key = poolKey(config);
let entry = pools.get(key);
if (!entry) {
const schemas = schemasFor(config);
const pool = new pg.Pool({
host: config.host,
port: config.port,
database: config.database,
user: config.user,
password: config.password,
ssl: config.ssl ? { rejectUnauthorized: false } : false,
options: '-c search_path=' + schemas.join(','),
max: config.max || 10,
idleTimeoutMillis: config.idleTimeoutMillis || 30000,
connectionTimeoutMillis: config.connectionTimeoutMillis || 10000,
});
pool.on('error', (err) => console.error('[postgres] Erro inesperado no pool:', err.message));
entry = { pool, schemas, tableSet: null, loadingTables: null };
pools.set(key, entry);
}
return entry;
}
// Carrega (uma vez por pool) os nomes de tabela MAIÚSCULOS p/ decidir aspas
async function ensureTableSet(entry) {
if (entry.tableSet) return entry.tableSet;
if (!entry.loadingTables) {
entry.loadingTables = entry.pool
.query('SELECT table_name FROM information_schema.tables WHERE table_schema = ANY($1)', [entry.schemas])
.then((res) => {
entry.tableSet = new Set(res.rows.map((r) => String(r.table_name).toUpperCase()));
return entry.tableSet;
})
.catch((err) => {
console.error('[postgres] Falha ao carregar nomes de tabela:', err.message);
entry.tableSet = new Set();
return entry.tableSet;
});
}
return entry.loadingTables;
}
// ? -> $1, $2, ... (ignora literais de string)
function convertPlaceholders(sql) {
let out = '', i = 0, n = 1, inStr = false;
while (i < sql.length) {
const ch = sql[i];
if (inStr) {
out += ch;
if (ch === "'") {
if (sql[i + 1] === "'") { out += sql[i + 1]; i += 2; continue; }
inStr = false;
}
i++;
continue;
}
if (ch === "'") { inStr = true; out += ch; i++; continue; }
if (ch === '?') { out += '$' + (n++); i++; continue; }
out += ch; i++;
}
return out;
}
// Aspas nos nomes de tabela conhecidos (MAIÚSCULOS) após FROM/JOIN/INTO/UPDATE/ALTER/DROP
function quoteTableNames(sql, tableSet) {
if (!tableSet || tableSet.size === 0) return sql;
return sql.replace(
/(\b(?:FROM|JOIN|INTO|UPDATE|ALTER\s+TABLE|DROP\s+TABLE)\s+)("?)([A-Za-z_][A-Za-z0-9_$]*)("?)/gi,
(match, kw, q1, name, q2) => {
if (q1 === '"' || q2 === '"') return match;
if (tableSet.has(name.toUpperCase())) return kw + '"' + name.toUpperCase() + '"';
return match;
}
);
}
// Rede de segurança: FIRST n [SKIP m] no topo -> LIMIT/OFFSET (SQL legado)
function translateFirstSkip(sql) {
return sql.replace(
/^(\s*SELECT\s+)FIRST\s+(\d+)(?:\s+SKIP\s+(\d+))?\s+/i,
(m, sel, first, skip) => sel + ' LIMITTAIL LIMIT ' + first + (skip ? ' OFFSET ' + skip : '') + ' '
);
}
function applyLimitTail(sql) {
const marker = / LIMITTAIL( LIMIT \d+(?: OFFSET \d+)?) /;
const m = sql.match(marker);
if (!m) return sql;
return sql.replace(marker, ' ').trimEnd() + m[1];
}
// Coloca aspas em nomes de colunas MAIÚSCULOS (formato NOME_COLUNA)
// Reconhece padrao: letras_maiusculas com _ (ex: USU_CODIGO_ID, CON_NUMERO)
// Ignora SQL keywords, literais string e numeros
function quoteColumnNames(sql) {
// Lista de keywords SQL que nao devem ser colocadas em aspas
var sqlKeywords = ['AS', 'ON', 'AND', 'OR', 'NOT', 'IS', 'IN', 'NULL', 'TRUE', 'FALSE',
'ASC', 'DESC', 'DISTINCT', 'ALL', 'ANY', 'SOME', 'BETWEEN', 'LIKE', 'ILIKE',
'FIRST', 'SKIP', 'LIMIT', 'OFFSET', 'ORDER', 'GROUP', 'HAVING', 'CROSS', 'INNER',
'LEFT', 'RIGHT', 'FULL', 'OUTER', 'NATURAL', 'USING', 'EXCEPT', 'INTERSECT', 'UNION',
'COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'COALESCE', 'CAST', 'CASE', 'WHEN', 'THEN', 'ELSE',
'END', 'EXISTS', 'CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME', 'NOW', 'UPPER', 'LOWER', 'TRIM',
'ROW', 'ROWS', 'RANGE', 'FETCH', 'NEXT', 'ONLY', 'PERCENT', 'WITH', 'TIES', 'OVER', 'PARTITION',
'RETURNING', 'ACTION', 'ADD', 'ALTER', 'COLUMN', 'CONSTRAINT', 'CREATE', 'DELETE',
'DROP', 'EXECUTE', 'INSERT', 'INTO', 'TABLE', 'TRUNCATE', 'UPDATE', 'VALUES', 'WHERE',
'SET', 'SELECT', 'FROM', 'JOIN', 'PRIMARY', 'KEY', 'FOREIGN', 'REFERENCES', 'UNIQUE',
'CHECK', 'DEFAULT', 'INDEX', 'VIEW', 'MATERIALIZED', 'SCHEMA', 'DATABASE', 'GRANT',
'REVOKE', 'ABSOLUTE', 'ACTION', 'ADMIN', 'AFTER', 'AGGREGATE', 'ALSO', 'ANALYSE',
'ANALYZE', 'BEFORE', 'BEGIN', 'BINARY', 'BOTH', 'CALLED', 'CASCADE', 'CASCADED', 'CATALOG',
'CHAIN', 'CHARACTER', 'CHECKPOINT', 'CLASS', 'CLOSE', 'CLUSTER', 'COMMENT', 'COMMIT',
'COMMITTED', 'CONCURRENTLY', 'CONFIGURATION', 'CONFLICT', 'CONNECTION', 'CONSTRAINTS',
'CONTENT', 'CONTINUE', 'COPY', 'COSTS', 'CURRENT', 'CURSOR', 'CYCLE', 'DATA', 'DATABASE',
'DAY', 'DEALLOCATE', 'DEC', 'DECLARE', 'DEFERRABLE', 'DEFERRED', 'DEFINER', 'DELETE',
'DELIMITER', 'DELIMITERS', 'DEPENDS', 'DESCRIBE', 'DICTIONARY', 'DISABLE', 'DISCARD',
'DOMAIN', 'DOUBLE', 'DROP', 'EACH', 'ENABLE', 'ENCODING', 'ENCRYPTED', 'ENUM', 'ESCAPE',
'EVENT', 'EXCHANGE', 'EXCLUDE', 'EXCLUDING', 'EXCLUSIVE', 'EXECUTE', 'EXPLAIN',
'EXTENSION', 'EXTERNAL', 'FAMILY', 'FILTER', 'FIRST', 'FLOOR', 'FOLLOWING', 'FORCE',
'FORMAT', 'FORWARD', 'FREEZE', 'FUNCTION', 'FUNCTIONS', 'GLOBAL', 'GRANTED', 'GREATEST',
'GROUPING', 'GROUPS', 'HANDLER', 'HEADER', 'HOLD', 'HOUR', 'IDENTITY', 'IF', 'IMMEDIATE',
'IMMUTABLE', 'IMPLICIT', 'IMPORT', 'INCLUDING', 'INCREMENT', 'INDENT', 'INDEXES',
'INHERIT', 'INHERITS', 'INLINE', 'INPUT', 'INSENSITIVE', 'INSERTION', 'INSTEAD',
'INVOKER', 'ISOLATION', 'KEY', 'LABEL', 'LANGUAGE', 'LARGE', 'LAST', 'LEADING', 'LEAKPROOF',
'LEAST', 'LEVEL', 'LISTEN', 'LOAD', 'LOCAL', 'LOCATION', 'LOCK', 'LOCKED', 'LOGGED',
'MAPPING', 'MATCH', 'MATCHED', 'MAXVALUE', 'METHOD', 'MINUTE', 'MINVALUE', 'MODE', 'MONTH',
'MOVE', 'NAMES', 'NESTED', 'NEW', 'NEXT', 'NO', 'NONE', 'NOTHING', 'NOTIFY', 'NOWAIT',
'NULLS', 'OBJECT', 'OF', 'OFF', 'OIDS', 'OLD', 'OPERATOR', 'OPTION', 'OPTIONS', 'ORDERED',
'ORDINALITY', 'OTHERS', 'OUT', 'OVERLAY', 'OVERRIDING', 'OWNED', 'OWNER', 'PARALLEL',
'PARSER', 'PARTITION', 'PASSING', 'PASSWORD', 'PLACING', 'PLANS', 'POLICY', 'POSITION',
'PRECEDING', 'PRECISION', 'PREPARE', 'PREPARED', 'PRESERVE', 'PRIOR', 'PRIVILEGES',
'PROCEDURAL', 'PROCEDURE', 'PROCEDURES', 'PROGRAM', 'PUBLICATION', 'QUOTE', 'RANGE',
'READ', 'REASSIGN', 'RECHECK', 'RECURSIVE', 'REFERENCING', 'REFRESH', 'REINDEX',
'RELATIVE', 'RELEASE', 'RENAME', 'REPEATABLE', 'REPLACE', 'REPLICA', 'RESET',
'RESTART', 'RESTRICT', 'RETURN', 'RETURNS', 'REVOKE', 'ROLE', 'ROLLBACK', 'ROLLUP',
'ROUTINE', 'ROUTINES', 'RULE', 'SAVEPOINT', 'SCROLL', 'SEARCH', 'SECOND',
'SECURITY', 'SEQUENCE', 'SEQUENCES', 'SERIALIZABLE', 'SERVER', 'SESSION', 'SHARE',
'SHOW', 'SHUTDOWN', 'SIMPLE', 'SKIP', 'SNAPSHOT', 'SOME', 'SOURCE', 'SPACE', 'SPECIFIC',
'SPECIFICTYPE', 'SQL', 'SQLCODE', 'SQLERROR', 'SQLEXCEPTION', 'SQLSTATE', 'SQLWARNING',
'STABLE', 'STANDALONE', 'START', 'STATE', 'STATEMENT', 'STATIC', 'STATISTICS', 'STDIN',
'STDOUT', 'STORAGE', 'STRICT', 'STRIP', 'SUBSCRIPTION', 'SYSID', 'SYSTEM', 'TABLES',
'TABLESAMPLE', 'TABLESPACE', 'TEMP', 'TEMPLATE', 'TEMPORARY', 'TEXT', 'TEXT_SEARCH',
'THEN', 'TIME', 'TRANSACTION', 'TRIGGER', 'TRUNCATE', 'TRUSTED', 'TYPE', 'TYPES',
'UNBOUNDED', 'UNCOMMITTED', 'UNDER', 'UNENCRYPTED', 'UNKNOWN', 'UNLISTEN', 'UNLOGGED',
'UNTIL', 'UPDATE', 'USE', 'USER', 'USERS', 'VACUUM', 'VALID', 'VALIDATE', 'VALIDATOR',
'VALUE', 'VARIADIC', 'VARYING', 'VERBOSE', 'VERSION', 'VIEW', 'VIEWS', 'VOLATILE',
'WHITESPACE', 'WITHIN', 'WITHOUT', 'WORK', 'WRAPPER', 'WRITE', 'XML', 'XMLATTRIBUTES',
'XMLCONCAT', 'XMLELEMENT', 'XMLEXISTS', 'XMLFOREST', 'XMLNAMESPACES', 'XMLPARSE',
'XMLPI', 'XMLROOT', 'XMLSERIALIZE', 'XMLTABLE', 'YEAR', 'YES', 'ZONE'];
var kwSet = new Set(sqlKeywords.map(function(k) { return k.toUpperCase(); }));
return sql.replace(/("?)([A-Z][A-Z_0-9]{2,})("?)(?=[\s,)]|$)/g, function(match, q1, name, q2) {
if (q1 === '"' || q2 === '"') return match; // ja tem aspas
if (kwSet.has(name)) return match; // SQL keyword
if (/^\d/.test(name)) return match;
return '"' + name + '"';
});
}
function translateSql(sql, tableSet) {
let out = sql;
out = out.replace(/\bCONTAINING\s+(\?|\$\d+|'(?:[^']|'')*')/gi, "ILIKE ('%' || $1 || '%')");
out = out.replace(/\bFROM\s+RDB\$DATABASE\b/gi, '');
out = quoteTableNames(out, tableSet);
out = quoteColumnNames(out);
out = translateFirstSkip(out);
out = applyLimitTail(out);
out = convertPlaceholders(out);
return out;
}
function upperKeys(rows) {
return rows.map((row) => {
const o = {};
for (const k in row) o[k.toUpperCase()] = row[k];
return o;
});
}
async function query(config, sql, params = []) {
const entry = getEntry(config);
await ensureTableSet(entry);
const text = translateSql(sql, entry.tableSet);
try {
const res = await entry.pool.query(text, params);
return upperKeys(res.rows);
} catch (err) {
throw new Error(`Erro na consulta (postgres): ${err.message}`);
}
}
async function execute(config, sql, params = []) {
const entry = getEntry(config);
await ensureTableSet(entry);
const text = translateSql(sql, entry.tableSet);
try {
const res = await entry.pool.query(text, params);
return { affectedRows: res.rowCount || 0, result: upperKeys(res.rows || []) };
} catch (err) {
throw new Error(`Erro na execução (postgres): ${err.message}`);
}
}
async function testConnection(config) {
const entry = getEntry(config);
await entry.pool.query('SELECT 1');
return true;
}
async function listTables(config) {
const entry = getEntry(config);
const res = await entry.pool.query(
`SELECT table_name FROM information_schema.tables
WHERE table_schema = $1 AND table_type = 'BASE TABLE'
ORDER BY table_name`,
[safeSchema(config.schema)]
);
return res.rows.map((r) => r.table_name);
}
async function tableInfo(config, tableName) {
const entry = getEntry(config);
// Resolve no primeiro schema do search_path que contém a tabela
const res = await entry.pool.query(
`SELECT column_name, ordinal_position, data_type,
character_maximum_length, numeric_precision, numeric_scale, is_nullable
FROM information_schema.columns
WHERE UPPER(table_name) = UPPER($2)
AND table_schema = (
SELECT table_schema FROM information_schema.tables
WHERE UPPER(table_name) = UPPER($2) AND table_schema = ANY($1)
ORDER BY array_position($1, table_schema) LIMIT 1
)
ORDER BY ordinal_position`,
[entry.schemas, tableName]
);
return res.rows.map((row) => ({
name: row.column_name,
position: row.ordinal_position,
type: (row.data_type || '').toUpperCase(),
length: row.character_maximum_length,
precision: row.numeric_precision,
scale: row.numeric_scale,
nullable: row.is_nullable === 'YES',
}));
}
async function tableExists(config, tableName) {
const entry = getEntry(config);
await ensureTableSet(entry);
if (entry.tableSet) return entry.tableSet.has(String(tableName).toUpperCase());
const res = await entry.pool.query(
`SELECT 1 FROM information_schema.tables
WHERE table_schema = ANY($1) AND UPPER(table_name) = UPPER($2) LIMIT 1`,
[entry.schemas, tableName]
);
return res.rowCount > 0;
}
async function columnExists(config, tableName, columnName) {
const entry = getEntry(config);
const res = await entry.pool.query(
`SELECT 1 FROM information_schema.columns
WHERE table_schema = ANY($1) AND UPPER(table_name) = UPPER($2)
AND UPPER(column_name) = UPPER($3) LIMIT 1`,
[entry.schemas, tableName, columnName]
);
return res.rowCount > 0;
}
async function close() {
const all = Array.from(pools.values()).map((e) => e.pool.end().catch(() => {}));
pools.clear();
await Promise.all(all);
}
return {
query, execute, testConnection, listTables, tableInfo, tableExists, columnExists, close,
_translateSql: translateSql, // exportado para testes
};
})();
// ============================================================
// 4) DISPATCHER — API pública
// ============================================================
const drivers = { postgres: postgresDriver, firebird: firebirdDriver };
function getDriver(alias) {
function getConn(alias) {
const config = getConfig(alias);
const driver = drivers[config.driver];
if (!driver) {
throw new Error(`Driver "${config.driver}" não suportado para o alias "${alias}".`);
}
return { config, driver };
}
function query(alias, sql, params = []) {
let d;
try { d = getDriver(alias); } catch (err) { return Promise.reject(err); }
try { d = getConn(alias); } catch (err) { return Promise.reject(err); }
return d.driver.query(d.config, sql, params).catch((err) => { throw new Error(`[${alias}] ${err.message}`); });
}
function execute(alias, sql, params = []) {
let d;
try { d = getDriver(alias); } catch (err) { return Promise.reject(err); }
try { d = getConn(alias); } catch (err) { return Promise.reject(err); }
return d.driver.execute(d.config, sql, params).catch((err) => { throw new Error(`[${alias}] ${err.message}`); });
}
async function testConnection(alias) {
try {
const { config, driver } = getDriver(alias);
const { config } = getConn(alias);
await driver.testConnection(config);
return true;
} catch (err) {
@@ -697,33 +342,33 @@ async function testAllConnections() {
}
function listTables(alias) {
const { config, driver } = getDriver(alias);
const { config } = getConn(alias);
return driver.listTables(config);
}
function tableInfo(alias, tableName) {
const { config, driver } = getDriver(alias);
const { config } = getConn(alias);
return driver.tableInfo(config, tableName);
}
function tableExists(alias, tableName) {
const { config, driver } = getDriver(alias);
const { config } = getConn(alias);
return driver.tableExists(config, tableName);
}
function columnExists(alias, tableName, columnName) {
const { config, driver } = getDriver(alias);
const { config } = getConn(alias);
return driver.columnExists(config, tableName, columnName);
}
/** Nome do driver de um alias (ex.: 'postgres'). */
/** Nome do driver de um alias (sempre 'firebird'). */
function driverOf(alias) {
return getConfig(alias).driver;
return 'firebird';
}
/** Encerra os pools de todos os drivers (shutdown gracioso). */
/** Encerra recursos (noop para Firebird). */
async function closeAll() {
await Promise.all(Object.values(drivers).map((d) => d.close && d.close()));
await driver.close();
}
module.exports = {