diff --git a/src/main/ipc/database-handler.ts b/src/main/ipc/database-handler.ts index 7146ebe..a4276f4 100644 --- a/src/main/ipc/database-handler.ts +++ b/src/main/ipc/database-handler.ts @@ -214,7 +214,18 @@ export function registerDatabaseHandlers(): void { } log.debug('Executing SQL Server query', { windowId, sql: sqlString.substring(0, 100) }) - return await service.query(sqlString, params) + + // Use queryWithParams for named parameters, or query for no params + if (params && Object.keys(params).length > 0) { + // Convert to the format expected by queryWithParams + const typedParams: Record = {} + for (const [key, value] of Object.entries(params)) { + typedParams[key] = { value } + } + return await service.queryWithParams(sqlString, typedParams) + } else { + return await service.query(sqlString) + } } catch (error) { const message = error instanceof Error ? error.message : 'SQL Server query failed' log.error('SQL Server query failed', { error: message }) diff --git a/src/main/ipc/extractor-handler.ts b/src/main/ipc/extractor-handler.ts index 4a13e87..8b1e11e 100644 --- a/src/main/ipc/extractor-handler.ts +++ b/src/main/ipc/extractor-handler.ts @@ -2,7 +2,7 @@ import { ipcMain } from 'electron' import { ErpAuthService } from '../services/erp/erp-auth' import { ExtractorService } from '../services/erp/extractor' import { OrderNumberResolver } from '../services/erp/order-resolver' -import { MySqlService } from '../services/database/mysql' +import { create, type IDatabaseService } from '../services/database' import { createLogger } from '../services/logger' import { withErrorHandling, type IpcResult } from './index' import { ErpConnectionError, ValidationError, DatabaseQueryError } from '../types/errors' @@ -19,7 +19,7 @@ export function registerExtractorHandlers(): void { async (_event, input: ExtractorInput): Promise> => { return withErrorHandling(async () => { let authService: ErpAuthService | null = null - let mysqlService: MySqlService | null = null + let dbService: IDatabaseService | null = null try { // Check environment variables @@ -39,29 +39,20 @@ export function registerExtractorHandlers(): void { ) } - // Resolve order numbers (convert productionIDs to 生产订单号) - const mysqlConfig = { - host: process.env.DB_MYSQL_HOST || 'localhost', - port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10), - user: process.env.DB_USERNAME || 'root', - password: process.env.DB_PASSWORD || '', - database: process.env.DB_NAME || '' - } - - log.info('Connecting to MySQL for order resolution...') - mysqlService = new MySqlService(mysqlConfig) - + // Create database service using factory + log.info('Connecting to database for order resolution...') try { - await mysqlService.connect() + dbService = await create() } catch (error) { throw new DatabaseQueryError( - 'MySQL 连接失败', + '数据库连接失败', 'DB_CONNECTION_FAILED', error instanceof Error ? error : undefined ) } - const resolver = new OrderNumberResolver(mysqlService) + // Resolve order numbers (convert productionIDs to 生产订单号) + const resolver = new OrderNumberResolver(dbService) const mappings = await resolver.resolve(input.orderNumbers) // Get valid order numbers and warnings @@ -136,13 +127,13 @@ export function registerExtractorHandlers(): void { } } - // Clean up: disconnect MySQL - if (mysqlService) { + // Clean up: disconnect database + if (dbService) { try { - await mysqlService.disconnect() - log.debug('MySQL disconnected') + await dbService.disconnect() + log.debug('Database disconnected') } catch (closeError) { - log.warn('Error disconnecting MySQL', { + log.warn('Error disconnecting database', { error: closeError instanceof Error ? closeError.message : String(closeError) }) } diff --git a/src/main/ipc/resolver-handler.ts b/src/main/ipc/resolver-handler.ts index bc9fade..6255d99 100644 --- a/src/main/ipc/resolver-handler.ts +++ b/src/main/ipc/resolver-handler.ts @@ -7,10 +7,9 @@ */ import { ipcMain } from 'electron' -import { MySqlService } from '../services/database/mysql' +import { create, type IDatabaseService } from '../services/database' import { OrderNumberResolver } from '../services/erp/order-resolver' import { createLogger } from '../services/logger' -import { DatabaseQueryError } from '../types/errors' import type { OrderMapping, ResolutionStats } from '../services/erp/order-resolver' const log = createLogger('ResolverHandler') @@ -21,14 +20,6 @@ const log = createLogger('ResolverHandler') export interface ResolverInput { /** List of order numbers/productionIDs to resolve */ inputs: string[] - /** MySQL configuration (optional, uses default if not provided) */ - mysqlConfig?: { - host: string - port: number - user: string - password: string - database: string - } } /** @@ -60,25 +51,15 @@ export function registerResolverHandlers(): void { ipcMain.handle( 'resolver:resolve', async (_event, input: ResolverInput): Promise => { - let mysqlService: MySqlService | null = null + let dbService: IDatabaseService | null = null try { - // Use provided config or environment variables - const mysqlConfig = input.mysqlConfig || { - host: process.env.DB_MYSQL_HOST || 'localhost', - port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10), - user: process.env.DB_USERNAME || 'root', - password: process.env.DB_PASSWORD || '', - database: process.env.DB_NAME || '' - } - - // Create MySQL service - log.info('Connecting to MySQL for resolution', { inputCount: input.inputs.length }) - mysqlService = new MySqlService(mysqlConfig) - await mysqlService.connect() + // Create database service using factory + log.info('Connecting to database for resolution', { inputCount: input.inputs.length }) + dbService = await create() // Create resolver and resolve inputs - const resolver = new OrderNumberResolver(mysqlService) + const resolver = new OrderNumberResolver(dbService) const mappings = await resolver.resolve(input.inputs) // Get valid order numbers and warnings @@ -107,13 +88,13 @@ export function registerResolverHandlers(): void { error: `解析失败:${message}` } } finally { - // Clean up MySQL connection - if (mysqlService) { + // Clean up database connection + if (dbService) { try { - await mysqlService.disconnect() - log.debug('MySQL disconnected') + await dbService.disconnect() + log.debug('Database disconnected') } catch (closeError) { - log.warn('Error disconnecting MySQL', { + log.warn('Error disconnecting database', { error: closeError instanceof Error ? closeError.message : String(closeError) }) } @@ -136,9 +117,11 @@ export function registerResolverHandlers(): void { error?: string }> => { try { + // Create a mock resolver without database connection const resolver = new OrderNumberResolver({ - isConnected: () => false - } as MySqlService) + isConnected: () => false, + type: 'mysql' + } as IDatabaseService) const results = inputs.map((input) => ({ input, diff --git a/src/main/services/database/discrete-material-plan-dao.ts b/src/main/services/database/discrete-material-plan-dao.ts index dc5d19a..4d47e94 100644 --- a/src/main/services/database/discrete-material-plan-dao.ts +++ b/src/main/services/database/discrete-material-plan-dao.ts @@ -8,9 +8,7 @@ * - Statistics gathering */ -import { MySqlService } from './mysql' -import { SqlServerService } from './sql-server' -import sql from 'mssql' +import { create, type IDatabaseService } from './index' import { createLogger } from '../logger' const log = createLogger('DiscreteMaterialPlanDAO') @@ -95,70 +93,37 @@ export const DISCRETE_MATERIAL_PLAN_CONFIG = { * DiscreteMaterialPlanDAO Class */ export class DiscreteMaterialPlanDAO { - private mysqlService: MySqlService | null = null - private sqlServerService: SqlServerService | null = null - private dbType: 'mysql' | 'sqlserver' = 'mysql' - - /** - * Constructor - determine database type from environment - */ - constructor() { - const dbType = process.env.DB_TYPE?.toLowerCase() - if (dbType === 'sqlserver' || dbType === 'mssql') { - this.dbType = 'sqlserver' - } else { - this.dbType = 'mysql' - } - } + private dbService: IDatabaseService | null = null /** * Get the appropriate table name based on database type */ private getTableName(): string { - return this.dbType === 'sqlserver' + const isSqlServer = this.dbService?.type === 'sqlserver' + return isSqlServer ? DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_SQLSERVER : DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL } /** - * Get database service instance (MySQL or SQL Server) + * Get database service instance using DatabaseFactory */ - private async getDatabaseService(): Promise { - if (this.dbType === 'sqlserver') { - if (this.sqlServerService && this.sqlServerService.isConnected()) { - return this.sqlServerService - } - - this.sqlServerService = new SqlServerService({ - server: process.env.DB_SERVER || 'localhost', - port: parseInt(process.env.DB_SQLSERVER_PORT || '1433', 10), - user: process.env.DB_USERNAME || 'sa', - password: process.env.DB_PASSWORD || '', - database: process.env.DB_NAME || '', - options: { - encrypt: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes', - trustServerCertificate: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes' - } - }) - - await this.sqlServerService.connect() - return this.sqlServerService - } else { - if (this.mysqlService && this.mysqlService.isConnected()) { - return this.mysqlService - } - - this.mysqlService = new MySqlService({ - host: process.env.DB_MYSQL_HOST || 'localhost', - port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10), - user: process.env.DB_USERNAME || 'root', - password: process.env.DB_PASSWORD || '', - database: process.env.DB_NAME || '' - }) - - await this.mysqlService.connect() - return this.mysqlService + private async getDatabaseService(): Promise { + if (this.dbService && this.dbService.isConnected()) { + return this.dbService } + + this.dbService = await create() + return this.dbService + } + + /** + * Build placeholders for IN clause based on database type + */ + private buildPlaceholders(count: number, isSqlServer: boolean): string { + return isSqlServer + ? Array.from({ length: count }, (_, idx) => `@p${idx}`).join(',') + : Array.from({ length: count }, () => '?').join(',') } // ==================== QUERY ALL ==================== @@ -173,11 +138,7 @@ export class DiscreteMaterialPlanDAO { const tableName = this.getTableName() const sqlString = `SELECT * FROM ${tableName}` - - const result = - this.dbType === 'sqlserver' - ? await (dbService as SqlServerService).query(sqlString) - : await (dbService as MySqlService).query(sqlString) + const result = await dbService.query(sqlString) return result.rows } catch (error) { @@ -221,11 +182,7 @@ export class DiscreteMaterialPlanDAO { WHERE rn = 1 ` - const result = - this.dbType === 'sqlserver' - ? await (dbService as SqlServerService).query(sqlString) - : await (dbService as MySqlService).query(sqlString) - + const result = await dbService.query(sqlString) return result.rows } catch (error) { log.error('Query all distinct by material code error', { @@ -250,40 +207,22 @@ export class DiscreteMaterialPlanDAO { try { const dbService = await this.getDatabaseService() const tableName = this.getTableName() + const isSqlServer = dbService.type === 'sqlserver' const batchSize = 2000 const allResults: any[] = [] for (let i = 0; i < sourceNumbers.length; i += batchSize) { const batch = sourceNumbers.slice(i, i + batchSize) + const placeholders = this.buildPlaceholders(batch.length, isSqlServer) - if (this.dbType === 'sqlserver') { - const placeholders = batch.map((_, idx) => `@p${idx}`).join(',') - const params: Record = {} + const sqlString = ` + SELECT * + FROM ${tableName} + WHERE SourceNumber IN (${placeholders}) + ` - batch.forEach((num, idx) => { - params[`p${idx}`] = { value: num, type: sql.NVarChar } - }) - - const sqlString = ` - SELECT * - FROM ${tableName} - WHERE SourceNumber IN (${placeholders}) - ` - - const result = await (dbService as SqlServerService).queryWithParams(sqlString, params) - allResults.push(...result.rows) - } else { - const placeholders = batch.map(() => '?').join(',') - - const sqlString = ` - SELECT * - FROM ${tableName} - WHERE SourceNumber IN (${placeholders}) - ` - - const result = await (dbService as MySqlService).query(sqlString, batch) - allResults.push(...result.rows) - } + const result = await dbService.query(sqlString, batch) + allResults.push(...result.rows) } return allResults @@ -310,74 +249,39 @@ export class DiscreteMaterialPlanDAO { try { const dbService = await this.getDatabaseService() const tableName = this.getTableName() + const isSqlServer = dbService.type === 'sqlserver' const batchSize = 2000 const allResults: any[] = [] for (let i = 0; i < sourceNumbers.length; i += batchSize) { const batch = sourceNumbers.slice(i, i + batchSize) + const placeholders = this.buildPlaceholders(batch.length, isSqlServer) - if (this.dbType === 'sqlserver') { - const placeholders = batch.map((_, idx) => `@p${idx}`).join(',') - const params: Record = {} - - batch.forEach((num, idx) => { - params[`p${idx}`] = { value: num, type: sql.NVarChar } - }) - - const sqlString = ` - WITH RankedRecords AS ( - SELECT - *, - ROW_NUMBER() OVER ( - PARTITION BY MaterialCode - ORDER BY CreateDate ASC, SequenceNumber ASC - ) AS rn - FROM ${tableName} - WHERE SourceNumber IN (${placeholders}) - AND MaterialCode IS NOT NULL - ) + const sqlString = ` + WITH RankedRecords AS ( SELECT - Factory, MaterialStatus, PlanNumber, SourceNumber, MaterialType, - ProductCode, ProductName, ProductUnit, ProductPlanQuantity, - UseDepartment, Remark, Creator, CreateDate, Approver, ApproveDate, - SequenceNumber, MaterialCode, MaterialName, Specification, Model, - DrawingNumber, MaterialQuality, PlanQuantity, Unit, RequiredDate, - Warehouse, UnitUsage, CumulativeOutputQuantity, BOMVersion - FROM RankedRecords - WHERE rn = 1 - ` + *, + ROW_NUMBER() OVER ( + PARTITION BY MaterialCode + ORDER BY CreateDate ASC, SequenceNumber ASC + ) AS rn + FROM ${tableName} + WHERE SourceNumber IN (${placeholders}) + AND MaterialCode IS NOT NULL + ) + SELECT + Factory, MaterialStatus, PlanNumber, SourceNumber, MaterialType, + ProductCode, ProductName, ProductUnit, ProductPlanQuantity, + UseDepartment, Remark, Creator, CreateDate, Approver, ApproveDate, + SequenceNumber, MaterialCode, MaterialName, Specification, Model, + DrawingNumber, MaterialQuality, PlanQuantity, Unit, RequiredDate, + Warehouse, UnitUsage, CumulativeOutputQuantity, BOMVersion + FROM RankedRecords + WHERE rn = 1 + ` - const result = await (dbService as SqlServerService).queryWithParams(sqlString, params) - allResults.push(...result.rows) - } else { - const placeholders = batch.map(() => '?').join(',') - - const sqlString = ` - WITH RankedRecords AS ( - SELECT - *, - ROW_NUMBER() OVER ( - PARTITION BY MaterialCode - ORDER BY CreateDate ASC, SequenceNumber ASC - ) AS rn - FROM ${tableName} - WHERE SourceNumber IN (${placeholders}) - AND MaterialCode IS NOT NULL - ) - SELECT - Factory, MaterialStatus, PlanNumber, SourceNumber, MaterialType, - ProductCode, ProductName, ProductUnit, ProductPlanQuantity, - UseDepartment, Remark, Creator, CreateDate, Approver, ApproveDate, - SequenceNumber, MaterialCode, MaterialName, Specification, Model, - DrawingNumber, MaterialQuality, PlanQuantity, Unit, RequiredDate, - Warehouse, UnitUsage, CumulativeOutputQuantity, BOMVersion - FROM RankedRecords - WHERE rn = 1 - ` - - const result = await (dbService as MySqlService).query(sqlString, batch) - allResults.push(...result.rows) - } + const result = await dbService.query(sqlString, batch) + allResults.push(...result.rows) } return allResults @@ -398,30 +302,17 @@ export class DiscreteMaterialPlanDAO { try { const dbService = await this.getDatabaseService() const tableName = this.getTableName() + const isSqlServer = dbService.type === 'sqlserver' - if (this.dbType === 'sqlserver') { - const sqlString = ` - SELECT * - FROM ${tableName} - WHERE SourceNumber = @sourceNumber - ` + const placeholder = isSqlServer ? '@p0' : '?' + const sqlString = ` + SELECT * + FROM ${tableName} + WHERE SourceNumber = ${placeholder} + ` - const result = await (dbService as SqlServerService).queryWithParams(sqlString, { - sourceNumber: { value: sourceNumber, type: sql.NVarChar } - }) - - return result.rows - } else { - const sqlString = ` - SELECT * - FROM ${tableName} - WHERE SourceNumber = ? - ` - - const result = await (dbService as MySqlService).query(sqlString, [sourceNumber]) - - return result.rows - } + const result = await dbService.query(sqlString, [sourceNumber]) + return result.rows } catch (error) { log.error('Query by source number error', { error: error instanceof Error ? error.message : String(error) @@ -441,30 +332,17 @@ export class DiscreteMaterialPlanDAO { try { const dbService = await this.getDatabaseService() const tableName = this.getTableName() + const isSqlServer = dbService.type === 'sqlserver' - if (this.dbType === 'sqlserver') { - const sqlString = ` - SELECT * - FROM ${tableName} - WHERE PlanNumber = @planNumber - ` + const placeholder = isSqlServer ? '@p0' : '?' + const sqlString = ` + SELECT * + FROM ${tableName} + WHERE PlanNumber = ${placeholder} + ` - const result = await (dbService as SqlServerService).queryWithParams(sqlString, { - planNumber: { value: planNumber, type: sql.NVarChar } - }) - - return result.rows - } else { - const sqlString = ` - SELECT * - FROM ${tableName} - WHERE PlanNumber = ? - ` - - const result = await (dbService as MySqlService).query(sqlString, [planNumber]) - - return result.rows - } + const result = await dbService.query(sqlString, [planNumber]) + return result.rows } catch (error) { log.error('Query by plan number error', { error: error instanceof Error ? error.message : String(error) @@ -486,35 +364,17 @@ export class DiscreteMaterialPlanDAO { try { const dbService = await this.getDatabaseService() const tableName = this.getTableName() + const isSqlServer = dbService.type === 'sqlserver' + const placeholders = this.buildPlaceholders(planNumbers.length, isSqlServer) - if (this.dbType === 'sqlserver') { - const placeholders = planNumbers.map((_, idx) => `@p${idx}`).join(',') - const params: Record = {} + const sqlString = ` + SELECT * + FROM ${tableName} + WHERE PlanNumber IN (${placeholders}) + ` - planNumbers.forEach((num, idx) => { - params[`p${idx}`] = { value: num, type: sql.NVarChar } - }) - - const sqlString = ` - SELECT * - FROM ${tableName} - WHERE PlanNumber IN (${placeholders}) - ` - - const result = await (dbService as SqlServerService).queryWithParams(sqlString, params) - return result.rows - } else { - const placeholders = planNumbers.map(() => '?').join(',') - - const sqlString = ` - SELECT * - FROM ${tableName} - WHERE PlanNumber IN (${placeholders}) - ` - - const result = await (dbService as MySqlService).query(sqlString, planNumbers) - return result.rows - } + const result = await dbService.query(sqlString, planNumbers) + return result.rows } catch (error) { log.error('Query by plan numbers error', { error: error instanceof Error ? error.message : String(error) @@ -535,11 +395,7 @@ export class DiscreteMaterialPlanDAO { const tableName = this.getTableName() const sqlString = `SELECT COUNT(*) as count FROM ${tableName}` - - const result = - this.dbType === 'sqlserver' - ? await (dbService as SqlServerService).query(sqlString) - : await (dbService as MySqlService).query(sqlString) + const result = await dbService.query(sqlString) return result.rows.length > 0 ? (result.rows[0].count as number) : 0 } catch (error) { @@ -559,30 +415,17 @@ export class DiscreteMaterialPlanDAO { try { const dbService = await this.getDatabaseService() const tableName = this.getTableName() + const isSqlServer = dbService.type === 'sqlserver' - if (this.dbType === 'sqlserver') { - const sqlString = ` - SELECT COUNT(*) as count - FROM ${tableName} - WHERE PlanNumber = @planNumber - ` + const placeholder = isSqlServer ? '@p0' : '?' + const sqlString = ` + SELECT COUNT(*) as count + FROM ${tableName} + WHERE PlanNumber = ${placeholder} + ` - const result = await (dbService as SqlServerService).queryWithParams(sqlString, { - planNumber: { value: planNumber, type: sql.NVarChar } - }) - - return result.rows.length > 0 ? (result.rows[0].count as number) : 0 - } else { - const sqlString = ` - SELECT COUNT(*) as count - FROM ${tableName} - WHERE PlanNumber = ? - ` - - const result = await (dbService as MySqlService).query(sqlString, [planNumber]) - - return result.rows.length > 0 ? (result.rows[0].count as number) : 0 - } + const result = await dbService.query(sqlString, [planNumber]) + return result.rows.length > 0 ? (result.rows[0].count as number) : 0 } catch (error) { log.error('Count by plan number error', { error: error instanceof Error ? error.message : String(error) @@ -600,38 +443,20 @@ export class DiscreteMaterialPlanDAO { try { const dbService = await this.getDatabaseService() const tableName = this.getTableName() + const isSqlServer = dbService.type === 'sqlserver' if (sourceNumbers && sourceNumbers.length > 0) { - if (this.dbType === 'sqlserver') { - const placeholders = sourceNumbers.map((_, idx) => `@p${idx}`).join(',') - const params: Record = {} + const placeholders = this.buildPlaceholders(sourceNumbers.length, isSqlServer) - sourceNumbers.forEach((num, idx) => { - params[`p${idx}`] = { value: num, type: sql.NVarChar } - }) + const sqlString = ` + SELECT DISTINCT MaterialName + FROM ${tableName} + WHERE SourceNumber IN (${placeholders}) + AND MaterialName IS NOT NULL + ` - const sqlString = ` - SELECT DISTINCT MaterialName - FROM ${tableName} - WHERE SourceNumber IN (${placeholders}) - AND MaterialName IS NOT NULL - ` - - const result = await (dbService as SqlServerService).queryWithParams(sqlString, params) - return result.rows.map((row) => row.MaterialName as string).filter(Boolean) - } else { - const placeholders = sourceNumbers.map(() => '?').join(',') - - const sqlString = ` - SELECT DISTINCT MaterialName - FROM ${tableName} - WHERE SourceNumber IN (${placeholders}) - AND MaterialName IS NOT NULL - ` - - const result = await (dbService as MySqlService).query(sqlString, sourceNumbers) - return result.rows.map((row) => row.MaterialName as string).filter(Boolean) - } + const result = await dbService.query(sqlString, sourceNumbers) + return result.rows.map((row) => row.MaterialName as string).filter(Boolean) } else { const sqlString = ` SELECT DISTINCT MaterialName @@ -639,11 +464,7 @@ export class DiscreteMaterialPlanDAO { WHERE MaterialName IS NOT NULL ` - const result = - this.dbType === 'sqlserver' - ? await (dbService as SqlServerService).query(sqlString) - : await (dbService as MySqlService).query(sqlString) - + const result = await dbService.query(sqlString) return result.rows.map((row) => row.MaterialName as string).filter(Boolean) } } catch (error) { @@ -673,11 +494,7 @@ export class DiscreteMaterialPlanDAO { FROM ${tableName} ` - const result = - this.dbType === 'sqlserver' - ? await (dbService as SqlServerService).query(sqlString) - : await (dbService as MySqlService).query(sqlString) - + const result = await dbService.query(sqlString) return result.rows.length > 0 ? result.rows[0] : {} } catch (error) { log.error('Get statistics error', { @@ -691,13 +508,9 @@ export class DiscreteMaterialPlanDAO { * Disconnect from database */ async disconnect(): Promise { - if (this.mysqlService) { - await this.mysqlService.disconnect() - this.mysqlService = null - } - if (this.sqlServerService) { - await this.sqlServerService.disconnect() - this.sqlServerService = null + if (this.dbService) { + await this.dbService.disconnect() + this.dbService = null } } } diff --git a/src/main/services/database/index.ts b/src/main/services/database/index.ts new file mode 100644 index 0000000..c8b5bf4 --- /dev/null +++ b/src/main/services/database/index.ts @@ -0,0 +1,182 @@ +/** + * Database Factory + * + * Creates and manages database service instances based on configuration. + * Supports both MySQL and SQL Server databases. + */ + +import { MySqlService } from './mysql' +import { SqlServerService } from './sql-server' +import type { + IDatabaseService, + DatabaseType, + MySqlConfig, + SqlServerConfig +} from '../../types/database.types' +import { createLogger } from '../logger' + +const log = createLogger('DatabaseFactory') + +/** + * Cached database service instances + */ +const instances: Map = new Map() + +/** + * Get the current database type from environment + */ +export function getDatabaseType(): DatabaseType { + const dbType = process.env.DB_TYPE?.toLowerCase() + if (dbType === 'sqlserver' || dbType === 'mssql') { + return 'sqlserver' + } + return 'mysql' +} + +/** + * Create MySQL configuration from environment variables + */ +export function createMySqlConfig(): MySqlConfig { + return { + host: process.env.DB_MYSQL_HOST || 'localhost', + port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10), + user: process.env.DB_USERNAME || 'root', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || '' + } +} + +/** + * Create SQL Server configuration from environment variables + */ +export function createSqlServerConfig(): SqlServerConfig { + return { + server: process.env.DB_SERVER || 'localhost', + port: parseInt(process.env.DB_SQLSERVER_PORT || '1433', 10), + user: process.env.DB_USERNAME || 'sa', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || '', + options: { + encrypt: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes', + trustServerCertificate: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes' + } + } +} + +/** + * Create a database service instance + * + * Uses singleton pattern - returns cached instance if available. + * + * @param type - Optional database type override (defaults to DB_TYPE env var) + * @returns Database service instance + */ +export async function create(type?: DatabaseType): Promise { + const dbType = type || getDatabaseType() + + // Return cached instance if available and connected + const cached = instances.get(dbType) + if (cached && cached.isConnected()) { + log.debug('Returning cached database instance', { type: dbType }) + return cached + } + + // Create new instance + let service: IDatabaseService + + if (dbType === 'sqlserver') { + log.info('Creating SQL Server database service') + service = new SqlServerService(createSqlServerConfig()) + } else { + log.info('Creating MySQL database service') + service = new MySqlService(createMySqlConfig()) + } + + // Connect to database + await service.connect() + log.info('Database connected', { type: dbType }) + + // Cache the instance + instances.set(dbType, service) + + return service +} + +/** + * Get existing database service without creating new one + * + * @param type - Optional database type (defaults to DB_TYPE env var) + * @returns Database service instance or undefined + */ +export function get(type?: DatabaseType): IDatabaseService | undefined { + const dbType = type || getDatabaseType() + return instances.get(dbType) +} + +/** + * Disconnect and remove a specific database service + * + * @param type - Optional database type (defaults to DB_TYPE env var) + */ +export async function disconnect(type?: DatabaseType): Promise { + const dbType = type || getDatabaseType() + const service = instances.get(dbType) + + if (service) { + try { + await service.disconnect() + log.info('Database disconnected', { type: dbType }) + } catch (error) { + log.warn('Error disconnecting database', { + type: dbType, + error: error instanceof Error ? error.message : String(error) + }) + } + instances.delete(dbType) + } +} + +/** + * Disconnect all database services + */ +export async function disconnectAll(): Promise { + log.info('Disconnecting all database services') + + const disconnectPromises = Array.from(instances.entries()).map(async ([type, service]) => { + try { + await service.disconnect() + log.debug('Database disconnected', { type }) + } catch (error) { + log.warn('Error disconnecting database', { + type, + error: error instanceof Error ? error.message : String(error) + }) + } + }) + + await Promise.all(disconnectPromises) + instances.clear() + log.info('All database services disconnected') +} + +/** + * Check if a database service is connected + * + * @param type - Optional database type (defaults to DB_TYPE env var) + */ +export function isConnected(type?: DatabaseType): boolean { + const dbType = type || getDatabaseType() + const service = instances.get(dbType) + return service?.isConnected() ?? false +} + +// Re-export types and services +export { MySqlService } from './mysql' +export { SqlServerService } from './sql-server' +export type { + IDatabaseService, + DatabaseType, + QueryResult, + MySqlConfig, + SqlServerConfig +} from '../../types/database.types' diff --git a/src/main/services/database/materials-to-be-deleted-dao.ts b/src/main/services/database/materials-to-be-deleted-dao.ts index b4f6a7e..491554e 100644 --- a/src/main/services/database/materials-to-be-deleted-dao.ts +++ b/src/main/services/database/materials-to-be-deleted-dao.ts @@ -8,9 +8,7 @@ * - Statistics gathering */ -import { MySqlService } from './mysql' -import { SqlServerService } from './sql-server' -import sql from 'mssql' +import { create, type IDatabaseService } from './index' import { createLogger } from '../logger' const log = createLogger('MaterialsToBeDeletedDAO') @@ -59,70 +57,37 @@ export const MATERIALS_TO_BE_DELETED_CONFIG = { * MaterialsToBeDeleted DAO Class */ export class MaterialsToBeDeletedDAO { - private mysqlService: MySqlService | null = null - private sqlServerService: SqlServerService | null = null - private dbType: 'mysql' | 'sqlserver' = 'mysql' - - /** - * Constructor - determine database type from environment - */ - constructor() { - const dbType = process.env.DB_TYPE?.toLowerCase() - if (dbType === 'sqlserver' || dbType === 'mssql') { - this.dbType = 'sqlserver' - } else { - this.dbType = 'mysql' - } - } + private dbService: IDatabaseService | null = null /** * Get the appropriate table name based on database type */ private getTableName(): string { - return this.dbType === 'sqlserver' + const isSqlServer = this.dbService?.type === 'sqlserver' + return isSqlServer ? MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_SQLSERVER : MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL } /** - * Get database service instance (MySQL or SQL Server) + * Get database service instance using DatabaseFactory */ - private async getDatabaseService(): Promise { - if (this.dbType === 'sqlserver') { - if (this.sqlServerService && this.sqlServerService.isConnected()) { - return this.sqlServerService - } - - this.sqlServerService = new SqlServerService({ - server: process.env.DB_SERVER || 'localhost', - port: parseInt(process.env.DB_SQLSERVER_PORT || '1433', 10), - user: process.env.DB_USERNAME || 'sa', - password: process.env.DB_PASSWORD || '', - database: process.env.DB_NAME || '', - options: { - encrypt: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes', - trustServerCertificate: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes' - } - }) - - await this.sqlServerService.connect() - return this.sqlServerService - } else { - if (this.mysqlService && this.mysqlService.isConnected()) { - return this.mysqlService - } - - this.mysqlService = new MySqlService({ - host: process.env.DB_MYSQL_HOST || 'localhost', - port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10), - user: process.env.DB_USERNAME || 'root', - password: process.env.DB_PASSWORD || '', - database: process.env.DB_NAME || '' - }) - - await this.mysqlService.connect() - return this.mysqlService + private async getDatabaseService(): Promise { + if (this.dbService && this.dbService.isConnected()) { + return this.dbService } + + this.dbService = await create() + return this.dbService + } + + /** + * Build placeholders for IN clause based on database type + */ + private buildPlaceholders(count: number, isSqlServer: boolean): string { + return isSqlServer + ? Array.from({ length: count }, (_, idx) => `@p${idx}`).join(',') + : Array.from({ length: count }, () => '?').join(',') } // ==================== UPSERT (MERGE) ==================== @@ -144,28 +109,28 @@ export class MaterialsToBeDeletedDAO { const tableName = this.getTableName() const code = materialCode.trim() const manager = managerName?.trim() || null + const isSqlServer = dbService.type === 'sqlserver' - if (this.dbType === 'sqlserver') { + if (isSqlServer) { + // SQL Server MERGE statement const sqlString = ` MERGE ${tableName} AS target - USING (VALUES (@materialCode, @managerName)) AS source (MaterialCode, ManagerName) + USING (VALUES (@p0, @p1)) AS source (MaterialCode, ManagerName) ON target.MaterialCode = source.MaterialCode WHEN MATCHED THEN UPDATE SET ManagerName = source.ManagerName WHEN NOT MATCHED THEN INSERT (MaterialCode, ManagerName) VALUES (source.MaterialCode, source.ManagerName); ` - await (dbService as SqlServerService).queryWithParams(sqlString, { - materialCode: { value: code, type: sql.NVarChar }, - managerName: { value: manager, type: sql.NVarChar } - }) + await dbService.query(sqlString, [code, manager]) } else { + // MySQL ON DUPLICATE KEY UPDATE const sqlString = ` INSERT INTO ${tableName} (MaterialCode, ManagerName) VALUES (?, ?) ON DUPLICATE KEY UPDATE ManagerName = VALUES(ManagerName) ` - await (dbService as MySqlService).query(sqlString, [code, manager]) + await dbService.query(sqlString, [code, manager]) } return true @@ -198,6 +163,7 @@ export class MaterialsToBeDeletedDAO { try { const dbService = await this.getDatabaseService() const tableName = this.getTableName() + const isSqlServer = dbService.type === 'sqlserver' for (const material of materials) { const materialCode = material.materialCode?.trim() @@ -209,27 +175,26 @@ export class MaterialsToBeDeletedDAO { } try { - if (this.dbType === 'sqlserver') { + if (isSqlServer) { + // SQL Server MERGE statement const sqlString = ` MERGE ${tableName} AS target - USING (VALUES (@materialCode, @managerName)) AS source (MaterialCode, ManagerName) + USING (VALUES (@p0, @p1)) AS source (MaterialCode, ManagerName) ON target.MaterialCode = source.MaterialCode WHEN MATCHED THEN UPDATE SET ManagerName = source.ManagerName WHEN NOT MATCHED THEN INSERT (MaterialCode, ManagerName) VALUES (source.MaterialCode, source.ManagerName); ` - await (dbService as SqlServerService).queryWithParams(sqlString, { - materialCode: { value: materialCode, type: sql.NVarChar }, - managerName: { value: managerName || null, type: sql.NVarChar } - }) + await dbService.query(sqlString, [materialCode, managerName || null]) } else { + // MySQL ON DUPLICATE KEY UPDATE const sqlString = ` INSERT INTO ${tableName} (MaterialCode, ManagerName) VALUES (?, ?) ON DUPLICATE KEY UPDATE ManagerName = VALUES(ManagerName) ` - await (dbService as MySqlService).query(sqlString, [materialCode, managerName || null]) + await dbService.query(sqlString, [materialCode, managerName || null]) } stats.success++ @@ -268,11 +233,7 @@ export class MaterialsToBeDeletedDAO { WHERE MaterialCode IS NOT NULL ` - const result = - this.dbType === 'sqlserver' - ? await (dbService as SqlServerService).query(sqlString) - : await (dbService as MySqlService).query(sqlString) - + const result = await dbService.query(sqlString) return new Set(result.rows.map((row) => row.MaterialCode as string).filter(Boolean)) } catch (error) { log.error('Get all material codes error', { @@ -298,11 +259,7 @@ export class MaterialsToBeDeletedDAO { ORDER BY ManagerName, MaterialCode ` - const result = - this.dbType === 'sqlserver' - ? await (dbService as SqlServerService).query(sqlString) - : await (dbService as MySqlService).query(sqlString) - + const result = await dbService.query(sqlString) return result.rows.map((row) => ({ id: row.ID as number, materialCode: row.MaterialCode as string, @@ -325,40 +282,22 @@ export class MaterialsToBeDeletedDAO { try { const dbService = await this.getDatabaseService() const tableName = this.getTableName() + const isSqlServer = dbService.type === 'sqlserver' - if (this.dbType === 'sqlserver') { - const sqlString = ` - SELECT ID, MaterialCode, ManagerName - FROM ${tableName} - WHERE ManagerName = @managerName AND MaterialCode IS NOT NULL - ORDER BY MaterialCode - ` + const placeholder = isSqlServer ? '@p0' : '?' + const sqlString = ` + SELECT ID, MaterialCode, ManagerName + FROM ${tableName} + WHERE ManagerName = ${placeholder} AND MaterialCode IS NOT NULL + ORDER BY MaterialCode + ` - const result = await (dbService as SqlServerService).queryWithParams(sqlString, { - managerName: { value: managerName, type: sql.NVarChar } - }) - - return result.rows.map((row) => ({ - id: row.ID as number, - materialCode: row.MaterialCode as string, - managerName: row.ManagerName as string - })) - } else { - const sqlString = ` - SELECT ID, MaterialCode, ManagerName - FROM ${tableName} - WHERE ManagerName = ? AND MaterialCode IS NOT NULL - ORDER BY MaterialCode - ` - - const result = await (dbService as MySqlService).query(sqlString, [managerName]) - - return result.rows.map((row) => ({ - id: row.ID as number, - materialCode: row.MaterialCode as string, - managerName: row.ManagerName as string - })) - } + const result = await dbService.query(sqlString, [managerName]) + return result.rows.map((row) => ({ + id: row.ID as number, + materialCode: row.MaterialCode as string, + managerName: row.ManagerName as string + })) } catch (error) { log.error('Get materials by manager error', { error: error instanceof Error ? error.message : String(error) @@ -383,11 +322,7 @@ export class MaterialsToBeDeletedDAO { ORDER BY ManagerName ` - const result = - this.dbType === 'sqlserver' - ? await (dbService as SqlServerService).query(sqlString) - : await (dbService as MySqlService).query(sqlString) - + const result = await dbService.query(sqlString) return result.rows.map((row) => row.ManagerName as string).filter(Boolean) } catch (error) { log.error('Get managers error', { @@ -407,47 +342,26 @@ export class MaterialsToBeDeletedDAO { const dbService = await this.getDatabaseService() const tableName = this.getTableName() const code = materialCode.trim() + const isSqlServer = dbService.type === 'sqlserver' - if (this.dbType === 'sqlserver') { - const sqlString = ` - SELECT ID, MaterialCode, ManagerName - FROM ${tableName} - WHERE MaterialCode = @materialCode - ` + const placeholder = isSqlServer ? '@p0' : '?' + const sqlString = ` + SELECT ID, MaterialCode, ManagerName + FROM ${tableName} + WHERE MaterialCode = ${placeholder} + ` - const result = await (dbService as SqlServerService).queryWithParams(sqlString, { - materialCode: { value: code, type: sql.NVarChar } - }) + const result = await dbService.query(sqlString, [code]) - if (result.rows.length === 0) { - return null - } + if (result.rows.length === 0) { + return null + } - const row = result.rows[0] - return { - id: row.ID as number, - materialCode: row.MaterialCode as string, - managerName: row.ManagerName as string - } - } else { - const sqlString = ` - SELECT ID, MaterialCode, ManagerName - FROM ${tableName} - WHERE MaterialCode = ? - ` - - const result = await (dbService as MySqlService).query(sqlString, [code]) - - if (result.rows.length === 0) { - return null - } - - const row = result.rows[0] - return { - id: row.ID as number, - materialCode: row.MaterialCode as string, - managerName: row.ManagerName as string - } + const row = result.rows[0] + return { + id: row.ID as number, + materialCode: row.MaterialCode as string, + managerName: row.ManagerName as string } } catch (error) { log.error('Get record by material code error', { @@ -469,28 +383,16 @@ export class MaterialsToBeDeletedDAO { const dbService = await this.getDatabaseService() const tableName = this.getTableName() const code = materialCode.trim() + const isSqlServer = dbService.type === 'sqlserver' - if (this.dbType === 'sqlserver') { - const sqlString = ` - DELETE FROM ${tableName} - WHERE MaterialCode = @materialCode - ` + const placeholder = isSqlServer ? '@p0' : '?' + const sqlString = ` + DELETE FROM ${tableName} + WHERE MaterialCode = ${placeholder} + ` - const result = await (dbService as SqlServerService).queryWithParams(sqlString, { - materialCode: { value: code, type: sql.NVarChar } - }) - - return result.rowCount > 0 - } else { - const sqlString = ` - DELETE FROM ${tableName} - WHERE MaterialCode = ? - ` - - const result = await (dbService as MySqlService).query(sqlString, [code]) - - return result.rowCount > 0 - } + const result = await dbService.query(sqlString, [code]) + return result.rowCount > 0 } catch (error) { log.error('Delete by material code error', { error: error instanceof Error ? error.message : String(error) @@ -508,28 +410,16 @@ export class MaterialsToBeDeletedDAO { try { const dbService = await this.getDatabaseService() const tableName = this.getTableName() + const isSqlServer = dbService.type === 'sqlserver' - if (this.dbType === 'sqlserver') { - const sqlString = ` - DELETE FROM ${tableName} - WHERE ManagerName = @managerName - ` + const placeholder = isSqlServer ? '@p0' : '?' + const sqlString = ` + DELETE FROM ${tableName} + WHERE ManagerName = ${placeholder} + ` - const result = await (dbService as SqlServerService).queryWithParams(sqlString, { - managerName: { value: managerName, type: sql.NVarChar } - }) - - return result.rowCount - } else { - const sqlString = ` - DELETE FROM ${tableName} - WHERE ManagerName = ? - ` - - const result = await (dbService as MySqlService).query(sqlString, [managerName]) - - return result.rowCount - } + const result = await dbService.query(sqlString, [managerName]) + return result.rowCount } catch (error) { log.error('Delete by manager error', { error: error instanceof Error ? error.message : String(error) @@ -548,12 +438,7 @@ export class MaterialsToBeDeletedDAO { const tableName = this.getTableName() const sqlString = `DELETE FROM ${tableName}` - - const result = - this.dbType === 'sqlserver' - ? await (dbService as SqlServerService).query(sqlString) - : await (dbService as MySqlService).query(sqlString) - + const result = await dbService.query(sqlString) return result.rowCount } catch (error) { log.error('Delete all materials error', { @@ -579,36 +464,22 @@ export class MaterialsToBeDeletedDAO { try { const dbService = await this.getDatabaseService() const tableName = this.getTableName() + const isSqlServer = dbService.type === 'sqlserver' for (let i = 0; i < materialCodes.length; i += batchSize) { const batch = materialCodes.slice(i, i + batchSize) + const placeholders = this.buildPlaceholders(batch.length, isSqlServer) - if (this.dbType === 'sqlserver') { - const placeholders = batch.map((_, idx) => `@p${idx}`).join(',') - const params: Record = {} + const sqlString = ` + DELETE FROM ${tableName} + WHERE MaterialCode IN (${placeholders}) + ` - batch.forEach((code, idx) => { - params[`p${idx}`] = { value: code.trim(), type: sql.NVarChar } - }) - - const sqlString = ` - DELETE FROM ${tableName} - WHERE MaterialCode IN (${placeholders}) - ` - - const result = await (dbService as SqlServerService).queryWithParams(sqlString, params) - totalDeleted += result.rowCount - } else { - const placeholders = batch.map(() => '?').join(',') - - const sqlString = ` - DELETE FROM ${tableName} - WHERE MaterialCode IN (${placeholders}) - ` - - const result = await (dbService as MySqlService).query(sqlString, batch) - totalDeleted += result.rowCount - } + const result = await dbService.query( + sqlString, + batch.map((c) => c.trim()) + ) + totalDeleted += result.rowCount } } catch (error) { log.error('Delete by material codes error', { @@ -631,30 +502,17 @@ export class MaterialsToBeDeletedDAO { const dbService = await this.getDatabaseService() const tableName = this.getTableName() const code = materialCode.trim() + const isSqlServer = dbService.type === 'sqlserver' - if (this.dbType === 'sqlserver') { - const sqlString = ` - SELECT COUNT(*) as count - FROM ${tableName} - WHERE MaterialCode = @materialCode - ` + const placeholder = isSqlServer ? '@p0' : '?' + const sqlString = ` + SELECT COUNT(*) as count + FROM ${tableName} + WHERE MaterialCode = ${placeholder} + ` - const result = await (dbService as SqlServerService).queryWithParams(sqlString, { - materialCode: { value: code, type: sql.NVarChar } - }) - - return result.rows.length > 0 && (result.rows[0].count as number) > 0 - } else { - const sqlString = ` - SELECT COUNT(*) as count - FROM ${tableName} - WHERE MaterialCode = ? - ` - - const result = await (dbService as MySqlService).query(sqlString, [code]) - - return result.rows.length > 0 && (result.rows[0].count as number) > 0 - } + const result = await dbService.query(sqlString, [code]) + return result.rows.length > 0 && (result.rows[0].count as number) > 0 } catch (error) { log.error('Material exists error', { error: error instanceof Error ? error.message : String(error) @@ -673,11 +531,7 @@ export class MaterialsToBeDeletedDAO { const tableName = this.getTableName() const sqlString = `SELECT COUNT(*) as count FROM ${tableName}` - - const result = - this.dbType === 'sqlserver' - ? await (dbService as SqlServerService).query(sqlString) - : await (dbService as MySqlService).query(sqlString) + const result = await dbService.query(sqlString) return result.rows.length > 0 ? (result.rows[0].count as number) : 0 } catch (error) { @@ -697,30 +551,17 @@ export class MaterialsToBeDeletedDAO { try { const dbService = await this.getDatabaseService() const tableName = this.getTableName() + const isSqlServer = dbService.type === 'sqlserver' - if (this.dbType === 'sqlserver') { - const sqlString = ` - SELECT COUNT(*) as count - FROM ${tableName} - WHERE ManagerName = @managerName - ` + const placeholder = isSqlServer ? '@p0' : '?' + const sqlString = ` + SELECT COUNT(*) as count + FROM ${tableName} + WHERE ManagerName = ${placeholder} + ` - const result = await (dbService as SqlServerService).queryWithParams(sqlString, { - managerName: { value: managerName, type: sql.NVarChar } - }) - - return result.rows.length > 0 ? (result.rows[0].count as number) : 0 - } else { - const sqlString = ` - SELECT COUNT(*) as count - FROM ${tableName} - WHERE ManagerName = ? - ` - - const result = await (dbService as MySqlService).query(sqlString, [managerName]) - - return result.rows.length > 0 ? (result.rows[0].count as number) : 0 - } + const result = await dbService.query(sqlString, [managerName]) + return result.rows.length > 0 ? (result.rows[0].count as number) : 0 } catch (error) { log.error('Count by manager error', { error: error instanceof Error ? error.message : String(error) @@ -747,11 +588,7 @@ export class MaterialsToBeDeletedDAO { WHERE MaterialCode IS NOT NULL ` - const statsResult = - this.dbType === 'sqlserver' - ? await (dbService as SqlServerService).query(statsSql) - : await (dbService as MySqlService).query(statsSql) - + const statsResult = await dbService.query(statsSql) const stats = statsResult.rows[0] || {} // Get materials per manager @@ -763,11 +600,7 @@ export class MaterialsToBeDeletedDAO { ORDER BY count DESC ` - const managerResult = - this.dbType === 'sqlserver' - ? await (dbService as SqlServerService).query(managerSql) - : await (dbService as MySqlService).query(managerSql) - + const managerResult = await dbService.query(managerSql) const materialsPerManager = managerResult.rows.map((row) => ({ [row.ManagerName as string]: row.count as number })) @@ -793,13 +626,9 @@ export class MaterialsToBeDeletedDAO { * Disconnect from database */ async disconnect(): Promise { - if (this.mysqlService) { - await this.mysqlService.disconnect() - this.mysqlService = null - } - if (this.sqlServerService) { - await this.sqlServerService.disconnect() - this.sqlServerService = null + if (this.dbService) { + await this.dbService.disconnect() + this.dbService = null } } } diff --git a/src/main/services/database/mysql.ts b/src/main/services/database/mysql.ts index 30cac6d..556e0f4 100644 --- a/src/main/services/database/mysql.ts +++ b/src/main/services/database/mysql.ts @@ -1,20 +1,17 @@ import mysql from 'mysql2/promise' +import type { + IDatabaseService, + DatabaseType, + QueryResult, + MySqlConfig +} from '../../types/database.types' -export interface MySqlConfig { - host: string - port: number - user: string - password: string - database: string -} +export type { MySqlConfig } from '../../types/database.types' -export interface MySqlQueryResult { - rows: Record[] - columns: string[] - rowCount: number -} +export class MySqlService implements IDatabaseService { + /** Database type identifier */ + readonly type: DatabaseType = 'mysql' -export class MySqlService { private connection: mysql.Connection | null = null private config: MySqlConfig @@ -72,7 +69,7 @@ export class MySqlService { /** * Execute a query and return results */ - async query(sql: string, params?: any[]): Promise { + async query(sql: string, params?: any[]): Promise { if (!this.connection) { throw new Error('Not connected to MySQL. Call connect() first.') } diff --git a/src/main/services/database/sql-server.ts b/src/main/services/database/sql-server.ts index a2936a3..549b9d4 100644 --- a/src/main/services/database/sql-server.ts +++ b/src/main/services/database/sql-server.ts @@ -1,24 +1,17 @@ import sql from 'mssql' +import type { + IDatabaseService, + DatabaseType, + QueryResult, + SqlServerConfig +} from '../../types/database.types' -export interface SqlServerConfig { - server: string - port: number - user: string - password: string - database: string - options?: { - encrypt?: boolean - trustServerCertificate?: boolean - } -} +export type { SqlServerConfig } from '../../types/database.types' -export interface SqlServerQueryResult { - rows: Record[] - columns: string[] - rowCount: number -} +export class SqlServerService implements IDatabaseService { + /** Database type identifier */ + readonly type: DatabaseType = 'sqlserver' -export class SqlServerService { private pool: sql.ConnectionPool | null = null private config: SqlServerConfig @@ -79,8 +72,10 @@ export class SqlServerService { /** * Execute a query and return results + * @param sqlString - SQL query string with @p0, @p1, ... placeholders + * @param params - Query parameters as an array (converted to @p0, @p1, ...) */ - async query(sqlString: string, params?: Record): Promise { + async query(sqlString: string, params?: any[]): Promise { if (!this.pool) { throw new Error('Not connected to SQL Server. Call connect() first.') } @@ -88,11 +83,11 @@ export class SqlServerService { try { const request = this.pool.request() - // Add parameters if provided - if (params) { - for (const [key, value] of Object.entries(params)) { - request.input(key, 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) @@ -113,12 +108,14 @@ export class SqlServerService { } /** - * Execute a prepared statement with parameters + * Execute a prepared statement with named parameters + * @param sqlString - SQL query string with @paramName placeholders + * @param params - Parameters as an object with { value, type? } structure */ async queryWithParams( sqlString: string, params: Record - ): Promise { + ): Promise { if (!this.pool) { throw new Error('Not connected to SQL Server. Call connect() first.') } @@ -154,8 +151,9 @@ export class SqlServerService { /** * Execute multiple queries in a transaction + * @param queries - Array of queries with array-based parameters */ - async transaction(queries: { sql: string; params?: Record }[]): Promise { + async transaction(queries: { sql: string; params?: any[] }[]): Promise { if (!this.pool) { throw new Error('Not connected to SQL Server. Call connect() first.') } @@ -168,10 +166,11 @@ export class SqlServerService { for (const { sql: sqlString, params } of queries) { const request = new sql.Request(transaction) - if (params) { - for (const [key, value] of Object.entries(params)) { - request.input(key, 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) + }) } await request.query(sqlString) diff --git a/src/main/services/erp/order-resolver.ts b/src/main/services/erp/order-resolver.ts index bcaf5d4..eab0a71 100644 --- a/src/main/services/erp/order-resolver.ts +++ b/src/main/services/erp/order-resolver.ts @@ -2,7 +2,7 @@ * Order Number Resolver Service * * Automatically recognizes productionID and 生产订单号 (production order number), - * and converts them via MySQL database lookup. + * and converts them via database lookup. * * - productionID format: 2 digits + 1 letter + serial number (e.g., "22A1", "22A1234") * - 生产订单号 format: SC + 14 digits (e.g., "SC70202602120085") @@ -11,7 +11,7 @@ * Fields: 总排号 (productionID), 生产订单号 (production order number) */ -import { MySqlService } from '../database/mysql' +import type { IDatabaseService } from '../database' /** * Order mapping result @@ -71,10 +71,10 @@ export const DB_CONFIG = { * Order Number Resolver Service */ export class OrderNumberResolver { - private mysqlService: MySqlService + private dbService: IDatabaseService - constructor(mysqlService: MySqlService) { - this.mysqlService = mysqlService + constructor(dbService: IDatabaseService) { + this.dbService = dbService } /** @@ -202,7 +202,7 @@ export class OrderNumberResolver { private async resolveProductionIds(productionIds: string[]): Promise { const mappings: OrderMapping[] = [] - if (!this.mysqlService.isConnected()) { + if (!this.dbService.isConnected()) { // Database not connected, return all as failed for (const pid of productionIds) { mappings.push({ @@ -217,15 +217,21 @@ export class OrderNumberResolver { } try { - // Build query with placeholders - const placeholders = productionIds.map(() => '?').join(', ') + // Build query - use different placeholder style based on database type + const isSqlServer = this.dbService.type === 'sqlserver' + const placeholders = isSqlServer + ? productionIds.map((_, idx) => `@p${idx}`).join(', ') + : productionIds.map(() => '?').join(', ') + + // Use appropriate quoting for table/field names + const quote = isSqlServer ? '' : '`' const query = ` - SELECT \`${DB_CONFIG.FIELD_PRODUCTION_ID}\`, \`${DB_CONFIG.FIELD_ORDER_NUMBER}\` - FROM \`${DB_CONFIG.TABLE_NAME}\` - WHERE \`${DB_CONFIG.FIELD_PRODUCTION_ID}\` IN (${placeholders}) + SELECT ${quote}${DB_CONFIG.FIELD_PRODUCTION_ID}${quote}, ${quote}${DB_CONFIG.FIELD_ORDER_NUMBER}${quote} + FROM ${quote}${DB_CONFIG.TABLE_NAME}${quote} + WHERE ${quote}${DB_CONFIG.FIELD_PRODUCTION_ID}${quote} IN (${placeholders}) ` - const result = await this.mysqlService.query(query, productionIds) + const result = await this.dbService.query(query, productionIds) // Create a map for quick lookup const resultMap = new Map() @@ -281,19 +287,26 @@ export class OrderNumberResolver { * @returns List of valid order numbers */ private async verifyOrderNumbers(orderNumbers: string[]): Promise { - if (!this.mysqlService.isConnected()) { + if (!this.dbService.isConnected()) { return orderNumbers // Skip verification if not connected } try { - const placeholders = orderNumbers.map(() => '?').join(', ') + // Build query - use different placeholder style based on database type + const isSqlServer = this.dbService.type === 'sqlserver' + const placeholders = isSqlServer + ? orderNumbers.map((_, idx) => `@p${idx}`).join(', ') + : orderNumbers.map(() => '?').join(', ') + + // Use appropriate quoting for table/field names + const quote = isSqlServer ? '' : '`' const query = ` - SELECT \`${DB_CONFIG.FIELD_ORDER_NUMBER}\` - FROM \`${DB_CONFIG.TABLE_NAME}\` - WHERE \`${DB_CONFIG.FIELD_ORDER_NUMBER}\` IN (${placeholders}) + SELECT ${quote}${DB_CONFIG.FIELD_ORDER_NUMBER}${quote} + FROM ${quote}${DB_CONFIG.TABLE_NAME}${quote} + WHERE ${quote}${DB_CONFIG.FIELD_ORDER_NUMBER}${quote} IN (${placeholders}) ` - const result = await this.mysqlService.query(query, orderNumbers) + const result = await this.dbService.query(query, orderNumbers) return result.rows.map((row) => row[DB_CONFIG.FIELD_ORDER_NUMBER] as string) } catch (error) { console.warn('[OrderResolver] Failed to verify order numbers:', error) diff --git a/src/main/types/database.types.ts b/src/main/types/database.types.ts new file mode 100644 index 0000000..8874fc3 --- /dev/null +++ b/src/main/types/database.types.ts @@ -0,0 +1,104 @@ +/** + * Database Type Definitions + * + * Provides abstract interfaces for database operations, + * supporting both MySQL and SQL Server databases. + */ + +/** + * Supported database types + */ +export type DatabaseType = 'mysql' | 'sqlserver' + +/** + * Standard query result interface + */ +export interface QueryResult { + /** Query result rows */ + rows: Record[] + /** Column names from the query */ + columns: string[] + /** Number of rows affected or returned */ + rowCount: number +} + +/** + * Database service interface + * + * Defines the common interface that all database services must implement. + * This allows for database-agnostic operations throughout the application. + */ +export interface IDatabaseService { + /** Database type identifier */ + readonly type: DatabaseType + + /** + * Connect to the database + */ + connect(): Promise + + /** + * Disconnect from the database + */ + disconnect(): Promise + + /** + * Check if connected to the database + */ + isConnected(): boolean + + /** + * Execute a query and return results + * @param sql - SQL query string + * @param params - Query parameters as an array + */ + query(sql: string, params?: any[]): Promise + + /** + * Execute multiple queries in a transaction + * @param queries - Array of queries with optional parameters + */ + transaction(queries: { sql: string; params?: any[] }[]): Promise +} + +/** + * Base database configuration interface + */ +export interface DatabaseConfig { + /** Database server host */ + host?: string + /** Database server port */ + port?: number + /** Database username */ + user?: string + /** Database password */ + password?: string + /** Database name */ + database?: string +} + +/** + * MySQL-specific configuration + */ +export interface MySqlConfig extends DatabaseConfig { + host: string + port: number + user: string + password: string + database: string +} + +/** + * SQL Server-specific configuration + */ +export interface SqlServerConfig extends DatabaseConfig { + server: string + port: number + user: string + password: string + database: string + options?: { + encrypt?: boolean + trustServerCertificate?: boolean + } +}