diff --git a/src/main/services/database/data-source.ts b/src/main/services/database/data-source.ts index 7ab0865..6d48416 100644 --- a/src/main/services/database/data-source.ts +++ b/src/main/services/database/data-source.ts @@ -11,6 +11,9 @@ import 'reflect-metadata' import { DataSource, DataSourceOptions } from 'typeorm' import { ConfigManager } from '../config/config-manager' +import { createLogger } from '../logger' + +const log = createLogger('DataSource') /** * Get database type from config manager @@ -26,6 +29,7 @@ function getDatabaseType(): 'mysql' | 'mssql' { */ function buildDataSourceOptions(): DataSourceOptions { const type = getDatabaseType() + log.debug('Building DataSource options', { type }) const configManager = ConfigManager.getInstance() const config = configManager.getConfig() @@ -74,7 +78,11 @@ let dataSource: DataSource | null = null */ export function getDataSource(): DataSource { if (!dataSource) { + const type = getDatabaseType() + log.info('Creating new TypeORM DataSource', { type }) dataSource = new DataSource(buildDataSourceOptions()) + } else { + log.debug('Reusing existing DataSource') } return dataSource } @@ -85,7 +93,14 @@ export function getDataSource(): DataSource { export async function initializeDataSource(): Promise { const ds = getDataSource() if (!ds.isInitialized) { - await ds.initialize() + try { + await ds.initialize() + const type = getDatabaseType() + log.info('TypeORM DataSource initialized', { type }) + } catch (error) { + log.error('Failed to initialize DataSource', { error }) + throw error + } } return ds } @@ -95,8 +110,13 @@ export async function initializeDataSource(): Promise { */ export async function destroyDataSource(): Promise { if (dataSource && dataSource.isInitialized) { - await dataSource.destroy() - dataSource = null + try { + await dataSource.destroy() + dataSource = null + log.info('TypeORM DataSource destroyed') + } catch (error) { + log.error('Failed to destroy DataSource', { error }) + } } } diff --git a/src/main/services/database/mysql.ts b/src/main/services/database/mysql.ts index 556e0f4..45c2e0e 100644 --- a/src/main/services/database/mysql.ts +++ b/src/main/services/database/mysql.ts @@ -5,6 +5,9 @@ import type { QueryResult, MySqlConfig } from '../../types/database.types' +import { createLogger, trackDuration } from '../logger' + +const log = createLogger('MySqlService') export type { MySqlConfig } from '../../types/database.types' @@ -24,6 +27,7 @@ export class MySqlService implements IDatabaseService { */ async connect(): Promise { if (this.connection) { + log.warn('Already connected to MySQL') throw new Error('Already connected to MySQL') } @@ -38,7 +42,18 @@ export class MySqlService implements IDatabaseService { // Test connection await this.connection.ping() + log.info('Connected to MySQL', { + host: this.config.host, + port: this.config.port, + database: this.config.database + }) } catch (error) { + log.error('Failed to connect to MySQL', { + host: this.config.host, + port: this.config.port, + database: this.config.database, + error + }) throw new Error(`Failed to connect to MySQL: ${(error as Error).message}`) } } @@ -54,7 +69,9 @@ export class MySqlService implements IDatabaseService { try { await this.connection.end() this.connection = null + log.info('Disconnected from MySQL') } catch (error) { + log.error('Failed to disconnect from MySQL', { error }) throw new Error(`Failed to disconnect from MySQL: ${(error as Error).message}`) } } @@ -74,32 +91,40 @@ export class MySqlService implements IDatabaseService { throw new Error('Not connected to MySQL. Call connect() first.') } + const sqlPreview = sql.substring(0, 100) + const paramCount = params?.length ?? 0 + try { - const [result, fields] = await this.connection.execute(sql, params) + const { result: queryResult } = await trackDuration( + async () => { + const [result, fields] = await this.connection!.execute(sql, params) - // Convert to plain objects and extract column names - const columns = Array.isArray(fields) ? fields.map((field) => field.name) : [] + // Convert to plain objects and extract column names + const columns = Array.isArray(fields) ? fields.map((field) => field.name) : [] - // Handle different result types - let rows: Record[] = [] - let rowCount = 0 + // Handle different result types + let rows: Record[] = [] + let rowCount = 0 - if (Array.isArray(result)) { - // SELECT query - result is an array of rows - rows = result as Record[] - rowCount = rows.length - } else if (typeof result === 'object' && result !== null) { - // INSERT/UPDATE/DELETE query - result is OkPacket - const okPacket = result as any - rowCount = okPacket.affectedRows || okPacket.changedRows || 0 - } + if (Array.isArray(result)) { + // SELECT query - result is an array of rows + rows = result as Record[] + rowCount = rows.length + } else if (typeof result === 'object' && result !== null) { + // INSERT/UPDATE/DELETE query - result is OkPacket + const okPacket = result as any + rowCount = okPacket.affectedRows || okPacket.changedRows || 0 + } - return { - rows, - columns, - rowCount - } + return { rows, columns, rowCount } + }, + { operationName: 'MySqlService.query' } + ) + + log.debug('Query executed', { sqlPreview, rowCount: queryResult.rowCount, paramCount }) + return queryResult } catch (error) { + log.error('MySQL query failed', { sqlPreview, paramCount, error }) throw new Error(`MySQL query failed: ${(error as Error).message}`) } } @@ -112,17 +137,24 @@ export class MySqlService implements IDatabaseService { throw new Error('Not connected to MySQL. Call connect() first.') } + const queryCount = queries.length + log.info('Transaction started', { queryCount }) + try { await this.connection.beginTransaction() - for (const { sql, params } of queries) { + for (let i = 0; i < queries.length; i++) { + const { sql, params } = queries[i] await this.connection.execute(sql, params) + log.debug('Transaction query executed', { index: i, sqlPreview: sql.substring(0, 100) }) } await this.connection.commit() + log.info('Transaction committed', { queryCount }) } catch (error) { if (this.connection) { await this.connection.rollback() + log.warn('Transaction rolled back', { queryCount, error }) } throw new Error(`MySQL transaction failed: ${(error as Error).message}`) } diff --git a/src/main/services/database/sql-server.ts b/src/main/services/database/sql-server.ts index 1f9c255..1bc9769 100644 --- a/src/main/services/database/sql-server.ts +++ b/src/main/services/database/sql-server.ts @@ -5,6 +5,9 @@ import type { QueryResult, SqlServerConfig } from '../../types/database.types' +import { createLogger, trackDuration } from '../logger' + +const log = createLogger('SqlServerService') export type { SqlServerConfig } from '../../types/database.types' @@ -24,6 +27,7 @@ export class SqlServerService implements IDatabaseService { */ async connect(): Promise { if (this.pool) { + log.warn('Already connected to SQL Server') throw new Error('Already connected to SQL Server') } @@ -42,7 +46,18 @@ export class SqlServerService implements IDatabaseService { this.pool = new sql.ConnectionPool(poolConfig) await this.pool.connect() + log.info('Connected to SQL Server', { + server: this.config.server, + port: this.config.port, + database: this.config.database + }) } catch (error) { + log.error('Failed to connect to SQL Server', { + server: this.config.server, + port: this.config.port, + database: this.config.database, + error + }) throw new Error(`Failed to connect to SQL Server: ${(error as Error).message}`) } } @@ -58,7 +73,9 @@ export class SqlServerService implements IDatabaseService { try { await this.pool.close() this.pool = null + log.info('Disconnected from SQL Server') } catch (error) { + log.error('Failed to disconnect from SQL Server', { error }) throw new Error(`Failed to disconnect from SQL Server: ${(error as Error).message}`) } } @@ -80,29 +97,41 @@ export class SqlServerService implements IDatabaseService { throw new Error('Not connected to SQL Server. Call connect() first.') } + const sqlPreview = sqlString.substring(0, 100) + const paramCount = params?.length ?? 0 + try { - const request = this.pool.request() + const { result: queryResult } = await trackDuration( + async () => { + const request = this.pool!.request() - // Add parameters if provided - convert array to @p0, @p1, ... format - if (params && params.length > 0) { - params.forEach((value, index) => { - request.input(`p${index}`, value) - }) - } + // Add parameters if provided - convert array to @p0, @p1, ... format + if (params && params.length > 0) { + params.forEach((value, index) => { + request.input(`p${index}`, value) + }) + } - const result = await request.query(sqlString) + const result = await request.query(sqlString) - // Convert recordset to array of objects (may be undefined for DELETE/INSERT/UPDATE) - const rows = (result.recordset as Record[]) || [] - // Extract column names from the first row if available - const columns = rows.length > 0 ? Object.keys(rows[0]) : [] + // Convert recordset to array of objects (may be undefined for DELETE/INSERT/UPDATE) + const rows = (result.recordset as Record[]) || [] + // Extract column names from the first row if available + const columns = rows.length > 0 ? Object.keys(rows[0]) : [] - return { - rows, - columns, - rowCount: result.rowsAffected?.[0] || rows.length - } + return { + rows, + columns, + rowCount: result.rowsAffected?.[0] || rows.length + } + }, + { operationName: 'SqlServerService.query' } + ) + + log.debug('Query executed', { sqlPreview, rowCount: queryResult.rowCount, paramCount }) + return queryResult } catch (error) { + log.error('SQL Server query failed', { sqlPreview, paramCount, error }) throw new Error(`SQL Server query failed: ${(error as Error).message}`) } } @@ -126,31 +155,47 @@ export class SqlServerService implements IDatabaseService { throw new Error('Not connected to SQL Server. Call connect() first.') } + const sqlPreview = sqlString.substring(0, 100) + const paramNames = Object.keys(params) + try { - const request = this.pool.request() + const { result: queryResult } = await trackDuration( + async () => { + const request = this.pool!.request() - // Add parameters with explicit types - for (const [key, { value, type }] of Object.entries(params)) { - if (type) { - request.input(key, type, value) - } else { - request.input(key, value) - } - } + // Add parameters with explicit types + for (const [key, { value, type }] of Object.entries(params)) { + if (type) { + request.input(key, type, value) + } else { + request.input(key, value) + } + } - const result = await request.query(sqlString) + const result = await request.query(sqlString) - // Convert recordset to array of objects - const rows = result.recordset as Record[] - // Extract column names from the first row if available - const columns = rows.length > 0 ? Object.keys(rows[0]) : [] + // Convert recordset to array of objects + const rows = result.recordset as Record[] + // Extract column names from the first row if available + const columns = rows.length > 0 ? Object.keys(rows[0]) : [] - return { - rows, - columns, - rowCount: result.rowsAffected?.[0] || rows.length - } + return { + rows, + columns, + rowCount: result.rowsAffected?.[0] || rows.length + } + }, + { operationName: 'SqlServerService.queryWithParams' } + ) + + log.debug('Query with params executed', { + sqlPreview, + rowCount: queryResult.rowCount, + paramNames + }) + return queryResult } catch (error) { + log.error('SQL Server query with params failed', { sqlPreview, paramNames, error }) throw new Error(`SQL Server query failed: ${(error as Error).message}`) } } @@ -164,12 +209,15 @@ export class SqlServerService implements IDatabaseService { throw new Error('Not connected to SQL Server. Call connect() first.') } + const queryCount = queries.length const transaction = new sql.Transaction(this.pool) + log.info('Transaction started', { queryCount }) try { await transaction.begin() - for (const { sql: sqlString, params } of queries) { + for (let i = 0; i < queries.length; i++) { + const { sql: sqlString, params } = queries[i] const request = new sql.Request(transaction) // Add parameters if provided - convert array to @p0, @p1, ... format @@ -180,11 +228,17 @@ export class SqlServerService implements IDatabaseService { } await request.query(sqlString) + log.debug('Transaction query executed', { + index: i, + sqlPreview: sqlString.substring(0, 100) + }) } await transaction.commit() + log.info('Transaction committed', { queryCount }) } catch (error) { await transaction.rollback() + log.warn('Transaction rolled back', { queryCount, error }) throw new Error(`SQL Server transaction failed: ${(error as Error).message}`) } }