Files
chatc2/scripts/migrationRunner.js
T
AyronSantos bc03f5dc1d fix: corrigir nome da tabela de MENSAGENS_CLIENTES para MENSAGEM_CLIENTES
- fix: migration #25 agora so cria o generator (tabela ja existe no ERP)
- fix: insert usa MENSAGEM_CLIENTES (singular) e GEN_MENSAGEM_CLIENTES
- fix: database.js whitelist atualizado para GEN_MENSAGEM_CLIENTES
2026-07-01 16:36:25 +00:00

236 lines
15 KiB
JavaScript

/**
* Runner de migrações — executável via CLI ou automaticamente no boot.
*
* Exporta uma função `executarMigracoes(alias)` que aplica todas as migrações
* pendentes de forma idempotente. Usado por server.js no boot e por
* scripts/migracoes.js para execução manual.
*/
const db = require('../src/database');
// ============================================================
// LISTA DE MIGRAÇÕES
// ============================================================
// SEMPRE adicione NOVAS migrações no FINAL do array, com ID sequencial.
// NUNCA remova ou modifique migrações já existentes.
const MIGRACOES = [
{ id: 1, descricao: 'Adicionar USU_TIPO_CHAT em USUARIOS (A=Atendente, G=Gerente)', sql: ['ALTER TABLE USUARIOS ADD USU_TIPO_CHAT CHAR(1) DEFAULT \'A\''] },
{ id: 2, descricao: 'Criar tabela CHATC2_INSTANCIAS para conexões WhatsApp', sql: [`CREATE TABLE CHATC2_INSTANCIAS (INS_CODIGO_ID INTEGER NOT NULL PRIMARY KEY, INS_EMPRESA_ID INTEGER NOT NULL, INS_NOME VARCHAR(100), INS_URL VARCHAR(500), INS_API_KEY VARCHAR(500), INS_INSTANCE_NAME VARCHAR(100), INS_STATUS CHAR(1) DEFAULT 'D', INS_QR_CODE BLOB SUB_TYPE 0, INS_DT_CONEXAO TIMESTAMP, INS_DT_CADASTRO TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INS_SITUACAO CHAR(1) DEFAULT 'A')`] },
{ id: 3, descricao: 'Criar tabela CHATC2_EQUIPES', sql: [`CREATE TABLE CHATC2_EQUIPES (EQU_CODIGO_ID INTEGER NOT NULL PRIMARY KEY, EQU_EMPRESA_ID INTEGER NOT NULL, EQU_NOME VARCHAR(100), EQU_ORDEM INTEGER DEFAULT 0, EQU_SITUACAO CHAR(1) DEFAULT 'A')`] },
{ id: 4, descricao: 'Criar tabela CHATC2_USU_EQUIPES (relação N:N)', sql: ['CREATE TABLE CHATC2_USU_EQUIPES (EQU_EQUIPE_ID INTEGER NOT NULL, EQU_USUARIO_ID INTEGER NOT NULL, PRIMARY KEY (EQU_EQUIPE_ID, EQU_USUARIO_ID))'] },
{ id: 5, descricao: 'Criar tabela CHATC2_ETIQUETAS', sql: [`CREATE TABLE CHATC2_ETIQUETAS (ETI_CODIGO_ID INTEGER NOT NULL PRIMARY KEY, ETI_EMPRESA_ID INTEGER NOT NULL, ETI_NOME VARCHAR(50), ETI_COR VARCHAR(7) DEFAULT '#667eea', ETI_SITUACAO CHAR(1) DEFAULT 'A')`] },
{ id: 6, descricao: 'Criar tabela CHATC2_CONVERSAS', sql: [`CREATE TABLE CHATC2_CONVERSAS (CON_CODIGO_ID INTEGER NOT NULL PRIMARY KEY, CON_EMPRESA_ID INTEGER NOT NULL, CON_INSTANCIA_ID INTEGER, CON_CLIENTE_ID INTEGER, CON_NUMERO VARCHAR(20), CON_NOME_CONTATO VARCHAR(100), CON_STATUS CHAR(1) DEFAULT 'E', CON_USUARIO_ID INTEGER, CON_EQUIPE_ID INTEGER, CON_DT_INICIO TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CON_DT_FINAL TIMESTAMP, CON_DT_ULTIMA_MSG TIMESTAMP, CON_SAUDACAO_ENVIADA CHAR(1) DEFAULT 'N', CON_CSAT_ENVIADO CHAR(1) DEFAULT 'N', CON_PRIMEIRA_MSG TIMESTAMP, CON_ORIGEM VARCHAR(20) DEFAULT 'whatsapp', CON_SITUACAO CHAR(1) DEFAULT 'A')`] },
{ id: 7, descricao: 'Criar tabela CHATC2_CONVERSAS_MENSAGENS', sql: [`CREATE TABLE CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID INTEGER NOT NULL PRIMARY KEY, CME_CONVERSA_ID INTEGER NOT NULL, CME_REMETENTE CHAR(1) DEFAULT 'C', CME_USUARIO_ID INTEGER, CME_MENSAGEM BLOB SUB_TYPE 1, CME_TEXTO VARCHAR(4000), CME_TIPO VARCHAR(20) DEFAULT 'text', CME_DT_ENVIO TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CME_PRIVADA CHAR(1) DEFAULT 'N', CME_LIDA CHAR(1) DEFAULT 'N', CME_MIDIA_ID INTEGER, CME_SITUACAO CHAR(1) DEFAULT 'A')`] },
{ id: 8, descricao: 'Criar tabela CHATC2_MENSAGENS_ATENDIMENTOS para mídias', sql: [`CREATE TABLE CHATC2_MENSAGENS_ATENDIMENTOS (MAT_CODIGO_ID INTEGER NOT NULL PRIMARY KEY, MAT_CONVERSA_ID INTEGER, MAT_MENSAGEM_ID INTEGER, MAT_NOME_ARQUIVO VARCHAR(255), MAT_TIPO_ARQUIVO VARCHAR(50), MAT_ARQUIVO BLOB SUB_TYPE 0, MAT_DT_CADASTRO TIMESTAMP DEFAULT CURRENT_TIMESTAMP, MAT_SITUACAO CHAR(1) DEFAULT 'A')`] },
{ id: 9, descricao: 'Criar tabela CHATC2_CONVERSAS_ETIQUETAS', sql: ['CREATE TABLE CHATC2_CONVERSAS_ETIQUETAS (CET_CONVERSA_ID INTEGER NOT NULL, CET_ETIQUETA_ID INTEGER NOT NULL, PRIMARY KEY (CET_CONVERSA_ID, CET_ETIQUETA_ID))'] },
{ id: 10, descricao: 'Criar tabela CHATC2_CSAT_AVALIACOES', sql: [`CREATE TABLE CHATC2_CSAT_AVALIACOES (CSA_CODIGO_ID INTEGER NOT NULL PRIMARY KEY, CSA_CONVERSA_ID INTEGER, CSA_EMPRESA_ID INTEGER, CSA_CLIENTE_ID INTEGER, CSA_NOTA INTEGER, CSA_COMENTARIO VARCHAR(500), CSA_DT_AVALIACAO TIMESTAMP DEFAULT CURRENT_TIMESTAMP)`] },
{ id: 11, descricao: 'Criar tabela CHATC2_CONFIGURACOES_EMPRESA (base)', sql: [`CREATE TABLE CHATC2_CONFIGURACOES_EMPRESA (CFE_EMPRESA_ID INTEGER NOT NULL PRIMARY KEY, CFE_INSTANCIA_PADRAO_ID INTEGER, CFE_FOTO_CELULAR CHAR(1) DEFAULT 'N', CFE_SAUDACAO_ATIVA CHAR(1) DEFAULT 'S', CFE_SAUDACAO_MENSAGEM VARCHAR(500), CFE_CSAT_ATIVO CHAR(1) DEFAULT 'N', CFE_CSAT_MENSAGEM VARCHAR(500) DEFAULT 'Avalie seu atendimento de 1 a 5 estrelas')`] },
{ id: 12, descricao: 'Adicionar EQU_ORDEM em CHATC2_EQUIPES (ordenação)', sql: ['ALTER TABLE CHATC2_EQUIPES ADD EQU_ORDEM INTEGER DEFAULT 0'] },
{ id: 13, descricao: 'Adicionar campos de triagem em CHATC2_CONFIGURACOES_EMPRESA', sql: ['ALTER TABLE CHATC2_CONFIGURACOES_EMPRESA ADD CFE_ENVIAR_NOME_USUARIO CHAR(1) DEFAULT \'N\'', 'ALTER TABLE CHATC2_CONFIGURACOES_EMPRESA ADD CFE_TRIAGEM_ATIVA CHAR(1) DEFAULT \'N\'', 'ALTER TABLE CHATC2_CONFIGURACOES_EMPRESA ADD CFE_TRIAGEM_MSG_WELCOME VARCHAR(500)', 'ALTER TABLE CHATC2_CONFIGURACOES_EMPRESA ADD CFE_TRIAGEM_MSG_AFTER VARCHAR(500)', 'ALTER TABLE CHATC2_CONFIGURACOES_EMPRESA ADD CFE_TRIAGEM_BOLETO_NUMERO VARCHAR(10) DEFAULT \'0\''] },
{ id: 14, descricao: 'Adicionar CON_MENU_ESTADO em CHATC2_CONVERSAS (navegação triagem)', sql: ['ALTER TABLE CHATC2_CONVERSAS ADD CON_MENU_ESTADO VARCHAR(100)'] },
{ id: 15, descricao: 'Adicionar CON_ETIQUETAS_DESC em CHATC2_CONVERSAS', sql: ['ALTER TABLE CHATC2_CONVERSAS ADD CON_ETIQUETAS_DESC VARCHAR(500)'] },
{ id: 16, descricao: 'Criar tabela CHATC2_MENUS_EMPRESA para fluxo personalizado', sql: [`CREATE TABLE CHATC2_MENUS_EMPRESA (MNE_CODIGO_ID INTEGER NOT NULL PRIMARY KEY, MNE_EMPRESA_ID INTEGER, MNE_EQUIPE_ID INTEGER, MNE_MENU_PAI_ID INTEGER, MNE_ORDEM INTEGER DEFAULT 0, MNE_TITULO VARCHAR(100), MNE_TIPO CHAR(1) DEFAULT 'M', MNE_TEXTO BLOB SUB_TYPE TEXT, MNE_ACAO_ROTA VARCHAR(100), MNE_ACAO_METODO VARCHAR(10), MNE_ACAO_PROMPT VARCHAR(300), MNE_SITUACAO CHAR(1) DEFAULT 'A')`] },
{ id: 17, descricao: 'Adicionar CON_USUARIO_NOME e CON_EQUIPE_NOME em CHATC2_CONVERSAS', sql: ['ALTER TABLE CHATC2_CONVERSAS ADD CON_USUARIO_NOME VARCHAR(100)', 'ALTER TABLE CHATC2_CONVERSAS ADD CON_EQUIPE_NOME VARCHAR(100)'] },
{ id: 18, descricao: 'Adicionar MNE_ETIQUETA_IDS em CHATC2_MENUS_EMPRESA', sql: ['ALTER TABLE CHATC2_MENUS_EMPRESA ADD MNE_ETIQUETA_IDS VARCHAR(200)'] },
{ id: 19, descricao: 'Adicionar MAT_TRANSCRICAO em CHATC2_MENSAGENS_ATENDIMENTOS', sql: ['ALTER TABLE CHATC2_MENSAGENS_ATENDIMENTOS ADD MAT_TRANSCRICAO BLOB SUB_TYPE TEXT'] },
{ id: 20, descricao: 'Criar generators Firebird para eliminar race condition',
sql: [
'CREATE GENERATOR GEN_CHATC2_CONVERSAS', 'CREATE GENERATOR GEN_CONVERSAS_MENSAGENS',
'CREATE GENERATOR GEN_MENSAGENS_ATENDIMENTOS', 'CREATE GENERATOR GEN_CHATC2_EQUIPES',
'CREATE GENERATOR GEN_CHATC2_ETIQUETAS', 'CREATE GENERATOR GEN_CHATC2_INSTANCIAS',
'CREATE GENERATOR GEN_CHATC2_MENUS_EMPRESA', 'CREATE GENERATOR GEN_CHATC2_CSAT_AVALIACOES',
'CREATE GENERATOR GEN_CHATC2_MOTIVOS_ATENDIMENTO',
],
async after(alias) {
const generators = [
{ gen: 'GEN_CHATC2_CONVERSAS', tabela: 'CHATC2_CONVERSAS', col: 'CON_CODIGO_ID' },
{ gen: 'GEN_CONVERSAS_MENSAGENS', tabela: 'CHATC2_CONVERSAS_MENSAGENS', col: 'CME_CODIGO_ID' },
{ gen: 'GEN_MENSAGENS_ATENDIMENTOS', tabela: 'CHATC2_MENSAGENS_ATENDIMENTOS', col: 'MAT_CODIGO_ID' },
{ gen: 'GEN_CHATC2_EQUIPES', tabela: 'CHATC2_EQUIPES', col: 'EQU_CODIGO_ID' },
{ gen: 'GEN_CHATC2_ETIQUETAS', tabela: 'CHATC2_ETIQUETAS', col: 'ETI_CODIGO_ID' },
{ gen: 'GEN_CHATC2_INSTANCIAS', tabela: 'CHATC2_INSTANCIAS', col: 'INS_CODIGO_ID' },
{ gen: 'GEN_CHATC2_MENUS_EMPRESA', tabela: 'CHATC2_MENUS_EMPRESA', col: 'MNE_CODIGO_ID' },
{ gen: 'GEN_CHATC2_CSAT_AVALIACOES', tabela: 'CHATC2_CSAT_AVALIACOES', col: 'CSA_CODIGO_ID' },
{ gen: 'GEN_CHATC2_MOTIVOS_ATENDIMENTO', tabela: '"CHATC2_MOTIVOS_ATENDIMENTO"', col: 'MOT_CODIGO_ID' },
];
for (const g of generators) {
try {
const r = await db.query(alias, `SELECT COALESCE(MAX(${g.col}), 0) + 1 AS VAL FROM ${g.tabela}`);
await db.execute(alias, `SET GENERATOR ${g.gen} TO ${r[0]?.VAL || 1}`);
} catch (_) {}
}
},
},
// ----------------------------------------------------------
// MIGRAÇÃO 21: Índices para busca de telefone (webhook)
// ----------------------------------------------------------
{
id: 21,
descricao: 'Índices para busca de cliente por telefone (acelera webhook)',
sql: [
// Índice na tabela CLIENTES para busca por CELULAR/FONE1/FONE2
'CREATE INDEX IDX_CLIENTES_CELULAR ON CLIENTES (CLI_CELULAR)',
'CREATE INDEX IDX_CLIENTES_FONE1 ON CLIENTES (CLI_FONE1)',
'CREATE INDEX IDX_CLIENTES_FONE2 ON CLIENTES (CLI_FONE2)',
'CREATE INDEX IDX_CLIENTES_EMPRESA ON CLIENTES (CLI_EMPRESA_ID)',
// Índice composto para a query mais comum do webhook
'CREATE INDEX IDX_CONV_NUMERO_EMPRESA ON CHATC2_CONVERSAS (CON_NUMERO, CON_EMPRESA_ID)',
'CREATE INDEX IDX_CONV_STATUS ON CHATC2_CONVERSAS (CON_STATUS)',
],
},
// ----------------------------------------------------------
// MIGRAÇÃO 22: Mensagem e etiqueta por equipe (triagem)
// ----------------------------------------------------------
{
id: 22,
descricao: 'Adicionar EQU_MENSAGEM e EQU_ETIQUETA_ID em CHATC2_EQUIPES',
sql: [
'ALTER TABLE CHATC2_EQUIPES ADD EQU_MENSAGEM VARCHAR(500)',
'ALTER TABLE CHATC2_EQUIPES ADD EQU_ETIQUETA_ID INTEGER',
],
},
// ----------------------------------------------------------
// MIGRAÇÃO 23: Agendamento de conversas
// ----------------------------------------------------------
{
id: 23,
descricao: 'Criar tabela CHATC2_CONVERSAS_AGENDAMENTOS (retorno agendado)',
sql: [
`CREATE TABLE CHATC2_CONVERSAS_AGENDAMENTOS (
CAG_CODIGO_ID INTEGER NOT NULL PRIMARY KEY,
CAG_CONVERSA_ID INTEGER NOT NULL,
CAG_EMPRESA_ID INTEGER,
CAG_DATA DATE NOT NULL,
CAG_HORA TIME,
CAG_NOTA VARCHAR(300),
CAG_STATUS CHAR(1) DEFAULT 'P',
CAG_USUARIO_ID INTEGER,
CAG_DT_CRIACAO TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`,
],
},
// ----------------------------------------------------------
// MIGRAÇÃO 24: DDL que estava no resolucaoSetup.js (runtime)
// ----------------------------------------------------------
{
id: 24,
descricao: 'Consolidar DDL do fluxo de resolução (antes criado em runtime)',
sql: [
`CREATE TABLE "CHATC2_MOTIVOS_ATENDIMENTO" (
MOT_CODIGO_ID INTEGER NOT NULL PRIMARY KEY,
MOT_EMPRESA_ID INTEGER,
MOT_DESCRICAO VARCHAR(150),
MOT_SITUACAO CHAR(1) DEFAULT 'A'
)`,
"ALTER TABLE CHATC2_CONFIGURACOES_EMPRESA ADD CFE_MOTIVO_VISUALIZAR CHAR(1) DEFAULT 'N'",
"ALTER TABLE CHATC2_CONFIGURACOES_EMPRESA ADD CFE_MOTIVO_OBRIGATORIO CHAR(1) DEFAULT 'N'",
"ALTER TABLE CHATC2_CONFIGURACOES_EMPRESA ADD CFE_RESOLUCAO_VISUALIZAR CHAR(1) DEFAULT 'N'",
"ALTER TABLE CHATC2_CONFIGURACOES_EMPRESA ADD CFE_RESOLUCAO_OBRIGATORIO CHAR(1) DEFAULT 'N'",
"ALTER TABLE CHATC2_CONFIGURACOES_EMPRESA ADD CFE_ENVIAR_BOLETO CHAR(1) DEFAULT 'N'",
"ALTER TABLE CHATC2_CONFIGURACOES_EMPRESA ADD CFE_RESOLUCAO_SALVAR_MSC CHAR(1) DEFAULT 'N'",
'ALTER TABLE CHATC2_CONVERSAS ADD CON_MOTIVO_ID INTEGER',
'ALTER TABLE CHATC2_CONVERSAS ADD CON_RESOLUCAO VARCHAR(4000)',
],
},
// ----------------------------------------------------------
// MIGRAÇÃO 25: Generator para MENSAGEM_CLIENTES (tabela do ERP)
// ----------------------------------------------------------
{
id: 25,
descricao: 'Criar generator para MENSAGEM_CLIENTES (tabela ja existe no ERP)',
sql: [
'CREATE GENERATOR GEN_MENSAGEM_CLIENTES',
],
async after(alias) {
try {
const r = await db.query(alias, 'SELECT COALESCE(MAX(MSC_CODIGO_ID),0)+1 AS V FROM MENSAGEM_CLIENTES');
await db.execute(alias, 'SET GENERATOR GEN_MENSAGEM_CLIENTES TO ' + (r[0]?.V || 1));
} catch (_) {}
},
},
];
// ============================================================
// EXECUTOR
// ============================================================
async function garantirControle(alias) {
try {
await db.execute(alias, `CREATE TABLE CHATC2_CONTROLE_MIGRACOES (MIG_ID INTEGER NOT NULL PRIMARY KEY, MIG_DESCRICAO VARCHAR(200), MIG_DATA_EXECUCAO TIMESTAMP DEFAULT CURRENT_TIMESTAMP, MIG_STATUS CHAR(1) DEFAULT 'A')`);
} catch (_) {}
}
async function migracaoJaExecutada(alias, id) {
try {
const r = await db.query(alias, 'SELECT COUNT(*) AS CT FROM CHATC2_CONTROLE_MIGRACOES WHERE MIG_ID = ?', [id]);
return r[0]?.CT > 0;
} catch (_) { return false; }
}
async function registrarMigracao(alias, id, descricao) {
try {
await db.execute(alias, 'INSERT INTO CHATC2_CONTROLE_MIGRACOES (MIG_ID, MIG_DESCRICAO) VALUES (?, ?)', [id, descricao]);
} catch (_) {}
}
/**
* Executa todas as migrações pendentes para um alias.
* Idempotente: cada migração roda uma única vez.
*
* @param {string} alias
* @param {boolean} [silencioso=false] - Se true, não loga no console
* @returns {Promise<number>} Quantidade de migrações executadas
*/
async function executarMigracoes(alias, silencioso) {
if (!silencioso) console.log(`📦 Verificando migrações para "${alias}"...`);
const driver = db.driverOf(alias);
if (driver !== 'firebird') {
if (!silencioso) console.log(` ⏭️ Driver "${driver}" — sem migrações DDL`);
return 0;
}
await garantirControle(alias);
let executadas = 0;
for (const mig of MIGRACOES) {
if (await migracaoJaExecutada(alias, mig.id)) continue;
if (!silencioso) console.log(` ▶️ [${mig.id}] ${mig.descricao}`);
let erros = 0;
for (const cmd of mig.sql) {
try {
await db.execute(alias, cmd);
} catch (e) {
if (e.message.includes('already exists') || e.message.includes('Violation') || e.message.includes('duplicate')) {
continue; // idempotente
}
if (!silencioso) console.log(`${e.message.substring(0, 100)}`);
erros++;
}
}
if (erros === 0) {
if (typeof mig.after === 'function') {
try { await mig.after(alias); } catch (_) {}
}
await registrarMigracao(alias, mig.id, mig.descricao);
executadas++;
}
}
if (executadas > 0 && !silencioso) console.log(`${executadas} migração(ões) aplicada(s)`);
else if (!silencioso) console.log(` ✅ Nenhuma migração pendente`);
return executadas;
}
module.exports = { executarMigracoes, MIGRACOES };