Files
chatc2/src/database.js
T
2026-06-23 12:42:14 +00:00

396 lines
12 KiB
JavaScript

/**
* CAMADA DE BANCO DE DADOS (arquivo único)
* ========================================
* Concentra tudo que diz respeito a banco de dados em UM lugar:
*
* 1) CONFIGURAÇÃO — aliases, drivers, conexões estáticas e customizadas
* 2) DRIVER FIREBIRD
* 3) DISPATCHER — API pública usada por todo o sistema
*
* 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');
// ============================================================
// 1) CONFIGURAÇÃO
// ============================================================
const CUSTOM_DB_PATH = path.resolve(__dirname, 'databases_custom.json');
// Conexões dinâmicas salvas em arquivo JSON
var customDatabases = {};
try {
if (fs.existsSync(CUSTOM_DB_PATH)) {
customDatabases = JSON.parse(fs.readFileSync(CUSTOM_DB_PATH, 'utf8'));
}
} catch (e) {
console.error('[Databases] Erro ao carregar databases_custom.json:', e.message);
}
const DRIVERS = ['firebird'];
const DEFAULT_DRIVER = 'firebird';
// Valores padrão para Firebird
const DRIVER_DEFAULTS = {
firebird: {
host: 'localhost',
port: 3050,
user: 'SYSDBA',
password: 'masterkey',
encoding: 'UTF-8',
lowercase_keys: false,
pageSize: 4096,
wireCrypt: 1, // WIRE_CRYPT_ENABLE
},
};
/**
* Conexões estáticas.
* - `novo_local` → Firebird (informe o CAMINHO do arquivo .FDB em `database`)
* Sobrescrevível pelo .env (DB_*).
*/
const databases = {
novo_local: {
driver: 'firebird',
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT, 10) || 3050,
// CAMINHO do arquivo .FDB: defina DB_DATABASE no .env (absoluto) ou ajuste
// o path.resolve abaixo. Ex.: path.resolve(__dirname, '../db/NOVO.FDB').
database: process.env.DB_DATABASE || path.resolve(__dirname, '../db/NOVO.FDB'),
user: process.env.DB_USER || 'SYSDBA',
password: process.env.DB_PASSWORD || 'masterkey',
encoding: process.env.DB_ENCODING || 'UTF-8',
lowercase_keys: false,
pageSize: 4096,
wireCrypt: 1, // WIRE_CRYPT_ENABLE
},
};
function salvarCustomDatabases() {
try {
fs.writeFileSync(CUSTOM_DB_PATH, JSON.stringify(customDatabases, null, 2), 'utf8');
return true;
} catch (e) {
console.error('[Databases] Erro ao salvar databases_custom.json:', e.message);
return false;
}
}
/** Normaliza uma config bruta aplicando o driver e seus defaults. */
function normalize(raw) {
const d = DRIVER_DEFAULTS['firebird'];
return {
driver: 'firebird',
host: raw.host || d.host,
port: raw.port || d.port,
database: raw.database,
user: raw.user || d.user,
password: raw.password || d.password,
encoding: raw.encoding || d.encoding,
lowercase_keys: raw.lowercase_keys || d.lowercase_keys,
role: raw.role || null,
pageSize: raw.pageSize || d.pageSize,
wireCrypt: raw.wireCrypt !== undefined ? raw.wireCrypt : d.wireCrypt,
};
}
/** Configuração normalizada (inclui `driver`) de um alias. */
function getConfig(alias) {
if (!alias) throw new Error('Alias do banco não informado.');
const aliasLower = alias.toLowerCase();
const raw = customDatabases[aliasLower] || databases[aliasLower];
if (!raw) {
throw new Error(`Alias "${alias}" não encontrado. Aliases disponíveis: ${listAliases().join(', ')}`);
}
return normalize(raw);
}
/** Lista de aliases disponíveis. */
function listAliases() {
return Object.keys(databases).concat(
Object.keys(customDatabases).filter((a) => !databases[a])
);
}
/** Todos os aliases com suas configs (custom + estáticas). */
function listAllDatabases() {
var result = {};
Object.keys(databases).forEach(function (alias) {
result[alias] = Object.assign({}, databases[alias], { _tipo: 'estatico' });
});
Object.keys(customDatabases).forEach(function (alias) {
result[alias] = Object.assign({}, customDatabases[alias], { _tipo: 'custom' });
});
return result;
}
/** Adiciona/atualiza uma conexão customizada. */
function addDatabase(alias, config) {
if (!alias || !config || !config.database) {
throw new Error('Alias e database são obrigatórios.');
}
var aliasLower = alias.toLowerCase().replace(/[^a-z0-9_]/g, '_');
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];
}
});
customDatabases[aliasLower] = stored;
return salvarCustomDatabases();
}
/** Remove uma conexão customizada. */
function removeDatabase(alias) {
if (!alias) return false;
var aliasLower = alias.toLowerCase();
if (!customDatabases[aliasLower]) return false;
delete customDatabases[aliasLower];
return salvarCustomDatabases();
}
// ============================================================
// 2) DRIVER 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 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; });
}
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 firebirdTestConnection(config) {
await firebirdQuery(config, 'SELECT 1 FROM RDB$DATABASE');
return true;
}
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 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,
}));
}
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 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 firebirdClose() { /* Firebird abre conexão por consulta; nada a encerrar */ }
// ============================================================
// 3) DISPATCHER — API pública
// ============================================================
const driver = {
query: firebirdQuery,
execute: firebirdExecute,
testConnection: firebirdTestConnection,
listTables: firebirdListTables,
tableInfo: firebirdTableInfo,
tableExists: firebirdTableExists,
columnExists: firebirdColumnExists,
close: firebirdClose,
};
function getConn(alias) {
const config = getConfig(alias);
return { config, driver };
}
function query(alias, sql, params = []) {
let d;
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 = 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 } = getConn(alias);
await driver.testConnection(config);
return true;
} catch (err) {
console.error(`[${alias}] Falha na conexão:`, err.message);
return false;
}
}
async function testAllConnections() {
const results = {};
for (const alias of listAliases()) {
results[alias] = await testConnection(alias);
}
return results;
}
function listTables(alias) {
const { config } = getConn(alias);
return driver.listTables(config);
}
function tableInfo(alias, tableName) {
const { config } = getConn(alias);
return driver.tableInfo(config, tableName);
}
function tableExists(alias, tableName) {
const { config } = getConn(alias);
return driver.tableExists(config, tableName);
}
function columnExists(alias, tableName, columnName) {
const { config } = getConn(alias);
return driver.columnExists(config, tableName, columnName);
}
/** Nome do driver de um alias (sempre 'firebird'). */
function driverOf(alias) {
return 'firebird';
}
/** Encerra recursos (noop para Firebird). */
async function closeAll() {
await driver.close();
}
module.exports = {
// Acesso a dados
query,
execute,
testConnection,
testAllConnections,
listTables,
tableInfo,
tableExists,
columnExists,
driverOf,
closeAll,
// Configuração / aliases
databases,
DRIVERS,
DEFAULT_DRIVER,
getConfig,
listAliases,
listAllDatabases,
addDatabase,
removeDatabase,
};