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:
Misaka
2026-03-01 15:46:01 +08:00
parent c39e1504aa
commit 5760b56f70
23 changed files with 2156 additions and 33 deletions

View File

@@ -0,0 +1,49 @@
import { ipcMain } from 'electron'
import { ErpAuthService } from '../services/erp/erp-auth'
import { CleanerService } from '../services/erp/cleaner'
import type { CleanerInput, CleanerResult } from '../types/cleaner.types'
/**
* Register IPC handlers for cleaner service
*/
export function registerCleanerHandlers(): void {
ipcMain.handle(
'cleaner:run',
async (
_event,
input: CleanerInput
): Promise<{ success: boolean; data?: CleanerResult; error?: string }> => {
let authService: ErpAuthService | null = null
try {
// Create auth service and login
authService = new ErpAuthService({
url: process.env.ERP_URL || '',
username: process.env.ERP_USERNAME || '',
password: process.env.ERP_PASSWORD || '',
headless: true
})
await authService.login()
// Create cleaner service and run cleaning
const cleaner = new CleanerService(authService)
const result = await cleaner.clean(input)
return { success: true, data: result }
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
return { success: false, error: message }
} finally {
// Clean up: close browser
if (authService) {
try {
await authService.close()
} catch {
// Ignore cleanup errors
}
}
}
}
)
}

View File

