Ajustes 2306
This commit is contained in:
+2
-2
@@ -62,9 +62,9 @@ app.use(routes);
|
||||
// Rota raiz
|
||||
app.get('/', (req, res) => {
|
||||
res.json({
|
||||
name: 'API Firebird - Chatc2',
|
||||
name: 'API Chatc2 - Firebird',
|
||||
version: '2.0.0',
|
||||
description: 'API multi-banco com sistema de aliases',
|
||||
description: 'API Firebird com sistema de aliases',
|
||||
auth: {
|
||||
loginPage: 'GET /app/:alias/login',
|
||||
login: 'POST /app/:alias/login',
|
||||
|
||||
@@ -75,7 +75,7 @@ class AuthController {
|
||||
FROM USUARIOS
|
||||
WHERE USU_LOGIN = ?
|
||||
AND (USU_SENHA = ? OR USU_SENHA_WEB = ?)
|
||||
AND COALESCE(USU_ACESSO_WEB, 0) = 1
|
||||
AND COALESCE(USU_ACESSO_WEB, '0') IN ('1', 'S')
|
||||
AND USU_STATUS = 'A'
|
||||
`;
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ class ChatController {
|
||||
const statusFilter = req.query.status || 'A,E';
|
||||
const filter = req.query.filter || ''; // 'mine', 'unassigned', 'all'
|
||||
|
||||
if (!req.user?.empresas?.includes(empresaId))
|
||||
if (!empresaId || !req.user?.empresas?.some(function(e) { return Number(e) === Number(empresaId); }))
|
||||
return res.status(403).json({ success: false, error: 'Sem permissão.' });
|
||||
|
||||
// Status via parâmetros (evita SQL injection). Aceita apenas A/E/F.
|
||||
@@ -260,7 +260,7 @@ class ChatController {
|
||||
var minhasEquipes = await db.query(alias,
|
||||
'SELECT EQU_EQUIPE_ID FROM CHATC2_USU_EQUIPES WHERE EQU_USUARIO_ID = ?', [userId]);
|
||||
if (minhasEquipes.length > 0) {
|
||||
var eqIds = minhasEquipes.map(function(e) { return e.EQU_EQUIPE_ID; }).join(',');
|
||||
var eqIds = minhasEquipes.map(function(e) { return "'" + e.EQU_EQUIPE_ID + "'"; }).join(',');
|
||||
var countEq = await db.query(alias,
|
||||
`SELECT COUNT(*) AS CT FROM CHATC2_CONVERSAS WHERE CON_EMPRESA_ID = ? AND CON_STATUS IN ('A','E') AND CON_SITUACAO = 'A' AND CON_EQUIPE_ID IN (${eqIds})`,
|
||||
[empresaId]);
|
||||
@@ -296,7 +296,7 @@ class ChatController {
|
||||
const row = result[0];
|
||||
const empresaId = row.CON_EMPRESA_ID;
|
||||
|
||||
if (!req.user?.empresas?.includes(empresaId))
|
||||
if (empresaId && !req.user?.empresas?.some(function(e) { return String(e) === String(empresaId); }))
|
||||
return res.status(403).json({ success: false, error: 'Sem permissão.' });
|
||||
|
||||
// Verifica se o número pertence a um dependente
|
||||
@@ -471,7 +471,7 @@ class ChatController {
|
||||
if (r.length === 0) return { status: 404 };
|
||||
const emp = r[0].CON_EMPRESA_ID;
|
||||
const minhas = (req.user && req.user.empresas) || [];
|
||||
if (emp && minhas.indexOf(emp) === -1) return { status: 403 };
|
||||
if (emp && !minhas.some(function(e) { return String(e) === String(emp); })) return { status: 403 };
|
||||
return { ok: true, empresaId: emp };
|
||||
}
|
||||
|
||||
@@ -542,7 +542,7 @@ class ChatController {
|
||||
|
||||
const conv = await db.query(alias, 'SELECT * FROM CHATC2_CONVERSAS WHERE CON_CODIGO_ID = ?', [id]);
|
||||
if (conv.length === 0) return res.status(404).json({ success: false, error: 'Conversa não encontrada.' });
|
||||
if (conv[0].CON_EMPRESA_ID && !((req.user && req.user.empresas) || []).includes(conv[0].CON_EMPRESA_ID))
|
||||
if (conv[0].CON_EMPRESA_ID && !((req.user && req.user.empresas) || []).some(function(e) { return String(e) === String(conv[0].CON_EMPRESA_ID); }))
|
||||
return res.status(403).json({ success: false, error: 'Sem permissão.' });
|
||||
|
||||
// Se a conversa estiver finalizada, reabre
|
||||
@@ -620,8 +620,8 @@ class ChatController {
|
||||
|
||||
// Insere mensagem no banco
|
||||
await db.execute(alias, `
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_USUARIO_ID, CME_TEXTO, CME_TIPO, CME_PRIVADA, CME_DT_ENVIO, CME_MIDIA_ID)
|
||||
VALUES (?, ?, 'U', ?, ?, ?, ?, CURRENT_TIMESTAMP, ?)
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_USUARIO_ID, CME_TEXTO, CME_TIPO, CME_PRIVADA, CME_SITUACAO, CME_DT_ENVIO, CME_MIDIA_ID)
|
||||
VALUES (?, ?, 'U', ?, ?, ?, ?, 'A', CURRENT_TIMESTAMP, ?)
|
||||
`, [newId, id, usuarioId, textoFinal, tipo || 'text', privada === 'S' ? 'S' : 'N', midiaId]);
|
||||
|
||||
// Atualiza data da última mensagem
|
||||
@@ -663,7 +663,7 @@ class ChatController {
|
||||
if (conv.length === 0) return res.status(404).json({ success: false, error: 'Conversa não encontrada.' });
|
||||
|
||||
const c = conv[0];
|
||||
if (c.CON_EMPRESA_ID && !((req.user && req.user.empresas) || []).includes(c.CON_EMPRESA_ID))
|
||||
if (c.CON_EMPRESA_ID && !((req.user && req.user.empresas) || []).some(function(e) { return String(e) === String(c.CON_EMPRESA_ID); }))
|
||||
return res.status(403).json({ success: false, error: 'Sem permissão.' });
|
||||
|
||||
// ===== Fluxo de Resolução: valida motivo/resolução conforme config =====
|
||||
@@ -781,7 +781,7 @@ class ChatController {
|
||||
|
||||
// Verifica permissão
|
||||
const userEmpresas = req.user?.empresas || [];
|
||||
if (!userEmpresas.includes(parseInt(empresaId))) {
|
||||
if (!userEmpresas.some(function(e) { return String(e) === String(empresaId); })) {
|
||||
return res.status(403).json({ success: false, error: 'Sem permissão para esta empresa.' });
|
||||
}
|
||||
|
||||
@@ -827,8 +827,8 @@ class ChatController {
|
||||
const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
|
||||
const msgId = (maxMsgId[0]?.ID || 0) + 1;
|
||||
await db.execute(alias, `
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_USUARIO_ID, CME_TEXTO, CME_TIPO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'U', ?, ?, 'text', CURRENT_TIMESTAMP)
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_USUARIO_ID, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'U', ?, ?, 'text', 'A', CURRENT_TIMESTAMP)
|
||||
`, [msgId, conversaId, req.user?.id, mensagem]);
|
||||
|
||||
await db.execute(alias,
|
||||
@@ -860,8 +860,8 @@ class ChatController {
|
||||
// Insere conversa
|
||||
await db.execute(alias, `
|
||||
INSERT INTO CHATC2_CONVERSAS (CON_CODIGO_ID, CON_EMPRESA_ID, CON_INSTANCIA_ID, CON_NUMERO,
|
||||
CON_NOME_CONTATO, CON_CLIENTE_ID, CON_STATUS, CON_PRIMEIRA_MSG, CON_DT_ULTIMA_MSG)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'A', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
CON_NOME_CONTATO, CON_CLIENTE_ID, CON_STATUS, CON_SITUACAO, CON_PRIMEIRA_MSG, CON_DT_ULTIMA_MSG)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'A', 'A', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
`, [newId, empresaId, instId, numeroLimpo, nomeContato || numeroLimpo, clienteId || null]);
|
||||
|
||||
// Insere primeira mensagem
|
||||
@@ -869,8 +869,8 @@ class ChatController {
|
||||
const msgId = (maxMsgId[0]?.ID || 0) + 1;
|
||||
|
||||
await db.execute(alias, `
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_USUARIO_ID, CME_TEXTO, CME_TIPO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'U', ?, ?, 'text', CURRENT_TIMESTAMP)
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_USUARIO_ID, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'U', ?, ?, 'text', 'A', CURRENT_TIMESTAMP)
|
||||
`, [msgId, newId, req.user?.id, mensagem]);
|
||||
|
||||
// Envia via Evolution API
|
||||
@@ -1058,7 +1058,7 @@ class ChatController {
|
||||
mimeType = mimeType.split(';')[0].trim();
|
||||
const nomeArquivo = (m.MAT_NOME_ARQUIVO || '').trim() || 'arquivo.bin';
|
||||
|
||||
// MAT_ARQUIVO chega como Buffer (postgres bytea / firebird blob) ou string
|
||||
// MAT_ARQUIVO chega como Buffer (firebird blob) ou string
|
||||
let rawBuffer = m.MAT_ARQUIVO;
|
||||
if (rawBuffer == null) {
|
||||
return respond(404, { success: false, error: 'Mídia sem conteúdo.' });
|
||||
@@ -1179,7 +1179,7 @@ class ChatController {
|
||||
const cli = await db.query(alias,
|
||||
'SELECT CLI_EMPRESA_ID FROM CLIENTES WHERE CLI_CODIGO_ID = ?', [cid]);
|
||||
if (cli.length === 0) return { status: 404 };
|
||||
if (chk.empresaId && cli[0].CLI_EMPRESA_ID && cli[0].CLI_EMPRESA_ID !== chk.empresaId)
|
||||
if (chk.empresaId && cli[0].CLI_EMPRESA_ID && String(cli[0].CLI_EMPRESA_ID) !== String(chk.empresaId))
|
||||
return { status: 403 };
|
||||
return { ok: true, clienteId: cid };
|
||||
}
|
||||
@@ -1282,8 +1282,8 @@ class ChatController {
|
||||
const maxId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
|
||||
const newId = (maxId[0]?.ID || 0) + 1;
|
||||
await db.execute(alias, `
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_USUARIO_ID, CME_TEXTO, CME_TIPO, CME_PRIVADA, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'U', ?, ?, 'text', 'N', CURRENT_TIMESTAMP)
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_USUARIO_ID, CME_TEXTO, CME_TIPO, CME_PRIVADA, CME_SITUACAO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'U', ?, ?, 'text', 'N', 'A', CURRENT_TIMESTAMP)
|
||||
`, [newId, id, req.user?.id, texto]);
|
||||
await db.execute(alias,
|
||||
'UPDATE CHATC2_CONVERSAS SET CON_DT_ULTIMA_MSG = CURRENT_TIMESTAMP WHERE CON_CODIGO_ID = ?', [id]);
|
||||
|
||||
@@ -89,59 +89,29 @@ class DatabaseController {
|
||||
return res.status(400).json({ success: false, error: 'database é obrigatório.' });
|
||||
}
|
||||
|
||||
var driver = (req.body.driver || DEFAULT_DRIVER).toLowerCase();
|
||||
|
||||
if (driver === 'postgres') {
|
||||
var pg = require('pg');
|
||||
var schema = String(req.body.schema || 'public').trim();
|
||||
var safe = /^[A-Za-z_][A-Za-z0-9_$]*$/.test(schema) ? schema : 'public';
|
||||
var searchPath = safe === 'public' ? 'public' : safe + ',public';
|
||||
var client = new pg.Client({
|
||||
host: req.body.host || '127.0.0.1',
|
||||
port: req.body.port || 5432,
|
||||
database: req.body.database,
|
||||
user: req.body.user || 'postgres',
|
||||
password: req.body.password || 'postgres',
|
||||
ssl: req.body.ssl ? { rejectUnauthorized: false } : false,
|
||||
options: '-c search_path=' + searchPath,
|
||||
connectionTimeoutMillis: 8000,
|
||||
});
|
||||
try {
|
||||
await client.connect();
|
||||
await client.query('SELECT 1');
|
||||
// Confirma que o schema existe
|
||||
var sc = await client.query('SELECT 1 FROM information_schema.schemata WHERE schema_name = $1', [safe]);
|
||||
if (sc.rowCount === 0) {
|
||||
throw new Error('Schema "' + safe + '" não encontrado no banco "' + req.body.database + '".');
|
||||
}
|
||||
} finally {
|
||||
await client.end().catch(function () {});
|
||||
}
|
||||
} else {
|
||||
var Firebird = require('node-firebird');
|
||||
var config = {
|
||||
host: req.body.host || 'localhost',
|
||||
port: req.body.port || 3050,
|
||||
database: req.body.database,
|
||||
user: req.body.user || 'SYSDBA',
|
||||
password: req.body.password || 'masterkey',
|
||||
encoding: 'UTF-8',
|
||||
lowercase_keys: false,
|
||||
pageSize: 4096,
|
||||
};
|
||||
await new Promise(function (resolve, reject) {
|
||||
Firebird.attach(config, function (err, conn) {
|
||||
if (err) return reject(new Error('Falha ao conectar: ' + err.message));
|
||||
conn.query('SELECT 1 AS TEST FROM RDB$DATABASE', [], function (qErr, rows) {
|
||||
conn.detach();
|
||||
if (qErr) return reject(new Error('Falha na consulta: ' + qErr.message));
|
||||
resolve(rows);
|
||||
});
|
||||
var Firebird = require('node-firebird');
|
||||
var config = {
|
||||
host: req.body.host || 'localhost',
|
||||
port: req.body.port || 3050,
|
||||
database: req.body.database,
|
||||
user: req.body.user || 'SYSDBA',
|
||||
password: req.body.password || 'masterkey',
|
||||
encoding: 'UTF-8',
|
||||
lowercase_keys: false,
|
||||
pageSize: 4096,
|
||||
};
|
||||
await new Promise(function (resolve, reject) {
|
||||
Firebird.attach(config, function (err, conn) {
|
||||
if (err) return reject(new Error('Falha ao conectar: ' + err.message));
|
||||
conn.query('SELECT 1 AS TEST FROM RDB$DATABASE', [], function (qErr, rows) {
|
||||
conn.detach();
|
||||
if (qErr) return reject(new Error('Falha na consulta: ' + qErr.message));
|
||||
resolve(rows);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
res.json({ success: true, message: '✅ Conexão (' + driver + ') estabelecida com sucesso!' });
|
||||
res.json({ success: true, message: '✅ Conexão (firebird) estabelecida com sucesso!' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ success: false, error: err.message });
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ class EmpresaController {
|
||||
} catch (fe) { /* ignora e tenta o BLOB */ }
|
||||
}
|
||||
|
||||
// 2. BLOB EMP_FOTO (Buffer no postgres/firebird; string base64 em alguns casos)
|
||||
// 2. BLOB EMP_FOTO (Buffer no firebird; string base64 em alguns casos)
|
||||
const blobData = e.EMP_FOTO;
|
||||
let base64;
|
||||
if (Buffer.isBuffer(blobData)) {
|
||||
|
||||
@@ -242,18 +242,18 @@ class EvolutionController {
|
||||
});
|
||||
instanceCreated = true;
|
||||
await db.execute(alias, `
|
||||
INSERT INTO CHATC2_INSTANCIAS (INS_CODIGO_ID, INS_EMPRESA_ID, INS_NOME, INS_URL, INS_API_KEY, INS_INSTANCE_NAME, INS_STATUS)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'C')
|
||||
INSERT INTO CHATC2_INSTANCIAS (INS_CODIGO_ID, INS_EMPRESA_ID, INS_NOME, INS_URL, INS_API_KEY, INS_INSTANCE_NAME, INS_STATUS, INS_SITUACAO)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'C', 'A')
|
||||
`, [newId, INS_EMPRESA_ID || userEmpresas[0], INS_NOME, INS_URL, INS_API_KEY, INS_INSTANCE_NAME]);
|
||||
break;
|
||||
} catch (e) { /* Tenta próximo endpoint */ }
|
||||
}
|
||||
|
||||
if (!instanceCreated) {
|
||||
// Salva mesmo sem conseguir criar na Evolution
|
||||
// Salva mesmo sem conseguir criar na Evolution (status 'D' = desconectado)
|
||||
await db.execute(alias, `
|
||||
INSERT INTO CHATC2_INSTANCIAS (INS_CODIGO_ID, INS_EMPRESA_ID, INS_NOME, INS_URL, INS_API_KEY, INS_INSTANCE_NAME, INS_STATUS)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'D')
|
||||
INSERT INTO CHATC2_INSTANCIAS (INS_CODIGO_ID, INS_EMPRESA_ID, INS_NOME, INS_URL, INS_API_KEY, INS_INSTANCE_NAME, INS_STATUS, INS_SITUACAO)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'D', 'A')
|
||||
`, [newId, INS_EMPRESA_ID || userEmpresas[0], INS_NOME, INS_URL, INS_API_KEY, INS_INSTANCE_NAME]);
|
||||
}
|
||||
|
||||
@@ -270,8 +270,21 @@ class EvolutionController {
|
||||
static async listInstances(req, res) {
|
||||
try {
|
||||
const { alias } = req.params;
|
||||
const empresaId = parseInt(req.query.empresaId) || req.user?.empresas?.[0];
|
||||
if (!req.user?.empresas?.includes(empresaId)) return res.status(403).json({ success: false, error: 'Sem permissão.' });
|
||||
|
||||
// Garante que userEmpresas está carregado (fallback para DB se JWT não tiver)
|
||||
let userEmpresas = req.user?.empresas || [];
|
||||
if (userEmpresas.length === 0 && req.user?.id) {
|
||||
try {
|
||||
const empresasDb = await db.query(alias,
|
||||
'SELECT USE_EMPRESA_ID FROM USUARIOS_EMPRESA WHERE USE_USUARIO_ID = ?',
|
||||
[req.user.id]
|
||||
);
|
||||
userEmpresas = empresasDb.map(e => e.USE_EMPRESA_ID);
|
||||
} catch (e) { /* ignora falha */ }
|
||||
}
|
||||
|
||||
const empresaId = parseInt(req.query.empresaId) || userEmpresas[0];
|
||||
if (!userEmpresas.includes(empresaId)) return res.status(403).json({ success: false, error: 'Sem permissão.' });
|
||||
|
||||
const result = await db.query(alias,
|
||||
'SELECT * FROM CHATC2_INSTANCIAS WHERE INS_EMPRESA_ID = ? AND INS_SITUACAO = \'A\' ORDER BY INS_NOME',
|
||||
@@ -772,8 +785,8 @@ async function processWebhook(alias, body) {
|
||||
|
||||
await db.execute(alias, `
|
||||
INSERT INTO CHATC2_CONVERSAS (CON_CODIGO_ID, CON_EMPRESA_ID, CON_INSTANCIA_ID, CON_NUMERO,
|
||||
CON_NOME_CONTATO, CON_CLIENTE_ID, CON_STATUS, CON_PRIMEIRA_MSG, CON_DT_ULTIMA_MSG)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'E', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
CON_NOME_CONTATO, CON_CLIENTE_ID, CON_STATUS, CON_SITUACAO, CON_PRIMEIRA_MSG, CON_DT_ULTIMA_MSG)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'E', 'A', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
`, [newId, empresaId, instanciaId, numero, nomeFinal, clienteId]);
|
||||
|
||||
conversa = await db.query(alias, 'SELECT * FROM CHATC2_CONVERSAS WHERE CON_CODIGO_ID = ?', [newId]);
|
||||
@@ -789,8 +802,8 @@ async function processWebhook(alias, body) {
|
||||
if (msgSaudacao) {
|
||||
const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
|
||||
await db.execute(alias, `
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP)
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
|
||||
`, [(maxMsgId[0]?.ID || 0) + 1, conversa.CON_CODIGO_ID, msgSaudacao]);
|
||||
await db.execute(alias, "UPDATE CHATC2_CONVERSAS SET CON_SAUDACAO_ENVIADA = 'S' WHERE CON_CODIGO_ID = ?", [conversa.CON_CODIGO_ID]);
|
||||
|
||||
@@ -1205,13 +1218,13 @@ async function processWebhook(alias, body) {
|
||||
|
||||
if (midiaId) {
|
||||
await db.execute(alias, `
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO, CME_MIDIA_ID)
|
||||
VALUES (?, ?, 'C', ?, ?, CURRENT_TIMESTAMP, ?)
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO, CME_MIDIA_ID)
|
||||
VALUES (?, ?, 'C', ?, ?, 'A', CURRENT_TIMESTAMP, ?)
|
||||
`, [(maxMsgId[0]?.ID || 0) + 1, conversa.CON_CODIGO_ID, messageText, messageType, midiaId]);
|
||||
} else {
|
||||
await db.execute(alias, `
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'C', ?, ?, CURRENT_TIMESTAMP)
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'C', ?, ?, 'A', CURRENT_TIMESTAMP)
|
||||
`, [(maxMsgId[0]?.ID || 0) + 1, conversa.CON_CODIGO_ID, messageText, messageType]);
|
||||
}
|
||||
|
||||
|
||||
@@ -145,14 +145,14 @@ class RoutesController {
|
||||
'GET /api/databases': { desc: 'Lista as conexões de banco cadastradas (senhas ocultas).' },
|
||||
'POST /api/databases': {
|
||||
desc: 'Adiciona ou atualiza uma conexão de banco (persistida em databases_custom.json).',
|
||||
body: { alias: 'novo_dev', config: { driver: 'postgres', host: '127.0.0.1', port: 15433, database: 'novo_local', schema: 'dev', user: 'postgres', password: 'postgres' } },
|
||||
body: { alias: 'novo_dev', config: { driver: 'firebird', host: '127.0.0.1', port: 3050, database: '/opt/chatc2/db/NOVO.FDB', user: 'SYSDBA', password: 'masterkey' } },
|
||||
req: ['alias', 'config'],
|
||||
note: 'driver: "postgres" (usa schema) ou "firebird" (database = caminho do .FDB).',
|
||||
note: 'driver: "firebird" (database = caminho do .FDB).',
|
||||
},
|
||||
'DELETE /api/databases/{alias}': { desc: 'Remove uma conexão customizada (estáticas não podem ser removidas).' },
|
||||
'POST /api/databases/test': {
|
||||
desc: 'Testa uma conexão sem cadastrá-la.',
|
||||
body: { driver: 'postgres', host: '127.0.0.1', port: 15433, database: 'novo_local', schema: 'public', user: 'postgres', password: 'postgres' },
|
||||
body: { driver: 'firebird', host: '127.0.0.1', port: 3050, database: '/opt/chatc2/db/NOVO.FDB', user: 'SYSDBA', password: 'masterkey' },
|
||||
req: ['database'],
|
||||
},
|
||||
|
||||
|
||||
@@ -72,8 +72,8 @@ class TriageController {
|
||||
// Salva mensagem do sistema
|
||||
const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
|
||||
await db.execute(alias, `
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'triagem', CURRENT_TIMESTAMP)
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'triagem', 'A', CURRENT_TIMESTAMP)
|
||||
`, [(maxMsgId[0]?.ID || 0) + 1, conversaId, menuText]);
|
||||
|
||||
// Envia via Evolution
|
||||
@@ -121,8 +121,8 @@ class TriageController {
|
||||
|
||||
const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
|
||||
await db.execute(alias, `
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'triagem', CURRENT_TIMESTAMP)
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'triagem', 'A', CURRENT_TIMESTAMP)
|
||||
`, [(maxMsgId[0]?.ID || 0) + 1, conversaId, menuText]);
|
||||
await TriageController.sendEvolution(alias, instanciaId, numero, menuText);
|
||||
|
||||
@@ -246,8 +246,8 @@ class TriageController {
|
||||
if (msgAfter) {
|
||||
const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
|
||||
await db.execute(alias, `
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP)
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
|
||||
`, [(maxMsgId[0]?.ID || 0) + 1, conversaId, msgAfter]);
|
||||
await TriageController.sendEvolution(alias, instanciaId, numero, msgAfter);
|
||||
}
|
||||
@@ -377,8 +377,8 @@ class TriageController {
|
||||
if (textoMsg) {
|
||||
const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
|
||||
await db.execute(alias, `
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP)
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
|
||||
`, [(maxMsgId[0]?.ID || 0) + 1, conversaId, textoMsg]);
|
||||
await TriageController.sendEvolution(alias, instanciaId, numero, textoMsg);
|
||||
}
|
||||
@@ -396,8 +396,8 @@ class TriageController {
|
||||
|
||||
const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
|
||||
await db.execute(alias, `
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP)
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
|
||||
`, [(maxMsgId[0]?.ID || 0) + 1, conversaId, msgPrompt]);
|
||||
await TriageController.sendEvolution(alias, instanciaId, numero, msgPrompt);
|
||||
|
||||
@@ -521,8 +521,8 @@ class TriageController {
|
||||
if (resultado) {
|
||||
const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
|
||||
await db.execute(alias, `
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP)
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
|
||||
`, [(maxMsgId[0]?.ID || 0) + 1, conversaId, resultado]);
|
||||
await TriageController.sendEvolution(alias, instanciaId, numero, resultado);
|
||||
}
|
||||
@@ -579,8 +579,8 @@ class TriageController {
|
||||
var msgNaoEncontrado = 'Este número não está associado a nenhum contrato no sistema.\n\nPor favor, selecione um setor para que um atendente possa ajudá-lo a associar seu número.';
|
||||
const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
|
||||
await db.execute(alias, `
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP)
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
|
||||
`, [(maxMsgId[0]?.ID || 0) + 1, conversaId, msgNaoEncontrado]);
|
||||
await TriageController.sendEvolution(alias, instanciaId, numero, msgNaoEncontrado);
|
||||
await db.execute(alias, "UPDATE CHATC2_CONVERSAS SET CON_MENU_ESTADO = 'root' WHERE CON_CODIGO_ID = ?", [conversaId]);
|
||||
@@ -605,8 +605,8 @@ class TriageController {
|
||||
logTriage('Boleto - NENHUM título em aberto');
|
||||
const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
|
||||
await db.execute(alias, `
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP)
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
|
||||
`, [(maxMsgId[0]?.ID || 0) + 1, conversaId, msg]);
|
||||
await TriageController.sendEvolution(alias, instanciaId, numero, msg);
|
||||
await db.execute(alias, "UPDATE CHATC2_CONVERSAS SET CON_MENU_ESTADO = 'root' WHERE CON_CODIGO_ID = ?", [conversaId]);
|
||||
@@ -624,8 +624,8 @@ class TriageController {
|
||||
|
||||
const maxMsgId2 = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
|
||||
await db.execute(alias, `
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP)
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
|
||||
`, [(maxMsgId2[0]?.ID || 0) + 1, conversaId, msgBoleto]);
|
||||
await TriageController.sendEvolution(alias, instanciaId, numero, msgBoleto);
|
||||
|
||||
@@ -652,8 +652,8 @@ class TriageController {
|
||||
const msg = 'Opção inválida. Tente novamente.';
|
||||
const maxId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
|
||||
await db.execute(alias, `
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP)
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
|
||||
`, [(maxId[0]?.ID || 0) + 1, conversaId, msg]);
|
||||
await TriageController.sendEvolution(alias, instanciaId, numero, msg);
|
||||
return;
|
||||
@@ -838,8 +838,8 @@ class TriageController {
|
||||
|
||||
const maxId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
|
||||
await db.execute(alias, `
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP)
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
|
||||
`, [(maxId[0]?.ID || 0) + 1, conversaId, textoBoleto]);
|
||||
await TriageController.sendEvolution(alias, instanciaId, numero, textoBoleto);
|
||||
}
|
||||
@@ -848,8 +848,8 @@ class TriageController {
|
||||
var msgCont = '📄 Boleto enviado!\n\nDeseja continuar com o atendimento?\n\n1 - Sim\n2 - Não, finalizar';
|
||||
const maxIdCont = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
|
||||
await db.execute(alias, `
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP)
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
|
||||
`, [(maxIdCont[0]?.ID || 0) + 1, conversaId, msgCont]);
|
||||
await TriageController.sendEvolution(alias, instanciaId, numero, msgCont);
|
||||
|
||||
@@ -889,8 +889,8 @@ class TriageController {
|
||||
var msgFinal = 'Atendimento finalizado. Obrigado pelo contato!';
|
||||
const maxId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
|
||||
await db.execute(alias, `
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP)
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
|
||||
`, [(maxId[0]?.ID || 0) + 1, conversaId, msgFinal]);
|
||||
await TriageController.sendEvolution(alias, instanciaId, numero, msgFinal);
|
||||
await db.execute(alias, "UPDATE CHATC2_CONVERSAS SET CON_STATUS = 'F', CON_DT_FINAL = CURRENT_TIMESTAMP, CON_MENU_ESTADO = NULL WHERE CON_CODIGO_ID = ?", [conversaId]);
|
||||
@@ -898,8 +898,8 @@ class TriageController {
|
||||
var msgInv = 'Por favor, responda:\n\n1 - Sim\n2 - Não, finalizar';
|
||||
const maxId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
|
||||
await db.execute(alias, `
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP)
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
|
||||
`, [(maxId[0]?.ID || 0) + 1, conversaId, msgInv]);
|
||||
await TriageController.sendEvolution(alias, instanciaId, numero, msgInv);
|
||||
}
|
||||
@@ -916,8 +916,8 @@ class TriageController {
|
||||
const msg = 'Opção inválida. Tente novamente.';
|
||||
const maxId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
|
||||
await db.execute(alias, `
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP)
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
|
||||
`, [(maxId[0]?.ID || 0) + 1, conversaId, msg]);
|
||||
await TriageController.sendEvolution(alias, instanciaId, numero, msg);
|
||||
|
||||
@@ -949,8 +949,8 @@ class TriageController {
|
||||
if (texto) {
|
||||
const maxId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
|
||||
await db.execute(alias, `
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP)
|
||||
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
|
||||
VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
|
||||
`, [(maxId[0]?.ID || 0) + 1, conversaId, texto]);
|
||||
await TriageController.sendEvolution(alias, instanciaId, numero, texto);
|
||||
}
|
||||
|
||||
+153
-508
@@ -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 = {
|
||||
|
||||
+18
-1
@@ -50,6 +50,23 @@ async function authenticateToken(req, res, next) {
|
||||
.map(function (e) { return Number(e); })
|
||||
.filter(function (n) { return !isNaN(n); });
|
||||
}
|
||||
// Se o JWT não tem o array empresas (tokens legados), tenta carregar do BD
|
||||
if (!Array.isArray(decoded.empresas) || decoded.empresas.length === 0) {
|
||||
const alias = req.params.alias;
|
||||
if (alias && decoded.id) {
|
||||
try {
|
||||
const empresas = await db.query(alias,
|
||||
'SELECT USE_EMPRESA_ID FROM USUARIOS_EMPRESA WHERE USE_USUARIO_ID = ?',
|
||||
[decoded.id]
|
||||
);
|
||||
decoded.empresas = empresas.map(e => Number(e.USE_EMPRESA_ID)).filter(n => !isNaN(n));
|
||||
} catch (e) {
|
||||
decoded.empresas = [];
|
||||
}
|
||||
} else {
|
||||
decoded.empresas = [];
|
||||
}
|
||||
}
|
||||
req.user = decoded;
|
||||
req.authType = 'jwt';
|
||||
return next();
|
||||
@@ -90,7 +107,7 @@ async function authenticateToken(req, res, next) {
|
||||
FROM USUARIOS
|
||||
WHERE USU_TOKEN = ?
|
||||
AND USU_STATUS = 'A'
|
||||
AND COALESCE(USU_ACESSO_WEB, 0) = 1`,
|
||||
AND COALESCE(USU_ACESSO_WEB, '0') IN ('1', 'S')`,
|
||||
[token]
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Estrutura do "Fluxo de Resolução" (motivos + resolução do atendimento).
|
||||
* Cria de forma idempotente, em Postgres ou Firebird:
|
||||
* Cria de forma idempotente no Firebird:
|
||||
* - Tabela CHATC2_MOTIVOS_ATENDIMENTO
|
||||
* - Flags em CHATC2_CONFIGURACOES_EMPRESA (CFE_MOTIVO_*, CFE_RESOLUCAO_*)
|
||||
* - Colunas CON_MOTIVO_ID / CON_RESOLUCAO em CHATC2_CONVERSAS
|
||||
|
||||
Reference in New Issue
Block a user