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) })
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) {
const message = error instanceof Error ? error.message : 'SQL Server query failed'
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 { 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<IpcResult<ExtractorResult>> => {
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)
})
}

View File

@@ -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<ResolverResponse> => {
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,

View File

@@ -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<MySqlService | SqlServerService> {
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<IDatabaseService> {
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<string, { value: string; type: sql.ISqlType }> = {}
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<string, { value: string; type: sql.ISqlType }> = {}
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<string, { value: string; type: sql.ISqlType }> = {}
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<string, { value: string; type: sql.ISqlType }> = {}
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<void> {
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
}
}
}

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
*/
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<MySqlService | SqlServerService> {
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<IDatabaseService> {
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<string, { value: string; type: sql.ISqlType }> = {}
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<void> {
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
}
}
}

View File

@@ -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<string, unknown>[]
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<MySqlQueryResult> {
async query(sql: string, params?: any[]): Promise<QueryResult> {
if (!this.connection) {
throw new Error('Not connected to MySQL. Call connect() first.')
}

View File

@@ -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<string, unknown>[]
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<string, unknown>): Promise<SqlServerQueryResult> {
async query(sqlString: string, params?: any[]): Promise<QueryResult> {
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<string, { value: unknown; type?: sql.ISqlType }>
): Promise<SqlServerQueryResult> {
): Promise<QueryResult> {
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<string, unknown> }[]): Promise<void> {
async transaction(queries: { sql: string; params?: any[] }[]): Promise<void> {
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)

View File

@@ -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<OrderMapping[]> {
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<string, string>()
@@ -281,19 +287,26 @@ export class OrderNumberResolver {
* @returns List of valid order numbers
*/
private async verifyOrderNumbers(orderNumbers: string[]): Promise<string[]> {
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)

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
}
}