refactor: abstract database layer to support MySQL and SQL Server

- Add IDatabaseService interface with unified query(transaction) methods
- Create DatabaseFactory for centralized database service creation
- Modify MySqlService and SqlServerService to implement IDatabaseService
- Unify SqlServerService.query() to accept array params (internally converts to @p0, @p1...)
- Refactor OrderNumberResolver to use IDatabaseService
- Refactor DiscreteMaterialPlanDAO to use DatabaseFactory
- Refactor MaterialsToBeDeletedDAO to use DatabaseFactory
- Update IPC handlers to use DatabaseFactory.create()
- Add database.types.ts with shared type definitions

This enables switching between MySQL and SQL Server via DB_TYPE env variable.

Co-Authored-By: Claude (glm-5) <noreply@anthropic.com>
This commit is contained in:
Misaka
2026-03-03 20:18:20 +08:00
parent 6698d82d6b
commit 0040ca7521
10 changed files with 624 additions and 702 deletions

View File

@@ -214,7 +214,18 @@ export function registerDatabaseHandlers(): void {
} }
log.debug('Executing SQL Server query', { windowId, sql: sqlString.substring(0, 100) }) 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<string, { value: unknown }> = {}
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) { } catch (error) {
const message = error instanceof Error ? error.message : 'SQL Server query failed' const message = error instanceof Error ? error.message : 'SQL Server query failed'
log.error('SQL Server query failed', { error: message }) log.error('SQL Server query failed', { error: message })

View File

@@ -2,7 +2,7 @@ import { ipcMain } from 'electron'
import { ErpAuthService } from '../services/erp/erp-auth' import { ErpAuthService } from '../services/erp/erp-auth'
import { ExtractorService } from '../services/erp/extractor' import { ExtractorService } from '../services/erp/extractor'
import { OrderNumberResolver } from '../services/erp/order-resolver' 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 { createLogger } from '../services/logger'
import { withErrorHandling, type IpcResult } from './index' import { withErrorHandling, type IpcResult } from './index'
import { ErpConnectionError, ValidationError, DatabaseQueryError } from '../types/errors' import { ErpConnectionError, ValidationError, DatabaseQueryError } from '../types/errors'
@@ -19,7 +19,7 @@ export function registerExtractorHandlers(): void {
async (_event, input: ExtractorInput): Promise<IpcResult<ExtractorResult>> => { async (_event, input: ExtractorInput): Promise<IpcResult<ExtractorResult>> => {
return withErrorHandling(async () => { return withErrorHandling(async () => {
let authService: ErpAuthService | null = null let authService: ErpAuthService | null = null
let mysqlService: MySqlService | null = null let dbService: IDatabaseService | null = null
try { try {
// Check environment variables // Check environment variables
@@ -39,29 +39,20 @@ export function registerExtractorHandlers(): void {
) )
} }
// Resolve order numbers (convert productionIDs to 生产订单号) // Create database service using factory
const mysqlConfig = { log.info('Connecting to database for order resolution...')
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)
try { try {
await mysqlService.connect() dbService = await create()
} catch (error) { } catch (error) {
throw new DatabaseQueryError( throw new DatabaseQueryError(
'MySQL 连接失败', '数据库连接失败',
'DB_CONNECTION_FAILED', 'DB_CONNECTION_FAILED',
error instanceof Error ? error : undefined 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) const mappings = await resolver.resolve(input.orderNumbers)
// Get valid order numbers and warnings // Get valid order numbers and warnings
@@ -136,13 +127,13 @@ export function registerExtractorHandlers(): void {
} }
} }
// Clean up: disconnect MySQL // Clean up: disconnect database
if (mysqlService) { if (dbService) {
try { try {
await mysqlService.disconnect() await dbService.disconnect()
log.debug('MySQL disconnected') log.debug('Database disconnected')
} catch (closeError) { } catch (closeError) {
log.warn('Error disconnecting MySQL', { log.warn('Error disconnecting database', {
error: closeError instanceof Error ? closeError.message : String(closeError) error: closeError instanceof Error ? closeError.message : String(closeError)
}) })
} }

View File

@@ -7,10 +7,9 @@
*/ */
import { ipcMain } from 'electron' 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 { OrderNumberResolver } from '../services/erp/order-resolver'
import { createLogger } from '../services/logger' import { createLogger } from '../services/logger'
import { DatabaseQueryError } from '../types/errors'
import type { OrderMapping, ResolutionStats } from '../services/erp/order-resolver' import type { OrderMapping, ResolutionStats } from '../services/erp/order-resolver'
const log = createLogger('ResolverHandler') const log = createLogger('ResolverHandler')
@@ -21,14 +20,6 @@ const log = createLogger('ResolverHandler')
export interface ResolverInput { export interface ResolverInput {
/** List of order numbers/productionIDs to resolve */ /** List of order numbers/productionIDs to resolve */
inputs: string[] 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( ipcMain.handle(
'resolver:resolve', 'resolver:resolve',
async (_event, input: ResolverInput): Promise<ResolverResponse> => { async (_event, input: ResolverInput): Promise<ResolverResponse> => {
let mysqlService: MySqlService | null = null let dbService: IDatabaseService | null = null
try { try {
// Use provided config or environment variables // Create database service using factory
const mysqlConfig = input.mysqlConfig || { log.info('Connecting to database for resolution', { inputCount: input.inputs.length })
host: process.env.DB_MYSQL_HOST || 'localhost', dbService = await create()
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 resolver and resolve inputs // Create resolver and resolve inputs
const resolver = new OrderNumberResolver(mysqlService) const resolver = new OrderNumberResolver(dbService)
const mappings = await resolver.resolve(input.inputs) const mappings = await resolver.resolve(input.inputs)
// Get valid order numbers and warnings // Get valid order numbers and warnings
@@ -107,13 +88,13 @@ export function registerResolverHandlers(): void {
error: `解析失败:${message}` error: `解析失败:${message}`
} }
} finally { } finally {
// Clean up MySQL connection // Clean up database connection
if (mysqlService) { if (dbService) {
try { try {
await mysqlService.disconnect() await dbService.disconnect()
log.debug('MySQL disconnected') log.debug('Database disconnected')
} catch (closeError) { } catch (closeError) {
log.warn('Error disconnecting MySQL', { log.warn('Error disconnecting database', {
error: closeError instanceof Error ? closeError.message : String(closeError) error: closeError instanceof Error ? closeError.message : String(closeError)
}) })
} }
@@ -136,9 +117,11 @@ export function registerResolverHandlers(): void {
error?: string error?: string
}> => { }> => {
try { try {
// Create a mock resolver without database connection
const resolver = new OrderNumberResolver({ const resolver = new OrderNumberResolver({
isConnected: () => false isConnected: () => false,
} as MySqlService) type: 'mysql'
} as IDatabaseService)
const results = inputs.map((input) => ({ const results = inputs.map((input) => ({
input, input,

View File

@@ -8,9 +8,7 @@
* - Statistics gathering * - Statistics gathering
*/ */
import { MySqlService } from './mysql' import { create, type IDatabaseService } from './index'
import { SqlServerService } from './sql-server'
import sql from 'mssql'
import { createLogger } from '../logger' import { createLogger } from '../logger'
const log = createLogger('DiscreteMaterialPlanDAO') const log = createLogger('DiscreteMaterialPlanDAO')
@@ -95,70 +93,37 @@ export const DISCRETE_MATERIAL_PLAN_CONFIG = {
* DiscreteMaterialPlanDAO Class * DiscreteMaterialPlanDAO Class
*/ */
export class DiscreteMaterialPlanDAO { export class DiscreteMaterialPlanDAO {
private mysqlService: MySqlService | null = null private dbService: IDatabaseService | 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'
}
}
/** /**
* Get the appropriate table name based on database type * Get the appropriate table name based on database type
*/ */
private getTableName(): string { 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_SQLSERVER
: DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL : 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<MySqlService | SqlServerService> { private async getDatabaseService(): Promise<IDatabaseService> {
if (this.dbType === 'sqlserver') { if (this.dbService && this.dbService.isConnected()) {
if (this.sqlServerService && this.sqlServerService.isConnected()) { return this.dbService
return this.sqlServerService
} }
this.sqlServerService = new SqlServerService({ this.dbService = await create()
server: process.env.DB_SERVER || 'localhost', return this.dbService
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', * Build placeholders for IN clause based on database type
port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10), */
user: process.env.DB_USERNAME || 'root', private buildPlaceholders(count: number, isSqlServer: boolean): string {
password: process.env.DB_PASSWORD || '', return isSqlServer
database: process.env.DB_NAME || '' ? Array.from({ length: count }, (_, idx) => `@p${idx}`).join(',')
}) : Array.from({ length: count }, () => '?').join(',')
await this.mysqlService.connect()
return this.mysqlService
}
} }
// ==================== QUERY ALL ==================== // ==================== QUERY ALL ====================
@@ -173,11 +138,7 @@ export class DiscreteMaterialPlanDAO {
const tableName = this.getTableName() const tableName = this.getTableName()
const sqlString = `SELECT * FROM ${tableName}` const sqlString = `SELECT * FROM ${tableName}`
const result = await dbService.query(sqlString)
const result =
this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
return result.rows return result.rows
} catch (error) { } catch (error) {
@@ -221,11 +182,7 @@ export class DiscreteMaterialPlanDAO {
WHERE rn = 1 WHERE rn = 1
` `
const result = const result = await dbService.query(sqlString)
this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
return result.rows return result.rows
} catch (error) { } catch (error) {
log.error('Query all distinct by material code error', { log.error('Query all distinct by material code error', {
@@ -250,19 +207,13 @@ export class DiscreteMaterialPlanDAO {
try { try {
const dbService = await this.getDatabaseService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName() const tableName = this.getTableName()
const isSqlServer = dbService.type === 'sqlserver'
const batchSize = 2000 const batchSize = 2000
const allResults: any[] = [] const allResults: any[] = []
for (let i = 0; i < sourceNumbers.length; i += batchSize) { for (let i = 0; i < sourceNumbers.length; i += batchSize) {
const batch = sourceNumbers.slice(i, 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<string, { value: string; type: sql.ISqlType }> = {}
batch.forEach((num, idx) => {
params[`p${idx}`] = { value: num, type: sql.NVarChar }
})
const sqlString = ` const sqlString = `
SELECT * SELECT *
@@ -270,20 +221,8 @@ export class DiscreteMaterialPlanDAO {
WHERE SourceNumber IN (${placeholders}) WHERE SourceNumber IN (${placeholders})
` `
const result = await (dbService as SqlServerService).queryWithParams(sqlString, params) const result = await dbService.query(sqlString, batch)
allResults.push(...result.rows) 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)
}
} }
return allResults return allResults
@@ -310,19 +249,13 @@ export class DiscreteMaterialPlanDAO {
try { try {
const dbService = await this.getDatabaseService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName() const tableName = this.getTableName()
const isSqlServer = dbService.type === 'sqlserver'
const batchSize = 2000 const batchSize = 2000
const allResults: any[] = [] const allResults: any[] = []
for (let i = 0; i < sourceNumbers.length; i += batchSize) { for (let i = 0; i < sourceNumbers.length; i += batchSize) {
const batch = sourceNumbers.slice(i, 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<string, { value: string; type: sql.ISqlType }> = {}
batch.forEach((num, idx) => {
params[`p${idx}`] = { value: num, type: sql.NVarChar }
})
const sqlString = ` const sqlString = `
WITH RankedRecords AS ( WITH RankedRecords AS (
@@ -347,37 +280,8 @@ export class DiscreteMaterialPlanDAO {
WHERE rn = 1 WHERE rn = 1
` `
const result = await (dbService as SqlServerService).queryWithParams(sqlString, params) const result = await dbService.query(sqlString, batch)
allResults.push(...result.rows) 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)
}
} }
return allResults return allResults
@@ -398,30 +302,17 @@ export class DiscreteMaterialPlanDAO {
try { try {
const dbService = await this.getDatabaseService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName() const tableName = this.getTableName()
const isSqlServer = dbService.type === 'sqlserver'
if (this.dbType === 'sqlserver') { const placeholder = isSqlServer ? '@p0' : '?'
const sqlString = ` const sqlString = `
SELECT * SELECT *
FROM ${tableName} FROM ${tableName}
WHERE SourceNumber = @sourceNumber WHERE SourceNumber = ${placeholder}
` `
const result = await (dbService as SqlServerService).queryWithParams(sqlString, { const result = await dbService.query(sqlString, [sourceNumber])
sourceNumber: { value: sourceNumber, type: sql.NVarChar }
})
return result.rows return result.rows
} else {
const sqlString = `
SELECT *
FROM ${tableName}
WHERE SourceNumber = ?
`
const result = await (dbService as MySqlService).query(sqlString, [sourceNumber])
return result.rows
}
} catch (error) { } catch (error) {
log.error('Query by source number error', { log.error('Query by source number error', {
error: error instanceof Error ? error.message : String(error) error: error instanceof Error ? error.message : String(error)
@@ -441,30 +332,17 @@ export class DiscreteMaterialPlanDAO {
try { try {
const dbService = await this.getDatabaseService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName() const tableName = this.getTableName()
const isSqlServer = dbService.type === 'sqlserver'
if (this.dbType === 'sqlserver') { const placeholder = isSqlServer ? '@p0' : '?'
const sqlString = ` const sqlString = `
SELECT * SELECT *
FROM ${tableName} FROM ${tableName}
WHERE PlanNumber = @planNumber WHERE PlanNumber = ${placeholder}
` `
const result = await (dbService as SqlServerService).queryWithParams(sqlString, { const result = await dbService.query(sqlString, [planNumber])
planNumber: { value: planNumber, type: sql.NVarChar }
})
return result.rows return result.rows
} else {
const sqlString = `
SELECT *
FROM ${tableName}
WHERE PlanNumber = ?
`
const result = await (dbService as MySqlService).query(sqlString, [planNumber])
return result.rows
}
} catch (error) { } catch (error) {
log.error('Query by plan number error', { log.error('Query by plan number error', {
error: error instanceof Error ? error.message : String(error) error: error instanceof Error ? error.message : String(error)
@@ -486,14 +364,8 @@ export class DiscreteMaterialPlanDAO {
try { try {
const dbService = await this.getDatabaseService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName() const tableName = this.getTableName()
const isSqlServer = dbService.type === 'sqlserver'
if (this.dbType === 'sqlserver') { const placeholders = this.buildPlaceholders(planNumbers.length, isSqlServer)
const placeholders = planNumbers.map((_, idx) => `@p${idx}`).join(',')
const params: Record<string, { value: string; type: sql.ISqlType }> = {}
planNumbers.forEach((num, idx) => {
params[`p${idx}`] = { value: num, type: sql.NVarChar }
})
const sqlString = ` const sqlString = `
SELECT * SELECT *
@@ -501,20 +373,8 @@ export class DiscreteMaterialPlanDAO {
WHERE PlanNumber IN (${placeholders}) WHERE PlanNumber IN (${placeholders})
` `
const result = await (dbService as SqlServerService).queryWithParams(sqlString, params) const result = await dbService.query(sqlString, planNumbers)
return result.rows 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
}
} catch (error) { } catch (error) {
log.error('Query by plan numbers error', { log.error('Query by plan numbers error', {
error: error instanceof Error ? error.message : String(error) error: error instanceof Error ? error.message : String(error)
@@ -535,11 +395,7 @@ export class DiscreteMaterialPlanDAO {
const tableName = this.getTableName() const tableName = this.getTableName()
const sqlString = `SELECT COUNT(*) as count FROM ${tableName}` const sqlString = `SELECT COUNT(*) as count FROM ${tableName}`
const result = await dbService.query(sqlString)
const result =
this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
return result.rows.length > 0 ? (result.rows[0].count as number) : 0 return result.rows.length > 0 ? (result.rows[0].count as number) : 0
} catch (error) { } catch (error) {
@@ -559,30 +415,17 @@ export class DiscreteMaterialPlanDAO {
try { try {
const dbService = await this.getDatabaseService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName() const tableName = this.getTableName()
const isSqlServer = dbService.type === 'sqlserver'
if (this.dbType === 'sqlserver') { const placeholder = isSqlServer ? '@p0' : '?'
const sqlString = ` const sqlString = `
SELECT COUNT(*) as count SELECT COUNT(*) as count
FROM ${tableName} FROM ${tableName}
WHERE PlanNumber = @planNumber WHERE PlanNumber = ${placeholder}
` `
const result = await (dbService as SqlServerService).queryWithParams(sqlString, { const result = await dbService.query(sqlString, [planNumber])
planNumber: { value: planNumber, type: sql.NVarChar }
})
return result.rows.length > 0 ? (result.rows[0].count as number) : 0 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
}
} catch (error) { } catch (error) {
log.error('Count by plan number error', { log.error('Count by plan number error', {
error: error instanceof Error ? error.message : String(error) error: error instanceof Error ? error.message : String(error)
@@ -600,15 +443,10 @@ export class DiscreteMaterialPlanDAO {
try { try {
const dbService = await this.getDatabaseService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName() const tableName = this.getTableName()
const isSqlServer = dbService.type === 'sqlserver'
if (sourceNumbers && sourceNumbers.length > 0) { if (sourceNumbers && sourceNumbers.length > 0) {
if (this.dbType === 'sqlserver') { const placeholders = this.buildPlaceholders(sourceNumbers.length, isSqlServer)
const placeholders = sourceNumbers.map((_, idx) => `@p${idx}`).join(',')
const params: Record<string, { value: string; type: sql.ISqlType }> = {}
sourceNumbers.forEach((num, idx) => {
params[`p${idx}`] = { value: num, type: sql.NVarChar }
})
const sqlString = ` const sqlString = `
SELECT DISTINCT MaterialName SELECT DISTINCT MaterialName
@@ -617,21 +455,8 @@ export class DiscreteMaterialPlanDAO {
AND MaterialName IS NOT NULL AND MaterialName IS NOT NULL
` `
const result = await (dbService as SqlServerService).queryWithParams(sqlString, params) const result = await dbService.query(sqlString, sourceNumbers)
return result.rows.map((row) => row.MaterialName as string).filter(Boolean) 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)
}
} else { } else {
const sqlString = ` const sqlString = `
SELECT DISTINCT MaterialName SELECT DISTINCT MaterialName
@@ -639,11 +464,7 @@ export class DiscreteMaterialPlanDAO {
WHERE MaterialName IS NOT NULL WHERE MaterialName IS NOT NULL
` `
const result = const result = await dbService.query(sqlString)
this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
return result.rows.map((row) => row.MaterialName as string).filter(Boolean) return result.rows.map((row) => row.MaterialName as string).filter(Boolean)
} }
} catch (error) { } catch (error) {
@@ -673,11 +494,7 @@ export class DiscreteMaterialPlanDAO {
FROM ${tableName} FROM ${tableName}
` `
const result = const result = await dbService.query(sqlString)
this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
return result.rows.length > 0 ? result.rows[0] : {} return result.rows.length > 0 ? result.rows[0] : {}
} catch (error) { } catch (error) {
log.error('Get statistics error', { log.error('Get statistics error', {
@@ -691,13 +508,9 @@ export class DiscreteMaterialPlanDAO {
* Disconnect from database * Disconnect from database
*/ */
async disconnect(): Promise<void> { async disconnect(): Promise<void> {
if (this.mysqlService) { if (this.dbService) {
await this.mysqlService.disconnect() await this.dbService.disconnect()
this.mysqlService = null this.dbService = null
}
if (this.sqlServerService) {
await this.sqlServerService.disconnect()
this.sqlServerService = null
} }
} }
} }

View File

@@ -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<DatabaseType, IDatabaseService> = 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<IDatabaseService> {
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<void> {
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<void> {
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'

View File

@@ -8,9 +8,7 @@
* - Statistics gathering * - Statistics gathering
*/ */
import { MySqlService } from './mysql' import { create, type IDatabaseService } from './index'
import { SqlServerService } from './sql-server'
import sql from 'mssql'
import { createLogger } from '../logger' import { createLogger } from '../logger'
const log = createLogger('MaterialsToBeDeletedDAO') const log = createLogger('MaterialsToBeDeletedDAO')
@@ -59,70 +57,37 @@ export const MATERIALS_TO_BE_DELETED_CONFIG = {
* MaterialsToBeDeleted DAO Class * MaterialsToBeDeleted DAO Class
*/ */
export class MaterialsToBeDeletedDAO { export class MaterialsToBeDeletedDAO {
private mysqlService: MySqlService | null = null private dbService: IDatabaseService | 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'
}
}
/** /**
* Get the appropriate table name based on database type * Get the appropriate table name based on database type
*/ */
private getTableName(): string { 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_SQLSERVER
: MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL : 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<MySqlService | SqlServerService> { private async getDatabaseService(): Promise<IDatabaseService> {
if (this.dbType === 'sqlserver') { if (this.dbService && this.dbService.isConnected()) {
if (this.sqlServerService && this.sqlServerService.isConnected()) { return this.dbService
return this.sqlServerService
} }
this.sqlServerService = new SqlServerService({ this.dbService = await create()
server: process.env.DB_SERVER || 'localhost', return this.dbService
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', * Build placeholders for IN clause based on database type
port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10), */
user: process.env.DB_USERNAME || 'root', private buildPlaceholders(count: number, isSqlServer: boolean): string {
password: process.env.DB_PASSWORD || '', return isSqlServer
database: process.env.DB_NAME || '' ? Array.from({ length: count }, (_, idx) => `@p${idx}`).join(',')
}) : Array.from({ length: count }, () => '?').join(',')
await this.mysqlService.connect()
return this.mysqlService
}
} }
// ==================== UPSERT (MERGE) ==================== // ==================== UPSERT (MERGE) ====================
@@ -144,28 +109,28 @@ export class MaterialsToBeDeletedDAO {
const tableName = this.getTableName() const tableName = this.getTableName()
const code = materialCode.trim() const code = materialCode.trim()
const manager = managerName?.trim() || null const manager = managerName?.trim() || null
const isSqlServer = dbService.type === 'sqlserver'
if (this.dbType === 'sqlserver') { if (isSqlServer) {
// SQL Server MERGE statement
const sqlString = ` const sqlString = `
MERGE ${tableName} AS target 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 ON target.MaterialCode = source.MaterialCode
WHEN MATCHED THEN UPDATE SET ManagerName = source.ManagerName WHEN MATCHED THEN UPDATE SET ManagerName = source.ManagerName
WHEN NOT MATCHED THEN INSERT (MaterialCode, ManagerName) VALUES (source.MaterialCode, source.ManagerName); WHEN NOT MATCHED THEN INSERT (MaterialCode, ManagerName) VALUES (source.MaterialCode, source.ManagerName);
` `
await (dbService as SqlServerService).queryWithParams(sqlString, { await dbService.query(sqlString, [code, manager])
materialCode: { value: code, type: sql.NVarChar },
managerName: { value: manager, type: sql.NVarChar }
})
} else { } else {
// MySQL ON DUPLICATE KEY UPDATE
const sqlString = ` const sqlString = `
INSERT INTO ${tableName} (MaterialCode, ManagerName) INSERT INTO ${tableName} (MaterialCode, ManagerName)
VALUES (?, ?) VALUES (?, ?)
ON DUPLICATE KEY UPDATE ManagerName = VALUES(ManagerName) ON DUPLICATE KEY UPDATE ManagerName = VALUES(ManagerName)
` `
await (dbService as MySqlService).query(sqlString, [code, manager]) await dbService.query(sqlString, [code, manager])
} }
return true return true
@@ -198,6 +163,7 @@ export class MaterialsToBeDeletedDAO {
try { try {
const dbService = await this.getDatabaseService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName() const tableName = this.getTableName()
const isSqlServer = dbService.type === 'sqlserver'
for (const material of materials) { for (const material of materials) {
const materialCode = material.materialCode?.trim() const materialCode = material.materialCode?.trim()
@@ -209,27 +175,26 @@ export class MaterialsToBeDeletedDAO {
} }
try { try {
if (this.dbType === 'sqlserver') { if (isSqlServer) {
// SQL Server MERGE statement
const sqlString = ` const sqlString = `
MERGE ${tableName} AS target 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 ON target.MaterialCode = source.MaterialCode
WHEN MATCHED THEN UPDATE SET ManagerName = source.ManagerName WHEN MATCHED THEN UPDATE SET ManagerName = source.ManagerName
WHEN NOT MATCHED THEN INSERT (MaterialCode, ManagerName) VALUES (source.MaterialCode, source.ManagerName); WHEN NOT MATCHED THEN INSERT (MaterialCode, ManagerName) VALUES (source.MaterialCode, source.ManagerName);
` `
await (dbService as SqlServerService).queryWithParams(sqlString, { await dbService.query(sqlString, [materialCode, managerName || null])
materialCode: { value: materialCode, type: sql.NVarChar },
managerName: { value: managerName || null, type: sql.NVarChar }
})
} else { } else {
// MySQL ON DUPLICATE KEY UPDATE
const sqlString = ` const sqlString = `
INSERT INTO ${tableName} (MaterialCode, ManagerName) INSERT INTO ${tableName} (MaterialCode, ManagerName)
VALUES (?, ?) VALUES (?, ?)
ON DUPLICATE KEY UPDATE ManagerName = VALUES(ManagerName) 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++ stats.success++
@@ -268,11 +233,7 @@ export class MaterialsToBeDeletedDAO {
WHERE MaterialCode IS NOT NULL WHERE MaterialCode IS NOT NULL
` `
const result = const result = await dbService.query(sqlString)
this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
return new Set(result.rows.map((row) => row.MaterialCode as string).filter(Boolean)) return new Set(result.rows.map((row) => row.MaterialCode as string).filter(Boolean))
} catch (error) { } catch (error) {
log.error('Get all material codes error', { log.error('Get all material codes error', {
@@ -298,11 +259,7 @@ export class MaterialsToBeDeletedDAO {
ORDER BY ManagerName, MaterialCode ORDER BY ManagerName, MaterialCode
` `
const result = const result = await dbService.query(sqlString)
this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
return result.rows.map((row) => ({ return result.rows.map((row) => ({
id: row.ID as number, id: row.ID as number,
materialCode: row.MaterialCode as string, materialCode: row.MaterialCode as string,
@@ -325,40 +282,22 @@ export class MaterialsToBeDeletedDAO {
try { try {
const dbService = await this.getDatabaseService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName() const tableName = this.getTableName()
const isSqlServer = dbService.type === 'sqlserver'
if (this.dbType === 'sqlserver') { const placeholder = isSqlServer ? '@p0' : '?'
const sqlString = ` const sqlString = `
SELECT ID, MaterialCode, ManagerName SELECT ID, MaterialCode, ManagerName
FROM ${tableName} FROM ${tableName}
WHERE ManagerName = @managerName AND MaterialCode IS NOT NULL WHERE ManagerName = ${placeholder} AND MaterialCode IS NOT NULL
ORDER BY MaterialCode ORDER BY MaterialCode
` `
const result = await (dbService as SqlServerService).queryWithParams(sqlString, { const result = await dbService.query(sqlString, [managerName])
managerName: { value: managerName, type: sql.NVarChar }
})
return result.rows.map((row) => ({ return result.rows.map((row) => ({
id: row.ID as number, id: row.ID as number,
materialCode: row.MaterialCode as string, materialCode: row.MaterialCode as string,
managerName: row.ManagerName 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
}))
}
} catch (error) { } catch (error) {
log.error('Get materials by manager error', { log.error('Get materials by manager error', {
error: error instanceof Error ? error.message : String(error) error: error instanceof Error ? error.message : String(error)
@@ -383,11 +322,7 @@ export class MaterialsToBeDeletedDAO {
ORDER BY ManagerName ORDER BY ManagerName
` `
const result = const result = await dbService.query(sqlString)
this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
return result.rows.map((row) => row.ManagerName as string).filter(Boolean) return result.rows.map((row) => row.ManagerName as string).filter(Boolean)
} catch (error) { } catch (error) {
log.error('Get managers error', { log.error('Get managers error', {
@@ -407,17 +342,16 @@ export class MaterialsToBeDeletedDAO {
const dbService = await this.getDatabaseService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName() const tableName = this.getTableName()
const code = materialCode.trim() const code = materialCode.trim()
const isSqlServer = dbService.type === 'sqlserver'
if (this.dbType === 'sqlserver') { const placeholder = isSqlServer ? '@p0' : '?'
const sqlString = ` const sqlString = `
SELECT ID, MaterialCode, ManagerName SELECT ID, MaterialCode, ManagerName
FROM ${tableName} FROM ${tableName}
WHERE MaterialCode = @materialCode WHERE MaterialCode = ${placeholder}
` `
const result = await (dbService as SqlServerService).queryWithParams(sqlString, { const result = await dbService.query(sqlString, [code])
materialCode: { value: code, type: sql.NVarChar }
})
if (result.rows.length === 0) { if (result.rows.length === 0) {
return null return null
@@ -429,26 +363,6 @@ export class MaterialsToBeDeletedDAO {
materialCode: row.MaterialCode as string, materialCode: row.MaterialCode as string,
managerName: row.ManagerName 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
}
}
} catch (error) { } catch (error) {
log.error('Get record by material code error', { log.error('Get record by material code error', {
error: error instanceof Error ? error.message : String(error) error: error instanceof Error ? error.message : String(error)
@@ -469,28 +383,16 @@ export class MaterialsToBeDeletedDAO {
const dbService = await this.getDatabaseService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName() const tableName = this.getTableName()
const code = materialCode.trim() const code = materialCode.trim()
const isSqlServer = dbService.type === 'sqlserver'
if (this.dbType === 'sqlserver') { const placeholder = isSqlServer ? '@p0' : '?'
const sqlString = ` const sqlString = `
DELETE FROM ${tableName} DELETE FROM ${tableName}
WHERE MaterialCode = @materialCode WHERE MaterialCode = ${placeholder}
` `
const result = await (dbService as SqlServerService).queryWithParams(sqlString, { const result = await dbService.query(sqlString, [code])
materialCode: { value: code, type: sql.NVarChar }
})
return result.rowCount > 0 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
}
} catch (error) { } catch (error) {
log.error('Delete by material code error', { log.error('Delete by material code error', {
error: error instanceof Error ? error.message : String(error) error: error instanceof Error ? error.message : String(error)
@@ -508,28 +410,16 @@ export class MaterialsToBeDeletedDAO {
try { try {
const dbService = await this.getDatabaseService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName() const tableName = this.getTableName()
const isSqlServer = dbService.type === 'sqlserver'
if (this.dbType === 'sqlserver') { const placeholder = isSqlServer ? '@p0' : '?'
const sqlString = ` const sqlString = `
DELETE FROM ${tableName} DELETE FROM ${tableName}
WHERE ManagerName = @managerName WHERE ManagerName = ${placeholder}
` `
const result = await (dbService as SqlServerService).queryWithParams(sqlString, { const result = await dbService.query(sqlString, [managerName])
managerName: { value: managerName, type: sql.NVarChar }
})
return result.rowCount return result.rowCount
} else {
const sqlString = `
DELETE FROM ${tableName}
WHERE ManagerName = ?
`
const result = await (dbService as MySqlService).query(sqlString, [managerName])
return result.rowCount
}
} catch (error) { } catch (error) {
log.error('Delete by manager error', { log.error('Delete by manager error', {
error: error instanceof Error ? error.message : String(error) error: error instanceof Error ? error.message : String(error)
@@ -548,12 +438,7 @@ export class MaterialsToBeDeletedDAO {
const tableName = this.getTableName() const tableName = this.getTableName()
const sqlString = `DELETE FROM ${tableName}` const sqlString = `DELETE FROM ${tableName}`
const result = await dbService.query(sqlString)
const result =
this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
return result.rowCount return result.rowCount
} catch (error) { } catch (error) {
log.error('Delete all materials error', { log.error('Delete all materials error', {
@@ -579,36 +464,22 @@ export class MaterialsToBeDeletedDAO {
try { try {
const dbService = await this.getDatabaseService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName() const tableName = this.getTableName()
const isSqlServer = dbService.type === 'sqlserver'
for (let i = 0; i < materialCodes.length; i += batchSize) { for (let i = 0; i < materialCodes.length; i += batchSize) {
const batch = materialCodes.slice(i, 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<string, { value: string; type: sql.ISqlType }> = {}
batch.forEach((code, idx) => {
params[`p${idx}`] = { value: code.trim(), type: sql.NVarChar }
})
const sqlString = ` const sqlString = `
DELETE FROM ${tableName} DELETE FROM ${tableName}
WHERE MaterialCode IN (${placeholders}) WHERE MaterialCode IN (${placeholders})
` `
const result = await (dbService as SqlServerService).queryWithParams(sqlString, params) const result = await dbService.query(
sqlString,
batch.map((c) => c.trim())
)
totalDeleted += result.rowCount 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
}
} }
} catch (error) { } catch (error) {
log.error('Delete by material codes error', { log.error('Delete by material codes error', {
@@ -631,30 +502,17 @@ export class MaterialsToBeDeletedDAO {
const dbService = await this.getDatabaseService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName() const tableName = this.getTableName()
const code = materialCode.trim() const code = materialCode.trim()
const isSqlServer = dbService.type === 'sqlserver'
if (this.dbType === 'sqlserver') { const placeholder = isSqlServer ? '@p0' : '?'
const sqlString = ` const sqlString = `
SELECT COUNT(*) as count SELECT COUNT(*) as count
FROM ${tableName} FROM ${tableName}
WHERE MaterialCode = @materialCode WHERE MaterialCode = ${placeholder}
` `
const result = await (dbService as SqlServerService).queryWithParams(sqlString, { const result = await dbService.query(sqlString, [code])
materialCode: { value: code, type: sql.NVarChar }
})
return result.rows.length > 0 && (result.rows[0].count as number) > 0 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
}
} catch (error) { } catch (error) {
log.error('Material exists error', { log.error('Material exists error', {
error: error instanceof Error ? error.message : String(error) error: error instanceof Error ? error.message : String(error)
@@ -673,11 +531,7 @@ export class MaterialsToBeDeletedDAO {
const tableName = this.getTableName() const tableName = this.getTableName()
const sqlString = `SELECT COUNT(*) as count FROM ${tableName}` const sqlString = `SELECT COUNT(*) as count FROM ${tableName}`
const result = await dbService.query(sqlString)
const result =
this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(sqlString)
: await (dbService as MySqlService).query(sqlString)
return result.rows.length > 0 ? (result.rows[0].count as number) : 0 return result.rows.length > 0 ? (result.rows[0].count as number) : 0
} catch (error) { } catch (error) {
@@ -697,30 +551,17 @@ export class MaterialsToBeDeletedDAO {
try { try {
const dbService = await this.getDatabaseService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName() const tableName = this.getTableName()
const isSqlServer = dbService.type === 'sqlserver'
if (this.dbType === 'sqlserver') { const placeholder = isSqlServer ? '@p0' : '?'
const sqlString = ` const sqlString = `
SELECT COUNT(*) as count SELECT COUNT(*) as count
FROM ${tableName} FROM ${tableName}
WHERE ManagerName = @managerName WHERE ManagerName = ${placeholder}
` `
const result = await (dbService as SqlServerService).queryWithParams(sqlString, { const result = await dbService.query(sqlString, [managerName])
managerName: { value: managerName, type: sql.NVarChar }
})
return result.rows.length > 0 ? (result.rows[0].count as number) : 0 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
}
} catch (error) { } catch (error) {
log.error('Count by manager error', { log.error('Count by manager error', {
error: error instanceof Error ? error.message : String(error) error: error instanceof Error ? error.message : String(error)
@@ -747,11 +588,7 @@ export class MaterialsToBeDeletedDAO {
WHERE MaterialCode IS NOT NULL WHERE MaterialCode IS NOT NULL
` `
const statsResult = const statsResult = await dbService.query(statsSql)
this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(statsSql)
: await (dbService as MySqlService).query(statsSql)
const stats = statsResult.rows[0] || {} const stats = statsResult.rows[0] || {}
// Get materials per manager // Get materials per manager
@@ -763,11 +600,7 @@ export class MaterialsToBeDeletedDAO {
ORDER BY count DESC ORDER BY count DESC
` `
const managerResult = const managerResult = await dbService.query(managerSql)
this.dbType === 'sqlserver'
? await (dbService as SqlServerService).query(managerSql)
: await (dbService as MySqlService).query(managerSql)
const materialsPerManager = managerResult.rows.map((row) => ({ const materialsPerManager = managerResult.rows.map((row) => ({
[row.ManagerName as string]: row.count as number [row.ManagerName as string]: row.count as number
})) }))
@@ -793,13 +626,9 @@ export class MaterialsToBeDeletedDAO {
* Disconnect from database * Disconnect from database
*/ */
async disconnect(): Promise<void> { async disconnect(): Promise<void> {
if (this.mysqlService) { if (this.dbService) {
await this.mysqlService.disconnect() await this.dbService.disconnect()
this.mysqlService = null this.dbService = null
}
if (this.sqlServerService) {
await this.sqlServerService.disconnect()
this.sqlServerService = null
} }
} }
} }

View File

@@ -1,20 +1,17 @@
import mysql from 'mysql2/promise' import mysql from 'mysql2/promise'
import type {
IDatabaseService,
DatabaseType,
QueryResult,
MySqlConfig
} from '../../types/database.types'
export interface MySqlConfig { export type { MySqlConfig } from '../../types/database.types'
host: string
port: number
user: string
password: string
database: string
}
export interface MySqlQueryResult { export class MySqlService implements IDatabaseService {
rows: Record<string, unknown>[] /** Database type identifier */
columns: string[] readonly type: DatabaseType = 'mysql'
rowCount: number
}
export class MySqlService {
private connection: mysql.Connection | null = null private connection: mysql.Connection | null = null
private config: MySqlConfig private config: MySqlConfig
@@ -72,7 +69,7 @@ export class MySqlService {
/** /**
* Execute a query and return results * Execute a query and return results
*/ */
async query(sql: string, params?: any[]): Promise<MySqlQueryResult> { async query(sql: string, params?: any[]): Promise<QueryResult> {
if (!this.connection) { if (!this.connection) {
throw new Error('Not connected to MySQL. Call connect() first.') throw new Error('Not connected to MySQL. Call connect() first.')
} }

View File

@@ -1,24 +1,17 @@
import sql from 'mssql' import sql from 'mssql'
import type {
IDatabaseService,
DatabaseType,
QueryResult,
SqlServerConfig
} from '../../types/database.types'
export interface SqlServerConfig { export type { SqlServerConfig } from '../../types/database.types'
server: string
port: number
user: string
password: string
database: string
options?: {
encrypt?: boolean
trustServerCertificate?: boolean
}
}
export interface SqlServerQueryResult { export class SqlServerService implements IDatabaseService {
rows: Record<string, unknown>[] /** Database type identifier */
columns: string[] readonly type: DatabaseType = 'sqlserver'
rowCount: number
}
export class SqlServerService {
private pool: sql.ConnectionPool | null = null private pool: sql.ConnectionPool | null = null
private config: SqlServerConfig private config: SqlServerConfig
@@ -79,8 +72,10 @@ export class SqlServerService {
/** /**
* Execute a query and return results * 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<string, unknown>): Promise<SqlServerQueryResult> { async query(sqlString: string, params?: any[]): Promise<QueryResult> {
if (!this.pool) { if (!this.pool) {
throw new Error('Not connected to SQL Server. Call connect() first.') throw new Error('Not connected to SQL Server. Call connect() first.')
} }
@@ -88,11 +83,11 @@ export class SqlServerService {
try { try {
const request = this.pool.request() const request = this.pool.request()
// Add parameters if provided // Add parameters if provided - convert array to @p0, @p1, ... format
if (params) { if (params && params.length > 0) {
for (const [key, value] of Object.entries(params)) { params.forEach((value, index) => {
request.input(key, value) request.input(`p${index}`, value)
} })
} }
const result = await request.query(sqlString) 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( async queryWithParams(
sqlString: string, sqlString: string,
params: Record<string, { value: unknown; type?: sql.ISqlType }> params: Record<string, { value: unknown; type?: sql.ISqlType }>
): Promise<SqlServerQueryResult> { ): Promise<QueryResult> {
if (!this.pool) { if (!this.pool) {
throw new Error('Not connected to SQL Server. Call connect() first.') throw new Error('Not connected to SQL Server. Call connect() first.')
} }
@@ -154,8 +151,9 @@ export class SqlServerService {
/** /**
* Execute multiple queries in a transaction * Execute multiple queries in a transaction
* @param queries - Array of queries with array-based parameters
*/ */
async transaction(queries: { sql: string; params?: Record<string, unknown> }[]): Promise<void> { async transaction(queries: { sql: string; params?: any[] }[]): Promise<void> {
if (!this.pool) { if (!this.pool) {
throw new Error('Not connected to SQL Server. Call connect() first.') 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) { for (const { sql: sqlString, params } of queries) {
const request = new sql.Request(transaction) const request = new sql.Request(transaction)
if (params) { // Add parameters if provided - convert array to @p0, @p1, ... format
for (const [key, value] of Object.entries(params)) { if (params && params.length > 0) {
request.input(key, value) params.forEach((value, index) => {
} request.input(`p${index}`, value)
})
} }
await request.query(sqlString) await request.query(sqlString)

View File

@@ -2,7 +2,7 @@
* Order Number Resolver Service * Order Number Resolver Service
* *
* Automatically recognizes productionID and 生产订单号 (production order number), * 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") * - productionID format: 2 digits + 1 letter + serial number (e.g., "22A1", "22A1234")
* - 生产订单号 format: SC + 14 digits (e.g., "SC70202602120085") * - 生产订单号 format: SC + 14 digits (e.g., "SC70202602120085")
@@ -11,7 +11,7 @@
* Fields: 总排号 (productionID), 生产订单号 (production order number) * Fields: 总排号 (productionID), 生产订单号 (production order number)
*/ */
import { MySqlService } from '../database/mysql' import type { IDatabaseService } from '../database'
/** /**
* Order mapping result * Order mapping result
@@ -71,10 +71,10 @@ export const DB_CONFIG = {
* Order Number Resolver Service * Order Number Resolver Service
*/ */
export class OrderNumberResolver { export class OrderNumberResolver {
private mysqlService: MySqlService private dbService: IDatabaseService
constructor(mysqlService: MySqlService) { constructor(dbService: IDatabaseService) {
this.mysqlService = mysqlService this.dbService = dbService
} }
/** /**
@@ -202,7 +202,7 @@ export class OrderNumberResolver {
private async resolveProductionIds(productionIds: string[]): Promise<OrderMapping[]> { private async resolveProductionIds(productionIds: string[]): Promise<OrderMapping[]> {
const mappings: OrderMapping[] = [] const mappings: OrderMapping[] = []
if (!this.mysqlService.isConnected()) { if (!this.dbService.isConnected()) {
// Database not connected, return all as failed // Database not connected, return all as failed
for (const pid of productionIds) { for (const pid of productionIds) {
mappings.push({ mappings.push({
@@ -217,15 +217,21 @@ export class OrderNumberResolver {
} }
try { try {
// Build query with placeholders // Build query - use different placeholder style based on database type
const placeholders = productionIds.map(() => '?').join(', ') 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 = ` const query = `
SELECT \`${DB_CONFIG.FIELD_PRODUCTION_ID}\`, \`${DB_CONFIG.FIELD_ORDER_NUMBER}\` SELECT ${quote}${DB_CONFIG.FIELD_PRODUCTION_ID}${quote}, ${quote}${DB_CONFIG.FIELD_ORDER_NUMBER}${quote}
FROM \`${DB_CONFIG.TABLE_NAME}\` FROM ${quote}${DB_CONFIG.TABLE_NAME}${quote}
WHERE \`${DB_CONFIG.FIELD_PRODUCTION_ID}\` IN (${placeholders}) 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 // Create a map for quick lookup
const resultMap = new Map<string, string>() const resultMap = new Map<string, string>()
@@ -281,19 +287,26 @@ export class OrderNumberResolver {
* @returns List of valid order numbers * @returns List of valid order numbers
*/ */
private async verifyOrderNumbers(orderNumbers: string[]): Promise<string[]> { private async verifyOrderNumbers(orderNumbers: string[]): Promise<string[]> {
if (!this.mysqlService.isConnected()) { if (!this.dbService.isConnected()) {
return orderNumbers // Skip verification if not connected return orderNumbers // Skip verification if not connected
} }
try { 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 = ` const query = `
SELECT \`${DB_CONFIG.FIELD_ORDER_NUMBER}\` SELECT ${quote}${DB_CONFIG.FIELD_ORDER_NUMBER}${quote}
FROM \`${DB_CONFIG.TABLE_NAME}\` FROM ${quote}${DB_CONFIG.TABLE_NAME}${quote}
WHERE \`${DB_CONFIG.FIELD_ORDER_NUMBER}\` IN (${placeholders}) 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) return result.rows.map((row) => row[DB_CONFIG.FIELD_ORDER_NUMBER] as string)
} catch (error) { } catch (error) {
console.warn('[OrderResolver] Failed to verify order numbers:', error) console.warn('[OrderResolver] Failed to verify order numbers:', error)

View File

@@ -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<string, unknown>[]
/** 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<void>
/**
* Disconnect from the database
*/
disconnect(): Promise<void>
/**
* 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<QueryResult>
/**
* Execute multiple queries in a transaction
* @param queries - Array of queries with optional parameters
*/
transaction(queries: { sql: string; params?: any[] }[]): Promise<void>
}
/**
* 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
}
}