feat: implement IPC handlers, database services and Extractor UI
- Add SqlServerService and MySqlService for database persistence - Implement IPC handlers for file, extractor, cleaner, and database operations - Define IPC API types and update preload script - Create ExtractorPage UI with OrderNumberInput component - Add unit and integration tests for MySQL and SQL Server - Update vitest config with path aliases
This commit is contained in:
123
src/main/services/database/mysql.ts
Normal file
123
src/main/services/database/mysql.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import mysql from 'mysql2/promise'
|
||||
|
||||
export interface MySqlConfig {
|
||||
host: string
|
||||
port: number
|
||||
user: string
|
||||
password: string
|
||||
database: string
|
||||
}
|
||||
|
||||
export interface MySqlQueryResult {
|
||||
rows: Record<string, unknown>[]
|
||||
columns: string[]
|
||||
rowCount: number
|
||||
}
|
||||
|
||||
export class MySqlService {
|
||||
private connection: mysql.Connection | null = null
|
||||
private config: MySqlConfig
|
||||
|
||||
constructor(config: MySqlConfig) {
|
||||
this.config = config
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to MySQL database
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
if (this.connection) {
|
||||
throw new Error('Already connected to MySQL')
|
||||
}
|
||||
|
||||
try {
|
||||
this.connection = await mysql.createConnection({
|
||||
host: this.config.host,
|
||||
port: this.config.port,
|
||||
user: this.config.user,
|
||||
password: this.config.password,
|
||||
database: this.config.database
|
||||
})
|
||||
|
||||
// Test connection
|
||||
await this.connection.ping()
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to connect to MySQL: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from MySQL database
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
if (!this.connection) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await this.connection.end()
|
||||
this.connection = null
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to disconnect from MySQL: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if connected to MySQL database
|
||||
*/
|
||||
isConnected(): boolean {
|
||||
return this.connection !== null
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a query and return results
|
||||
*/
|
||||
async query(sql: string, params?: any[]): Promise<MySqlQueryResult> {
|
||||
if (!this.connection) {
|
||||
throw new Error('Not connected to MySQL. Call connect() first.')
|
||||
}
|
||||
|
||||
try {
|
||||
const [rows, fields] = await this.connection.execute(sql, params)
|
||||
|
||||
// Convert rows to plain objects and extract column names
|
||||
const columns = fields.map((field) => field.name)
|
||||
const rowCount = Array.isArray(rows) ? rows.length : 0
|
||||
|
||||
// Type assertion for rows - mysql2 returns different types based on query
|
||||
const typedRows = Array.isArray(rows) ? (rows as Record<string, unknown>[]) : []
|
||||
|
||||
return {
|
||||
rows: typedRows,
|
||||
columns,
|
||||
rowCount
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`MySQL query failed: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute multiple queries in a transaction
|
||||
*/
|
||||
async transaction(queries: { sql: string; params?: any[] }[]): Promise<void> {
|
||||
if (!this.connection) {
|
||||
throw new Error('Not connected to MySQL. Call connect() first.')
|
||||
}
|
||||
|
||||
try {
|
||||
await this.connection.beginTransaction()
|
||||
|
||||
for (const { sql, params } of queries) {
|
||||
await this.connection.execute(sql, params)
|
||||
}
|
||||
|
||||
await this.connection.commit()
|
||||
} catch (error) {
|
||||
if (this.connection) {
|
||||
await this.connection.rollback()
|
||||
}
|
||||
throw new Error(`MySQL transaction failed: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
184
src/main/services/database/sql-server.ts
Normal file
184
src/main/services/database/sql-server.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import sql from 'mssql'
|
||||
|
||||
export interface SqlServerConfig {
|
||||
server: string
|
||||
port: number
|
||||
user: string
|
||||
password: string
|
||||
database: string
|
||||
options?: {
|
||||
encrypt?: boolean
|
||||
trustServerCertificate?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface SqlServerQueryResult {
|
||||
rows: Record<string, unknown>[]
|
||||
columns: string[]
|
||||
rowCount: number
|
||||
}
|
||||
|
||||
export class SqlServerService {
|
||||
private pool: sql.ConnectionPool | null = null
|
||||
private config: SqlServerConfig
|
||||
|
||||
constructor(config: SqlServerConfig) {
|
||||
this.config = config
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to SQL Server database
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
if (this.pool) {
|
||||
throw new Error('Already connected to SQL Server')
|
||||
}
|
||||
|
||||
try {
|
||||
const poolConfig: sql.config = {
|
||||
server: this.config.server,
|
||||
port: this.config.port,
|
||||
user: this.config.user,
|
||||
password: this.config.password,
|
||||
database: this.config.database,
|
||||
options: {
|
||||
encrypt: this.config.options?.encrypt ?? true,
|
||||
trustServerCertificate: this.config.options?.trustServerCertificate ?? false
|
||||
}
|
||||
}
|
||||
|
||||
this.pool = new sql.ConnectionPool(poolConfig)
|
||||
await this.pool.connect()
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to connect to SQL Server: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from SQL Server database
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
if (!this.pool) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await this.pool.close()
|
||||
this.pool = null
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to disconnect from SQL Server: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if connected to SQL Server database
|
||||
*/
|
||||
isConnected(): boolean {
|
||||
return this.pool !== null && this.pool.connected
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a query and return results
|
||||
*/
|
||||
async query(sqlString: string, params?: Record<string, unknown>): Promise<SqlServerQueryResult> {
|
||||
if (!this.pool) {
|
||||
throw new Error('Not connected to SQL Server. Call connect() first.')
|
||||
}
|
||||
|
||||
try {
|
||||
const request = this.pool.request()
|
||||
|
||||
// Add parameters if provided
|
||||
if (params) {
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
request.input(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
const result = await request.query(sqlString)
|
||||
|
||||
// Convert recordset to array of objects
|
||||
const columns = result.recordset.columns?.map((col) => col.name) || []
|
||||
const rows = result.recordset as Record<string, unknown>[]
|
||||
|
||||
return {
|
||||
rows,
|
||||
columns,
|
||||
rowCount: result.rowsAffected?.[0] || rows.length
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`SQL Server query failed: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a prepared statement with parameters
|
||||
*/
|
||||
async queryWithParams(
|
||||
sqlString: string,
|
||||
params: Record<string, { value: unknown; type?: sql.ISqlType }>
|
||||
): Promise<SqlServerQueryResult> {
|
||||
if (!this.pool) {
|
||||
throw new Error('Not connected to SQL Server. Call connect() first.')
|
||||
}
|
||||
|
||||
try {
|
||||
const request = this.pool.request()
|
||||
|
||||
// Add parameters with explicit types
|
||||
for (const [key, { value, type }] of Object.entries(params)) {
|
||||
if (type) {
|
||||
request.input(key, type, value)
|
||||
} else {
|
||||
request.input(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
const result = await request.query(sqlString)
|
||||
|
||||
// Convert recordset to array of objects
|
||||
const columns = result.recordset.columns?.map((col) => col.name) || []
|
||||
const rows = result.recordset as Record<string, unknown>[]
|
||||
|
||||
return {
|
||||
rows,
|
||||
columns,
|
||||
rowCount: result.rowsAffected?.[0] || rows.length
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`SQL Server query failed: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute multiple queries in a transaction
|
||||
*/
|
||||
async transaction(queries: { sql: string; params?: Record<string, unknown> }[]): Promise<void> {
|
||||
if (!this.pool) {
|
||||
throw new Error('Not connected to SQL Server. Call connect() first.')
|
||||
}
|
||||
|
||||
const transaction = new sql.Transaction(this.pool)
|
||||
|
||||
try {
|
||||
await transaction.begin()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
await request.query(sqlString)
|
||||
}
|
||||
|
||||
await transaction.commit()
|
||||
} catch (error) {
|
||||
await transaction.rollback()
|
||||
throw new Error(`SQL Server transaction failed: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user