@@ -0,0 +1,181 @@
import { ipcMain } from 'electron'
import { MySqlService } from '../services/database/mysql'
import { SqlServerService } from '../services/database/sql-server'
import type {
MySqlConfig,
MySqlQueryResult,
SqlServerConfig,
SqlServerQueryResult
} from '../types/ipc-api.types'
// Store MySQL service instances per window/connection
const mysqlServices = new Map<string, MySqlService>()
// Store SQL Server service instances per window/connection
const sqlServerServices = new Map<string, SqlServerService>()
/**
* Get or create MySQL service for a connection ID
*/
function getMySqlService(connectionId: string): MySqlService | undefined {
return mysqlServices.get(connectionId)
}
/**
* Set MySQL service for a connection ID
*/
function setMySqlService(connectionId: string, service: MySqlService): void {
mysqlServices.set(connectionId, service)
}
/**
* Delete MySQL service for a connection ID
*/
function deleteMySqlService(connectionId: string): void {
mysqlServices.delete(connectionId)
}
/**
* Get or create SQL Server service for a connection ID
*/
function getSqlServerService(connectionId: string): SqlServerService | undefined {
return sqlServerServices.get(connectionId)
}
/**
* Set SQL Server service for a connection ID
*/
function setSqlServerService(connectionId: string, service: SqlServerService): void {
sqlServerServices.set(connectionId, service)
}
/**
* Delete SQL Server service for a connection ID
*/
function deleteSqlServerService(connectionId: string): void {
sqlServerServices.delete(connectionId)
}
/**
* Register IPC handlers for database operations
*/
export function registerDatabaseHandlers(): void {
// Connect to MySQL
ipcMain.handle('database:mysql:connect', async (event, config: MySqlConfig): Promise<void> => {
try {
// Use window ID as connection identifier
const windowId = (event.sender as any).id.toString()
const service = new MySqlService(config)
await service.connect()
setMySqlService(windowId, service)
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to connect to MySQL'
throw new Error(message)
}
})
// Disconnect from MySQL
ipcMain.handle('database:mysql:disconnect', async (event): Promise<void> => {
try {
const windowId = (event.sender as any).id.toString()
const service = getMySqlService(windowId)
if (service) {
await service.disconnect()
deleteMySqlService(windowId)
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to disconnect from MySQL'
throw new Error(message)
}
})
// Check if MySQL is connected
ipcMain.handle('database:mysql:isConnected', async (event): Promise<boolean> => {
const windowId = (event.sender as any).id.toString()
const service = getMySqlService(windowId)
return service ? service.isConnected() : false
})
// Execute MySQL query
ipcMain.handle(
'database:mysql:query',
async (event, sql: string, params?: any[]): Promise<MySqlQueryResult> => {
try {
const windowId = (event.sender as any).id.toString()
const service = getMySqlService(windowId)
if (!service) {
throw new Error('Not connected to MySQL. Call connect() first.')
}
return await service.query(sql, params)
} catch (error) {
const message = error instanceof Error ? error.message : 'MySQL query failed'
throw new Error(message)
}
}
)
// Connect to SQL Server
ipcMain.handle(
'database:sqlserver:connect',
async (event, config: SqlServerConfig): Promise<void> => {
try {
const windowId = (event.sender as any).id.toString()
const service = new SqlServerService(config)
await service.connect()
setSqlServerService(windowId, service)
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to connect to SQL Server'
throw new Error(message)
}
}
)
// Disconnect from SQL Server
ipcMain.handle('database:sqlserver:disconnect', async (event): Promise<void> => {
try {
const windowId = (event.sender as any).id.toString()
const service = getSqlServerService(windowId)
if (service) {
await service.disconnect()
deleteSqlServerService(windowId)
}
} catch (error) {
const message =
error instanceof Error ? error.message : 'Failed to disconnect from SQL Server'
throw new Error(message)
}
})
// Check if SQL Server is connected
ipcMain.handle('database:sqlserver:isConnected', async (event): Promise<boolean> => {
const windowId = (event.sender as any).id.toString()
const service = getSqlServerService(windowId)
return service ? service.isConnected() : false
})
// Execute SQL Server query
ipcMain.handle(
'database:sqlserver:query',
async (
event,
sqlString: string,
params?: Record<string, unknown>
): Promise<SqlServerQueryResult> => {
try {
const windowId = (event.sender as any).id.toString()
const service = getSqlServerService(windowId)
if (!service) {
throw new Error('Not connected to SQL Server. Call connect() first.')
}
return await service.query(sqlString, params)
} catch (error) {
const message = error instanceof Error ? error.message : 'SQL Server query failed'
throw new Error(message)
}
}
)
}

View File

@@ -0,0 +1,49 @@
import { ipcMain } from 'electron'
import { ErpAuthService } from '../services/erp/erp-auth'
import { ExtractorService } from '../services/erp/extractor'
import type { ExtractorInput, ExtractorResult } from '../types/extractor.types'
/**
* Register IPC handlers for extractor service
*/
export function registerExtractorHandlers(): void {
ipcMain.handle(
'extractor:run',
async (
_event,
input: ExtractorInput
): Promise<{ success: boolean; data?: ExtractorResult; error?: string }> => {
let authService: ErpAuthService | null = null
try {
// Create auth service and login
authService = new ErpAuthService({
url: process.env.ERP_URL || '',
username: process.env.ERP_USERNAME || '',
password: process.env.ERP_PASSWORD || '',
headless: true
})
await authService.login()
// Create extractor service and run extraction
const extractor = new ExtractorService(authService)
const result = await extractor.extract(input)
return { success: true, data: result }
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
return { success: false, error: message }
} finally {
// Clean up: close browser
if (authService) {
try {
await authService.close()
} catch {
// Ignore cleanup errors
}
}
}
}
)
}

View File

@@ -0,0 +1,55 @@
import { ipcMain } from 'electron'
import * as fs from 'fs/promises'
import * as path from 'path'
/**
* Register IPC handlers for file operations
*/
export function registerFileHandlers(): void {
// Read file content
ipcMain.handle('file:read', async (_event, filePath: string): Promise<string> => {
try {
return await fs.readFile(filePath, 'utf-8')
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to read file'
throw new Error(message)
}
})
// Write content to file
ipcMain.handle('file:write', async (_event, filePath: string, content: string): Promise<void> => {
try {
// Ensure directory exists
const dir = path.dirname(filePath)
await fs.mkdir(dir, { recursive: true })
await fs.writeFile(filePath, content, 'utf-8')
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to write file'
throw new Error(message)
}
})
// Check if file exists
ipcMain.handle('file:exists', async (_event, filePath: string): Promise<boolean> => {
try {
await fs.access(filePath)
return true
} catch {
return false
}
})
// List files in directory
ipcMain.handle('file:list', async (_event, dirPath: string): Promise<string[]> => {
try {
const entries = await fs.readdir(dirPath, { withFileTypes: true })
return entries
.filter((entry) => entry.isFile())
.map((entry) => entry.name)
.sort()
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to list directory'
throw new Error(message)
}
})
}

19
src/main/ipc/index.ts Normal file
View File

@@ -0,0 +1,19 @@
/**
* IPC Handler registration
* Centralized registration for all IPC handlers
*/
import { registerFileHandlers } from './file-handler'
import { registerExtractorHandlers } from './extractor-handler'
import { registerCleanerHandlers } from './cleaner-handler'
import { registerDatabaseHandlers } from './database-handler'
/**
* Register all IPC handlers
*/
export function registerIpcHandlers(): void {
registerFileHandlers()
registerExtractorHandlers()
registerCleanerHandlers()
registerDatabaseHandlers()
}