UNPKG

@200systems/mf-db-mysql

Version:

MySQL database client with connection pooling, migrations, and health monitoring

889 lines (729 loc) 24.1 kB
# @200systems/mf-db-mysql Cliente MySQL para o microframework TypeScript com connection pooling, monitoramento de saúde, gerenciamento de transações e sistema de migrações completo. ## Features Implementadas - **🔗 Connection Pooling** - Usando `mysql2` com configuração avançada - **🏭 Singleton Factory** - Gerenciamento centralizado de instâncias de clientes - **🔄 Transaction Management** - Transações automáticas e manuais com BaseTransaction - **📊 Health Monitoring** - Verificações de saúde com métricas de timing e pool - **🗃️ Migration System** - Sistema completo com UP/DOWN scripts, checksum e rollback - **⚙️ TypeScript Support** - Tipagem completa para queries e configurações - **🛡️ Error Handling** - Tipos de erro específicos (ConnectionError, QueryError, etc.) - **📈 Pool Monitoring** - Informações detalhadas sobre estado do pool de conexões ## Instalação ```bash npm install @200systems/mf-db-mysql ``` ### Peer Dependencies ```bash npm install mysql2 ``` ## Quick Start ### 1. Configuração Básica ```typescript // src/database.ts import { MySQLFactory, MySQLConfig } from '@200systems/mf-db-mysql'; import { createLogger } from '@200systems/mf-logger'; const config: MySQLConfig = { host: process.env.MYSQL_HOST || 'localhost', port: parseInt(process.env.MYSQL_PORT || '3306'), database: process.env.MYSQL_DATABASE || 'myapp', user: process.env.MYSQL_USER || 'root', password: process.env.MYSQL_PASSWORD || 'secret', // Pool configuration max: 10, min: 2, connectionTimeoutMillis: 60000, idleTimeoutMillis: 30000, // MySQL specific charset: 'utf8mb4', timezone: 'Z', acquireTimeout: 60000, timeout: 60000, reconnect: true, multipleStatements: false, // SSL configuration (production) ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: true } : false }; const logger = createLogger({ context: 'database' }); export const dbClient = MySQLFactory.getInstance(config, logger); ``` ### 2. Inicialização e Queries ```typescript // src/server.ts import { dbClient } from './database.js'; // Inicializar conexão await dbClient.initialize(); // Query simples com placeholders MySQL (?) const users = await dbClient.query<{ id: number; name: string; email: string }>( 'SELECT id, name, email FROM users WHERE active = ?', [true] ); console.log(`Found ${users.rowCount} users:`, users.rows); // Cleanup gracioso process.on('SIGTERM', async () => { await dbClient.close(); process.exit(0); }); ``` ### 3. Transações ```typescript // Transação automática (recomendado) const result = await dbClient.transaction(async (trx) => { // Log de auditoria await trx.query('INSERT INTO audit_logs (action, user_id) VALUES (?, ?)', ['USER_CREATED', 123]); // Criar usuário const user = await trx.query<{ id: number }>( 'INSERT INTO users (name, email) VALUES (?, ?)', ['João Silva', 'joao@example.com'] ); // Criar perfil associado await trx.query('INSERT INTO user_profiles (user_id, settings) VALUES (?, ?)', [user.rows[0].id, '{}']); return user.rows[0]; }); console.log('Usuário criado:', result); ``` ## API Reference ### MySQLFactory ```typescript interface MySQLConfig extends DatabaseConfig { // Conexão básica host: string; port: number; database: string; user: string; password: string; // Pool de conexões max?: number; // Máximo de conexões (padrão: 10) min?: number; // Mínimo de conexões (padrão: 2) connectionTimeoutMillis?: number; // Timeout de conexão (padrão: 60000) idleTimeoutMillis?: number; // Timeout de idle (padrão: 30000) // MySQL específico charset?: string; // Charset (padrão: 'utf8mb4') timezone?: string; // Timezone (padrão: 'Z') acquireTimeout?: number; // Timeout para adquirir conexão (padrão: 60000) timeout?: number; // Timeout de query (padrão: 60000) reconnect?: boolean; // Auto-reconnect (padrão: true) multipleStatements?: boolean; // Múltiplas statements (padrão: false) // SSL (produção) ssl?: boolean | string | { rejectUnauthorized?: boolean; ca?: string; cert?: string; key?: string; }; } // Singleton Factory class MySQLDatabaseFactory { static getInstance(): MySQLDatabaseFactory; getInstance(config: DatabaseConfig, logger?: Logger): DatabaseClient; closeInstance(): Promise<void>; } // Instância padrão para conveniência const MySQLFactory: MySQLDatabaseFactory; ``` ### MySQLClient ```typescript class MySQLClient implements DatabaseClient { // Ciclo de vida initialize(): Promise<void>; close(): Promise<void>; isReady(): boolean; // Queries (usando ? como placeholder - padrão MySQL) query<T>(sql: string, params?: any[]): Promise<QueryResult<T>>; transaction<T>(callback: (trx: MySQLTransaction) => Promise<T>): Promise<T>; // Monitoramento healthCheck(): Promise<HealthCheckResult>; getConnectionInfo(): { total: number; idle: number; waiting: number }; } interface QueryResult<T> { rows: T[]; rowCount: number; fields: Array<{ name: string; dataTypeID: number }>; } interface HealthCheckResult { status: 'healthy' | 'unhealthy'; message: string; timestamp: Date; responseTime: number; details?: { activeConnections: number; totalConnections: number; }; } ``` ### MySQLTransaction ```typescript class MySQLTransaction extends BaseTransaction { query<T>(sql: string, params?: any[]): Promise<QueryResult<T>>; commit(): Promise<void>; rollback(): Promise<void>; isCompleted(): boolean; } // Uso manual (não recomendado) const trx = await dbClient.transaction(); try { await trx.query('INSERT INTO logs (event) VALUES (?)', ['START']); await trx.query('UPDATE counters SET value = value + 1'); await trx.commit(); } catch (error) { await trx.rollback(); throw error; } ``` ## Sistema de Migrações ### MySQLMigrator ```typescript import { MySQLMigrator } from '@200systems/mf-db-mysql'; const migrator = new MySQLMigrator( dbClient, './migrations', logger.child('migrator') ); // Aplicar todas as migrações pendentes await migrator.migrate(); // Rollback da última migração await migrator.rollback(); // Rollback até uma migração específica await migrator.rollbackTo('20241201_001_create_users'); // Reset completo (cuidado!) await migrator.reset(); // Verificar status const status = await migrator.getStatus(); console.log('Migrações aplicadas:', status.map(m => m.id)); // Listar migrações pendentes const pending = await migrator.getPending(); console.log('Pendentes:', pending.map(m => m.id)); ``` ### Estrutura de Arquivos de Migração ``` migrations/ ├── 20241201_001_create_users_table.sql ├── 20241201_002_add_user_profiles.sql ├── 20241202_001_add_indexes.sql └── 20241203_001_add_audit_logs.sql ``` ### Formato de Arquivo de Migração ```sql -- Criar tabela de usuários com engine InnoDB -- UP CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, email VARCHAR(100) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE INDEX idx_users_email ON users(email); CREATE INDEX idx_users_active ON users(active); -- DOWN DROP INDEX IF EXISTS idx_users_active ON users; DROP INDEX IF EXISTS idx_users_email ON users; DROP TABLE IF EXISTS users; ``` **Regras importantes:** - Arquivo deve ter seção `-- UP` e seção `-- DOWN` **obrigatórias** - Primeira linha com `--` vira a descrição da migração - Arquivos aplicados em ordem lexicográfica - Rollback executa a seção `-- DOWN` em ordem reversa - Checksum garante integridade dos arquivos já aplicados ### Exemplos de Migrações MySQL ```sql -- migrations/20241201_001_create_users_table.sql -- Criar tabela inicial de usuários -- UP CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, email VARCHAR(100) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, role ENUM('admin', 'user') DEFAULT 'user', active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- DOWN DROP TABLE IF EXISTS users; ``` ```sql -- migrations/20241201_002_add_user_profiles.sql -- Adicionar tabela de perfis com foreign key -- UP CREATE TABLE user_profiles ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, first_name VARCHAR(50), last_name VARCHAR(50), phone VARCHAR(20), avatar_url TEXT, settings JSON, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, UNIQUE KEY uk_user_profiles_user_id (user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- DOWN DROP TABLE IF EXISTS user_profiles; ``` ```sql -- migrations/20241202_001_optimize_indexes.sql -- Otimizar índices para queries comuns -- UP CREATE INDEX idx_users_role_active ON users(role, active); CREATE INDEX idx_user_profiles_name ON user_profiles(first_name, last_name); -- Adicionar índice fulltext para busca ALTER TABLE users ADD FULLTEXT(name, email); -- DOWN ALTER TABLE users DROP INDEX name; DROP INDEX IF EXISTS idx_user_profiles_name ON user_profiles; DROP INDEX IF EXISTS idx_users_role_active ON users; ``` ## Exemplos Práticos ### Configuração com Variáveis de Ambiente ```typescript // src/config/database.ts import { MySQLConfig } from '@200systems/mf-db-mysql'; export const dbConfig: MySQLConfig = { host: process.env.MYSQL_HOST || 'localhost', port: parseInt(process.env.MYSQL_PORT || '3306'), database: process.env.MYSQL_DATABASE!, user: process.env.MYSQL_USER!, password: process.env.MYSQL_PASSWORD!, // Pool otimizado para produção max: parseInt(process.env.DB_POOL_MAX || '10'), min: parseInt(process.env.DB_POOL_MIN || '2'), connectionTimeoutMillis: parseInt(process.env.DB_CONNECTION_TIMEOUT || '60000'), idleTimeoutMillis: parseInt(process.env.DB_IDLE_TIMEOUT || '30000'), // Configurações MySQL específicas charset: 'utf8mb4', timezone: 'Z', acquireTimeout: 60000, timeout: 10000, reconnect: true, multipleStatements: false, // SSL em produção ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: process.env.DB_SSL_REJECT_UNAUTHORIZED === 'true', ca: process.env.DB_SSL_CA, cert: process.env.DB_SSL_CERT, key: process.env.DB_SSL_KEY } : false }; ``` ### Repository Pattern ```typescript // src/repositories/UserRepository.ts import { DatabaseClient, QueryResult } from '@200systems/mf-db-mysql'; export interface User { id: number; name: string; email: string; role: 'admin' | 'user'; active: boolean; created_at: Date; updated_at: Date; } export class UserRepository { constructor(private db: DatabaseClient) {} async findById(id: number): Promise<User | null> { const result = await this.db.query<User>( 'SELECT * FROM users WHERE id = ?', [id] ); return result.rows[0] || null; } async findByEmail(email: string): Promise<User | null> { const result = await this.db.query<User>( 'SELECT * FROM users WHERE email = ?', [email] ); return result.rows[0] || null; } async findPaginated(limit: number, offset: number): Promise<{ users: User[]; total: number }> { const [usersResult, countResult] = await Promise.all([ this.db.query<User>( 'SELECT * FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?', [limit, offset] ), this.db.query<{ count: number }>( 'SELECT COUNT(*) as count FROM users' ) ]); return { users: usersResult.rows, total: countResult.rows[0].count }; } async create(userData: Omit<User, 'id' | 'created_at' | 'updated_at'>): Promise<User> { const result = await this.db.query<{ insertId: number }>( `INSERT INTO users (name, email, role, active) VALUES (?, ?, ?, ?)`, [userData.name, userData.email, userData.role, userData.active] ); return this.findById(result.rows[0].insertId)!; } async update(id: number, userData: Partial<Omit<User, 'id' | 'created_at' | 'updated_at'>>): Promise<User | null> { const setClause = Object.keys(userData) .map(key => `${key} = ?`) .join(', '); const values = [...Object.values(userData), id]; await this.db.query( `UPDATE users SET ${setClause} WHERE id = ?`, values ); return this.findById(id); } async delete(id: number): Promise<boolean> { const result = await this.db.query( 'DELETE FROM users WHERE id = ?', [id] ); return (result.rowCount || 0) > 0; } async searchByName(searchTerm: string): Promise<User[]> { const result = await this.db.query<User>( 'SELECT * FROM users WHERE MATCH(name) AGAINST(? IN NATURAL LANGUAGE MODE)', [searchTerm] ); return result.rows; } } ``` ### Service com Transações ```typescript // src/services/UserService.ts import { DatabaseClient } from '@200systems/mf-db-mysql'; import { UserRepository } from '../repositories/UserRepository.js'; export class UserService { constructor( private db: DatabaseClient, private userRepo: UserRepository ) {} async createUserWithProfile(userData: { name: string; email: string; role: 'admin' | 'user'; firstName: string; lastName: string; }): Promise<{ user: User; profile: UserProfile }> { return this.db.transaction(async (trx) => { // Criar usuário const userResult = await trx.query<{ insertId: number }>( 'INSERT INTO users (name, email, role) VALUES (?, ?, ?)', [userData.name, userData.email, userData.role] ); const userId = userResult.rows[0].insertId; // Criar perfil await trx.query( `INSERT INTO user_profiles (user_id, first_name, last_name) VALUES (?, ?, ?)`, [userId, userData.firstName, userData.lastName] ); // Log de auditoria await trx.query( 'INSERT INTO audit_logs (action, entity_type, entity_id, details) VALUES (?, ?, ?, ?)', ['CREATE', 'user', userId, JSON.stringify(userData)] ); // Buscar dados criados const [user, profile] = await Promise.all([ trx.query<User>('SELECT * FROM users WHERE id = ?', [userId]), trx.query<UserProfile>('SELECT * FROM user_profiles WHERE user_id = ?', [userId]) ]); return { user: user.rows[0], profile: profile.rows[0] }; }); } async transferCredits(fromUserId: number, toUserId: number, amount: number): Promise<void> { return this.db.transaction(async (trx) => { // Verificar saldo suficiente const fromUser = await trx.query<{ credits: number }>( 'SELECT credits FROM user_accounts WHERE user_id = ? FOR UPDATE', [fromUserId] ); if (fromUser.rows[0].credits < amount) { throw new Error('Insufficient credits'); } // Debitar do usuário origem await trx.query( 'UPDATE user_accounts SET credits = credits - ? WHERE user_id = ?', [amount, fromUserId] ); // Creditar para usuário destino await trx.query( 'UPDATE user_accounts SET credits = credits + ? WHERE user_id = ?', [amount, toUserId] ); // Log da transação await trx.query( `INSERT INTO credit_transactions (from_user_id, to_user_id, amount, type) VALUES (?, ?, ?, ?)`, [fromUserId, toUserId, amount, 'transfer'] ); }); } } ``` ### Health Check e Monitoring ```typescript // src/health.ts import { dbClient } from './database.js'; export async function checkDatabaseHealth() { const health = await dbClient.healthCheck(); const connectionInfo = dbClient.getConnectionInfo(); return { database: { status: health.status, message: health.message, responseTime: health.responseTime, timestamp: health.timestamp, details: health.details }, connectionPool: { total: connectionInfo.total, idle: connectionInfo.idle, waiting: connectionInfo.waiting, active: connectionInfo.total - connectionInfo.idle, utilization: `${Math.round((connectionInfo.total - connectionInfo.idle) / connectionInfo.total * 100)}%` } }; } // src/routes/health.ts (Express) app.get('/health', async (req, res) => { try { const health = await checkDatabaseHealth(); const statusCode = health.database.status === 'healthy' ? 200 : 503; res.status(statusCode).json(health); } catch (error) { res.status(503).json({ database: { status: 'unhealthy', message: error.message }, connectionPool: { total: 0, idle: 0, waiting: 0, active: 0, utilization: '0%' } }); } }); ``` ## Diferenças MySQL vs PostgreSQL ### Placeholders de Query ```typescript // MySQL usa ? como placeholder await mysqlClient.query('SELECT * FROM users WHERE id = ? AND active = ?', [123, true]); // PostgreSQL usa $1, $2, etc. await postgresClient.query('SELECT * FROM users WHERE id = $1 AND active = $2', [123, true]); ``` ### Auto-increment vs Serial ```sql -- MySQL CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) ); -- PostgreSQL CREATE TABLE users ( id SERIAL PRIMARY KEY, name VARCHAR(100) ); ``` ### Tipos de Dados ```sql -- MySQL JSON, ENUM('value1', 'value2'), TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- PostgreSQL JSONB, VARCHAR(20) CHECK (column IN ('value1', 'value2')), TIMESTAMP WITH TIME ZONE ``` ## Error Handling ### Tipos de Erro Disponíveis ```typescript import { ConnectionError, QueryError, TransactionError, MigrationError } from '@200systems/mf-db-mysql'; try { await dbClient.query('SELECT * FROM users'); } catch (error) { if (error instanceof ConnectionError) { console.error('Erro de conexão:', error.message); // Tentar reconectar ou usar fallback } else if (error instanceof QueryError) { console.error('Erro na query:', error.message); console.error('SQL:', error.sql); console.error('Params:', error.params); // Tratar erros específicos do MySQL if (error.message.includes('Duplicate entry')) { console.error('Violação de constraint UNIQUE'); } } else if (error instanceof TransactionError) { console.error('Erro na transação:', error.message); // Transaction já foi automaticamente revertida } } ``` ### Padrão Singleton e Múltiplos Clientes ```typescript // Múltiplos clientes para diferentes bases const mainDbClient = MySQLFactory.getInstance({ host: 'localhost', database: 'main_app', user: 'app_user', password: 'secret' }); const analyticsDbClient = MySQLFactory.getInstance({ host: 'analytics-server', database: 'analytics', user: 'analytics_user', password: 'secret' }); // Clients são automaticamente reutilizados se config for idêntica const sameClient = MySQLFactory.getInstance({ host: 'localhost', database: 'main_app', user: 'app_user', password: 'secret' }); // Retorna a mesma instância de mainDbClient // Cleanup de todas as conexões await MySQLDatabaseFactory.getInstance().closeInstance(); ``` ## Configuração de Ambiente ```bash # .env # Configuração do MySQL MYSQL_HOST=localhost MYSQL_PORT=3306 MYSQL_DATABASE=microframework_app MYSQL_USER=root MYSQL_PASSWORD=your_password # SSL (produção) DB_SSL=true DB_SSL_REJECT_UNAUTHORIZED=true DB_SSL_CA=/path/to/ca.pem DB_SSL_CERT=/path/to/cert.pem DB_SSL_KEY=/path/to/key.pem # Pool de Conexões DB_POOL_MAX=10 DB_POOL_MIN=2 DB_CONNECTION_TIMEOUT=60000 DB_IDLE_TIMEOUT=30000 # Configuração da Aplicação NODE_ENV=production LOG_LEVEL=info ``` ## Migration CLI Script ```typescript // scripts/migrate.ts import { dbClient } from '../src/database.js'; import { MySQLMigrator } from '@200systems/mf-db-mysql'; import { createLogger } from '@200systems/mf-logger'; const logger = createLogger({ context: 'migration-cli' }); const migrator = new MySQLMigrator(dbClient, './migrations', logger); async function main() { const command = process.argv[2]; const arg = process.argv[3]; await dbClient.initialize(); try { switch (command) { case 'up': await migrator.migrate(); break; case 'down': if (arg) { await migrator.rollbackTo(arg); } else { await migrator.rollback(); } break; case 'status': const status = await migrator.getStatus(); console.log(`Applied migrations: ${status.length}`); status.forEach(m => console.log(` ✓ ${m.id} - ${m.description}`)); const pending = await migrator.getPending(); if (pending.length > 0) { console.log(`\nPending migrations: ${pending.length}`); pending.forEach(m => console.log(` ○ ${m.id} - ${m.description}`)); } break; case 'reset': if (arg === '--confirm') { await migrator.reset(); } else { console.log('Use: npm run migrate reset --confirm'); } break; default: console.log('Usage: npm run migrate [up|down|status|reset] [migration_id|--confirm]'); } } finally { await dbClient.close(); } } main().catch(console.error); ``` ```json // package.json scripts { "scripts": { "migrate": "tsx scripts/migrate.ts", "migrate:up": "npm run migrate up", "migrate:down": "npm run migrate down", "migrate:status": "npm run migrate status", "migrate:reset": "npm run migrate reset" } } ``` ## Integração com Express Adapter ```typescript // src/server.ts - Integração com @200systems/mf-express-adapter import { ExpressApp } from '@200systems/mf-express-adapter'; import { dbClient } from './database.js'; const app = new ExpressApp({ port: 3000, cors: { origin: true } }); // Middleware de database (personalizado) app.getApp().use((req, res, next) => { req.db = dbClient; next(); }); // Health check endpoint app.addRoute({ method: 'get', path: '/health', handler: async (req, res) => { const health = await checkDatabaseHealth(); const statusCode = health.database.status === 'healthy' ? 200 : 503; res.status(statusCode).json(health); } }); // Aplicar migrações na inicialização const migrator = new MySQLMigrator(dbClient, './migrations', logger); await dbClient.initialize(); await migrator.migrate(); await app.start(); ``` ## Melhores Práticas 1. **Use o padrão Singleton** para evitar múltiplas conexões desnecessárias 2. **Sempre inicialize** o cliente antes de usar (`await dbClient.initialize()`) 3. **Use transações** para operações que modificam múltiplas tabelas 4. **Configure charset utf8mb4** para suporte completo a Unicode 5. **Use InnoDB engine** para suporte a transações e foreign keys 6. **Implemente health checks** para monitoramento de produção 7. **Configure SSL** adequadamente em produção 8. **Use Repository pattern** para organizar queries 9. **Monitore o pool** de conexões em produção 10. **Teste migrações** em ambiente de desenvolvimento primeiro 11. **Use índices apropriados** para queries frequentes 12. **Configure timeouts** adequados para sua aplicação ## Dependencies ### Required - `@200systems/mf-logger` - Sistema de logging - `@200systems/mf-db-core` - Abstrações de banco de dados ### Peer Dependencies - `mysql2` - Driver MySQL para Node.js (^3.6.0) ## License MIT