From 25458d5e44c15630099b7b74a1e964bf8f2b9187 Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Wed, 19 Nov 2025 16:01:37 -0300 Subject: [PATCH] =?UTF-8?q?REFACTOR/BUG:=20Corre=C3=A7=C3=A3o=20de=20bugs?= =?UTF-8?q?=20e=20alte=C3=A7=C3=A3o=20de=20=20run?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Alterado Código para que use o node cron e o gerenciamento seja feito pelo pm2 - Bug que não envia uma atualização para o SN quando um chamo é fechado fora do escopo corrigido --- .env.development | 18 +- .env.example | 57 ++ README.md | 78 ++ config/databaseConfig.js | 5 +- cron.js | 44 + ecosystem.config.js | 83 ++ package-lock.json | 795 +++++++++++++++++- package.json | 16 +- src/app.js | 8 +- src/controllers/processCommentsController.js | 6 + src/controllers/processStatusController.js | 2 +- src/data/database.js | 26 +- src/models/ticketGlpiModel.js | 20 +- src/models/ticketSyncModel.js | 2 +- src/models/ticketUpdateModel.js | 16 +- src/scripts/database/scriptBD.sql | 17 +- .../scripts/python/requirements.txt | 4 +- src/scripts/python/update_location_mapping.py | 147 ++-- src/utils/logger.js | 62 +- 19 files changed, 1269 insertions(+), 137 deletions(-) create mode 100644 .env.example create mode 100644 cron.js create mode 100644 ecosystem.config.js rename requirements.txt => src/scripts/python/requirements.txt (66%) diff --git a/.env.development b/.env.development index 2af03e7..cc4355a 100644 --- a/.env.development +++ b/.env.development @@ -4,6 +4,9 @@ NODE_ENV=development PORT=3000 +# Frequência de execução do cron job (formato cron). Padrão: '* * * * *' (a cada 1 minutos) +CRON_SCHEDULE='* * * * *' + # ServiceNow API (Dev) SERVICENOW_INSTANCE=https://caoadev.service-now.com SERVICENOW_USERNAME=SOTHIS.CAOA @@ -26,22 +29,25 @@ SERVICENOW_DEFAULT_USER_SYSID=b1312fda977a1250501634a6f053af98 # GLPI (banco real) GLPI_DB_TYPE=mysql -GLPI_DB_HOST=177.73.177.32 +GLPI_DB_HOST=177.73.177.44 GLPI_DB_PORT=3306 -GLPI_DB_USER=snglpi -GLPI_DB_PASSWORD=j2633669 +GLPI_DB_USER=desenvolvimento +GLPI_DB_PASSWORD=Ut@2S@$M9Xs@@W GLPI_DB_NAME=glpi_data GLPI_DB_CHARSET=utf8mb4 +# ID do usuário padrão do GLPI para criação de tickets e comentários +GLPI_DEFAULT_USER_ID=11111 + # Banco intermediário (PostgreSQL) -SNGLPI_DB_HOST=localhost +SNGLPI_DB_HOST=10.0.120.75 SNGLPI_DB_PORT=5432 -SNGLPI_DB_NAME=snglpi +SNGLPI_DB_NAME=snglpi-development SNGLPI_DB_USER=desenvolvimento SNGLPI_DB_PASSWORD=Ut@2S@$M9Xs@@W # CSV de mapeamento -LOCATION_MAPPING_CSV_PATH=/mnt/csv_share/OCATION_MAPPING.csv +LOCATION_MAPPING_CSV_PATH=\\10.0.121.40\Tecnica\Controle CAOA\ENTIDADE SERVICE NOW\LOCATION_MAPPING.csv # Logging LOG_LEVEL=debug diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..69ccb95 --- /dev/null +++ b/.env.example @@ -0,0 +1,57 @@ +# =================================================================== +# EXEMPLO DE ARQUIVO DE CONFIGURAÇÃO DE AMBIENTE +# +# Copie este arquivo para .env.development e .env.production +# e preencha com os valores corretos para cada ambiente. +# Não adicione senhas ou chaves secretas diretamente neste arquivo. +# =================================================================== + +# --- Configuração Geral da Aplicação --- +NODE_ENV=development +PORT=3000 + +# Frequência de execução do cron job (formato cron). Padrão: '* * * * *' (a cada 1 minutos) +CRON_SCHEDULE='* * * * *' + +# --- Configuração da API do ServiceNow --- +# URL base da sua instância do ServiceNow +SERVICENOW_INSTANCE=https://your-instance.service-now.com +SERVICENOW_USERNAME= +SERVICENOW_PASSWORD= + +# Sys_id do grupo de atribuição no ServiceNow que será monitorado para novos tickets +SERVICENOW_ASSIGNMENT_GROUP= + +# Sys_id do usuário padrão no ServiceNow que será usado pela integração para criar/atualizar tickets +SERVICENOW_DEFAULT_USER_SYSID= + +# --- Configuração do Banco de Dados do GLPI (MySQL) --- +GLPI_DB_TYPE=mysql +GLPI_DB_HOST=localhost +GLPI_DB_PORT=3306 +GLPI_DB_USER= +GLPI_DB_PASSWORD= +GLPI_DB_NAME=glpi +GLPI_DB_CHARSET=utf8mb4 + +# ID do usuário padrão do GLPI para criação de tickets e comentários +GLPI_DEFAULT_USER_ID=1118 + +# --- Configuração do Banco de Dados Intermediário (PostgreSQL) --- +SNGLPI_DB_HOST=localhost +SNGLPI_DB_PORT=5432 +SNGLPI_DB_NAME=snglpi_sync +SNGLPI_DB_USER= +SNGLPI_DB_PASSWORD= + +# --- Configuração de Logs --- +LOG_LEVEL=debug + +# --- Mapeamento de Localidades (Opcional) --- +# Caminho absoluto para o arquivo .csv que mapeia localidades do SNOW para entidades do GLPI +LOCATION_MAPPING_CSV_PATH=/path/to/your/location_mapping.csv + +# Logging +LOG_LEVEL=debug +LOG_RETENTION_DAYS=10 +LOG_DIR=logs diff --git a/README.md b/README.md index 96ae7ae..c45d427 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,84 @@ A aplicação funciona como um job agendado (por exemplo, via cron) que executa --- +## Como Executar a Aplicação + +Esta seção contém as instruções essenciais para configurar e rodar a aplicação em diferentes ambientes. + +### 1. Pré-requisitos + +- **Node.js**: Certifique-se de ter o Node.js (versão 18 ou superior) instalado. +- **Dependências**: Na raiz do projeto, execute o comando abaixo para instalar todas as dependências necessárias: + ```bash + npm install + ``` +- **Arquivos de Ambiente**: Crie os arquivos `.env.development` e `.env.production` na raiz do projeto, baseando-se no exemplo `.env.example` (se houver) e preenchendo com as credenciais e URLs corretas para cada ambiente. + +### 2. Executando com Scripts NPM + +### 2. Configuração do Ambiente Python + +A aplicação utiliza um script Python para sincronizar o mapeamento de localidades. + +#### a) Dependências Python + +Navegue até a pasta do script e instale as dependências listadas no `requirements.txt`: + +```bash +cd src/scripts/python +pip install -r requirements.txt +``` + +#### b) Montagem do Compartilhamento de Arquivos (CIFS/Samba) + +O script Python lê um arquivo `.csv` de um compartilhamento de rede. No servidor de produção, é necessário montar este compartilhamento para que o caminho definido em `LOCATION_MAPPING_CSV_PATH` seja acessível. + +Adicione a seguinte linha ao seu arquivo `/etc/fstab` para montar o compartilhamento automaticamente durante o boot do sistema. **Ajuste o IP, credenciais e caminhos conforme necessário.** + +```fstab +//10.0.121.40/Tecnica/Controle\040CAOA/ENTIDADE\040SERVICE\040NOW /mnt/csv_share cifs username=,password=,uid=1000,gid=1000,iocharset=utf8 0 0 +``` + +Após editar o `/etc/fstab`, execute `sudo mount -a` para montar o compartilhamento imediatamente. + +Os comandos abaixo utilizam a variável `NODE_ENV` para carregar o arquivo de ambiente (`.env`) correto. + +#### Ambiente de Desenvolvimento +Para rodar em modo de desenvolvimento com reinício automático a cada alteração de arquivo (usando `nodemon`): +```bash +npm run dev +``` + +#### Ambiente de Produção +Para rodar a aplicação em modo de produção (usando `node`): +```bash +npm start +``` +ou o comando explícito: +```bash +npm run start:prod +``` + +### 3. Executando com PM2 + +O PM2 é o gerenciador de processos recomendado para ambientes de produção. Ele garante que a aplicação reinicie automaticamente em caso de falhas e facilita o gerenciamento. + +- **Para iniciar em modo de produção:** + ```bash + pm2 start ecosystem.config.js --env production + ``` +- **Para iniciar em modo de desenvolvimento:** + ```bash + pm2 start ecosystem.config.js --env development + ``` +- **Comandos úteis do PM2:** + ```bash + pm2 list # Lista todos os processos + pm2 logs sn-glpi-sync-cron # Exibe os logs em tempo real + pm2 restart sn-glpi-sync-cron # Reinicia a aplicação + pm2 stop sn-glpi-sync-cron # Para a aplicação + ``` + ## Fluxo de Execução A aplicação segue uma ordem de execução estrita para garantir a consistência dos dados. O ponto de entrada é o arquivo `src/app.js`, que orquestra a chamada dos seguintes controladores: diff --git a/config/databaseConfig.js b/config/databaseConfig.js index a6bbafe..c833ba8 100644 --- a/config/databaseConfig.js +++ b/config/databaseConfig.js @@ -1,4 +1,5 @@ const { Sequelize } = require('sequelize'); +const { logInfo, logError } = require('../src/utils/logger'); // Database connection configuration const dbConfig = { @@ -14,9 +15,9 @@ const sequelize = new Sequelize(process.env.DB_NAME, process.env.DB_USER, proces const testConnection = async () => { try { await sequelize.authenticate(); - console.log('Connection to the database has been established successfully.'); + logInfo('Connection to the database has been established successfully.'); } catch (error) { - console.error('Unable to connect to the database:', error); + logError('Unable to connect to the database:', error); } }; diff --git a/cron.js b/cron.js new file mode 100644 index 0000000..b0a64d2 --- /dev/null +++ b/cron.js @@ -0,0 +1,44 @@ +const path = require('path'); +const dotenv = require('dotenv'); +const nodeEnv = process.env.NODE_ENV || 'development'; +const envFile = nodeEnv === 'production' ? '.env.production' : '.env.development'; +dotenv.config({ path: path.resolve(__dirname, envFile) }); + +const cron = require('node-cron'); +const { main: runSyncCycle } = require('./src/app.js'); +const { logInfo, logError } = require('./src/utils/logger.js'); + +let isCronRunning = false; +const CRON_SCHEDULE = process.env.CRON_SCHEDULE || '*/5 * * * *'; // A cada 5 minutos por padrão + + +logInfo(`⏰ Agendando ciclo de sincronização para rodar com a frequência: ${CRON_SCHEDULE}`); + +cron.schedule(CRON_SCHEDULE, async () => { + if (isCronRunning) { + logInfo('CRON: Ciclo anterior ainda em execução. Pulando esta rodada.'); + return; + } + + isCronRunning = true; + logInfo('CRON: Iniciando ciclo de sincronização...'); + + try { + await runSyncCycle(); + // A função main já loga sua própria mensagem de sucesso, então não precisamos duplicar aqui. + } catch (error) { + logError('CRON: Ocorreu um erro inesperado durante o ciclo de sincronização.', error); + } finally { + logInfo('CRON: Finalizando ciclo de sincronização.'); + isCronRunning = false; + } +}); + +/** + * @file cron.js + * @description Responsável por agendar e executar a rotina de sincronização periodicamente. + * Este arquivo: + * 1. Carrega as variáveis de ambiente apropriadas para o ambiente (desenvolvimento/produção). + * 2. Agenda um `cron job` (usando node-cron) para executar a função principal de sincronização. + * 3. Garante que apenas uma instância do ciclo de sincronização rode por vez. + */ \ No newline at end of file diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 0000000..5c3a192 --- /dev/null +++ b/ecosystem.config.js @@ -0,0 +1,83 @@ +module.exports = { + apps: [ + // 🕒 --- APLICAÇÃO 1: CRON JOB DE SINCRONIZAÇÃO (Node.js) --- + { + name: 'sn-glpi-sync-cron', // Nome que aparecerá no PM2 + script: 'cron.js', // Caminho do arquivo principal que executa o cron + + // 👇 Modo "fork" = apenas 1 instância, sem cluster (evita rodar crons duplicados) + exec_mode: 'fork', + instances: 1, // Força a rodar somente um processo + + // ⚙️ Variáveis de ambiente padrão (modo development) + env: { + NODE_ENV: 'development', + }, + + // ⚙️ Variáveis de ambiente quando rodar com `--env production` + env_production: { + NODE_ENV: 'production', + // Você pode sobrescrever outras variáveis aqui para produção, se necessário + // Ex: CRON_SCHEDULE: '*/2 * * * *' + }, + + // Configurações de log + output: './logs/pm2-out.log', + error: './logs/pm2-error.log', + log_date_format: 'YYYY-MM-DD HH:mm:ss', + }, + // 🗺️ --- APLICAÇÃO 2: MAPEADOR DE LOCALIDADES (Python) --- + { + name: 'sn-glpi-location-mapper', // Nome do serviço do mapeador + script: 'src/scripts/python/update_location_mapping.py', // Caminho do script Python + interpreter: 'python3', // Especifica o interpretador a ser usado + exec_mode: 'fork', + instances: 1, + autorestart: true, // Reinicia automaticamente em caso de falha + watch: false, // O script já roda em loop, não precisa de watch + + // Garante que o script Python também use as variáveis de ambiente corretas + env: { + NODE_ENV: 'development', + }, + env_production: { + NODE_ENV: 'production', + }, + + // Deixamos o script Python gerenciar seus próprios arquivos de log + output: '/dev/null', + error: '/dev/null', + }, + ], +}; + +/** + * @file ecosystem.config.js + * @description Define a configuração do PM2 para gerenciar a aplicação de sincronização. + * + * - `sn-glpi-sync-cron`: Serviço Node.js que executa o ciclo principal de sincronização. + * - `sn-glpi-location-mapper`: Serviço Python que monitora e atualiza o mapeamento de localidades. + * Ambos rodam em modo `fork` para garantir que + * apenas uma instância das tarefas agendadas seja executada por vez, evitando duplicidade. + * + * 💡 Dicas de uso: + * + * 1. Iniciar a aplicação: + * // Para ambiente de desenvolvimento + * pm2 start ecosystem.config.js --env development + * + * // Para ambiente de produção + * pm2 start ecosystem.config.js --env production + * + * 2. Comandos úteis: + * pm2 list // Lista todos os processos gerenciados pelo PM2 + * pm2 logs // Exibe os logs de um processo (ex: pm2 logs sn-glpi-sync-cron) + * pm2 monit // Abre um painel de monitoramento + * pm2 restart sn-glpi-sync-cron // Reinicia a aplicação + * pm2 stop sn-glpi-sync-cron // Para a aplicação + * pm2 delete sn-glpi-sync-cron // Remove a aplicação da lista do PM2 + * + * 3. Para iniciar com o sistema operacional (em produção): + * pm2 startup + * pm2 save + */ diff --git a/package-lock.json b/package-lock.json index 480db6c..03848a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,8 +13,15 @@ "dotenv": "^10.0.0", "express": "^5.1.0", "mysql2": "^3.14.5", + "node-cron": "^4.2.1", "pg": "^8.7.1", - "winston": "^3.17.0" + "winston": "^3.17.0", + "winston-daily-rotate-file": "^5.0.0" + }, + "devDependencies": { + "cross-env": "^7.0.3", + "nodemon": "^3.1.4", + "pino-pretty": "^11.3.0" } }, "node_modules/@colors/colors": { @@ -43,6 +50,19 @@ "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "license": "MIT" }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -56,12 +76,36 @@ "node": ">= 0.6" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/aws-ssl-profiles": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", @@ -80,6 +124,47 @@ "follow-redirects": "^1.14.0" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/body-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", @@ -100,6 +185,55 @@ "node": ">=18" } }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -138,6 +272,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/color": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", @@ -173,6 +332,13 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, "node_modules/colorspace": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", @@ -183,6 +349,13 @@ "text-hex": "1.0.x" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", @@ -222,6 +395,50 @@ "node": ">=6.6.0" } }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -301,6 +518,16 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -346,6 +573,26 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/express": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", @@ -388,12 +635,48 @@ "url": "https://opencollective.com/express" } }, + "node_modules/fast-copy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", + "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", "license": "MIT" }, + "node_modules/file-stream-rotator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz", + "integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/finalhandler": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", @@ -455,6 +738,21 @@ "node": ">= 0.8" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -510,6 +808,19 @@ "node": ">= 0.4" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -522,6 +833,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -546,6 +867,13 @@ "node": ">= 0.4" } }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -583,6 +911,34 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -604,6 +960,52 @@ "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", "license": "MIT" }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -628,6 +1030,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/kuler": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", @@ -732,6 +1151,38 @@ "node": ">= 0.6" } }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -795,6 +1246,63 @@ "node": ">= 0.6" } }, + "node_modules/node-cron": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", + "integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nodemon": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", + "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -807,6 +1315,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -846,6 +1364,16 @@ "node": ">= 0.8" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-to-regexp": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", @@ -861,6 +1389,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -945,6 +1474,72 @@ "split2": "^4.1.0" } }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-11.3.0.tgz", + "integrity": "sha512-oXwn7ICywaZPHmu3epHGU2oJX4nPmKvHvB/bwrJHlGcbEWaVcotkpyVHMKLKmiVryWYByNp0jpgAcXpFJDXJzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.2", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pump": "^3.0.0", + "readable-stream": "^4.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-pretty/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -984,6 +1579,16 @@ "node": ">=0.10.0" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -997,6 +1602,24 @@ "node": ">= 0.10" } }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -1050,6 +1673,19 @@ "node": ">= 6" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -1101,6 +1737,26 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", @@ -1149,6 +1805,29 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -1230,6 +1909,29 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", + "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "dev": true, + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -1275,12 +1977,51 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", "license": "MIT" }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1290,6 +2031,16 @@ "node": ">=0.6" } }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, "node_modules/triple-beam": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", @@ -1313,6 +2064,13 @@ "node": ">= 0.6" } }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -1337,11 +2095,28 @@ "node": ">= 0.8" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/winston": { "version": "3.17.0", "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", "license": "MIT", + "peer": true, "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", @@ -1359,6 +2134,24 @@ "node": ">= 12.0.0" } }, + "node_modules/winston-daily-rotate-file": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-5.0.0.tgz", + "integrity": "sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==", + "license": "MIT", + "dependencies": { + "file-stream-rotator": "^0.6.1", + "object-hash": "^3.0.0", + "triple-beam": "^1.4.1", + "winston-transport": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "winston": "^3" + } + }, "node_modules/winston-transport": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", diff --git a/package.json b/package.json index c635be4..4a793d3 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,12 @@ "name": "servicenow-glpi-sync", "version": "1.0.0", "description": "A Node.js application for integrating ServiceNow and GLPI.", - "main": "app.js", + "main": "cron.js", "scripts": { - "start": "node server.js", - "dev": "nodemon server.js", + "start": "npm run start:prod", + "dev": "cross-env NODE_ENV=development nodemon cron.js", + "start:dev": "cross-env NODE_ENV=development node cron.js", + "start:prod": "cross-env NODE_ENV=production node cron.js", "test": "echo \"No tests specified\" && exit 0" }, "dependencies": { @@ -13,8 +15,14 @@ "dotenv": "^10.0.0", "express": "^5.1.0", "mysql2": "^3.14.5", + "node-cron": "^4.2.1", "pg": "^8.7.1", - "winston": "^3.17.0" + "winston": "^3.17.0", + "winston-daily-rotate-file": "^5.0.0" + }, + "devDependencies": { + "cross-env": "^7.0.3", + "nodemon": "^3.1.4" }, "author": "Your Name", "license": "MIT" diff --git a/src/app.js b/src/app.js index 62f6047..eb7b266 100644 --- a/src/app.js +++ b/src/app.js @@ -1,7 +1,7 @@ const path = require('path'); const dotenv = require('dotenv'); const nodeEnv = process.env.NODE_ENV; -let envFile = '.env.devlopment'; +let envFile = '.env.development'; if (nodeEnv === 'production') {envFile = '.env.production';} else {envFile = '.env.development';} dotenv.config({ path: path.resolve(__dirname, '..', envFile) }); @@ -30,7 +30,11 @@ async function main() { } } -main(); +// A chamada main() foi removida daqui. +// Agora, o app.js apenas define e exporta a função main. +// A responsabilidade de EXECUTAR a função main é do cron.js, +// que a importará e a chamará dentro do agendamento. +module.exports = { main }; /** * @file app.js diff --git a/src/controllers/processCommentsController.js b/src/controllers/processCommentsController.js index 9e54c9d..5183a77 100644 --- a/src/controllers/processCommentsController.js +++ b/src/controllers/processCommentsController.js @@ -1,14 +1,20 @@ const {fetchCommentsFromServiceNow } = require('../services/servicenowService'); const { syncCommentsGlpitoSN, syncCommentsSNtoGlpi } = require('../services/glpiCommentService'); const TicketSyncModel = require('../models/ticketSyncModel'); +const { logInfo } = require('../utils/logger'); const processCommentsController = async () => { const ticketsToMonitor = await TicketSyncModel.getTicketsToMonitor(); + logInfo(`📋Tickets em monitoramento: ${ticketsToMonitor.length}`); + for (const ticket of ticketsToMonitor){ + // Sincroniza comentários do ServiceNow para o GLPI const comments = await fetchCommentsFromServiceNow(ticket); + // Se houver comentários novos no ServiceNow, sincroniza para o GLPI if(comments.length != 0){await syncCommentsSNtoGlpi(comments, ticket);} + // Sincroniza comentários do GLPI para o ServiceNow await syncCommentsGlpitoSN(ticket); } } diff --git a/src/controllers/processStatusController.js b/src/controllers/processStatusController.js index 8ebf998..99851ad 100644 --- a/src/controllers/processStatusController.js +++ b/src/controllers/processStatusController.js @@ -90,7 +90,7 @@ const processStatusAndClosureController = async () => { if (solution?.solutiontypes_id === 27) { logInfo(`REGRA DETECTADA: Ticket ${ticket.glpi_ticket_id} está fora do escopo (SolutionType ID: 27).`); const justification = solution?.content || 'Ticket marcado como "Fora do escopo" no GLPI.'; - await addWorkNoteToServiceNow(ticket.sn_ticket_id, `Ticket fechado no GLPI com a justificativa: ${justification}`); + await addWorkNoteToServiceNow(ticket.sn_ticket_id, `Ticket não será mais exibido para equipe Sothis, com a justificativa: ${justification}`); await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'closed', 'ignored'); await TicketGlpiModel.forceCloseTicket(ticket.glpi_ticket_id); } else { diff --git a/src/data/database.js b/src/data/database.js index aaf7791..000c37e 100644 --- a/src/data/database.js +++ b/src/data/database.js @@ -1,23 +1,31 @@ // src/data/database.js // Configuração da conexão com o banco de dados PostgreSQL const { Pool } = require('pg'); +const { logInfo, logError } = require('../utils/logger'); -const pool = new Pool({ - host: '10.0.120.75', - port: 5432, - database: 'snglpi', - user: 'desenvolvimento', - password: 'Ut@2S@$M9Xs@@W' -}); +const poolConfig = { + host: process.env.SNGLPI_DB_HOST , + port: process.env.SNGLPI_DB_PORT || 5432, + database: process.env.SNGLPI_DB_NAME, + user: process.env.SNGLPI_DB_USER, + password: process.env.SNGLPI_DB_PASSWORD +}; +const pool = new Pool(poolConfig); + +// Log da configuração do pool para depuração (sem a senha) +const sanitizedConfig = { ...poolConfig, password: '*****' }; +logInfo('--- Configuração do Pool PostgreSQL (Banco Intermediário) ---'); +logInfo(sanitizedConfig); +logInfo('----------------------------------------------------------'); // testa a conexao pool.on('connect', () => { - console.log('Conectado ao PostgreSQL'); + logInfo('Conectado ao PostgreSQL'); }); pool.on('error', (err) => { - console.error('Erro de conexão PostgreSQL:', err); + logError('Erro de conexão PostgreSQL:', err); }); //exporta o pool para ser usado em outros arquivos diff --git a/src/models/ticketGlpiModel.js b/src/models/ticketGlpiModel.js index b898c70..e11a181 100644 --- a/src/models/ticketGlpiModel.js +++ b/src/models/ticketGlpiModel.js @@ -2,6 +2,9 @@ const glpiPool = require('../data/glpiDataBase'); const { logInfo, logError } = require('../utils/logger'); const LocationMappingModel = require('./locationMappingModel'); +// Carrega o ID do usuário padrão do GLPI a partir das variáveis de ambiente +const GLPI_DEFAULT_USER_ID = parseInt(process.env.GLPI_DEFAULT_USER_ID, 10) || 1118; + class TicketGlpiModel { static mapStatus(snState) { @@ -186,8 +189,8 @@ class TicketGlpiModel { name: formattedTitle, date: ticketData.opened_at || new Date(), date_mod: ticketData.updated_at || new Date(), - status: TicketGlpiModel.mapStatus(ticketData.state || '1'), - users_id_recipient: 1118, + status: TicketGlpiModel.mapStatus(ticketData.state || '1'), // O status é mapeado + users_id_recipient: GLPI_DEFAULT_USER_ID, requesttypes_id: 1, content: formattedDescription, urgency: 3, @@ -211,8 +214,8 @@ class TicketGlpiModel { await connection.execute( `INSERT INTO glpi_tickets_users (tickets_id, users_id, type, use_notification, alternative_email) - VALUES (?, ?, 1, 1, '')`, - [ticketId, 1118] + VALUES (?, ?, 1, 1, '')`, // 1 = Requerente + [ticketId, GLPI_DEFAULT_USER_ID] ); await connection.execute( @@ -260,8 +263,9 @@ class TicketGlpiModel { static async getFollowupsByItemId(itemId) { const connection = await glpiPool.getConnection(); try { - const query = 'SELECT id, content, date, date_creation, date_mod FROM glpi_itilfollowups WHERE items_id = ? AND users_id != 1118'; - const [rows] = await connection.execute(query, [itemId]); + + const query = 'SELECT id, content, date, date_creation, date_mod FROM glpi_itilfollowups WHERE items_id = ? AND users_id != ?'; + const [rows] = await connection.execute(query,[itemId,GLPI_DEFAULT_USER_ID]); return rows; } catch (error) { logError(error, 'Erro ao buscar followups do ticket'); @@ -273,7 +277,7 @@ class TicketGlpiModel { const connection = await glpiPool.getConnection(); try { const query = 'INSERT INTO glpi_itilfollowups(itemtype, items_id, date, users_id, content, date_creation, date_mod, timeline_position) VALUES(?, ?, NOW(), ?, ?, NOW(), NOW(), ?)'; - const values = ['Ticket', items_id, 1118, comment.content , 1]; + const values = ['Ticket', items_id, GLPI_DEFAULT_USER_ID, comment.content , 1]; const [result] = await connection.execute(query, values); return [{ id: result.insertId }]; @@ -313,7 +317,7 @@ class TicketGlpiModel { await connection.beginTransaction(); const solutionQuery = `INSERT INTO glpi_itilsolutions (itemtype, items_id, content, users_id_editor, date_creation, date_mod) VALUES ('Ticket', ?, ?, ?, NOW(), NOW())`; - await connection.execute(solutionQuery, [ticketId, solutionContent, 1118]); + await connection.execute(solutionQuery, [ticketId, solutionContent, GLPI_DEFAULT_USER_ID]); const updateTicketQuery = `UPDATE glpi_tickets SET status = 5, solvedate = NOW() WHERE id = ?`; await connection.execute(updateTicketQuery, [ticketId]); diff --git a/src/models/ticketSyncModel.js b/src/models/ticketSyncModel.js index f448612..6476fea 100644 --- a/src/models/ticketSyncModel.js +++ b/src/models/ticketSyncModel.js @@ -1,6 +1,6 @@ const { query } = require('winston'); const pool = require('../data/database'); -const { logError } = require('../utils/logger'); +const { logError, logInfo } = require('../utils/logger'); class TicketSyncModel { diff --git a/src/models/ticketUpdateModel.js b/src/models/ticketUpdateModel.js index fd28535..ae33bef 100644 --- a/src/models/ticketUpdateModel.js +++ b/src/models/ticketUpdateModel.js @@ -10,14 +10,14 @@ class TicketUpdateModel { INSERT INTO ticket_updates (ticket_sync_id, update_type, source_system, content, author, created_at, updated_at, source_id, destiny_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ON CONFLICT (source_id) DO UPDATE SET - ticket_sync_id = $1, - update_type = $2, - source_system = $3, - content = $4, - author = $5, - created_at = $6, - updated_at = $7, - destiny_id = $9 + ticket_sync_id = EXCLUDED.ticket_sync_id, + update_type = EXCLUDED.update_type, + source_system = EXCLUDED.source_system, + content = EXCLUDED.content, + author = EXCLUDED.author, + created_at = EXCLUDED.created_at, + updated_at = EXCLUDED.updated_at, + destiny_id = EXCLUDED.destiny_id RETURNING *; `; const values = [ diff --git a/src/scripts/database/scriptBD.sql b/src/scripts/database/scriptBD.sql index 226f463..59ae0f8 100644 --- a/src/scripts/database/scriptBD.sql +++ b/src/scripts/database/scriptBD.sql @@ -98,6 +98,9 @@ CREATE TABLE ticket_updates ( ON DELETE CASCADE ); +-- Adciona a restrição de unicidade na coluna source_id +ALTER TABLE ticket_updates ADD CONSTRAINT ticket_updates_source_id_unique UNIQUE (source_id); + -- Índices para melhor performance nas consultas frequentes CREATE INDEX idx_ticket_updates_sync ON ticket_updates(ticket_sync_id); CREATE INDEX idx_ticket_updates_source ON ticket_updates(source_system); @@ -244,9 +247,6 @@ COMMENT ON COLUMN ticket_updates.update_type IS 'Tipo de atualização: task, no COMMENT ON COLUMN ticket_updates.source_system IS 'Sistema de origem: sn (ServiceNow) ou glpi'; COMMENT ON COLUMN ticket_updates.content IS 'Conteúdo da atualização/tarefa'; COMMENT ON COLUMN ticket_updates.author IS 'Autor da atualização'; -COMMENT ON COLUMN ticket_updates.created_at_source IS 'Data e hora de criação no sistema de origem'; -COMMENT ON COLUMN ticket_updates.sync_status IS 'Status da sincronização: pending, completed, error'; -COMMENT ON COLUMN ticket_updates.sync_date IS 'Data e hora da sincronização'; COMMENT ON COLUMN ticket_updates.created_at IS 'Data e hora de criação do registro'; COMMENT ON COLUMN ticket_updates.updated_at IS 'Data e hora da última atualização do registro'; @@ -283,4 +283,13 @@ COMMENT ON COLUMN sync_control.last_run_status IS 'Status da última execução COMMENT ON COLUMN sync_control.last_run_at IS 'Timestamp da última vez que o job rodou.'; -- Registro inicial para a marca d'água do GLPI. -INSERT INTO sync_control (system_name, last_watermark) VALUES ('glpi', '1970-01-01 00:00:00+00'); \ No newline at end of file +INSERT INTO sync_control (system_name, last_watermark) VALUES ('glpi', '1970-01-01 00:00:00+00'); + +SELECT * FROM ticket_sync WHERE glpi_sync_status != 'ignored'; +SELECT * FROM tickets_sn WHERE status != 'Encerrado' AND status != 'Encerrado - Omitido'; +SELECT * FROM public.location_mapping + +DELETE FROM tickets_sn WHERE status != 'Encerrado' AND status != 'Encerrado - Omitido'; + +UPDATE sync_control +SET last_watermark = '1970-01-01 00:00:00+00'; diff --git a/requirements.txt b/src/scripts/python/requirements.txt similarity index 66% rename from requirements.txt rename to src/scripts/python/requirements.txt index 1473b33..00fb47f 100644 --- a/requirements.txt +++ b/src/scripts/python/requirements.txt @@ -1,4 +1,4 @@ +pandas psycopg2-binary mysql-connector-python -python-dotenv -pandas \ No newline at end of file +python-dotenv \ No newline at end of file diff --git a/src/scripts/python/update_location_mapping.py b/src/scripts/python/update_location_mapping.py index d58afc4..fe2bb1a 100644 --- a/src/scripts/python/update_location_mapping.py +++ b/src/scripts/python/update_location_mapping.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 import os +from dotenv import load_dotenv import time -import logging import psycopg2 import mysql.connector from mysql.connector import Error import pandas as pd +import logging +from logging.handlers import TimedRotatingFileHandler from datetime import datetime, timedelta -from dotenv import load_dotenv from pathlib import Path # =========================== @@ -16,7 +17,7 @@ from pathlib import Path env = os.getenv('NODE_ENV', 'production') env_file = f".env.{env}" -env_path = Path(__file__).resolve().parents[2] / env_file +env_path = Path(__file__).resolve().parents[3] / env_file load_dotenv(dotenv_path=env_path) print(f"🌎 Ambiente carregado: {env} ({env_path})") @@ -25,24 +26,41 @@ print(f"🌎 Ambiente carregado: {env} ({env_path})") # 🪵 Configuração de Logs # =========================== -LOG_DIR = Path(__file__).resolve().parents[3] / "logs" / "location" -ERROR_DIR = Path(__file__).resolve().parents[3] / "logs" / "error" - +LOG_DIR = Path(__file__).resolve().parents[3] / "logs" LOG_DIR.mkdir(parents=True, exist_ok=True) -ERROR_DIR.mkdir(parents=True, exist_ok=True) -log_file = LOG_DIR / f"location_{datetime.now().strftime('%Y-%m-%d')}.log" -error_file = ERROR_DIR / f"location_error_{datetime.now().strftime('%Y-%m-%d')}.log" +# Cria um logger +logger = logging.getLogger("LocationMappingLogger") +logger.setLevel(logging.INFO) -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(levelname)s - %(message)s", - handlers=[ - logging.FileHandler(log_file, encoding="utf-8"), - logging.StreamHandler(), - logging.FileHandler(error_file, encoding="utf-8") - ] +# Evita que os logs se propaguem para o logger raiz +logger.propagate = False + +# Formato do log +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + +# Handler para o console (útil em desenvolvimento) +console_handler = logging.StreamHandler() +console_handler.setFormatter(formatter) + +# Handler para rotação diária de arquivos de log gerais +app_log_handler = TimedRotatingFileHandler( + LOG_DIR / "location-app.log", when="midnight", interval=1, backupCount=10, encoding="utf-8" ) +app_log_handler.setFormatter(formatter) + +# Handler para rotação diária de arquivos de log de erro +error_log_handler = TimedRotatingFileHandler( + LOG_DIR / "location-error.log", when="midnight", interval=1, backupCount=10, encoding="utf-8" +) +error_log_handler.setLevel(logging.ERROR) +error_log_handler.setFormatter(formatter) + +# Adiciona os handlers ao logger +logger.addHandler(console_handler) +logger.addHandler(app_log_handler) +logger.addHandler(error_log_handler) + # =========================== # 💾 Configurações dos Bancos @@ -74,15 +92,15 @@ CSV_PATH = os.getenv('LOCATION_MAPPING_CSV_PATH') def csv_recently_modified(minutes=2): """Verifica se o CSV foi alterado recentemente.""" if not os.path.exists(CSV_PATH): - logging.error(f"Arquivo CSV não encontrado: {CSV_PATH}") + logger.error(f"Arquivo CSV não encontrado: {CSV_PATH}") return False last_mod_time = datetime.fromtimestamp(os.path.getmtime(CSV_PATH)) if datetime.now() - last_mod_time <= timedelta(minutes=minutes): - logging.info(f"CSV modificado recentemente ({last_mod_time})") + logger.info(f"CSV modificado recentemente ({last_mod_time})") return True else: - logging.debug(f"CSV não foi modificado nos últimos {minutes} minutos.") + logger.debug(f"CSV não foi modificado nos últimos {minutes} minutos.") return False @@ -90,7 +108,7 @@ def get_snglpi_connection(): try: return psycopg2.connect(**SNGLPI_DB_CONFIG) except Exception as e: - logging.error(f"Erro ao conectar ao banco SNGLPI: {e}") + logger.error(f"Erro ao conectar ao banco SNGLPI: {e}", exc_info=True) return None @@ -98,7 +116,7 @@ def get_glpi_connection(): try: return mysql.connector.connect(**GLPI_DB_CONFIG) except Error as e: - logging.error(f"Erro ao conectar ao banco GLPI: {e}") + logger.error(f"Erro ao conectar ao banco GLPI: {e}", exc_info=True) return None @@ -114,7 +132,7 @@ def find_sn_location_id(location_name, cursor): result = cursor.fetchone() return result[0] if result else None except Exception as e: - logging.error(f"Erro ao buscar location_id ({location_name}): {e}") + logger.error(f"Erro ao buscar location_id ({location_name}): {e}", exc_info=True) return None @@ -130,7 +148,7 @@ def find_glpi_entity_id(entity_name, cursor): result = cursor.fetchone() return result[0] if result else None except Exception as e: - logging.error(f"Erro ao buscar entity_id ({entity_name}): {e}") + logger.error(f"Erro ao buscar entity_id ({entity_name}): {e}", exc_info=True) return None @@ -138,59 +156,48 @@ def update_mapping_table(): """Atualiza a tabela de mapeamento a partir do CSV.""" try: if not os.path.exists(CSV_PATH): - logging.error(f"Arquivo CSV não encontrado: {CSV_PATH}") + logger.error(f"Arquivo CSV não encontrado: {CSV_PATH}") return False - snglpi_conn = get_snglpi_connection() - glpi_conn = get_glpi_connection() + with get_snglpi_connection() as snglpi_conn, get_glpi_connection() as glpi_conn: + if not snglpi_conn or not glpi_conn: + return False - if not snglpi_conn or not glpi_conn: - return False + with snglpi_conn.cursor() as snglpi_cur, glpi_conn.cursor() as glpi_cur: + df = pd.read_csv(CSV_PATH, encoding='latin-1', sep=';') + df['sn_location_id'] = None + df['glpi_entity_id'] = None - snglpi_cur = snglpi_conn.cursor() - glpi_cur = glpi_conn.cursor() + mappings = [] + for _, row in df.iterrows(): + sn_name = row['sn_location_name'] + glpi_name = row['glpi_entity_name'] - df = pd.read_csv(CSV_PATH, encoding='latin-1', sep=';') - df['sn_location_id'] = None - df['glpi_entity_id'] = None + sn_id = find_sn_location_id(sn_name, snglpi_cur) + glpi_id = find_glpi_entity_id(glpi_name, glpi_cur) - mappings = [] - for _, row in df.iterrows(): - sn_name = row['sn_location_name'] - glpi_name = row['glpi_entity_name'] + if glpi_id: + mappings.append((sn_id, sn_name, glpi_id, glpi_name)) + else: + logger.warning(f"Entity não encontrada: {glpi_name}") - sn_id = find_sn_location_id(sn_name, snglpi_cur) - glpi_id = find_glpi_entity_id(glpi_name, glpi_cur) - - if glpi_id: - mappings.append((sn_id, sn_name, glpi_id, glpi_name)) - else: - logging.warning(f"Entity não encontrada: {glpi_name}") - - if mappings: - snglpi_cur.execute("DELETE FROM location_mapping") - insert_query = """ - INSERT INTO location_mapping - (sn_location_id, sn_location_name, glpi_entity_id, glpi_entity_name) - VALUES (%s, %s, %s, %s) - """ - snglpi_cur.executemany(insert_query, mappings) - snglpi_conn.commit() - logging.info(f"Tabela atualizada com {len(mappings)} registros.") - else: - logging.warning("Nenhum mapeamento válido encontrado.") - - snglpi_cur.close() - glpi_cur.close() - snglpi_conn.close() - glpi_conn.close() + if mappings: + snglpi_cur.execute("DELETE FROM location_mapping") + insert_query = """ + INSERT INTO location_mapping + (sn_location_id, sn_location_name, glpi_entity_id, glpi_entity_name) + VALUES (%s, %s, %s, %s) + """ + snglpi_cur.executemany(insert_query, mappings) + snglpi_conn.commit() + logger.info(f"Tabela atualizada com {len(mappings)} registros.") + else: + logger.warning("Nenhum mapeamento válido encontrado.") return True except Exception as e: - logging.error(f"Erro no processo de atualização: {e}") - if 'snglpi_conn' in locals(): - snglpi_conn.rollback() + logger.error(f"Erro no processo de atualização: {e}", exc_info=True) return False @@ -199,16 +206,16 @@ def update_mapping_table(): # =========================== if __name__ == "__main__": - logging.info("🚀 Iniciando monitoramento contínuo do Location Mapping") + logger.info("🚀 Iniciando monitoramento contínuo do Location Mapping") while True: try: if csv_recently_modified(minutes=2): success = update_mapping_table() if success: - logging.info("✅ Atualização concluída com sucesso.") + logger.info("✅ Atualização concluída com sucesso.") else: - logging.error("❌ Falha na atualização.") + logger.error("❌ Falha na atualização.") time.sleep(60) except Exception as e: - logging.error(f"Erro inesperado no loop principal: {e}") + logger.error(f"Erro inesperado no loop principal: {e}", exc_info=True) time.sleep(60) diff --git a/src/utils/logger.js b/src/utils/logger.js index 1aec85a..a33d83e 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -1,5 +1,6 @@ const winston = require('winston'); const path = require('path'); +require('winston-daily-rotate-file'); const fs = require('fs'); // verifica se a pasta de logs existe, se não, cria @@ -11,27 +12,45 @@ if (!fs.existsSync(logsDir)) { // Configuração do logger com winston const logger = winston.createLogger({ level: 'info', - format: winston.format.combine( - winston.format.timestamp({ - format: 'YYYY-MM-DD HH:mm:ss' - }), - winston.format.errors({ stack: true }), // ← Mostra stack trace de erros - winston.format.json() - ), + format: winston.format.combine(winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.errors({ stack: true })), transports: [ // Log geral da aplicação - new winston.transports.File({ - filename: path.join(logsDir, 'app.log'), - maxsize: 5242880, // 5MB - maxFiles: 5 + new winston.transports.DailyRotateFile({ + filename: path.join(logsDir, 'app-%DATE%.log'), + datePattern: 'YYYY-MM-DD', + zippedArchive: true, + maxSize: '5m', + maxFiles: '10d', + format: winston.format.combine( + winston.format.printf((info) => { + const { timestamp, level, message, stack, ...meta } = info; + let logMessage = `${timestamp} [${level}]: ${stack || message}`; + if (Object.keys(meta).length) { + logMessage += ` ${JSON.stringify(meta, null, 2)}`; + } + return logMessage; + }) + ), }), // Log de erros - new winston.transports.File({ - filename: path.join(logsDir, 'error.log'), + new winston.transports.DailyRotateFile({ + filename: path.join(logsDir, 'error-%DATE%.log'), level: 'error', - maxsize: 5242880, - maxFiles: 3 - }), + datePattern: 'YYYY-MM-DD', + zippedArchive: true, + maxSize: '5m', + maxFiles: '10d', + format: winston.format.combine( + winston.format.printf((info) => { + const { timestamp, level, message, stack, ...meta } = info; + let logMessage = `${timestamp} [${level}]: ${stack || message}`; + if (Object.keys(meta).length) { + logMessage += ` ${JSON.stringify(meta, null, 2)}`; + } + return logMessage; + }) + ), + }) ], }); @@ -40,8 +59,13 @@ if (process.env.NODE_ENV !== 'production') { logger.add(new winston.transports.Console({ format: winston.format.combine( winston.format.colorize(), - winston.format.printf(({ timestamp, level, message, stack }) => { - return `${timestamp} [${level}]: ${stack || message}`; + winston.format.printf((info) => { + const { timestamp, level, message, stack, ...meta } = info; + let logMessage = `${timestamp} [${level}]: ${stack || message}`; + if (Object.keys(meta).length) { + logMessage += ` ${JSON.stringify(meta, null, 2)}`; + } + return logMessage; }) ) })); @@ -78,5 +102,5 @@ module.exports = { logError, logInfo, logWarning, - logSync + logSync, }; \ No newline at end of file