@200systems/mf-db-mysql
Version:
MySQL database client with connection pooling, migrations, and health monitoring
188 lines • 6.58 kB
JavaScript
// packages/db-mysql/src/client.ts
import mysql from 'mysql2/promise';
import { ConnectionError, QueryError, TransactionError } from '@200systems/mf-db-core';
import { MySQLTransaction } from './transaction.js';
import { MySqlSqlAdapter } from './adapter.js';
/**
* Unified MySQL Database Client implementation
* Follows the exact same interface as PostgreSQL for easy switching
*/
export class MySQLClient {
pool;
logger;
sqlAdapter;
isInitialized = false;
constructor(config, logger) {
this.logger = logger.child('mysql-client');
this.sqlAdapter = new MySqlSqlAdapter();
this.pool = mysql.createPool({
host: config.host,
port: config.port,
database: config.database,
user: config.user,
password: config.password,
charset: config.charset || 'utf8mb4',
timezone: config.timezone || 'Z',
connectionLimit: config.max || 10,
queueLimit: 0,
});
}
async initialize() {
if (this.isInitialized)
return;
try {
// Test connection
const connection = await this.pool.getConnection();
await connection.query('SELECT 1');
connection.release();
this.isInitialized = true;
this.logger.info('MySQL client initialized');
}
catch (error) {
this.logger.error('Failed to initialize MySQL client', error);
throw error;
}
}
getDatabaseType() {
return 'mysql';
}
getSqlAdapter() {
return this.sqlAdapter;
}
supportsReturning() {
return false;
}
async close() {
if (!this.isInitialized || !this.pool)
return;
try {
await this.pool.end();
this.isInitialized = false;
this.logger.info('MySQL client closed');
}
catch (error) {
this.logger.error('Failed to close MySQL client', error);
throw error;
}
}
async query(sql, params = []) {
if (!this.isInitialized || !this.pool) {
throw new ConnectionError('MySQL client not initialized');
}
const timer = this.logger.startTimer('query');
try {
const [rows, fields] = await this.pool.execute(sql, params);
const rowsArray = Array.isArray(rows) ? rows : [];
this.logger.debug('Query executed', {
rowCount: rowsArray.length,
sql: sql.substring(0, 100) + (sql.length > 100 ? '...' : '')
});
return {
rows: rows,
rowCount: Array.isArray(rows) ? rows.length : rows.affectedRows || 0,
fields: fields?.map(field => ({
name: field.name,
dataTypeID: field.type,
})),
};
}
catch (error) {
this.logger.error('Query failed', error, { sql, params });
throw new QueryError(`Query execution failed: ${error.message}`, sql, params, error);
}
finally {
timer();
}
}
async transaction(callback) {
if (!this.pool || !this.isInitialized) {
throw new ConnectionError('Database not initialized');
}
const connection = await this.pool.getConnection();
const transactionLogger = this.logger.child('transaction');
const timer = transactionLogger.startTimer('transaction');
try {
// Create MySQL transaction using existing BaseTransaction
const transaction = new MySQLTransaction(connection, this.logger);
transactionLogger.debug('Transaction started');
const result = await callback(transaction);
await connection.commit();
transactionLogger.debug('Transaction committed');
return result;
}
catch (error) {
await connection.rollback();
transactionLogger.error('Transaction rolled back', error);
throw new TransactionError(`Transaction failed: ${error.message}`, error);
}
finally {
// don't release the connection on a transaction, since the pool will handle it
// connection.release();
timer();
}
}
async healthCheck() {
const startTime = Date.now();
try {
if (!this.pool || !this.isInitialized) {
return {
status: 'unhealthy',
message: 'Database not initialized',
timestamp: new Date(),
responseTime: Date.now() - startTime
};
}
// Simple health check query
await this.pool.execute('SELECT 1 as health');
const responseTime = Date.now() - startTime;
const poolStatus = this.getPoolStatus();
this.logger.debug('Health check passed', { responseTime, poolStatus });
return {
status: 'healthy',
message: 'Connection successful',
timestamp: new Date(),
responseTime,
details: {
activeConnections: poolStatus.active,
totalConnections: poolStatus.total
}
};
}
catch (error) {
return {
status: 'unhealthy',
message: error.message,
timestamp: new Date(),
responseTime: Date.now() - startTime
};
}
}
isReady() {
return this.isInitialized && this.pool !== null;
}
getConnectionInfo() {
if (!this.pool) {
return { total: 0, idle: 0, waiting: 0 };
}
const poolStatus = this.getPoolStatus();
return {
total: poolStatus.total,
idle: poolStatus.idle,
waiting: poolStatus.waiting
};
}
getPoolStatus() {
if (!this.pool) {
return { active: 0, total: 0, idle: 0, waiting: 0 };
}
// MySQL2 pool properties (may need adjustment based on version)
const pool = this.pool;
return {
active: pool._allConnections?.length || 0,
total: pool._allConnections?.length || 0,
idle: pool._freeConnections?.length || 0,
waiting: pool._connectionQueue?.length || 0
};
}
}
//# sourceMappingURL=client.js.map