@200systems/mf-db-mysql
v1.1.7
Published
MySQL database client with connection pooling, migrations, and health monitoring
Downloads
70
Maintainers
Readme
@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
mysql2com 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
npm install @200systems/mf-db-mysqlPeer Dependencies
npm install mysql2Quick Start
1. Configuração Básica
// 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
// 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
// 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', '[email protected]']
);
// 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
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
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
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
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.sqlFormato de Arquivo de Migração
-- 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
-- UPe seção-- DOWNobrigatórias - Primeira linha com
--vira a descrição da migração - Arquivos aplicados em ordem lexicográfica
- Rollback executa a seção
-- DOWNem ordem reversa - Checksum garante integridade dos arquivos já aplicados
Exemplos de Migrações MySQL
-- 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;-- 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;-- 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
// 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
// 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
// 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
// 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
// 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
-- 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
-- MySQL
JSON, ENUM('value1', 'value2'), TIMESTAMP DEFAULT CURRENT_TIMESTAMP
-- PostgreSQL
JSONB, VARCHAR(20) CHECK (column IN ('value1', 'value2')), TIMESTAMP WITH TIME ZONEError Handling
Tipos de Erro Disponíveis
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
// 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
# .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=infoMigration CLI Script
// 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);// 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
// 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
- Use o padrão Singleton para evitar múltiplas conexões desnecessárias
- Sempre inicialize o cliente antes de usar (
await dbClient.initialize()) - Use transações para operações que modificam múltiplas tabelas
- Configure charset utf8mb4 para suporte completo a Unicode
- Use InnoDB engine para suporte a transações e foreign keys
- Implemente health checks para monitoramento de produção
- Configure SSL adequadamente em produção
- Use Repository pattern para organizar queries
- Monitore o pool de conexões em produção
- Teste migrações em ambiente de desenvolvimento primeiro
- Use índices apropriados para queries frequentes
- 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
