UNPKG

@04l3x4ndr3/microbase-orm

Version:

Um micro ORM em JavaScript ES6 inspirado no CodeIgniter 3 Query Builder com suporte para MySQL/MariaDB e PostgreSQL

659 lines (564 loc) 22.7 kB
import mysql from 'mysql2/promise'; import mariadb from 'mariadb'; import pg from 'pg'; class Connection { constructor(config) { this.config = config; this.connection = null; this.pool = null; this.isConnected = false; this.connectionId = this.generateConnectionId(); this.retryCount = 0; this.maxRetries = config.retryAttempts || 3; this.retryDelay = config.retryDelay || 1000; // ✅ Métricas de conexão this.metrics = { connectAttempts: 0, successfulConnections: 0, failedConnections: 0, reconnections: 0, lastConnected: null, totalUptime: 0, startTime: Date.now() }; // ✅ Health monitoring this.healthCheckInterval = null; this.lastHealthCheck = null; // ✅ Event handlers this.eventHandlers = new Map(); } // ✅ Sistema de eventos melhorado on(event, handler) { if (!this.eventHandlers.has(event)) { this.eventHandlers.set(event, []); } this.eventHandlers.get(event).push(handler); } emit(event, data) { if (this.eventHandlers.has(event)) { this.eventHandlers.get(event).forEach(handler => { try { handler(data); } catch (error) { console.error(`Erro em event handler ${event}:`, error.message); } }); } } async connect() { this.metrics.connectAttempts++; try { console.log(`🏁 Driver: ${this.config.driver.toUpperCase()} - Conectando ao banco de dados ${this.config.database}... [ID: ${this.connectionId}]`); await this._connectWithRetry(); this.isConnected = true; this.metrics.successfulConnections++; this.metrics.lastConnected = Date.now(); // ✅ Configurar health check se habilitado if (this.config.healthCheck) { this._startHealthCheck(); } this.emit('connected', { connectionId: this.connectionId, driver: this.config.driver, timestamp: Date.now() }); console.log(`✅ Conexão estabelecida com sucesso [ID: ${this.connectionId}]`); return this.connection; } catch (error) { this.metrics.failedConnections++; this.emit('connectionError', { connectionId: this.connectionId, error: error.message, timestamp: Date.now() }); console.error(`❌ Falha na conexão [ID: ${this.connectionId}]:`, error.message); throw error; } } async _connectWithRetry() { let lastError; for (let attempt = 1; attempt <= this.maxRetries; attempt++) { try { await this._performConnection(); return; } catch (error) { lastError = error; if (attempt < this.maxRetries && this._shouldRetry(error)) { const delay = this.retryDelay * Math.pow(2, attempt - 1); // Exponential backoff console.log(`⚠️ Tentativa ${attempt} falhou, tentando novamente em ${delay}ms...`); await this._sleep(delay); } else { break; } } } throw lastError; } _shouldRetry(error) { const retryableErrors = [ 'ECONNREFUSED', 'ENOTFOUND', 'ETIMEDOUT', 'ECONNRESET', 'ER_ACCESS_DENIED_ERROR' ]; return retryableErrors.some(code => error.code === code || error.message.includes(code) ); } _sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async _performConnection() { switch (this.config.driver.toLowerCase()) { case 'mysql': await this._connectMySQL(); break; case 'mariadb': await this._connectMariaDB(); break; case 'postgres': await this._connectPostgreSQL(); break; default: throw new Error(`Driver não suportado: ${this.config.driver}`); } } // ✅ Conexão MySQL melhorada async _connectMySQL() { const mysqlConfig = { host: this.config.host, user: this.config.username, password: this.config.password, database: this.config.database, port: this.config.port || 3306, charset: this.config.charset || 'utf8mb4', timezone: this.config.timezone || 'local', dateStrings: this.config.dateStrings || false, debug: this.config.debug || false, trace: this.config.trace || true, multipleStatements: this.config.multipleStatements || false, reconnect: this.config.reconnect !== false, maxReconnects: this.config.maxReconnects || 3, reconnectDelay: this.config.reconnectDelay || 2000, ...(this.config.ssl && {ssl: this.config.ssl}), ...(this.config.connectionTimeoutMillis && {connectTimeout: this.config.connectionTimeoutMillis}), ...(this.config.acquireTimeout && {acquireTimeout: this.config.acquireTimeout}), typeCast: function (field, next) { if (field.type === 'TINY' && field.length === 1) { const val = field.string(); return val === null ? null : val === '1'; } return next(); } }; if (this.config.max) { const pool = mysql.createPool({ ...mysqlConfig, connectionLimit: this.config.max || 10, queueLimit: this.config.queueLimit || 0, acquireTimeout: this.config.acquireTimeout || 60000, timeout: this.config.timeout || 60000, createConnection: mysql.createConnection, Promise: Promise }); // ✅ Event handlers melhorados para pool pool.on('connection', (connection) => { console.log(`🔗 Nova conexão MySQL no pool:`, connection.threadId); this.emit('poolConnection', { threadId: connection.threadId }); }); pool.on('error', (err) => { console.error('❌ Erro no pool MySQL:', err.message); this.emit('poolError', { error: err.message }); this._handleConnectionError(err); }); pool.on('enqueue', () => { console.log('⏳ Requisição enfileirada no pool MySQL'); this.emit('poolEnqueue', { timestamp: Date.now() }); }); // ✅ Testar pool antes de retornar await this._testPoolConnection(pool); this.pool = pool; this.connection = pool; } else { const connection = await mysql.createConnection(mysqlConfig); connection.on('error', (err) => { console.error(`❌ Erro na conexão MySQL:`, err.message); this.emit('connectionError', { error: err.message }); if (err.code === 'PROTOCOL_CONNECTION_LOST') { console.log('🔄 Tentando reconectar...'); this._handleDisconnect(); } }); // ✅ Testar conexão await connection.execute('SELECT 1'); this.connection = connection; } } // ✅ Conexão MariaDB melhorada async _connectMariaDB() { const mariadbConfig = { host: this.config.host, user: this.config.username, password: this.config.password, database: this.config.database, port: this.config.port || 3306, multipleStatements: this.config.multipleStatements || false, allowUserPasswords: true, charset: this.config.charset || 'utf8mb4', timezone: this.config.timezone || 'local', bigIntAsNumber: true, insertIdAsNumber: true, decimalAsNumber: false, dateStrings: this.config.dateStrings || false, ...(this.config.ssl && {ssl: this.config.ssl}), ...(this.config.connectionTimeoutMillis && {connectTimeout: this.config.connectionTimeoutMillis}), socketPath: this.config.socketPath || null }; if (this.config.max) { const pool = mariadb.createPool({ ...mariadbConfig, connectionLimit: this.config.max || 10, acquireTimeout: this.config.acquireTimeout || 60000, idleTimeoutMillis: this.config.idleTimeoutMillis || 30000, removeNodeErrorCount: this.config.removeNodeErrorCount || 5, restoreNodeTimeout: this.config.restoreNodeTimeout || 0, resetAfterUse: true, validateConnection: true }); // ✅ Event handlers para MariaDB pool pool.on('connection', (connection) => { console.log(`🔗 Nova conexão MariaDB no pool`); this.emit('poolConnection', { connectionId: connection.threadId }); }); pool.on('error', (err) => { console.error('❌ Erro no pool MariaDB:', err.message); this.emit('poolError', { error: err.message }); }); // ✅ Testar pool const testConnection = await pool.getConnection(); await testConnection.query('SELECT 1'); testConnection.release(); this.pool = pool; this.connection = pool; } else { const connection = await mariadb.createConnection(mariadbConfig); connection.on('error', (err) => { console.error(`❌ Erro na conexão MariaDB:`, err.message); this.emit('connectionError', { error: err.message }); }); // ✅ Testar conexão await connection.query('SELECT 1'); this.connection = connection; } } // ✅ Conexão PostgreSQL melhorada async _connectPostgreSQL() { const pgConfig = { host: this.config.host, user: this.config.username, password: this.config.password, database: this.config.database, port: this.config.port || 5432, application_name: `node_app_${this.connectionId}`, ...(this.config.ssl !== undefined && {ssl: this.config.ssl}), ...(this.config.connectionTimeoutMillis && { connectionTimeoutMillis: this.config.connectionTimeoutMillis }), ...(this.config.idleTimeoutMillis && { idleTimeoutMillis: this.config.idleTimeoutMillis }) }; if (this.config.options) { pgConfig.options = this.config.options; } if (this.config.max) { this.pool = new pg.Pool({ ...pgConfig, max: this.config.max, min: this.config.min || 2, idleTimeoutMillis: this.config.idleTimeoutMillis || 30000, connectionTimeoutMillis: this.config.connectionTimeoutMillis || 5000, maxUses: this.config.maxUses || 7500, allowExitOnIdle: true }); // ✅ Event handlers para PostgreSQL pool this.pool.on('connect', (client) => { console.log(`🔗 Nova conexão PostgreSQL no pool`); this.emit('poolConnection', { processID: client.processID }); }); this.pool.on('error', (err, client) => { console.error('❌ Erro no pool PostgreSQL:', err.message); this.emit('poolError', { error: err.message, processID: client?.processID }); }); this.pool.on('remove', (client) => { console.log(`🗑️ Conexão PostgreSQL removida do pool`); this.emit('poolRemove', { processID: client.processID }); }); // ✅ Testar pool const client = await this.pool.connect(); await client.query('SELECT 1'); client.release(); this.connection = this.pool; } else { const client = new pg.Client(pgConfig); await client.connect(); client.on('error', (err) => { console.error(`❌ Erro na conexão PostgreSQL:`, err.message); this.emit('connectionError', { error: err.message }); }); client.on('end', () => { console.log('🔌 Conexão PostgreSQL finalizada'); this.emit('connectionEnd', { timestamp: Date.now() }); }); // ✅ Testar conexão await client.query('SELECT 1'); this.connection = client; } } // ✅ Teste de conexão do pool async _testPoolConnection(pool) { try { const connection = await pool.getConnection(); await connection.execute('SELECT 1'); connection.release(); } catch (error) { await pool.end(); throw error; } } // ✅ Health check automático _startHealthCheck() { const interval = this.config.healthCheckInterval || 30000; this.healthCheckInterval = setInterval(async () => { await this._performHealthCheck(); }, interval); } async _performHealthCheck() { try { const startTime = Date.now(); if (this.pool) { if (['mysql', 'mariadb'].includes(this.config.driver)) { const connection = await this.pool.getConnection(); await connection.execute('SELECT 1 as health'); connection.release(); } else if (this.config.driver === 'postgres') { const client = await this.pool.connect(); await client.query('SELECT 1 as health'); client.release(); } } else { if (this.config.driver === 'postgres') { await this.connection.query('SELECT 1 as health'); } else { await this.connection.execute('SELECT 1 as health'); } } const duration = Date.now() - startTime; this.lastHealthCheck = { timestamp: Date.now(), status: 'healthy', duration, connectionId: this.connectionId }; this.emit('healthCheck', this.lastHealthCheck); } catch (error) { this.lastHealthCheck = { timestamp: Date.now(), status: 'unhealthy', error: error.message, connectionId: this.connectionId }; this.emit('healthCheckFailed', this.lastHealthCheck); console.error(`💔 Health check falhou [${this.connectionId}]:`, error.message); // Tentar reconectar se necessário if (this._shouldReconnect(error)) { await this._attemptReconnect(); } } } _shouldReconnect(error) { const reconnectableErrors = [ 'PROTOCOL_CONNECTION_LOST', 'ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT' ]; return reconnectableErrors.some(code => error.code === code || error.message.includes(code) ); } async _attemptReconnect() { if (this.retryCount >= this.maxRetries) { console.error(`❌ Máximo de tentativas de reconexão atingido [${this.connectionId}]`); return; } this.retryCount++; this.metrics.reconnections++; try { console.log(`🔄 Tentando reconectar [${this.connectionId}] - Tentativa ${this.retryCount}...`); await this.disconnect(); await this._sleep(this.retryDelay * this.retryCount); await this.connect(); this.retryCount = 0; // Reset no sucesso console.log(`✅ Reconexão bem-sucedida [${this.connectionId}]`); } catch (error) { console.error(`❌ Falha na reconexão [${this.connectionId}]:`, error.message); if (this.retryCount < this.maxRetries) { setTimeout(() => this._attemptReconnect(), this.retryDelay * this.retryCount * 1000); } } } async disconnect() { try { // ✅ Parar health check if (this.healthCheckInterval) { clearInterval(this.healthCheckInterval); this.healthCheckInterval = null; } if (this.connection) { if (this.pool) { // Fechar pool await this.pool.end(); console.log(`👋 Pool de conexões fechado [${this.connectionId}]`); } else { // Fechar conexão única if (this.config.driver === 'postgres') { await this.connection.end(); } else { await this.connection.end(); } console.log(`👋 Conexão única fechada [${this.connectionId}]`); } // ✅ Atualizar métricas if (this.metrics.lastConnected) { this.metrics.totalUptime += Date.now() - this.metrics.lastConnected; } this.connection = null; this.pool = null; this.isConnected = false; this.emit('disconnected', { connectionId: this.connectionId, timestamp: Date.now(), totalUptime: this.metrics.totalUptime }); } } catch (error) { console.error(`❌ Erro durante desconexão [${this.connectionId}]:`, error.message); throw error; } } // ✅ Métodos de pool melhorados async getConnection() { if (!this.isConnected) { throw new Error('Conexão não estabelecida'); } if (this.pool) { if (['mysql', 'mariadb'].includes(this.config.driver)) { return await this.pool.getConnection(); } else { return await this.pool.connect(); } } return this.connection; } async releaseConnection(connection) { if (this.pool && connection && connection.release) { connection.release(); } } // ✅ Estatísticas melhoradas do pool getPoolStats() { const baseStats = { connectionId: this.connectionId, isConnected: this.isConnected, driver: this.config.driver, metrics: { ...this.metrics }, lastHealthCheck: this.lastHealthCheck }; if (this.pool && ['mysql', 'mariadb'].includes(this.config.driver)) { return { ...baseStats, pool: { totalConnections: this.pool._allConnections?.length || 0, freeConnections: this.pool._freeConnections?.length || 0, queueLength: this.pool._connectionQueue?.length || 0, acquiringConnections: this.pool._acquiringConnections?.length || 0, activeConnections: (this.pool._allConnections?.length || 0) - (this.pool._freeConnections?.length || 0) } }; } else if (this.pool && this.config.driver === 'postgres') { return { ...baseStats, pool: { totalCount: this.pool.totalCount, idleCount: this.pool.idleCount, waitingCount: this.pool.waitingCount, activeCount: this.pool.totalCount - this.pool.idleCount } }; } return baseStats; } // ✅ Métodos utilitários generateConnectionId() { return `conn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } _handleConnectionError(error) { this.emit('connectionError', { connectionId: this.connectionId, error: error.message, code: error.code, timestamp: Date.now() }); } _handleDisconnect() { this.isConnected = false; this.emit('disconnect', { connectionId: this.connectionId, timestamp: Date.now() }); // Tentar reconectar automaticamente se configurado if (this.config.autoReconnect !== false) { setTimeout(() => this._attemptReconnect(), this.retryDelay); } } // ✅ Método para verificar se está conectado async ping() { try { if (this.pool) { const connection = await this.getConnection(); if (this.config.driver === 'postgres') { await connection.query('SELECT 1'); } else { await connection.execute('SELECT 1'); } this.releaseConnection(connection); } else { if (this.config.driver === 'postgres') { await this.connection.query('SELECT 1'); } else { await this.connection.execute('SELECT 1'); } } return true; } catch (error) { return false; } } // ✅ Informações da conexão getConnectionInfo() { return { connectionId: this.connectionId, driver: this.config.driver, host: this.config.host, port: this.config.port, database: this.config.database, isConnected: this.isConnected, hasPool: !!this.pool, metrics: this.metrics, lastHealthCheck: this.lastHealthCheck }; } } export default Connection;