From a05c8a9037b826ab8efe34b7457fb79aea780182 Mon Sep 17 00:00:00 2001 From: Misaka Date: Mon, 2 Mar 2026 23:10:15 +0800 Subject: [PATCH] feat: add TypeORM, logger, schemas, hooks and stores - Add TypeORM integration with data-source, entities and repositories - Add logger service for structured logging - Add Zod validation schemas for auth, cleaner and extractor - Add custom React hooks (useAuth, useCleaner, useExtractor, useValidation) - Add Zustand stores (useAppStore, useUserStore) - Add UI components (Button, Modal, Toast) - Add error types and ErpBrowserManager - Refactor IPC handlers and services - Add unit tests for new modules Co-Authored-By: Claude (glm-5) --- CLAUDE.md | 4 + docs/cleaner-validation-flow.md | 98 ++- docs/optimization-execution-plan.md | 93 +++ ...97718a299488a3d6c7b9753a8bc7cf9-audit.json | 15 + package-lock.json | 684 ++++++++++++++++-- package.json | 8 +- src/main/ipc/auth-handler.ts | 161 +++-- src/main/ipc/cleaner-handler.ts | 232 +++--- src/main/ipc/database-handler.ts | 82 ++- src/main/ipc/extractor-handler.ts | 231 +++--- src/main/ipc/file-handler.ts | 9 + src/main/ipc/index.ts | 50 ++ src/main/ipc/resolver-handler.ts | 27 +- src/main/ipc/settings-handler.ts | 284 ++++---- src/main/ipc/validation-handler.ts | 228 +++--- src/main/schemas/auth.schema.ts | 45 ++ src/main/schemas/cleaner.schema.ts | 47 ++ src/main/schemas/extractor.schema.ts | 46 ++ src/main/services/config/config-manager.ts | 175 +++-- src/main/services/database/data-source.ts | 97 +++ .../database/discrete-material-plan-dao.ts | 94 ++- .../database/entities/DiscreteMaterialPlan.ts | 143 ++++ .../database/entities/MaterialsToBeDeleted.ts | 27 + .../database/materials-to-be-deleted-dao.ts | 135 ++-- .../DiscreteMaterialPlanRepository.ts | 281 +++++++ .../MaterialsToBeDeletedRepository.ts | 266 +++++++ src/main/services/erp/ErpBrowserManager.ts | 208 ++++++ src/main/services/erp/erp-auth.ts | 13 +- src/main/services/erp/order-resolver.ts | 14 +- src/main/services/logger/index.ts | 97 +++ src/main/services/user/bip-users-dao.ts | 9 +- src/main/types/errors.ts | 169 +++++ src/main/types/settings.types.ts | 6 +- src/preload/index.d.ts | 7 +- src/preload/index.ts | 9 +- src/renderer/src/App.tsx | 83 ++- src/renderer/src/assets/main.css | 2 +- src/renderer/src/components/LoginDialog.tsx | 4 +- .../src/components/UserSelectionDialog.tsx | 11 +- src/renderer/src/components/ui/Button.tsx | 80 ++ src/renderer/src/components/ui/Modal.tsx | 92 +++ src/renderer/src/components/ui/Toast.tsx | 90 +++ src/renderer/src/hooks/useAuth.ts | 211 ++++++ src/renderer/src/hooks/useCleaner.ts | 74 ++ src/renderer/src/hooks/useExtractor.ts | 73 ++ src/renderer/src/hooks/useValidation.ts | 152 ++++ src/renderer/src/pages/CleanerPage.tsx | 183 +++-- src/renderer/src/pages/ExtractorPage.tsx | 87 ++- src/renderer/src/pages/SettingsPage.tsx | 22 +- src/renderer/src/stores/useAppStore.ts | 115 +++ src/renderer/src/stores/useUserStore.ts | 67 ++ tests/e2e/auth-flow.test.ts | 93 +++ tests/unit/errors.test.ts | 208 ++++++ tests/unit/logger.test.ts | 78 ++ tests/unit/repositories.test.ts | 67 ++ tests/unit/schemas.test.ts | 227 ++++++ tsconfig.node.json | 11 +- 57 files changed, 5150 insertions(+), 974 deletions(-) create mode 100644 docs/optimization-execution-plan.md create mode 100644 logs/.869a8c37397718a299488a3d6c7b9753a8bc7cf9-audit.json create mode 100644 src/main/schemas/auth.schema.ts create mode 100644 src/main/schemas/cleaner.schema.ts create mode 100644 src/main/schemas/extractor.schema.ts create mode 100644 src/main/services/database/data-source.ts create mode 100644 src/main/services/database/entities/DiscreteMaterialPlan.ts create mode 100644 src/main/services/database/entities/MaterialsToBeDeleted.ts create mode 100644 src/main/services/database/repositories/DiscreteMaterialPlanRepository.ts create mode 100644 src/main/services/database/repositories/MaterialsToBeDeletedRepository.ts create mode 100644 src/main/services/erp/ErpBrowserManager.ts create mode 100644 src/main/services/logger/index.ts create mode 100644 src/main/types/errors.ts create mode 100644 src/renderer/src/components/ui/Button.tsx create mode 100644 src/renderer/src/components/ui/Modal.tsx create mode 100644 src/renderer/src/components/ui/Toast.tsx create mode 100644 src/renderer/src/hooks/useAuth.ts create mode 100644 src/renderer/src/hooks/useCleaner.ts create mode 100644 src/renderer/src/hooks/useExtractor.ts create mode 100644 src/renderer/src/hooks/useValidation.ts create mode 100644 src/renderer/src/stores/useAppStore.ts create mode 100644 src/renderer/src/stores/useUserStore.ts create mode 100644 tests/e2e/auth-flow.test.ts create mode 100644 tests/unit/errors.test.ts create mode 100644 tests/unit/logger.test.ts create mode 100644 tests/unit/repositories.test.ts create mode 100644 tests/unit/schemas.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index 1a19eec..c4b4e12 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,6 +5,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Development Commands ### Running the Application + ```bash npm run dev # Start development server with hot reload npm run build # Full build with type checking @@ -14,6 +15,7 @@ npm run build:linux # Build Linux AppImage ``` ### Code Quality + ```bash npm run lint # ESLint check npm run format # Prettier format @@ -23,6 +25,7 @@ npm run typecheck:web # TypeScript check for renderer only ``` ### Testing + ```bash npm run test # Run unit tests (Vitest) npm run test:coverage # Run tests with coverage report @@ -80,6 +83,7 @@ The main process is organized around domain-specific services in `src/main/servi ### IPC Handler Pattern All IPC communication follows a consistent pattern: + - Handlers are in `src/main/ipc/`, organized by domain (8 modules) - Each handler module exports a `register*Handlers()` function - All handlers are registered in `src/main/ipc/index.ts` diff --git a/docs/cleaner-validation-flow.md b/docs/cleaner-validation-flow.md index 661ac9a..6b2f0a2 100644 --- a/docs/cleaner-validation-flow.md +++ b/docs/cleaner-validation-flow.md @@ -108,19 +108,13 @@ const handleValidation = async () => { setValidationResults(response.results) // 自动勾选已标记物料 const markedCodes = new Set( - response.results - .filter(r => r.isMarkedForDeletion) - .map(r => r.materialCode) + response.results.filter((r) => r.isMarkedForDeletion).map((r) => r.materialCode) ) setSelectedItems(markedCodes) // 管理员更新负责人列表 if (isAdmin) { - const uniqueManagers = new Set( - response.results - .map(r => r.managerName) - .filter(Boolean) - ) + const uniqueManagers = new Set(response.results.map((r) => r.managerName).filter(Boolean)) setManagers([...uniqueManagers]) setSelectedManagers(uniqueManagers) } @@ -350,10 +344,7 @@ for (const record of materialRecords) { // 优先级2: 匹配 MaterialsTypeToBeDeleted (MaterialName 包含匹配) if (!managerName) { for (const typeKeyword of typeKeywords) { - if ( - typeKeyword.materialName && - typeKeyword.materialName.includes(materialName) - ) { + if (typeKeyword.materialName && typeKeyword.materialName.includes(materialName)) { matchedTypeKeyword = typeKeyword.materialName managerName = typeKeyword.managerName break @@ -419,7 +410,7 @@ graph TB interface ValidationRequest { mode: 'database_full' | 'database_filtered' useSharedProductionIds?: boolean - productionIdFile?: string // 可选,文件路径 + productionIdFile?: string // 可选,文件路径 } ``` @@ -431,8 +422,8 @@ interface ValidationResponse { results?: ValidationResult[] stats?: { totalRecords: number - matchedCount: number // 有负责人(包括类型匹配) - markedCount: number // 已标记删除 + matchedCount: number // 有负责人(包括类型匹配) + markedCount: number // 已标记删除 } error?: string } @@ -446,9 +437,9 @@ interface ValidationResult { materialCode: string specification: string model: string - managerName: string // 负责人名称 + managerName: string // 负责人名称 isMarkedForDeletion: boolean // 是否精确匹配MaterialsToBeDeleted - matchedTypeKeyword?: string // 如果匹配了类型关键词,记录匹配项 + matchedTypeKeyword?: string // 如果匹配了类型关键词,记录匹配项 } ``` @@ -644,7 +635,8 @@ const handleConfirmDeletion = async () => { // 5. 用户确认 const confirmParts: string[] = [] - if (materialsToUpsert.length > 0) confirmParts.push(`写入/更新 ${materialsToUpsert.length} 条记录`) + if (materialsToUpsert.length > 0) + confirmParts.push(`写入/更新 ${materialsToUpsert.length} 条记录`) if (materialsToDelete.length > 0) confirmParts.push(`删除 ${materialsToDelete.length} 条记录`) if (!window.confirm(`确认以下操作吗?\n\n${confirmParts.join('\n')}`)) return @@ -686,14 +678,14 @@ const handleConfirmDeletion = async () => { **数据分类逻辑**: -| 物料状态 | 勾选状态 | 负责人信息 | 处理方式 | -|---------|---------|-----------|---------| -| 已标记删除 | ✅ 勾选 | ✅ 有 | 保存/更新到数据库 | -| 已标记删除 | ✅ 勾选 | ❌ 无 | 拒绝操作,弹出警告 | -| 已标记删除 | ❌ 未勾选 | - | 从数据库删除 | -| 未标记删除 | ✅ 勾选 | ✅ 有 | 保存/更新到数据库 | -| 未标记删除 | ✅ 勾选 | ❌ 无 | 拒绝操作,弹出警告 | -| 未标记删除 | ❌ 未勾选 | - | 从数据库删除 | +| 物料状态 | 勾选状态 | 负责人信息 | 处理方式 | +| ---------- | --------- | ---------- | ------------------ | +| 已标记删除 | ✅ 勾选 | ✅ 有 | 保存/更新到数据库 | +| 已标记删除 | ✅ 勾选 | ❌ 无 | 拒绝操作,弹出警告 | +| 已标记删除 | ❌ 未勾选 | - | 从数据库删除 | +| 未标记删除 | ✅ 勾选 | ✅ 有 | 保存/更新到数据库 | +| 未标记删除 | ✅ 勾选 | ❌ 无 | 拒绝操作,弹出警告 | +| 未标记删除 | ❌ 未勾选 | - | 从数据库删除 | --- @@ -976,12 +968,14 @@ for (let i = 0; i < materialCodes.length; i += batchSize) { **SQL示例**: **MySQL**: + ```sql DELETE FROM dbo_MaterialsToDeleted WHERE MaterialCode IN (?, ?, ?, ..., ?) -- 最多1000个占位符 ``` **SQL Server**: + ```sql DELETE FROM [dbo].[MaterialsToBeDeleted] WHERE MaterialCode IN (@p0, @p1, @p2, ..., @p999) -- 最多1000个参数 @@ -1116,8 +1110,8 @@ graph TB ```typescript interface MaterialUpsertBatchRequest { materials: { - materialCode: string // 物料代码 - managerName: string // 负责人名称 + materialCode: string // 物料代码 + managerName: string // 负责人名称 }[] } ``` @@ -1126,7 +1120,7 @@ interface MaterialUpsertBatchRequest { ```typescript interface MaterialDeleteRequest { - materialCodes: string[] // 要删除的物料代码数组 + materialCodes: string[] // 要删除的物料代码数组 } ``` @@ -1135,8 +1129,8 @@ interface MaterialDeleteRequest { ```typescript interface MaterialOperationResponse { success: boolean - stats?: UpsertStats // upsert操作返回 - count?: number // delete操作返回 + stats?: UpsertStats // upsert操作返回 + count?: number // delete操作返回 error?: string } ``` @@ -1145,9 +1139,9 @@ interface MaterialOperationResponse { ```typescript interface UpsertStats { - total: number // 总处理数量 - success: number // 成功数量 - failed: number // 失败数量 + total: number // 总处理数量 + success: number // 成功数量 + failed: number // 失败数量 } ``` @@ -1210,29 +1204,29 @@ flowchart TB ### 8. 与"获取校验状态"流程的对比 -| 对比维度 | 获取校验状态 | 确认删除(同步数据库) | -|---------|------------|-------------------| -| **操作方向** | 数据库 → 前端 (读取) | 前端 → 数据库 (写入) | -| **主要操作** | SELECT 查询 | MERGE/INSERT + DELETE | -| **数据量** | 可能很大(全表查询) | 取决于用户勾选数量 | -| **事务性** | 只读,无需事务 | 写操作,逐条处理 | -| **用户交互** | 单次点击 | 点击 → 确认对话框 → 执行 | -| **错误处理** | 整体失败或成功 | 部分失败继续处理 | -| **结果反馈** | 校验结果列表 | 操作统计(成功/删除条数) | -| **副作用** | 无 | 修改数据库内容 | -| **权限要求** | 读取权限 | 写入+删除权限 | +| 对比维度 | 获取校验状态 | 确认删除(同步数据库) | +| ------------ | -------------------- | ------------------------ | +| **操作方向** | 数据库 → 前端 (读取) | 前端 → 数据库 (写入) | +| **主要操作** | SELECT 查询 | MERGE/INSERT + DELETE | +| **数据量** | 可能很大(全表查询) | 取决于用户勾选数量 | +| **事务性** | 只读,无需事务 | 写操作,逐条处理 | +| **用户交互** | 单次点击 | 点击 → 确认对话框 → 执行 | +| **错误处理** | 整体失败或成功 | 部分失败继续处理 | +| **结果反馈** | 校验结果列表 | 操作统计(成功/删除条数) | +| **副作用** | 无 | 修改数据库内容 | +| **权限要求** | 读取权限 | 写入+删除权限 | --- ## 文件索引 -| 文件路径 | 说明 | 关键行号 | -|---------|------|---------| -| `src/renderer/src/pages/CleanerPage.tsx` | 前端清理页面 | 117-155 (handleValidation)
166-226 (handleConfirmDeletion) | -| `src/main/ipc/validation-handler.ts` | IPC处理器 | 209-392 (validation:validate)
399-422 (materials:upsertBatch)
427-449 (materials:delete) | -| `src/main/services/database/discrete-material-plan-dao.ts` | 物料计划DAO | 191-227 (queryAllDistinctByMaterialCode) | -| `src/main/services/database/discrete-material-plan-dao.ts` | 物料计划DAO | 294-377 (queryBySourceNumbersDistinct) | -| `src/main/services/database/materials-to-be-deleted-dao.ts` | 待删除物料DAO | 180-240 (upsertBatch)
248-268 (getAllMaterialCodes)
539-586 (deleteByMaterialCodes) | +| 文件路径 | 说明 | 关键行号 | +| ----------------------------------------------------------- | ------------- | ---------------------------------------------------------------------------------------------- | +| `src/renderer/src/pages/CleanerPage.tsx` | 前端清理页面 | 117-155 (handleValidation)
166-226 (handleConfirmDeletion) | +| `src/main/ipc/validation-handler.ts` | IPC处理器 | 209-392 (validation:validate)
399-422 (materials:upsertBatch)
427-449 (materials:delete) | +| `src/main/services/database/discrete-material-plan-dao.ts` | 物料计划DAO | 191-227 (queryAllDistinctByMaterialCode) | +| `src/main/services/database/discrete-material-plan-dao.ts` | 物料计划DAO | 294-377 (queryBySourceNumbersDistinct) | +| `src/main/services/database/materials-to-be-deleted-dao.ts` | 待删除物料DAO | 180-240 (upsertBatch)
248-268 (getAllMaterialCodes)
539-586 (deleteByMaterialCodes) | --- diff --git a/docs/optimization-execution-plan.md b/docs/optimization-execution-plan.md new file mode 100644 index 0000000..27652ee --- /dev/null +++ b/docs/optimization-execution-plan.md @@ -0,0 +1,93 @@ +# ERPAuto 优化执行计划文档 + +基于《ERPAuto 优化建议与规范指南》,本文档规划了具体的分阶段重构与优化执行步骤。每个阶段遵循“渐进式重构”原则,保证在优化期间项目依然可运行、可测试。 + +## 阶段一:基础设施建设 (Error & Logging) + +在进行大规模业务逻辑重构前,首先建立坚实的基础设施,以便后续问题排查与数据追踪。 + +1. **引入并配置统一日志库** + - **目标**: 替换分散的 `console.log`。 + - **执行**: + - 安装 `winston` (针对 Node.js 主进程)。 + - 在 `src/main/services/logger` 创建单例日志记录器。 + - 配置双通道输出:Console (Dev 环境) 与 File (生产环境按天切割,如 `%AppData%/ERPAuto/logs/app-%DATE%.log`)。 +2. **定义全局错误类型与 IPC 拦截器** + - **目标**: 规范前后端错误抛出与展示体系。 + - **执行**: + - 在 `src/main/types/errors.ts` 定义 `BaseError`, `ErpConnectionError`, `DatabaseQueryError`。 + - 在 `src/main/ipc/index.ts` 中封装高阶函数 `withErrorHandling`。所有 IPC Handler 统一用此高阶函数包裹,将捕获的错误统一转为 `{ success: false, error: string, code: string }` 结构。 + +## 阶段二:数据层抽象与 ORM 改造 + +彻底解决 SQL 语句散落和不同数据库适配成本高的问题。 + +1. **选型并引入 ORM** + - **目标**: 弃用原生 SQL 拼接。 + - **执行**: + - 引入 `Prisma` 或 `TypeORM`。结合当前多数据源 (MySQL + SQL Server) 需求,推荐 `TypeORM` 因为其在运行时切换数据源更为灵活。 +2. **创建 Repository 抽象** + - **目标**: 隔离数据库实现细节。 + - **执行**: + - 建立 `src/main/services/database/repositories` 目录。 + - 为业务实体 (如 Users, ExtractedPlans 等) 编写 Repository 类接口。 + - 将原有 `mysql2` 和 `mssql` 的调用逐步迁移至 Repository 中。 +3. **Zod 运行时校验** + - **目标**: 保护 IPC 边界免受恶意/格式错误的 payload 影响。 + - **执行**: + - 安装 `zod`。 + - 对所有的 IPC Handler 的入参(如 `ExtractorInput`, `LoginRequest`)添加 `zod` Schema 校验。 + +## 阶段三:React 渲染层规范化 + +提高前端代码复用率,解耦视图与逻辑。 + +1. **提取 IPC Hooks** + - **目标**: 清理组件中的大段异步调用。 + - **执行**: + - 在 `src/renderer/src/hooks` 创建 `useExtractor.ts`, `useCleaner.ts`。 + - 使用 React 的 `useState` 包装 `window.api` 调用,返回 `{ loading, data, error, execute }`。 +2. **状态管理引入 (Zustand)** + - **目标**: 解决跨组件状态共享 (如全局报错信息、用户认证状态)。 + - **执行**: + - 安装 `zustand`。 + - 创建 `useUserStore` 和 `useAppStore`。 +3. **UI 组件库/公共样式提取** + - **目标**: 统一 Tailwind 设计语言。 + - **执行**: + - 将高频使用的 Button, Input, Modal 抽取到 `src/renderer/src/components/ui/`。 + +## 阶段四:自动化服务解耦 (Domain Logic) + +将基于 Playwright 的具体执行细节与业务调度逻辑分离。 + +1. **重构 ERP 自动化服务 (`cleaner.ts` / `extractor.ts`)** + - **目标**: 遵循单一职责原则。 + - **执行**: + - 抽象出 `ErpBrowserManager` (负责浏览器启动与资源回收)。 + - 抽象出 `ErpAuthService` (专职处理登录和 Session)。 + - `extractor.ts` 将只负责调度:调用 Browser -> Auth -> Navigate -> Download -> Excel Parse。 +2. **加强 TypeScript 严格模式** + - **目标**: 提升代码健壮性。 + - **执行**: + - 开启 `tsconfig.json` 中的 `"strict": true` 和 `"noImplicitAny": true`。 + - 全局清理并替换现存的 `any` 为具体的 Type 或 `unknown` 并添加类型保护。 + +## 阶段五:测试覆盖率补充 + +确保核心流程不被破坏。 + +1. **补充关键服务的单元测试** + - **目标**: 防止复杂转换逻辑衰退。 + - **执行**: + - 使用 `Vitest` 测试所有的 Repository (使用内存数据库/Mock) 和工具函数 (如 ExcelParser)。 +2. **核心业务 E2E 测试** + - **目标**: 确保 IPC 及 Electron 整体运行顺畅。 + - **执行**: + - 使用 Playwright 针对 Electron 的测试框架 (`@playwright/test` 的 electron 插件) 编写主流程测试:登录 -> 点击提取 -> 验证本地结果文件生成。 + +## 执行建议与回顾 + +- 每个阶段应作为一个单独的 Git 分支 (Feature Branch) 开发。 +- 完成一个阶段后,必须全量运行既有的测试套件并通过 `npm run typecheck`。 +- 本文档可作为每次 PR Review 的检查清单使用。 diff --git a/logs/.869a8c37397718a299488a3d6c7b9753a8bc7cf9-audit.json b/logs/.869a8c37397718a299488a3d6c7b9753a8bc7cf9-audit.json new file mode 100644 index 0000000..f11d31e --- /dev/null +++ b/logs/.869a8c37397718a299488a3d6c7b9753a8bc7cf9-audit.json @@ -0,0 +1,15 @@ +{ + "keep": { + "days": true, + "amount": 14 + }, + "auditLog": "D:\\FileLib\\Projects\\CodeMigration\\ERPAuto\\logs\\.869a8c37397718a299488a3d6c7b9753a8bc7cf9-audit.json", + "files": [ + { + "date": 1772462852547, + "name": "D:\\FileLib\\Projects\\CodeMigration\\ERPAuto\\logs\\app-2026-03-02.log", + "hash": "baa4ab4c2dfd6ec62a003e44496d2f428a4ad4037ce2529a8da14da1b91ef7a2" + } + ], + "hashType": "sha256" +} diff --git a/package-lock.json b/package-lock.json index 3c36757..f9c205e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,14 +19,20 @@ "mssql": "^12.2.0", "mysql2": "^3.18.2", "playwright": "^1.58.2", + "reflect-metadata": "^0.2.2", + "typeorm": "^0.3.28", "uuid": "^13.0.0", - "zod": "^4.3.6" + "winston": "^3.19.0", + "winston-daily-rotate-file": "^5.0.0", + "zod": "^4.3.6", + "zustand": "^5.0.11" }, "devDependencies": { "@electron-toolkit/eslint-config-prettier": "^3.0.0", "@electron-toolkit/eslint-config-ts": "^3.1.0", "@electron-toolkit/tsconfig": "^2.0.0", "@playwright/test": "^1.58.2", + "@types/mssql": "^9.1.9", "@types/node": "^22.19.13", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", @@ -631,6 +637,26 @@ "node": ">=18" } }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "license": "MIT", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@develar/schema-utils": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", @@ -1915,7 +1941,6 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -1933,7 +1958,6 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -1946,7 +1970,6 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -1959,14 +1982,12 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, "license": "MIT" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -1984,7 +2005,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.2.2" @@ -2000,7 +2020,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -2210,7 +2229,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -2590,6 +2608,22 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "node_modules/@sqltools/formatter": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", + "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==", + "license": "MIT" + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -3002,6 +3036,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mssql": { + "version": "9.1.9", + "resolved": "https://registry.npmjs.org/@types/mssql/-/mssql-9.1.9.tgz", + "integrity": "sha512-P0nCgw6vzY23UxZMnbI4N7fnLGANt4LI4yvxze1paPj+LuN28cFv5EI+QidP8udnId/BKhkcRhm/BleNsjK65A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "tarn": "^3.0.1", + "tedious": "*" + } + }, "node_modules/@types/node": { "version": "22.19.13", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz", @@ -3028,7 +3074,7 @@ "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -3063,6 +3109,12 @@ "@types/node": "*" } }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, "node_modules/@types/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", @@ -3626,7 +3678,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3636,7 +3687,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -3648,6 +3698,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, "node_modules/app-builder-bin": { "version": "5.0.0-alpha.12", "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", @@ -3834,6 +3893,15 @@ "node": ">=10" } }, + "node_modules/app-root-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", + "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/archiver": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", @@ -4189,7 +4257,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" @@ -4630,7 +4697,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.0", @@ -4649,7 +4715,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4663,7 +4728,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -4827,7 +4891,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -4860,11 +4923,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/color": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", + "license": "MIT", + "dependencies": { + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -4877,9 +4952,50 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-string/node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -4995,7 +5111,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -5010,14 +5125,12 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/cross-spawn/node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -5033,7 +5146,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/data-view-buffer": { @@ -5150,6 +5263,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -5211,7 +5338,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "devOptional": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -5473,7 +5599,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -5533,7 +5658,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, "license": "MIT" }, "node_modules/ecdsa-sig-formatter": { @@ -5789,7 +5913,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, + "license": "MIT" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", "license": "MIT" }, "node_modules/encoding": { @@ -5914,7 +6043,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5924,7 +6052,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5969,7 +6096,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -6078,7 +6204,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -6597,6 +6722,12 @@ } } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -6610,6 +6741,15 @@ "node": ">=16.0.0" } }, + "node_modules/file-stream-rotator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz", + "integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.1" + } + }, "node_modules/filelist": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", @@ -6688,11 +6828,16 @@ "dev": true, "license": "ISC" }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, "license": "MIT", "dependencies": { "is-callable": "^1.2.7" @@ -6708,7 +6853,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -6725,7 +6869,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -6838,7 +6981,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6908,7 +7050,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -6918,7 +7059,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -6943,7 +7083,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -7113,7 +7252,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7180,7 +7318,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "devOptional": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -7209,7 +7346,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7222,7 +7358,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -7238,7 +7373,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -7560,7 +7694,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7665,7 +7798,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7829,6 +7961,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", @@ -7868,7 +8012,6 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, "license": "MIT", "dependencies": { "which-typed-array": "^1.1.16" @@ -7958,7 +8101,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, "license": "MIT" }, "node_modules/isbinaryfile": { @@ -8045,7 +8187,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -8300,6 +8441,12 @@ "json-buffer": "3.0.1" } }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, "node_modules/lazy-val": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", @@ -8789,6 +8936,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -8941,7 +9105,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -9031,7 +9194,6 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" @@ -9192,6 +9354,15 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -9485,6 +9656,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -9603,6 +9783,15 @@ "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -9755,7 +9944,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/pako": { @@ -9800,7 +9988,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9817,7 +10004,6 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -9834,7 +10020,6 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, "license": "ISC" }, "node_modules/pathe": { @@ -9933,7 +10118,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -10250,6 +10434,12 @@ "node": ">=10" } }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -10298,7 +10488,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10560,6 +10749,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -10641,7 +10839,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -10692,11 +10889,30 @@ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "license": "MIT" }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -10709,7 +10925,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10939,6 +11154,22 @@ "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" } }, + "node_modules/sql-highlight": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sql-highlight/-/sql-highlight-6.1.0.tgz", + "integrity": "sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==", + "funding": [ + "https://github.com/scriptcoded/sql-highlight?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/scriptcoded" + } + ], + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/ssri": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", @@ -10952,6 +11183,15 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -11003,7 +11243,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -11019,7 +11258,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -11132,7 +11370,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -11146,7 +11383,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -11445,6 +11681,12 @@ "node": ">= 10.0.0" } }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, "node_modules/tiny-async-pool": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", @@ -11527,6 +11769,20 @@ "tmp": "^0.2.0" } }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/traverse": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", @@ -11536,6 +11792,15 @@ "node": "*" } }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/truncate-utf8-bytes": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", @@ -11595,7 +11860,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -11669,6 +11933,208 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typeorm": { + "version": "0.3.28", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.28.tgz", + "integrity": "sha512-6GH7wXhtfq2D33ZuRXYwIsl/qM5685WZcODZb7noOOcRMteM9KF2x2ap3H0EBjnSV0VO4gNAfJT5Ukp0PkOlvg==", + "license": "MIT", + "dependencies": { + "@sqltools/formatter": "^1.2.5", + "ansis": "^4.2.0", + "app-root-path": "^3.1.0", + "buffer": "^6.0.3", + "dayjs": "^1.11.19", + "debug": "^4.4.3", + "dedent": "^1.7.0", + "dotenv": "^16.6.1", + "glob": "^10.5.0", + "reflect-metadata": "^0.2.2", + "sha.js": "^2.4.12", + "sql-highlight": "^6.1.0", + "tslib": "^2.8.1", + "uuid": "^11.1.0", + "yargs": "^17.7.2" + }, + "bin": { + "typeorm": "cli.js", + "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js", + "typeorm-ts-node-esm": "cli-ts-node-esm.js" + }, + "engines": { + "node": ">=16.13.0" + }, + "funding": { + "url": "https://opencollective.com/typeorm" + }, + "peerDependencies": { + "@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@sap/hana-client": "^2.14.22", + "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0", + "ioredis": "^5.0.4", + "mongodb": "^5.8.0 || ^6.0.0", + "mssql": "^9.1.1 || ^10.0.0 || ^11.0.0 || ^12.0.0", + "mysql2": "^2.2.5 || ^3.0.1", + "oracledb": "^6.3.0", + "pg": "^8.5.1", + "pg-native": "^3.0.0", + "pg-query-stream": "^4.0.0", + "redis": "^3.1.1 || ^4.0.0 || ^5.0.14", + "sql.js": "^1.4.0", + "sqlite3": "^5.0.3", + "ts-node": "^10.7.0", + "typeorm-aurora-data-api-driver": "^2.0.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@google-cloud/spanner": { + "optional": true + }, + "@sap/hana-client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "mssql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "pg-query-stream": { + "optional": true + }, + "redis": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "ts-node": { + "optional": true + }, + "typeorm-aurora-data-api-driver": { + "optional": true + } + } + }, + "node_modules/typeorm/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/typeorm/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typeorm/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/typeorm/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/typeorm/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -12627,7 +13093,6 @@ "version": "1.1.20", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", - "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", @@ -12662,6 +13127,61 @@ "node": ">=8" } }, + "node_modules/winston": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", + "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-daily-rotate-file": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-5.0.0.tgz", + "integrity": "sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==", + "license": "MIT", + "dependencies": { + "file-stream-rotator": "^0.6.1", + "object-hash": "^3.0.0", + "triple-beam": "^1.4.1", + "winston-transport": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "winston": "^3" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -12676,7 +13196,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -12695,7 +13214,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -12750,7 +13268,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -12767,7 +13284,6 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -12786,7 +13302,6 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -12872,6 +13387,35 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "node_modules/zustand": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", + "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 5db356e..b9ae369 100644 --- a/package.json +++ b/package.json @@ -37,14 +37,20 @@ "mssql": "^12.2.0", "mysql2": "^3.18.2", "playwright": "^1.58.2", + "reflect-metadata": "^0.2.2", + "typeorm": "^0.3.28", "uuid": "^13.0.0", - "zod": "^4.3.6" + "winston": "^3.19.0", + "winston-daily-rotate-file": "^5.0.0", + "zod": "^4.3.6", + "zustand": "^5.0.11" }, "devDependencies": { "@electron-toolkit/eslint-config-prettier": "^3.0.0", "@electron-toolkit/eslint-config-ts": "^3.1.0", "@electron-toolkit/tsconfig": "^2.0.0", "@playwright/test": "^1.58.2", + "@types/mssql": "^9.1.9", "@types/node": "^22.19.13", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", diff --git a/src/main/ipc/auth-handler.ts b/src/main/ipc/auth-handler.ts index e5fea4b..f9d5945 100644 --- a/src/main/ipc/auth-handler.ts +++ b/src/main/ipc/auth-handler.ts @@ -12,8 +12,11 @@ import { ipcMain } from 'electron' import { SessionManager } from '../services/user/session-manager' +import { createLogger } from '../services/logger' import type { UserInfo } from '../types/user.types' +const log = createLogger('AuthHandler') + /** * Login request */ @@ -75,110 +78,115 @@ export function registerAuthHandlers(): void { /** * Silent login by computer name */ - ipcMain.handle( - 'auth:silentLogin', - async (): Promise => { - try { - const success = await sessionManager.loginByComputerName() - const userInfo = sessionManager.getUserInfo() + ipcMain.handle('auth:silentLogin', async (): Promise => { + try { + log.info('Attempting silent login') + const success = await sessionManager.loginByComputerName() + const userInfo = sessionManager.getUserInfo() - if (success && userInfo) { - // Check if admin needs user selection - const requiresUserSelection = userInfo.userType === 'Admin' + if (success && userInfo) { + // Check if admin needs user selection + const requiresUserSelection = userInfo.userType === 'Admin' - return { - success: true, - userInfo, - requiresUserSelection - } - } + log.info('Silent login successful', { + username: userInfo.username, + userType: userInfo.userType, + requiresUserSelection + }) return { - success: false, - requiresUserSelection: false - } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - return { - success: false, - error: `无感登录失败:${message}` + success: true, + userInfo, + requiresUserSelection } } + + log.warn('Silent login failed - no matching user') + return { + success: false, + requiresUserSelection: false + } + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error' + log.error('Silent login error', { error: message }) + return { + success: false, + error: `无感登录失败:${message}` + } } - ) + }) /** * Login with username and password */ - ipcMain.handle( - 'auth:login', - async (_event, request: LoginRequest): Promise => { - try { - const { username, password } = request - - if (!username || !password) { - return { - success: false, - error: '请输入用户名和密码' - } - } - - const success = await sessionManager.login(username, password) - const userInfo = sessionManager.getUserInfo() - - if (success && userInfo) { - return { - success: true, - userInfo - } - } + ipcMain.handle('auth:login', async (_event, request: LoginRequest): Promise => { + try { + const { username, password } = request + if (!username || !password) { + log.warn('Login attempt with missing credentials') return { success: false, - error: '用户名或密码错误' - } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - return { - success: false, - error: `登录失败:${message}` + error: '请输入用户名和密码' } } + + log.info('Login attempt', { username }) + const success = await sessionManager.login(username, password) + const userInfo = sessionManager.getUserInfo() + + if (success && userInfo) { + log.info('Login successful', { username, userType: userInfo.userType }) + return { + success: true, + userInfo + } + } + + log.warn('Login failed - invalid credentials', { username }) + return { + success: false, + error: '用户名或密码错误' + } + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error' + log.error('Login error', { error: message }) + return { + success: false, + error: `登录失败:${message}` + } } - ) + }) /** * Logout */ ipcMain.handle('auth:logout', async (): Promise => { + const userInfo = sessionManager.getUserInfo() + log.info('User logout', { username: userInfo?.username }) sessionManager.logout() }) /** * Get current user */ - ipcMain.handle( - 'auth:getCurrentUser', - async (): Promise => { - const isAuthenticated = sessionManager.isAuthenticated() - const userInfo = sessionManager.getUserInfo() + ipcMain.handle('auth:getCurrentUser', async (): Promise => { + const isAuthenticated = sessionManager.isAuthenticated() + const userInfo = sessionManager.getUserInfo() - return { - isAuthenticated, - userInfo: userInfo ?? undefined - } + return { + isAuthenticated, + userInfo: userInfo ?? undefined } - ) + }) /** * Get all users (for admin user selection) */ - ipcMain.handle( - 'auth:getAllUsers', - async (): Promise => { - return await sessionManager.getAllUsers() - } - ) + ipcMain.handle('auth:getAllUsers', async (): Promise => { + log.debug('Fetching all users for admin selection') + return await sessionManager.getAllUsers() + }) /** * Switch user (admin only) @@ -187,22 +195,26 @@ export function registerAuthHandlers(): void { 'auth:switchUser', async (_event, userInfo: UserInfo): Promise => { try { + log.info('User switch attempt', { targetUser: userInfo.username }) const success = sessionManager.switchUser(userInfo) if (success) { const newUser = sessionManager.getUserInfo() + log.info('User switch successful', { newUsername: newUser?.username }) return { success: true, userInfo: newUser ?? undefined } } + log.warn('User switch failed') return { success: false, error: '用户切换失败' } } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error' + log.error('User switch error', { error: message }) return { success: false, error: `用户切换失败:${message}` @@ -214,10 +226,7 @@ export function registerAuthHandlers(): void { /** * Check if current user is admin */ - ipcMain.handle( - 'auth:isAdmin', - async (): Promise => { - return sessionManager.isAdmin() - } - ) + ipcMain.handle('auth:isAdmin', async (): Promise => { + return sessionManager.isAdmin() + }) } diff --git a/src/main/ipc/cleaner-handler.ts b/src/main/ipc/cleaner-handler.ts index 905d7ea..eb39150 100644 --- a/src/main/ipc/cleaner-handler.ts +++ b/src/main/ipc/cleaner-handler.ts @@ -3,125 +3,153 @@ import { ErpAuthService } from '../services/erp/erp-auth' import { CleanerService } from '../services/erp/cleaner' import { OrderNumberResolver } from '../services/erp/order-resolver' import { MySqlService } from '../services/database/mysql' +import { createLogger } from '../services/logger' +import { withErrorHandling, type IpcResult } from './index' +import { ErpConnectionError, ValidationError, DatabaseQueryError } from '../types/errors' import type { CleanerInput, CleanerResult } from '../types/cleaner.types' +const log = createLogger('CleanerHandler') + /** * 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 - let mysqlService: MySqlService | null = null + async (_event, input: CleanerInput): Promise> => { + return withErrorHandling(async () => { + let authService: ErpAuthService | null = null + let mysqlService: MySqlService | null = null - try { - // Check environment variables - const erpUrl = process.env.ERP_URL || '' - const erpUsername = process.env.ERP_USERNAME || '' - const erpPassword = process.env.ERP_PASSWORD || '' + try { + // Check environment variables + const erpUrl = process.env.ERP_URL || '' + const erpUsername = process.env.ERP_USERNAME || '' + const erpPassword = process.env.ERP_PASSWORD || '' - console.log('[Cleaner] Config:', { - url: erpUrl ? '***' : 'EMPTY', - username: erpUsername ? '***' : 'EMPTY' - }) + log.info('Config check', { + url: erpUrl ? 'configured' : 'EMPTY', + username: erpUsername ? 'configured' : 'EMPTY' + }) - if (!erpUrl || !erpUsername || !erpPassword) { - throw new Error( - 'ERP 配置不完整。请检查 .env 文件中的 ERP_URL, ERP_USERNAME, ERP_PASSWORD' - ) - } + if (!erpUrl || !erpUsername || !erpPassword) { + throw new ValidationError( + 'ERP 配置不完整。请检查 .env 文件中的 ERP_URL, ERP_USERNAME, ERP_PASSWORD', + 'VAL_MISSING_REQUIRED' + ) + } - // 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 || '' - } + // 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 || '' + } - console.log('[Cleaner] Connecting to MySQL for order resolution...') - mysqlService = new MySqlService(mysqlConfig) - await mysqlService.connect() + log.info('Connecting to MySQL for order resolution...') + mysqlService = new MySqlService(mysqlConfig) - const resolver = new OrderNumberResolver(mysqlService) - const mappings = await resolver.resolve(input.orderNumbers) - - // Get valid order numbers and warnings - const validOrderNumbers = resolver.getValidOrderNumbers(mappings) - const warnings = resolver.getWarnings(mappings) - - if (warnings.length > 0) { - console.warn('[Cleaner] Resolution warnings:', warnings) - } - - if (validOrderNumbers.length === 0) { - throw new Error('没有有效的生产订单号可处理。请检查输入的格式或数据库连接。') - } - - console.log('[Cleaner] Resolved order numbers:', validOrderNumbers) - - // Create auth service and login - authService = new ErpAuthService({ - url: erpUrl, - username: erpUsername, - password: erpPassword, - headless: true - }) - - console.log('[Cleaner] Logging in to ERP...') - await authService.login() - console.log('[Cleaner] Login successful') - - // Create cleaner service and run cleaning with resolved order numbers - const cleaner = new CleanerService(authService) - - const modifiedInput: CleanerInput = { - ...input, - orderNumbers: validOrderNumbers, - onProgress: input.onProgress - } - - console.log('[Cleaner] Starting cleaning:', modifiedInput) - const result = await cleaner.clean(modifiedInput) - console.log('[Cleaner] Cleaning completed:', result) - - // Add warnings to result errors if any - if (warnings.length > 0) { - result.errors = [...warnings, ...result.errors] - } - - return { success: true, data: result } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - console.error('[Cleaner] Error:', message) - console.error('[Cleaner] Stack:', error instanceof Error ? error.stack : 'N/A') - return { success: false, error: `清理失败:${message}` } - } finally { - // Clean up: close browser - if (authService) { try { - await authService.close() - console.log('[Cleaner] Browser closed') - } catch (closeError) { - console.warn('[Cleaner] Error closing browser:', closeError) + await mysqlService.connect() + } catch (error) { + throw new DatabaseQueryError( + 'MySQL 连接失败', + 'DB_CONNECTION_FAILED', + error instanceof Error ? error : undefined + ) + } + + const resolver = new OrderNumberResolver(mysqlService) + const mappings = await resolver.resolve(input.orderNumbers) + + // Get valid order numbers and warnings + const validOrderNumbers = resolver.getValidOrderNumbers(mappings) + const warnings = resolver.getWarnings(mappings) + + if (warnings.length > 0) { + log.warn('Resolution warnings', { warnings }) + } + + if (validOrderNumbers.length === 0) { + throw new ValidationError( + '没有有效的生产订单号可处理。请检查输入的格式或数据库连接。', + 'VAL_INVALID_INPUT' + ) + } + + log.info('Resolved order numbers', { count: validOrderNumbers.length }) + + // Create auth service and login + authService = new ErpAuthService({ + url: erpUrl, + username: erpUsername, + password: erpPassword, + headless: true + }) + + log.info('Logging in to ERP...') + try { + await authService.login() + } catch (error) { + throw new ErpConnectionError( + 'ERP 登录失败', + 'ERP_LOGIN_FAILED', + error instanceof Error ? error : undefined + ) + } + log.info('Login successful') + + // Create cleaner service and run cleaning with resolved order numbers + const cleaner = new CleanerService(authService) + + const modifiedInput: CleanerInput = { + ...input, + orderNumbers: validOrderNumbers, + onProgress: input.onProgress + } + + log.info('Starting cleaning', { orderCount: validOrderNumbers.length }) + const result = await cleaner.clean(modifiedInput) + + // Add warnings to result errors if any + if (warnings.length > 0) { + result.errors = [...warnings, ...result.errors] + } + + log.info('Cleaning completed', { + processedCount: result.ordersProcessed, + errorCount: result.errors.length + }) + + return result + } finally { + // Clean up: close browser + if (authService) { + try { + await authService.close() + log.debug('Browser closed') + } catch (closeError) { + log.warn('Error closing browser', { + error: closeError instanceof Error ? closeError.message : String(closeError) + }) + } + } + + // Clean up: disconnect MySQL + if (mysqlService) { + try { + await mysqlService.disconnect() + log.debug('MySQL disconnected') + } catch (closeError) { + log.warn('Error disconnecting MySQL', { + error: closeError instanceof Error ? closeError.message : String(closeError) + }) + } } } - - // Clean up: disconnect MySQL - if (mysqlService) { - try { - await mysqlService.disconnect() - console.log('[Cleaner] MySQL disconnected') - } catch (closeError) { - console.warn('[Cleaner] Error disconnecting MySQL:', closeError) - } - } - } + }, 'cleaner:run') } ) } diff --git a/src/main/ipc/database-handler.ts b/src/main/ipc/database-handler.ts index f0e592f..7146ebe 100644 --- a/src/main/ipc/database-handler.ts +++ b/src/main/ipc/database-handler.ts @@ -1,6 +1,8 @@ import { ipcMain } from 'electron' import { MySqlService } from '../services/database/mysql' import { SqlServerService } from '../services/database/sql-server' +import { createLogger } from '../services/logger' +import { DatabaseQueryError, ValidationError } from '../types/errors' import type { MySqlConfig, MySqlQueryResult, @@ -8,6 +10,8 @@ import type { SqlServerQueryResult } from '../types/ipc-api.types' +const log = createLogger('DatabaseHandler') + // Store MySQL service instances per window/connection const mysqlServices = new Map() @@ -64,34 +68,47 @@ export function registerDatabaseHandlers(): void { ipcMain.handle('database:mysql:connect', async (event, config: MySqlConfig): Promise => { try { // Use window ID as connection identifier - const windowId = (event.sender as any).id.toString() + const windowId = (event.sender as { id: number }).id.toString() + log.info('Connecting to MySQL', { windowId }) const service = new MySqlService(config) await service.connect() setMySqlService(windowId, service) + log.info('MySQL connected', { windowId }) } catch (error) { const message = error instanceof Error ? error.message : 'Failed to connect to MySQL' - throw new Error(message) + log.error('MySQL connection failed', { error: message }) + throw new DatabaseQueryError( + message, + 'DB_CONNECTION_FAILED', + error instanceof Error ? error : undefined + ) } }) // Disconnect from MySQL ipcMain.handle('database:mysql:disconnect', async (event): Promise => { try { - const windowId = (event.sender as any).id.toString() + const windowId = (event.sender as { id: number }).id.toString() const service = getMySqlService(windowId) if (service) { await service.disconnect() deleteMySqlService(windowId) + log.info('MySQL disconnected', { windowId }) } } catch (error) { const message = error instanceof Error ? error.message : 'Failed to disconnect from MySQL' - throw new Error(message) + log.error('MySQL disconnect failed', { error: message }) + throw new DatabaseQueryError( + message, + 'DB_CONNECTION_FAILED', + error instanceof Error ? error : undefined + ) } }) // Check if MySQL is connected ipcMain.handle('database:mysql:isConnected', async (event): Promise => { - const windowId = (event.sender as any).id.toString() + const windowId = (event.sender as { id: number }).id.toString() const service = getMySqlService(windowId) return service ? service.isConnected() : false }) @@ -99,19 +116,28 @@ export function registerDatabaseHandlers(): void { // Execute MySQL query ipcMain.handle( 'database:mysql:query', - async (event, sql: string, params?: any[]): Promise => { + async (event, sql: string, params?: unknown[]): Promise => { try { - const windowId = (event.sender as any).id.toString() + const windowId = (event.sender as { id: number }).id.toString() const service = getMySqlService(windowId) if (!service) { - throw new Error('Not connected to MySQL. Call connect() first.') + throw new ValidationError( + 'Not connected to MySQL. Call connect() first.', + 'VAL_INVALID_INPUT' + ) } + log.debug('Executing MySQL query', { windowId, sql: sql.substring(0, 100) }) return await service.query(sql, params) } catch (error) { const message = error instanceof Error ? error.message : 'MySQL query failed' - throw new Error(message) + log.error('MySQL query failed', { error: message }) + throw new DatabaseQueryError( + message, + 'DB_QUERY_FAILED', + error instanceof Error ? error : undefined + ) } } ) @@ -121,13 +147,20 @@ export function registerDatabaseHandlers(): void { 'database:sqlserver:connect', async (event, config: SqlServerConfig): Promise => { try { - const windowId = (event.sender as any).id.toString() + const windowId = (event.sender as { id: number }).id.toString() + log.info('Connecting to SQL Server', { windowId }) const service = new SqlServerService(config) await service.connect() setSqlServerService(windowId, service) + log.info('SQL Server connected', { windowId }) } catch (error) { const message = error instanceof Error ? error.message : 'Failed to connect to SQL Server' - throw new Error(message) + log.error('SQL Server connection failed', { error: message }) + throw new DatabaseQueryError( + message, + 'DB_CONNECTION_FAILED', + error instanceof Error ? error : undefined + ) } } ) @@ -135,22 +168,28 @@ export function registerDatabaseHandlers(): void { // Disconnect from SQL Server ipcMain.handle('database:sqlserver:disconnect', async (event): Promise => { try { - const windowId = (event.sender as any).id.toString() + const windowId = (event.sender as { id: number }).id.toString() const service = getSqlServerService(windowId) if (service) { await service.disconnect() deleteSqlServerService(windowId) + log.info('SQL Server disconnected', { windowId }) } } catch (error) { const message = error instanceof Error ? error.message : 'Failed to disconnect from SQL Server' - throw new Error(message) + log.error('SQL Server disconnect failed', { error: message }) + throw new DatabaseQueryError( + message, + 'DB_CONNECTION_FAILED', + error instanceof Error ? error : undefined + ) } }) // Check if SQL Server is connected ipcMain.handle('database:sqlserver:isConnected', async (event): Promise => { - const windowId = (event.sender as any).id.toString() + const windowId = (event.sender as { id: number }).id.toString() const service = getSqlServerService(windowId) return service ? service.isConnected() : false }) @@ -164,17 +203,26 @@ export function registerDatabaseHandlers(): void { params?: Record ): Promise => { try { - const windowId = (event.sender as any).id.toString() + const windowId = (event.sender as { id: number }).id.toString() const service = getSqlServerService(windowId) if (!service) { - throw new Error('Not connected to SQL Server. Call connect() first.') + throw new ValidationError( + 'Not connected to SQL Server. Call connect() first.', + 'VAL_INVALID_INPUT' + ) } + log.debug('Executing SQL Server query', { windowId, sql: sqlString.substring(0, 100) }) return await service.query(sqlString, params) } catch (error) { const message = error instanceof Error ? error.message : 'SQL Server query failed' - throw new Error(message) + log.error('SQL Server query failed', { error: message }) + throw new DatabaseQueryError( + message, + 'DB_QUERY_FAILED', + error instanceof Error ? error : undefined + ) } } ) diff --git a/src/main/ipc/extractor-handler.ts b/src/main/ipc/extractor-handler.ts index 0b44614..4a13e87 100644 --- a/src/main/ipc/extractor-handler.ts +++ b/src/main/ipc/extractor-handler.ts @@ -3,125 +3,152 @@ 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 { createLogger } from '../services/logger' +import { withErrorHandling, type IpcResult } from './index' +import { ErpConnectionError, ValidationError, DatabaseQueryError } from '../types/errors' import type { ExtractorInput, ExtractorResult } from '../types/extractor.types' +const log = createLogger('ExtractorHandler') + /** * 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 - let mysqlService: MySqlService | null = null + async (_event, input: ExtractorInput): Promise> => { + return withErrorHandling(async () => { + let authService: ErpAuthService | null = null + let mysqlService: MySqlService | null = null - try { - // Check environment variables - const erpUrl = process.env.ERP_URL || '' - const erpUsername = process.env.ERP_USERNAME || '' - const erpPassword = process.env.ERP_PASSWORD || '' + try { + // Check environment variables + const erpUrl = process.env.ERP_URL || '' + const erpUsername = process.env.ERP_USERNAME || '' + const erpPassword = process.env.ERP_PASSWORD || '' - console.log('[Extractor] Config:', { - url: erpUrl ? '***' : 'EMPTY', - username: erpUsername ? '***' : 'EMPTY' - }) + log.info('Config check', { + url: erpUrl ? 'configured' : 'EMPTY', + username: erpUsername ? 'configured' : 'EMPTY' + }) - if (!erpUrl || !erpUsername || !erpPassword) { - throw new Error( - 'ERP 配置不完整。请检查 .env 文件中的 ERP_URL, ERP_USERNAME, ERP_PASSWORD' - ) - } + if (!erpUrl || !erpUsername || !erpPassword) { + throw new ValidationError( + 'ERP 配置不完整。请检查 .env 文件中的 ERP_URL, ERP_USERNAME, ERP_PASSWORD', + 'VAL_MISSING_REQUIRED' + ) + } - // 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 || '' - } + // 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 || '' + } - console.log('[Extractor] Connecting to MySQL for order resolution...') - mysqlService = new MySqlService(mysqlConfig) - await mysqlService.connect() + log.info('Connecting to MySQL for order resolution...') + mysqlService = new MySqlService(mysqlConfig) - const resolver = new OrderNumberResolver(mysqlService) - const mappings = await resolver.resolve(input.orderNumbers) - - // Get valid order numbers and warnings - const validOrderNumbers = resolver.getValidOrderNumbers(mappings) - const warnings = resolver.getWarnings(mappings) - - if (warnings.length > 0) { - console.warn('[Extractor] Resolution warnings:', warnings) - } - - if (validOrderNumbers.length === 0) { - throw new Error('没有有效的生产订单号可处理。请检查输入的格式或数据库连接。') - } - - console.log('[Extractor] Resolved order numbers:', validOrderNumbers) - - // Create auth service and login - authService = new ErpAuthService({ - url: erpUrl, - username: erpUsername, - password: erpPassword, - headless: true - }) - - console.log('[Extractor] Logging in to ERP...') - await authService.login() - console.log('[Extractor] Login successful') - - // Create extractor service and run extraction with resolved order numbers - const extractor = new ExtractorService(authService) - console.log('[Extractor] Starting extraction for orders:', validOrderNumbers) - - const modifiedInput: ExtractorInput = { - ...input, - orderNumbers: validOrderNumbers - } - - const result = await extractor.extract(modifiedInput) - - // Add warnings to result errors if any - if (warnings.length > 0) { - result.errors = [...warnings, ...result.errors] - } - - console.log('[Extractor] Extraction completed:', result) - - return { success: true, data: result } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - console.error('[Extractor] Error:', message) - console.error('[Extractor] Stack:', error instanceof Error ? error.stack : 'N/A') - return { success: false, error: `提取失败:${message}` } - } finally { - // Clean up: close browser - if (authService) { try { - await authService.close() - console.log('[Extractor] Browser closed') - } catch (closeError) { - console.warn('[Extractor] Error closing browser:', closeError) + await mysqlService.connect() + } catch (error) { + throw new DatabaseQueryError( + 'MySQL 连接失败', + 'DB_CONNECTION_FAILED', + error instanceof Error ? error : undefined + ) + } + + const resolver = new OrderNumberResolver(mysqlService) + const mappings = await resolver.resolve(input.orderNumbers) + + // Get valid order numbers and warnings + const validOrderNumbers = resolver.getValidOrderNumbers(mappings) + const warnings = resolver.getWarnings(mappings) + + if (warnings.length > 0) { + log.warn('Resolution warnings', { warnings }) + } + + if (validOrderNumbers.length === 0) { + throw new ValidationError( + '没有有效的生产订单号可处理。请检查输入的格式或数据库连接。', + 'VAL_INVALID_INPUT' + ) + } + + log.info('Resolved order numbers', { count: validOrderNumbers.length }) + + // Create auth service and login + authService = new ErpAuthService({ + url: erpUrl, + username: erpUsername, + password: erpPassword, + headless: true + }) + + log.info('Logging in to ERP...') + try { + await authService.login() + } catch (error) { + throw new ErpConnectionError( + 'ERP 登录失败', + 'ERP_LOGIN_FAILED', + error instanceof Error ? error : undefined + ) + } + log.info('Login successful') + + // Create extractor service and run extraction with resolved order numbers + const extractor = new ExtractorService(authService) + log.info('Starting extraction', { orderCount: validOrderNumbers.length }) + + const modifiedInput: ExtractorInput = { + ...input, + orderNumbers: validOrderNumbers + } + + const result = await extractor.extract(modifiedInput) + + // Add warnings to result errors if any + if (warnings.length > 0) { + result.errors = [...warnings, ...result.errors] + } + + log.info('Extraction completed', { + rowCount: result.recordCount, + errorCount: result.errors.length + }) + + return result + } finally { + // Clean up: close browser + if (authService) { + try { + await authService.close() + log.debug('Browser closed') + } catch (closeError) { + log.warn('Error closing browser', { + error: closeError instanceof Error ? closeError.message : String(closeError) + }) + } + } + + // Clean up: disconnect MySQL + if (mysqlService) { + try { + await mysqlService.disconnect() + log.debug('MySQL disconnected') + } catch (closeError) { + log.warn('Error disconnecting MySQL', { + error: closeError instanceof Error ? closeError.message : String(closeError) + }) + } } } - - // Clean up: disconnect MySQL - if (mysqlService) { - try { - await mysqlService.disconnect() - console.log('[Extractor] MySQL disconnected') - } catch (closeError) { - console.warn('[Extractor] Error disconnecting MySQL:', closeError) - } - } - } + }, 'extractor:run') } ) } diff --git a/src/main/ipc/file-handler.ts b/src/main/ipc/file-handler.ts index 4bf9d2a..2d4ea7f 100644 --- a/src/main/ipc/file-handler.ts +++ b/src/main/ipc/file-handler.ts @@ -1,6 +1,9 @@ import { ipcMain } from 'electron' import * as fs from 'fs/promises' import * as path from 'path' +import { createLogger } from '../services/logger' + +const log = createLogger('FileHandler') /** * Register IPC handlers for file operations @@ -9,9 +12,11 @@ export function registerFileHandlers(): void { // Read file content ipcMain.handle('file:read', async (_event, filePath: string): Promise => { try { + log.debug('Reading file', { filePath }) return await fs.readFile(filePath, 'utf-8') } catch (error) { const message = error instanceof Error ? error.message : 'Failed to read file' + log.error('Failed to read file', { filePath, error: message }) throw new Error(message) } }) @@ -19,12 +24,14 @@ export function registerFileHandlers(): void { // Write content to file ipcMain.handle('file:write', async (_event, filePath: string, content: string): Promise => { try { + log.debug('Writing file', { filePath }) // 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' + log.error('Failed to write file', { filePath, error: message }) throw new Error(message) } }) @@ -42,6 +49,7 @@ export function registerFileHandlers(): void { // List files in directory ipcMain.handle('file:list', async (_event, dirPath: string): Promise => { try { + log.debug('Listing directory', { dirPath }) const entries = await fs.readdir(dirPath, { withFileTypes: true }) return entries .filter((entry) => entry.isFile()) @@ -49,6 +57,7 @@ export function registerFileHandlers(): void { .sort() } catch (error) { const message = error instanceof Error ? error.message : 'Failed to list directory' + log.error('Failed to list directory', { dirPath, error: message }) throw new Error(message) } }) diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts index 8631b8f..d79b4a7 100644 --- a/src/main/ipc/index.ts +++ b/src/main/ipc/index.ts @@ -11,11 +11,60 @@ import { registerResolverHandlers } from './resolver-handler' import { registerAuthHandlers } from './auth-handler' import { registerValidationHandlers } from './validation-handler' import { registerSettingsHandlers } from './settings-handler' +import { createLogger } from '../services/logger' +import { getErrorMessage, getErrorCode, isBaseError } from '../types/errors' + +const log = createLogger('IPC') + +/** + * Standard result type for all IPC handlers + */ +export interface IpcResult { + success: boolean + data?: T + error?: string + code?: string +} + +/** + * Higher-order function to wrap IPC handlers with consistent error handling + * @param handler - The async handler function to wrap + * @param context - The context name for logging + * @returns A wrapped handler that returns IpcResult + */ +export function withErrorHandling( + handler: () => Promise, + context: string +): Promise> { + return handler() + .then((data) => { + log.debug(`[${context}] Handler completed successfully`) + return { success: true, data } + }) + .catch((error: unknown) => { + const message = getErrorMessage(error) + const code = getErrorCode(error) + + if (isBaseError(error)) { + log.error(`[${context}] ${error.name}: ${message}`, { code, cause: error.cause?.message }) + } else { + log.error(`[${context}] Error: ${message}`, { code }) + } + + // Include stack trace in development + if (process.env.NODE_ENV !== 'production' && error instanceof Error) { + log.debug(`[${context}] Stack trace:`, { stack: error.stack }) + } + + return { success: false, error: message, code } + }) +} /** * Register all IPC handlers */ export function registerIpcHandlers(): void { + log.info('Registering IPC handlers...') registerFileHandlers() registerExtractorHandlers() registerCleanerHandlers() @@ -24,4 +73,5 @@ export function registerIpcHandlers(): void { registerAuthHandlers() registerValidationHandlers() registerSettingsHandlers() + log.info('All IPC handlers registered') } diff --git a/src/main/ipc/resolver-handler.ts b/src/main/ipc/resolver-handler.ts index 65a060f..bc9fade 100644 --- a/src/main/ipc/resolver-handler.ts +++ b/src/main/ipc/resolver-handler.ts @@ -9,8 +9,12 @@ import { ipcMain } from 'electron' import { MySqlService } from '../services/database/mysql' 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') + /** * Resolver input from renderer */ @@ -69,6 +73,7 @@ export function registerResolverHandlers(): void { } // Create MySQL service + log.info('Connecting to MySQL for resolution', { inputCount: input.inputs.length }) mysqlService = new MySqlService(mysqlConfig) await mysqlService.connect() @@ -81,6 +86,12 @@ export function registerResolverHandlers(): void { const warnings = resolver.getWarnings(mappings) const stats = resolver.getStats(mappings) + log.info('Resolution completed', { + inputCount: input.inputs.length, + validCount: validOrderNumbers.length, + warningCount: warnings.length + }) + return { success: true, mappings, @@ -90,6 +101,7 @@ export function registerResolverHandlers(): void { } } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error' + log.error('Resolution failed', { error: message }) return { success: false, error: `解析失败:${message}` @@ -99,8 +111,11 @@ export function registerResolverHandlers(): void { if (mysqlService) { try { await mysqlService.disconnect() + log.debug('MySQL disconnected') } catch (closeError) { - console.warn('[Resolver] Error disconnecting MySQL:', closeError) + log.warn('Error disconnecting MySQL', { + error: closeError instanceof Error ? closeError.message : String(closeError) + }) } } } @@ -112,7 +127,10 @@ export function registerResolverHandlers(): void { */ ipcMain.handle( 'resolver:validateFormat', - async (_event, inputs: string[]): Promise<{ + async ( + _event, + inputs: string[] + ): Promise<{ success: boolean results?: Array<{ input: string; type: 'productionId' | 'orderNumber' | 'unknown' }> error?: string @@ -122,14 +140,17 @@ export function registerResolverHandlers(): void { isConnected: () => false } as MySqlService) - const results = inputs.map(input => ({ + const results = inputs.map((input) => ({ input, type: resolver.recognizeType(input) })) + log.debug('Format validation completed', { inputCount: inputs.length }) + return { success: true, results } } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error' + log.error('Format validation failed', { error: message }) return { success: false, error: `验证失败:${message}` diff --git a/src/main/ipc/settings-handler.ts b/src/main/ipc/settings-handler.ts index 91b91d7..08514cd 100644 --- a/src/main/ipc/settings-handler.ts +++ b/src/main/ipc/settings-handler.ts @@ -14,6 +14,7 @@ import { SessionManager } from '../services/user/session-manager' import { ErpAuthService } from '../services/erp/erp-auth' import { MySqlService } from '../services/database/mysql' import { SqlServerService } from '../services/database/sql-server' +import { createLogger } from '../services/logger' import type { SettingsData, UserType, @@ -21,14 +22,13 @@ import type { SaveSettingsResult } from '../types/settings.types' +const log = createLogger('SettingsHandler') + /** * Filter settings by user type * Admin users get all settings, User users get limited settings */ -function filterSettingsByUserType( - settings: SettingsData, - userType: UserType -): SettingsData { +function filterSettingsByUserType(settings: SettingsData, userType: UserType): SettingsData { if (userType === 'Admin') { return settings // Return all settings for Admin } @@ -72,6 +72,7 @@ export function registerSettingsHandlers(): void { */ ipcMain.handle('settings:getSettings', async (): Promise => { const userType = (sessionManager.getUserType() as UserType) || 'Guest' + log.debug('Getting settings', { userType }) const settings = configManager.getAllSettings() return filterSettingsByUserType(settings, userType) }) @@ -83,14 +84,18 @@ export function registerSettingsHandlers(): void { 'settings:saveSettings', async (_event, settings: SettingsData): Promise => { try { + log.info('Saving settings') const success = await configManager.saveAllSettings(settings) if (success) { + log.info('Settings saved successfully') return { success: true } } else { + log.warn('Failed to save settings') return { success: false, error: '保存设置失败' } } } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error' + log.error('Error saving settings', { error: message }) return { success: false, error: `保存设置失败:${message}` } } } @@ -99,157 +104,166 @@ export function registerSettingsHandlers(): void { /** * Reset to default settings (Admin only) */ - ipcMain.handle( - 'settings:resetDefaults', - async (): Promise => { - try { - const userType = sessionManager.getUserType() - if (userType !== 'Admin') { - return { success: false, error: '只有管理员可以恢复默认设置' } - } - - configManager.resetToDefaults() - const success = await configManager.save() - if (success) { - return { success: true } - } else { - return { success: false, error: '恢复默认设置失败' } - } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - return { success: false, error: `恢复默认设置失败:${message}` } + ipcMain.handle('settings:resetDefaults', async (): Promise => { + try { + const userType = sessionManager.getUserType() + if (userType !== 'Admin') { + log.warn('Non-admin user attempted to reset defaults', { userType }) + return { success: false, error: '只有管理员可以恢复默认设置' } } + + log.info('Resetting settings to defaults') + configManager.resetToDefaults() + const success = await configManager.save() + if (success) { + log.info('Settings reset to defaults successfully') + return { success: true } + } else { + log.warn('Failed to reset settings to defaults') + return { success: false, error: '恢复默认设置失败' } + } + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error' + log.error('Error resetting settings', { error: message }) + return { success: false, error: `恢复默认设置失败:${message}` } } - ) + }) /** * Test ERP connection */ - ipcMain.handle( - 'settings:testErpConnection', - async (): Promise => { - try { - const settings = configManager.getAllSettings() - const erpConfig = settings.erp + ipcMain.handle('settings:testErpConnection', async (): Promise => { + try { + log.info('Testing ERP connection') + const settings = configManager.getAllSettings() + const erpConfig = settings.erp - if (!erpConfig.url || !erpConfig.username || !erpConfig.password) { - return { - success: false, - message: '请先配置 ERP URL、用户名和密码' - } - } - - // Create ERP auth service and try to login - const erpAuthService = new ErpAuthService(erpConfig) - - try { - await erpAuthService.login() - // Login successful, close browser - await erpAuthService.close() - return { - success: true, - message: 'ERP 连接测试成功!' - } - } catch (loginError) { - const errorMessage = loginError instanceof Error ? loginError.message : '登录失败' - return { - success: false, - message: `ERP 连接测试失败:${errorMessage}` - } - } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' + if (!erpConfig.url || !erpConfig.username || !erpConfig.password) { + log.warn('ERP connection test failed - missing configuration') return { success: false, - message: `ERP 连接测试失败:${message}` + message: '请先配置 ERP URL、用户名和密码' } } + + // Create ERP auth service and try to login + const erpAuthService = new ErpAuthService(erpConfig) + + try { + await erpAuthService.login() + // Login successful, close browser + await erpAuthService.close() + log.info('ERP connection test successful') + return { + success: true, + message: 'ERP 连接测试成功!' + } + } catch (loginError) { + const errorMessage = loginError instanceof Error ? loginError.message : '登录失败' + log.error('ERP login failed', { error: errorMessage }) + return { + success: false, + message: `ERP 连接测试失败:${errorMessage}` + } + } + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error' + log.error('ERP connection test error', { error: message }) + return { + success: false, + message: `ERP 连接测试失败:${message}` + } } - ) + }) /** * Test database connection */ - ipcMain.handle( - 'settings:testDbConnection', - async (): Promise => { - try { - const settings = configManager.getAllSettings() - const dbConfig = settings.database + ipcMain.handle('settings:testDbConnection', async (): Promise => { + try { + log.info('Testing database connection') + const settings = configManager.getAllSettings() + const dbConfig = settings.database - if (dbConfig.dbType === 'mysql') { - // Test MySQL connection - if (!dbConfig.mysqlHost || !dbConfig.database || !dbConfig.username) { - return { - success: false, - message: '请先配置 MySQL 主机、数据库名和用户名' - } - } - - const mysqlService = new MySqlService({ - host: dbConfig.mysqlHost, - port: dbConfig.mysqlPort, - user: dbConfig.username, - password: dbConfig.password, - database: dbConfig.database - }) - - try { - await mysqlService.connect() - await mysqlService.disconnect() - return { - success: true, - message: 'MySQL 数据库连接测试成功!' - } - } catch (connError) { - const errorMessage = connError instanceof Error ? connError.message : '连接失败' - return { - success: false, - message: `MySQL 数据库连接测试失败:${errorMessage}` - } - } - } else { - // Test SQL Server connection - if (!dbConfig.server || !dbConfig.database || !dbConfig.username) { - return { - success: false, - message: '请先配置 SQL Server 服务器、数据库名和用户名' - } - } - - const sqlServerService = new SqlServerService({ - server: dbConfig.server, - port: 1433, // Default SQL Server port - user: dbConfig.username, - password: dbConfig.password, - database: dbConfig.database, - options: { - trustServerCertificate: true - } - }) - - try { - await sqlServerService.connect() - await sqlServerService.disconnect() - return { - success: true, - message: 'SQL Server 数据库连接测试成功!' - } - } catch (connError) { - const errorMessage = connError instanceof Error ? connError.message : '连接失败' - return { - success: false, - message: `SQL Server 数据库连接测试失败:${errorMessage}` - } + if (dbConfig.dbType === 'mysql') { + // Test MySQL connection + if (!dbConfig.mysqlHost || !dbConfig.database || !dbConfig.username) { + log.warn('MySQL connection test failed - missing configuration') + return { + success: false, + message: '请先配置 MySQL 主机、数据库名和用户名' } } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - return { - success: false, - message: `数据库连接测试失败:${message}` + + const mysqlService = new MySqlService({ + host: dbConfig.mysqlHost, + port: dbConfig.mysqlPort, + user: dbConfig.username, + password: dbConfig.password, + database: dbConfig.database + }) + + try { + await mysqlService.connect() + await mysqlService.disconnect() + log.info('MySQL connection test successful') + return { + success: true, + message: 'MySQL 数据库连接测试成功!' + } + } catch (connError) { + const errorMessage = connError instanceof Error ? connError.message : '连接失败' + log.error('MySQL connection failed', { error: errorMessage }) + return { + success: false, + message: `MySQL 数据库连接测试失败:${errorMessage}` + } + } + } else { + // Test SQL Server connection + if (!dbConfig.server || !dbConfig.database || !dbConfig.username) { + log.warn('SQL Server connection test failed - missing configuration') + return { + success: false, + message: '请先配置 SQL Server 服务器、数据库名和用户名' + } + } + + const sqlServerService = new SqlServerService({ + server: dbConfig.server, + port: 1433, // Default SQL Server port + user: dbConfig.username, + password: dbConfig.password, + database: dbConfig.database, + options: { + trustServerCertificate: true + } + }) + + try { + await sqlServerService.connect() + await sqlServerService.disconnect() + log.info('SQL Server connection test successful') + return { + success: true, + message: 'SQL Server 数据库连接测试成功!' + } + } catch (connError) { + const errorMessage = connError instanceof Error ? connError.message : '连接失败' + log.error('SQL Server connection failed', { error: errorMessage }) + return { + success: false, + message: `SQL Server 数据库连接测试失败:${errorMessage}` + } } } + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error' + log.error('Database connection test error', { error: message }) + return { + success: false, + message: `数据库连接测试失败:${message}` + } } - ) + }) } diff --git a/src/main/ipc/validation-handler.ts b/src/main/ipc/validation-handler.ts index 271d2ff..c98c166 100644 --- a/src/main/ipc/validation-handler.ts +++ b/src/main/ipc/validation-handler.ts @@ -12,6 +12,7 @@ import { MySqlService } from '../services/database/mysql' import { SqlServerService } from '../services/database/sql-server' import { MaterialsToBeDeletedDAO } from '../services/database/materials-to-be-deleted-dao' import { DiscreteMaterialPlanDAO } from '../services/database/discrete-material-plan-dao' +import { createLogger } from '../services/logger' import type { ValidationRequest, ValidationResponse, @@ -22,6 +23,8 @@ import type { MaterialRecordSummary } from '../types/validation.types' +const log = createLogger('ValidationHandler') + /** * Shared state for Production IDs from extractor page * This is a simple in-memory store for sharing Production IDs between pages @@ -32,7 +35,7 @@ const sharedProductionIds = new Set() * Set shared Production IDs */ export function setSharedProductionIds(ids: string[]): void { - ids.forEach(id => sharedProductionIds.add(id)) + ids.forEach((id) => sharedProductionIds.add(id)) } /** @@ -110,11 +113,11 @@ function getTableName(mysqlTableName: string): string { */ function readProductionIds(filePath: string): string[] { const fs = require('fs') - const content = fs.readFileSync(filePath, 'utf-8') + const content = fs.readFileSync(filePath, 'utf-8') as string return content .split('\n') - .map(line => line.trim()) - .filter(line => line.length > 0) + .map((line: string) => line.trim()) + .filter((line: string) => line.length > 0) } /** @@ -173,10 +176,11 @@ async function getSourceNumbersFromInputs( FROM ${contractTableName} WHERE 总排号 IN (${placeholders}) ` - const contractResult = await (dbService as SqlServerService).queryWithParams(contractSql, params) - const dbOrderNumbers = contractResult.rows.map( - row => row.生产订单号 as string + const contractResult = await (dbService as SqlServerService).queryWithParams( + contractSql, + params ) + const dbOrderNumbers = contractResult.rows.map((row) => row.生产订单号 as string) orderNumbers.push(...dbOrderNumbers) } else { const placeholders = productionIds.map(() => '?').join(',') @@ -186,9 +190,7 @@ async function getSourceNumbersFromInputs( WHERE 总排号 IN (${placeholders}) ` const contractResult = await (dbService as MySqlService).query(contractSql, productionIds) - const dbOrderNumbers = contractResult.rows.map( - row => row.生产订单号 as string - ) + const dbOrderNumbers = contractResult.rows.map((row) => row.生产订单号 as string) orderNumbers.push(...dbOrderNumbers) } } @@ -208,14 +210,11 @@ export function registerValidationHandlers(): void { */ ipcMain.handle( 'validation:validate', - async ( - _event, - request: ValidationRequest - ): Promise => { + async (_event, request: ValidationRequest): Promise => { let dbService: MySqlService | SqlServerService | null = null try { - console.log('[Validation] Starting validation:', request) + log.info('Starting validation', { mode: request.mode }) // Connect to database dbService = await getValidationDatabaseService() @@ -229,7 +228,7 @@ export function registerValidationHandlers(): void { if (request.useSharedProductionIds) { // Use shared Production IDs from extractor page const sharedIds = getSharedProductionIds() - console.log(`[Validation] Using ${sharedIds.length} shared Production IDs`) + log.info(`Using ${sharedIds.length} shared Production IDs`) if (sharedIds.length === 0) { return { @@ -244,19 +243,13 @@ export function registerValidationHandlers(): void { } sourceNumbers = await getSourceNumbersFromInputs(sharedIds, dbService) - console.log( - `[Validation] Got ${sourceNumbers.length} source numbers from shared Production IDs` - ) + log.info(`Got ${sourceNumbers.length} source numbers from shared Production IDs`) } else if (request.productionIdFile) { // Read from file const inputs = readProductionIds(request.productionIdFile) - console.log( - `[Validation] Read ${inputs.length} inputs from file` - ) + log.info(`Read ${inputs.length} inputs from file`) sourceNumbers = await getSourceNumbersFromInputs(inputs, dbService) - console.log( - `[Validation] Got ${sourceNumbers.length} source numbers` - ) + log.info(`Got ${sourceNumbers.length} source numbers`) } } @@ -270,9 +263,7 @@ export function registerValidationHandlers(): void { materialRecords = await materialDao.queryAllDistinctByMaterialCode() } else if (sourceNumbers && sourceNumbers.length > 0) { // Filtered query by source numbers - materialRecords = await materialDao.queryBySourceNumbersDistinct( - sourceNumbers - ) + materialRecords = await materialDao.queryBySourceNumbersDistinct(sourceNumbers) } if (materialRecords.length === 0) { @@ -298,7 +289,7 @@ export function registerValidationHandlers(): void { ? await (dbService as SqlServerService).query(typeKeywordSql) : await (dbService as MySqlService).query(typeKeywordSql) - const typeKeywords = typeKeywordResult.rows.map(row => ({ + const typeKeywords = typeKeywordResult.rows.map((row) => ({ materialName: row.MaterialName as string, managerName: row.ManagerName as string })) @@ -316,10 +307,7 @@ export function registerValidationHandlers(): void { const markedCodesDict = new Map() for (const row of markedResult.rows) { - markedCodesDict.set( - row.MaterialCode as string, - row.ManagerName as string - ) + markedCodesDict.set(row.MaterialCode as string, row.ManagerName as string) } // Match materials @@ -338,10 +326,7 @@ export function registerValidationHandlers(): void { // Priority 2: Match with MaterialsTypeToBeDeleted (MaterialName contains) if (!managerName) { for (const typeKeyword of typeKeywords) { - if ( - typeKeyword.materialName && - materialName.includes(typeKeyword.materialName) - ) { + if (typeKeyword.materialName && materialName.includes(typeKeyword.materialName)) { matchedTypeKeyword = typeKeyword.materialName managerName = typeKeyword.managerName break @@ -360,8 +345,8 @@ export function registerValidationHandlers(): void { }) } - const markedCount = results.filter(r => r.isMarkedForDeletion).length - const matchedCount = results.filter(r => r.managerName).length + const markedCount = results.filter((r) => r.isMarkedForDeletion).length + const matchedCount = results.filter((r) => r.managerName).length return { success: true, @@ -374,7 +359,9 @@ export function registerValidationHandlers(): void { } } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error' - console.error('[Validation] Validation error:', error) + log.error('Validation error', { + error: error instanceof Error ? error.message : String(error) + }) return { success: false, error: `Validation failed: ${message}` @@ -384,7 +371,9 @@ export function registerValidationHandlers(): void { try { await dbService.disconnect() } catch (closeError) { - console.warn('[Validation] Error disconnecting database:', closeError) + log.warn('Error disconnecting database', { + error: closeError instanceof Error ? closeError.message : String(closeError) + }) } } } @@ -398,10 +387,7 @@ export function registerValidationHandlers(): void { */ ipcMain.handle( 'materials:upsertBatch', - async ( - _event, - request: MaterialUpsertBatchRequest - ): Promise => { + async (_event, request: MaterialUpsertBatchRequest): Promise => { try { const dao = new MaterialsToBeDeletedDAO() const stats = await dao.upsertBatch(request.materials) @@ -412,7 +398,9 @@ export function registerValidationHandlers(): void { } } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error' - console.error('[Materials] Upsert batch error:', error) + log.error('Upsert batch error', { + error: error instanceof Error ? error.message : String(error) + }) return { success: false, error: `Upsert failed: ${message}` @@ -426,10 +414,7 @@ export function registerValidationHandlers(): void { */ ipcMain.handle( 'materials:delete', - async ( - _event, - request: MaterialDeleteRequest - ): Promise => { + async (_event, request: MaterialDeleteRequest): Promise => { try { const dao = new MaterialsToBeDeletedDAO() const count = await dao.deleteByMaterialCodes(request.materialCodes) @@ -440,7 +425,7 @@ export function registerValidationHandlers(): void { } } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error' - console.error('[Materials] Delete error:', error) + log.error('Delete error', { error: error instanceof Error ? error.message : String(error) }) return { success: false, error: `Delete failed: ${message}` @@ -452,29 +437,25 @@ export function registerValidationHandlers(): void { /** * Get unique manager names */ - ipcMain.handle( - 'materials:getManagers', - async (_event): Promise<{ managers: string[] }> => { - try { - const dao = new MaterialsToBeDeletedDAO() - const managers = await dao.getManagers() - return { managers } - } catch (error) { - console.error('[Materials] Get managers error:', error) - return { managers: [] } - } + ipcMain.handle('materials:getManagers', async (_event): Promise<{ managers: string[] }> => { + try { + const dao = new MaterialsToBeDeletedDAO() + const managers = await dao.getManagers() + return { managers } + } catch (error) { + log.error('Get managers error', { + error: error instanceof Error ? error.message : String(error) + }) + return { managers: [] } } - ) + }) /** * Get materials by manager */ ipcMain.handle( 'materials:getByManager', - async ( - _event, - managerName: string - ): Promise<{ materials: MaterialRecordSummary[] }> => { + async (_event, managerName: string): Promise<{ materials: MaterialRecordSummary[] }> => { let dbService: MySqlService | SqlServerService | null = null try { @@ -512,25 +493,16 @@ export function registerValidationHandlers(): void { WHERE MaterialCode = ? LIMIT 1 ` - detailResult = await (dbService as MySqlService).query(detailSql, [ - mat.materialCode - ]) + detailResult = await (dbService as MySqlService).query(detailSql, [mat.materialCode]) } enrichedMaterials.push({ materialCode: mat.materialCode, materialName: - detailResult.rows.length > 0 - ? (detailResult.rows[0].MaterialName as string) - : '', + detailResult.rows.length > 0 ? (detailResult.rows[0].MaterialName as string) : '', specification: - detailResult.rows.length > 0 - ? (detailResult.rows[0].Specification as string) - : '', - model: - detailResult.rows.length > 0 - ? (detailResult.rows[0].Model as string) - : '', + detailResult.rows.length > 0 ? (detailResult.rows[0].Specification as string) : '', + model: detailResult.rows.length > 0 ? (detailResult.rows[0].Model as string) : '', managerName: mat.managerName, isMarked: markedCodes.has(mat.materialCode) }) @@ -538,14 +510,18 @@ export function registerValidationHandlers(): void { return { materials: enrichedMaterials } } catch (error) { - console.error('[Materials] Get by manager error:', error) + log.error('Get by manager error', { + error: error instanceof Error ? error.message : String(error) + }) return { materials: [] } } finally { if (dbService) { try { await dbService.disconnect() } catch (closeError) { - console.warn('[Materials] Error disconnecting database:', closeError) + log.warn('Error disconnecting database', { + error: closeError instanceof Error ? closeError.message : String(closeError) + }) } } } @@ -592,25 +568,16 @@ export function registerValidationHandlers(): void { WHERE MaterialCode = ? LIMIT 1 ` - detailResult = await (dbService as MySqlService).query(detailSql, [ - mat.materialCode - ]) + detailResult = await (dbService as MySqlService).query(detailSql, [mat.materialCode]) } enrichedMaterials.push({ materialCode: mat.materialCode, materialName: - detailResult.rows.length > 0 - ? (detailResult.rows[0].MaterialName as string) - : '', + detailResult.rows.length > 0 ? (detailResult.rows[0].MaterialName as string) : '', specification: - detailResult.rows.length > 0 - ? (detailResult.rows[0].Specification as string) - : '', - model: - detailResult.rows.length > 0 - ? (detailResult.rows[0].Model as string) - : '', + detailResult.rows.length > 0 ? (detailResult.rows[0].Specification as string) : '', + model: detailResult.rows.length > 0 ? (detailResult.rows[0].Model as string) : '', managerName: mat.managerName, isMarked: markedCodes.has(mat.materialCode) }) @@ -618,14 +585,18 @@ export function registerValidationHandlers(): void { return { materials: enrichedMaterials } } catch (error) { - console.error('[Materials] Get all error:', error) + log.error('Get all error', { + error: error instanceof Error ? error.message : String(error) + }) return { materials: [] } } finally { if (dbService) { try { await dbService.disconnect() } catch (closeError) { - console.warn('[Materials] Error disconnecting database:', closeError) + log.warn('Error disconnecting database', { + error: closeError instanceof Error ? closeError.message : String(closeError) + }) } } } @@ -635,19 +606,18 @@ export function registerValidationHandlers(): void { /** * Get statistics */ - ipcMain.handle( - 'materials:getStatistics', - async (_event): Promise<{ stats: any }> => { - try { - const dao = new MaterialsToBeDeletedDAO() - const stats = await dao.getStatistics() - return { stats } - } catch (error) { - console.error('[Materials] Get statistics error:', error) - return { stats: null } - } + ipcMain.handle('materials:getStatistics', async (_event): Promise<{ stats: any }> => { + try { + const dao = new MaterialsToBeDeletedDAO() + const stats = await dao.getStatistics() + return { stats } + } catch (error) { + log.error('Get statistics error', { + error: error instanceof Error ? error.message : String(error) + }) + return { stats: null } } - ) + }) /** * Set shared Production IDs from extractor page @@ -655,7 +625,7 @@ export function registerValidationHandlers(): void { ipcMain.handle( 'validation:setSharedProductionIds', async (_event, productionIds: string[]): Promise => { - console.log(`[Validation] Received ${productionIds.length} shared Production IDs`) + log.info(`Received ${productionIds.length} shared Production IDs`) setSharedProductionIds(productionIds) } ) @@ -676,14 +646,18 @@ export function registerValidationHandlers(): void { */ ipcMain.handle( 'validation:getCleanerData', - async (_event): Promise<{ + async ( + _event + ): Promise<{ success: boolean orderNumbers?: string[] materialCodes?: string[] error?: string }> => { let dbService: MySqlService | SqlServerService | null = null - const sessionManager = (await import('../services/user/session-manager')).SessionManager.getInstance() + const sessionManager = ( + await import('../services/user/session-manager') + ).SessionManager.getInstance() try { // Get current user @@ -700,7 +674,7 @@ export function registerValidationHandlers(): void { const dbType = process.env.DB_TYPE?.toLowerCase() const isSqlServer = dbType === 'sqlserver' || dbType === 'mssql' - console.log(`[CleanerData] User: ${username}, isAdmin: ${isAdmin}`) + log.info(`User: ${username}, isAdmin: ${isAdmin}`) // Connect to database dbService = await getValidationDatabaseService() @@ -710,9 +684,9 @@ export function registerValidationHandlers(): void { let orderNumbers: string[] = [] if (sharedIds.length > 0) { - console.log(`[CleanerData] Using ${sharedIds.length} shared Production IDs`) + log.info(`Using ${sharedIds.length} shared Production IDs`) orderNumbers = await getSourceNumbersFromInputs(sharedIds, dbService) - console.log(`[CleanerData] Got ${orderNumbers.length} order numbers`) + log.info(`Got ${orderNumbers.length} order numbers`) } // 2. Get material codes from MaterialsToBeDeleted table @@ -730,10 +704,8 @@ export function registerValidationHandlers(): void { ? await (dbService as SqlServerService).query(allCodesSql) : await (dbService as MySqlService).query(allCodesSql) - materialCodes = result.rows - .map(row => row.MaterialCode as string) - .filter(Boolean) - console.log(`[CleanerData] Admin user: got ${materialCodes.length} materials`) + materialCodes = result.rows.map((row) => row.MaterialCode as string).filter(Boolean) + log.info(`Admin user: got ${materialCodes.length} materials`) } else { // Regular users only see their own materials if (isSqlServer) { @@ -746,9 +718,7 @@ export function registerValidationHandlers(): void { const result = await (dbService as SqlServerService).queryWithParams(userMaterialsSql, { username: { value: username, type: sql.NVarChar } }) - materialCodes = result.rows - .map(row => row.MaterialCode as string) - .filter(Boolean) + materialCodes = result.rows.map((row) => row.MaterialCode as string).filter(Boolean) } else { const userMaterialsSql = ` SELECT MaterialCode @@ -756,11 +726,9 @@ export function registerValidationHandlers(): void { WHERE ManagerName = ? AND MaterialCode IS NOT NULL ` const result = await (dbService as MySqlService).query(userMaterialsSql, [username]) - materialCodes = result.rows - .map(row => row.MaterialCode as string) - .filter(Boolean) + materialCodes = result.rows.map((row) => row.MaterialCode as string).filter(Boolean) } - console.log(`[CleanerData] Regular user: got ${materialCodes.length} materials`) + log.info(`Regular user: got ${materialCodes.length} materials`) } return { @@ -770,7 +738,9 @@ export function registerValidationHandlers(): void { } } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error' - console.error('[CleanerData] Error:', error) + log.error('CleanerData error', { + error: error instanceof Error ? error.message : String(error) + }) return { success: false, error: `获取清理数据失败:${message}` @@ -780,7 +750,9 @@ export function registerValidationHandlers(): void { try { await dbService.disconnect() } catch (closeError) { - console.warn('[CleanerData] Error disconnecting database:', closeError) + log.warn('Error disconnecting database', { + error: closeError instanceof Error ? closeError.message : String(closeError) + }) } } } diff --git a/src/main/schemas/auth.schema.ts b/src/main/schemas/auth.schema.ts new file mode 100644 index 0000000..b626c57 --- /dev/null +++ b/src/main/schemas/auth.schema.ts @@ -0,0 +1,45 @@ +/** + * Zod schemas for Authentication module validation + */ + +import { z } from 'zod' + +/** + * Schema for login request validation + */ +export const LoginRequestSchema = z.object({ + username: z.string().min(1, 'Username is required'), + password: z.string().min(1, 'Password is required') +}) + +export type LoginRequestZod = z.infer + +/** + * Schema for user info validation + */ +export const UserInfoSchema = z.object({ + id: z.number().int().positive(), + username: z.string().min(1), + userType: z.enum(['Admin', 'User', 'Guest']), + computerName: z.string().optional() +}) + +export type UserInfoZod = z.infer + +/** + * Validate login request + */ +export function validateLoginRequest(input: unknown): { + success: boolean + data?: LoginRequestZod + error?: string +} { + const result = LoginRequestSchema.safeParse(input) + if (result.success) { + return { success: true, data: result.data } + } + return { + success: false, + error: result.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join('; ') + } +} diff --git a/src/main/schemas/cleaner.schema.ts b/src/main/schemas/cleaner.schema.ts new file mode 100644 index 0000000..279c519 --- /dev/null +++ b/src/main/schemas/cleaner.schema.ts @@ -0,0 +1,47 @@ +/** + * Zod schemas for Cleaner module validation + */ + +import { z } from 'zod' + +/** + * Schema for cleaner input validation + */ +export const CleanerInputSchema = z.object({ + orderNumbers: z + .array(z.string().min(1, 'Order number cannot be empty')) + .min(1, 'At least one order number is required'), + materialCodes: z.array(z.string().min(1, 'Material code cannot be empty')), + dryRun: z.boolean() + // Note: onProgress is a function, not validated via Zod +}) + +export type CleanerInputZod = z.infer + +/** + * Schema for cleaner result validation + */ +export const CleanerResultSchema = z.object({ + processedCount: z.number().int().nonnegative(), + errors: z.array(z.string()) +}) + +export type CleanerResultZod = z.infer + +/** + * Validate cleaner input + */ +export function validateCleanerInput(input: unknown): { + success: boolean + data?: CleanerInputZod + error?: string +} { + const result = CleanerInputSchema.safeParse(input) + if (result.success) { + return { success: true, data: result.data } + } + return { + success: false, + error: result.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join('; ') + } +} diff --git a/src/main/schemas/extractor.schema.ts b/src/main/schemas/extractor.schema.ts new file mode 100644 index 0000000..f4a913b --- /dev/null +++ b/src/main/schemas/extractor.schema.ts @@ -0,0 +1,46 @@ +/** + * Zod schemas for Extractor module validation + */ + +import { z } from 'zod' + +/** + * Schema for extractor input validation + */ +export const ExtractorInputSchema = z.object({ + orderNumbers: z + .array(z.string().min(1, 'Order number cannot be empty')) + .min(1, 'At least one order number is required'), + batchSize: z.number().int().positive().optional().default(10) + // Note: onProgress is a function, not validated via Zod +}) + +export type ExtractorInputZod = z.infer + +/** + * Schema for extractor result validation + */ +export const ExtractorResultSchema = z.object({ + data: z.array(z.record(z.string(), z.unknown())), + errors: z.array(z.string()) +}) + +export type ExtractorResultZod = z.infer + +/** + * Validate extractor input + */ +export function validateExtractorInput(input: unknown): { + success: boolean + data?: ExtractorInputZod + error?: string +} { + const result = ExtractorInputSchema.safeParse(input) + if (result.success) { + return { success: true, data: result.data } + } + return { + success: false, + error: result.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join('; ') + } +} diff --git a/src/main/services/config/config-manager.ts b/src/main/services/config/config-manager.ts index 8e09dff..ced0fdc 100644 --- a/src/main/services/config/config-manager.ts +++ b/src/main/services/config/config-manager.ts @@ -186,11 +186,21 @@ export class ConfigManager { lines.push('# ERP 系统配置') lines.push('# ===========================') lines.push(`ERP_URL=${this.configCache.get('erp.url') || DEFAULT_SETTINGS.erp.url}`) - lines.push(`ERP_USERNAME=${this.configCache.get('erp.username') || DEFAULT_SETTINGS.erp.username}`) - lines.push(`ERP_PASSWORD=${this.configCache.get('erp.password') || DEFAULT_SETTINGS.erp.password}`) - lines.push(`ERP_HEADLESS=${this.configCache.get('erp.headless') || DEFAULT_SETTINGS.erp.headless}`) - lines.push(`ERP_IGNORE_HTTPS_ERRORS=${this.configCache.get('erp.ignoreHttpsErrors') || DEFAULT_SETTINGS.erp.ignoreHttpsErrors}`) - lines.push(`ERP_AUTO_CLOSE_BROWSER=${this.configCache.get('erp.autoCloseBrowser') || DEFAULT_SETTINGS.erp.autoCloseBrowser}`) + lines.push( + `ERP_USERNAME=${this.configCache.get('erp.username') || DEFAULT_SETTINGS.erp.username}` + ) + lines.push( + `ERP_PASSWORD=${this.configCache.get('erp.password') || DEFAULT_SETTINGS.erp.password}` + ) + lines.push( + `ERP_HEADLESS=${this.configCache.get('erp.headless') || DEFAULT_SETTINGS.erp.headless}` + ) + lines.push( + `ERP_IGNORE_HTTPS_ERRORS=${this.configCache.get('erp.ignoreHttpsErrors') || DEFAULT_SETTINGS.erp.ignoreHttpsErrors}` + ) + lines.push( + `ERP_AUTO_CLOSE_BROWSER=${this.configCache.get('erp.autoCloseBrowser') || DEFAULT_SETTINGS.erp.autoCloseBrowser}` + ) lines.push('') // Database Configuration - SQL Server @@ -210,12 +220,24 @@ export class ConfigManager { lines.push('# ===========================') lines.push('# 数据库配置 - MySQL (切换时使用)') lines.push('# ===========================') - lines.push(`DB_TYPE=${this.configCache.get('database.dbType') || DEFAULT_SETTINGS.database.dbType}`) - lines.push(`DB_NAME=${this.configCache.get('database.database') || DEFAULT_SETTINGS.database.database}`) - lines.push(`DB_USERNAME=${this.configCache.get('database.username') || DEFAULT_SETTINGS.database.username}`) - lines.push(`DB_PASSWORD=${this.configCache.get('database.password') || DEFAULT_SETTINGS.database.password}`) - lines.push(`DB_MYSQL_HOST=${this.configCache.get('database.mysqlHost') || DEFAULT_SETTINGS.database.mysqlHost}`) - lines.push(`DB_MYSQL_PORT=${this.configCache.get('database.mysqlPort') || DEFAULT_SETTINGS.database.mysqlPort}`) + lines.push( + `DB_TYPE=${this.configCache.get('database.dbType') || DEFAULT_SETTINGS.database.dbType}` + ) + lines.push( + `DB_NAME=${this.configCache.get('database.database') || DEFAULT_SETTINGS.database.database}` + ) + lines.push( + `DB_USERNAME=${this.configCache.get('database.username') || DEFAULT_SETTINGS.database.username}` + ) + lines.push( + `DB_PASSWORD=${this.configCache.get('database.password') || DEFAULT_SETTINGS.database.password}` + ) + lines.push( + `DB_MYSQL_HOST=${this.configCache.get('database.mysqlHost') || DEFAULT_SETTINGS.database.mysqlHost}` + ) + lines.push( + `DB_MYSQL_PORT=${this.configCache.get('database.mysqlPort') || DEFAULT_SETTINGS.database.mysqlPort}` + ) lines.push(`DB_MYSQL_CHARSET=utf8mb4`) lines.push('') @@ -233,49 +255,85 @@ export class ConfigManager { lines.push('# ===========================') lines.push('# 路径配置') lines.push('# ===========================') - lines.push(`PATH_DATA_DIR=${this.configCache.get('paths.dataDir') || DEFAULT_SETTINGS.paths.dataDir}`) + lines.push( + `PATH_DATA_DIR=${this.configCache.get('paths.dataDir') || DEFAULT_SETTINGS.paths.dataDir}` + ) lines.push(`PATH_PRODUCTION_ID_FILE=ProductionID.txt`) - lines.push(`PATH_DEFAULT_OUTPUT=${this.configCache.get('paths.defaultOutput') || DEFAULT_SETTINGS.paths.defaultOutput}`) - lines.push(`PATH_VALIDATION_OUTPUT=${this.configCache.get('paths.validationOutput') || DEFAULT_SETTINGS.paths.validationOutput}`) + lines.push( + `PATH_DEFAULT_OUTPUT=${this.configCache.get('paths.defaultOutput') || DEFAULT_SETTINGS.paths.defaultOutput}` + ) + lines.push( + `PATH_VALIDATION_OUTPUT=${this.configCache.get('paths.validationOutput') || DEFAULT_SETTINGS.paths.validationOutput}` + ) lines.push('') // Data Extraction Configuration lines.push('# ===========================') lines.push('# 数据提取配置') lines.push('# ===========================') - lines.push(`EXTRACTION_BATCH_SIZE=${this.configCache.get('extraction.batchSize') || DEFAULT_SETTINGS.extraction.batchSize}`) - lines.push(`EXTRACTION_VERBOSE=${this.configCache.get('extraction.verbose') || DEFAULT_SETTINGS.extraction.verbose}`) - lines.push(`EXTRACTION_AUTO_CONVERT=${this.configCache.get('extraction.autoConvert') || DEFAULT_SETTINGS.extraction.autoConvert}`) - lines.push(`EXTRACTION_MERGE_BATCHES=${this.configCache.get('extraction.mergeBatches') || DEFAULT_SETTINGS.extraction.mergeBatches}`) - lines.push(`EXTRACTION_ENABLE_DB_PERSISTENCE=${this.configCache.get('extraction.enableDbPersistence') || DEFAULT_SETTINGS.extraction.enableDbPersistence}`) + lines.push( + `EXTRACTION_BATCH_SIZE=${this.configCache.get('extraction.batchSize') || DEFAULT_SETTINGS.extraction.batchSize}` + ) + lines.push( + `EXTRACTION_VERBOSE=${this.configCache.get('extraction.verbose') || DEFAULT_SETTINGS.extraction.verbose}` + ) + lines.push( + `EXTRACTION_AUTO_CONVERT=${this.configCache.get('extraction.autoConvert') || DEFAULT_SETTINGS.extraction.autoConvert}` + ) + lines.push( + `EXTRACTION_MERGE_BATCHES=${this.configCache.get('extraction.mergeBatches') || DEFAULT_SETTINGS.extraction.mergeBatches}` + ) + lines.push( + `EXTRACTION_ENABLE_DB_PERSISTENCE=${this.configCache.get('extraction.enableDbPersistence') || DEFAULT_SETTINGS.extraction.enableDbPersistence}` + ) lines.push('') // Validation Configuration lines.push('# ===========================') lines.push('# 校验配置') lines.push('# ===========================') - lines.push(`VALIDATION_DATA_SOURCE=${this.configCache.get('validation.dataSource') || DEFAULT_SETTINGS.validation.dataSource}`) - lines.push(`VALIDATION_USE_DATABASE=${this.configCache.get('validation.useDatabase') || true}`) - lines.push(`VALIDATION_BATCH_SIZE=${this.configCache.get('validation.batchSize') || DEFAULT_SETTINGS.validation.batchSize}`) - lines.push(`VALIDATION_ENABLE_CRUD=${this.configCache.get('validation.enableCrud') || DEFAULT_SETTINGS.validation.enableCrud}`) - lines.push(`VALIDATION_DEFAULT_MANAGER=${this.configCache.get('validation.defaultManager') || DEFAULT_SETTINGS.validation.defaultManager}`) - lines.push(`VALIDATION_MATCH_MODE=${this.configCache.get('validation.matchMode') || DEFAULT_SETTINGS.validation.matchMode}`) + lines.push( + `VALIDATION_DATA_SOURCE=${this.configCache.get('validation.dataSource') || DEFAULT_SETTINGS.validation.dataSource}` + ) + lines.push( + `VALIDATION_USE_DATABASE=${this.configCache.get('validation.useDatabase') || true}` + ) + lines.push( + `VALIDATION_BATCH_SIZE=${this.configCache.get('validation.batchSize') || DEFAULT_SETTINGS.validation.batchSize}` + ) + lines.push( + `VALIDATION_ENABLE_CRUD=${this.configCache.get('validation.enableCrud') || DEFAULT_SETTINGS.validation.enableCrud}` + ) + lines.push( + `VALIDATION_DEFAULT_MANAGER=${this.configCache.get('validation.defaultManager') || DEFAULT_SETTINGS.validation.defaultManager}` + ) + lines.push( + `VALIDATION_MATCH_MODE=${this.configCache.get('validation.matchMode') || DEFAULT_SETTINGS.validation.matchMode}` + ) lines.push('') // UI Configuration lines.push('# ===========================') lines.push('# UI 配置') lines.push('# ===========================') - lines.push(`UI_FONT_FAMILY=${this.configCache.get('ui.fontFamily') || DEFAULT_SETTINGS.ui.fontFamily}`) - lines.push(`UI_FONT_SIZE=${this.configCache.get('ui.fontSize') || DEFAULT_SETTINGS.ui.fontSize}`) - lines.push(`UI_PRODUCTION_ID_INPUT_WIDTH=${this.configCache.get('ui.productionIdInputWidth') || DEFAULT_SETTINGS.ui.productionIdInputWidth}`) + lines.push( + `UI_FONT_FAMILY=${this.configCache.get('ui.fontFamily') || DEFAULT_SETTINGS.ui.fontFamily}` + ) + lines.push( + `UI_FONT_SIZE=${this.configCache.get('ui.fontSize') || DEFAULT_SETTINGS.ui.fontSize}` + ) + lines.push( + `UI_PRODUCTION_ID_INPUT_WIDTH=${this.configCache.get('ui.productionIdInputWidth') || DEFAULT_SETTINGS.ui.productionIdInputWidth}` + ) lines.push('') // Execution Configuration lines.push('# ===========================') lines.push('# 执行配置') lines.push('# ===========================') - lines.push(`EXECUTION_DRYRUN=${this.configCache.get('execution.dryRun') || DEFAULT_SETTINGS.execution.dryRun}`) + lines.push( + `EXECUTION_DRYRUN=${this.configCache.get('execution.dryRun') || DEFAULT_SETTINGS.execution.dryRun}` + ) const content = lines.join('\n') fs.writeFileSync(this.envPath, content, 'utf-8') @@ -296,11 +354,19 @@ export class ConfigManager { username: this.get('ERP_USERNAME', DEFAULT_SETTINGS.erp.username), password: this.get('ERP_PASSWORD', DEFAULT_SETTINGS.erp.password), headless: this.getBoolean('ERP_HEADLESS', DEFAULT_SETTINGS.erp.headless), - ignoreHttpsErrors: this.getBoolean('ERP_IGNORE_HTTPS_ERRORS', DEFAULT_SETTINGS.erp.ignoreHttpsErrors), - autoCloseBrowser: this.getBoolean('ERP_AUTO_CLOSE_BROWSER', DEFAULT_SETTINGS.erp.autoCloseBrowser) + ignoreHttpsErrors: this.getBoolean( + 'ERP_IGNORE_HTTPS_ERRORS', + DEFAULT_SETTINGS.erp.ignoreHttpsErrors + ), + autoCloseBrowser: this.getBoolean( + 'ERP_AUTO_CLOSE_BROWSER', + DEFAULT_SETTINGS.erp.autoCloseBrowser + ) }, database: { - dbType: (this.get('DB_TYPE', DEFAULT_SETTINGS.database.dbType) as DatabaseType) || DEFAULT_SETTINGS.database.dbType, + dbType: + (this.get('DB_TYPE', DEFAULT_SETTINGS.database.dbType) as DatabaseType) || + DEFAULT_SETTINGS.database.dbType, server: this.get('DB_SERVER', DEFAULT_SETTINGS.database.server), mysqlHost: this.get('DB_MYSQL_HOST', DEFAULT_SETTINGS.database.mysqlHost), mysqlPort: this.getNumber('DB_MYSQL_PORT', DEFAULT_SETTINGS.database.mysqlPort), @@ -311,26 +377,53 @@ export class ConfigManager { paths: { dataDir: this.get('PATH_DATA_DIR', DEFAULT_SETTINGS.paths.dataDir), defaultOutput: this.get('PATH_DEFAULT_OUTPUT', DEFAULT_SETTINGS.paths.defaultOutput), - validationOutput: this.get('PATH_VALIDATION_OUTPUT', DEFAULT_SETTINGS.paths.validationOutput) + validationOutput: this.get( + 'PATH_VALIDATION_OUTPUT', + DEFAULT_SETTINGS.paths.validationOutput + ) }, extraction: { batchSize: this.getNumber('EXTRACTION_BATCH_SIZE', DEFAULT_SETTINGS.extraction.batchSize), verbose: this.getBoolean('EXTRACTION_VERBOSE', DEFAULT_SETTINGS.extraction.verbose), - autoConvert: this.getBoolean('EXTRACTION_AUTO_CONVERT', DEFAULT_SETTINGS.extraction.autoConvert), - mergeBatches: this.getBoolean('EXTRACTION_MERGE_BATCHES', DEFAULT_SETTINGS.extraction.mergeBatches), - enableDbPersistence: this.getBoolean('EXTRACTION_ENABLE_DB_PERSISTENCE', DEFAULT_SETTINGS.extraction.enableDbPersistence) + autoConvert: this.getBoolean( + 'EXTRACTION_AUTO_CONVERT', + DEFAULT_SETTINGS.extraction.autoConvert + ), + mergeBatches: this.getBoolean( + 'EXTRACTION_MERGE_BATCHES', + DEFAULT_SETTINGS.extraction.mergeBatches + ), + enableDbPersistence: this.getBoolean( + 'EXTRACTION_ENABLE_DB_PERSISTENCE', + DEFAULT_SETTINGS.extraction.enableDbPersistence + ) }, validation: { - dataSource: (this.get('VALIDATION_DATA_SOURCE', DEFAULT_SETTINGS.validation.dataSource) as ValidationDataSource) || DEFAULT_SETTINGS.validation.dataSource, + dataSource: + (this.get( + 'VALIDATION_DATA_SOURCE', + DEFAULT_SETTINGS.validation.dataSource + ) as ValidationDataSource) || DEFAULT_SETTINGS.validation.dataSource, batchSize: this.getNumber('VALIDATION_BATCH_SIZE', DEFAULT_SETTINGS.validation.batchSize), - matchMode: (this.get('VALIDATION_MATCH_MODE', DEFAULT_SETTINGS.validation.matchMode) as MatchMode) || DEFAULT_SETTINGS.validation.matchMode, - enableCrud: this.getBoolean('VALIDATION_ENABLE_CRUD', DEFAULT_SETTINGS.validation.enableCrud), - defaultManager: this.get('VALIDATION_DEFAULT_MANAGER', DEFAULT_SETTINGS.validation.defaultManager) + matchMode: + (this.get('VALIDATION_MATCH_MODE', DEFAULT_SETTINGS.validation.matchMode) as MatchMode) || + DEFAULT_SETTINGS.validation.matchMode, + enableCrud: this.getBoolean( + 'VALIDATION_ENABLE_CRUD', + DEFAULT_SETTINGS.validation.enableCrud + ), + defaultManager: this.get( + 'VALIDATION_DEFAULT_MANAGER', + DEFAULT_SETTINGS.validation.defaultManager + ) }, ui: { fontFamily: this.get('UI_FONT_FAMILY', DEFAULT_SETTINGS.ui.fontFamily), fontSize: this.getNumber('UI_FONT_SIZE', DEFAULT_SETTINGS.ui.fontSize), - productionIdInputWidth: this.getNumber('UI_PRODUCTION_ID_INPUT_WIDTH', DEFAULT_SETTINGS.ui.productionIdInputWidth) + productionIdInputWidth: this.getNumber( + 'UI_PRODUCTION_ID_INPUT_WIDTH', + DEFAULT_SETTINGS.ui.productionIdInputWidth + ) }, execution: { dryRun: this.getBoolean('EXECUTION_DRYRUN', DEFAULT_SETTINGS.execution.dryRun) diff --git a/src/main/services/database/data-source.ts b/src/main/services/database/data-source.ts new file mode 100644 index 0000000..9e3a284 --- /dev/null +++ b/src/main/services/database/data-source.ts @@ -0,0 +1,97 @@ +/** + * TypeORM Data Source Configuration + * + * Provides a centralized database connection for TypeORM entities. + * Supports both MySQL and SQL Server based on DB_TYPE environment variable. + */ + +import 'reflect-metadata' +import { DataSource, DataSourceOptions } from 'typeorm' + +/** + * Get database type from environment + */ +function getDatabaseType(): 'mysql' | 'mssql' { + const dbType = process.env.DB_TYPE?.toLowerCase() + if (dbType === 'sqlserver' || dbType === 'mssql') { + return 'mssql' + } + return 'mysql' +} + +/** + * Build DataSourceOptions based on database type + */ +function buildDataSourceOptions(): DataSourceOptions { + const type = getDatabaseType() + + const commonOptions: Partial = { + entities: [__dirname + '/entities/*.{ts,js}'], + synchronize: false, // Never auto-sync in production + logging: process.env.NODE_ENV !== 'production' + } + + if (type === 'mssql') { + return { + type: 'mssql', + host: process.env.DB_SERVER || 'localhost', + port: parseInt(process.env.DB_SQLSERVER_PORT || '1433', 10), + username: 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' + }, + ...commonOptions + } as DataSourceOptions + } + + return { + type: 'mysql', + host: process.env.DB_MYSQL_HOST || 'localhost', + port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10), + username: process.env.DB_USERNAME || 'root', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || '', + ...commonOptions + } as DataSourceOptions +} + +/** + * TypeORM DataSource singleton + */ +let dataSource: DataSource | null = null + +/** + * Get or create the DataSource + */ +export function getDataSource(): DataSource { + if (!dataSource) { + dataSource = new DataSource(buildDataSourceOptions()) + } + return dataSource +} + +/** + * Initialize the DataSource + */ +export async function initializeDataSource(): Promise { + const ds = getDataSource() + if (!ds.isInitialized) { + await ds.initialize() + } + return ds +} + +/** + * Destroy the DataSource + */ +export async function destroyDataSource(): Promise { + if (dataSource && dataSource.isInitialized) { + await dataSource.destroy() + dataSource = null + } +} + +export default getDataSource diff --git a/src/main/services/database/discrete-material-plan-dao.ts b/src/main/services/database/discrete-material-plan-dao.ts index e2c9e92..dc5d19a 100644 --- a/src/main/services/database/discrete-material-plan-dao.ts +++ b/src/main/services/database/discrete-material-plan-dao.ts @@ -11,6 +11,9 @@ import { MySqlService } from './mysql' import { SqlServerService } from './sql-server' import sql from 'mssql' +import { createLogger } from '../logger' + +const log = createLogger('DiscreteMaterialPlanDAO') /** * Material plan record interface @@ -171,13 +174,16 @@ export class DiscreteMaterialPlanDAO { const sqlString = `SELECT * FROM ${tableName}` - const result = this.dbType === 'sqlserver' - ? await (dbService as SqlServerService).query(sqlString) - : await (dbService as MySqlService).query(sqlString) + const result = + this.dbType === 'sqlserver' + ? await (dbService as SqlServerService).query(sqlString) + : await (dbService as MySqlService).query(sqlString) return result.rows } catch (error) { - console.error('[DiscreteMaterialPlanDAO] Query all error:', error) + log.error('Query all error', { + error: error instanceof Error ? error.message : String(error) + }) return [] } } @@ -215,13 +221,16 @@ 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 = + this.dbType === 'sqlserver' + ? await (dbService as SqlServerService).query(sqlString) + : await (dbService as MySqlService).query(sqlString) return result.rows } catch (error) { - console.error('[DiscreteMaterialPlanDAO] Query all distinct by material code error:', error) + log.error('Query all distinct by material code error', { + error: error instanceof Error ? error.message : String(error) + }) return [] } } @@ -279,7 +288,9 @@ export class DiscreteMaterialPlanDAO { return allResults } catch (error) { - console.error('[DiscreteMaterialPlanDAO] Query by source numbers error:', error) + log.error('Query by source numbers error', { + error: error instanceof Error ? error.message : String(error) + }) return [] } } @@ -371,7 +382,9 @@ export class DiscreteMaterialPlanDAO { return allResults } catch (error) { - console.error('[DiscreteMaterialPlanDAO] Query by source numbers distinct error:', error) + log.error('Query by source numbers distinct error', { + error: error instanceof Error ? error.message : String(error) + }) return [] } } @@ -410,7 +423,9 @@ export class DiscreteMaterialPlanDAO { return result.rows } } catch (error) { - console.error('[DiscreteMaterialPlanDAO] Query by source number error:', error) + log.error('Query by source number error', { + error: error instanceof Error ? error.message : String(error) + }) return [] } } @@ -451,7 +466,9 @@ export class DiscreteMaterialPlanDAO { return result.rows } } catch (error) { - console.error('[DiscreteMaterialPlanDAO] Query by plan number error:', error) + log.error('Query by plan number error', { + error: error instanceof Error ? error.message : String(error) + }) return [] } } @@ -499,7 +516,9 @@ export class DiscreteMaterialPlanDAO { return result.rows } } catch (error) { - console.error('[DiscreteMaterialPlanDAO] Query by plan numbers error:', error) + log.error('Query by plan numbers error', { + error: error instanceof Error ? error.message : String(error) + }) return [] } } @@ -517,13 +536,16 @@ export class DiscreteMaterialPlanDAO { 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 = + 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 } catch (error) { - console.error('[DiscreteMaterialPlanDAO] Count all error:', error) + log.error('Count all error', { + error: error instanceof Error ? error.message : String(error) + }) return 0 } } @@ -562,7 +584,9 @@ export class DiscreteMaterialPlanDAO { return result.rows.length > 0 ? (result.rows[0].count as number) : 0 } } catch (error) { - console.error('[DiscreteMaterialPlanDAO] Count by plan number error:', error) + log.error('Count by plan number error', { + error: error instanceof Error ? error.message : String(error) + }) return 0 } } @@ -594,9 +618,7 @@ export class DiscreteMaterialPlanDAO { ` const result = await (dbService as SqlServerService).queryWithParams(sqlString, params) - 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(',') @@ -608,9 +630,7 @@ export class DiscreteMaterialPlanDAO { ` const result = await (dbService as MySqlService).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 sqlString = ` @@ -619,16 +639,17 @@ 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 = + 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) { - console.error('[DiscreteMaterialPlanDAO] Get unique material names error:', error) + log.error('Get unique material names error', { + error: error instanceof Error ? error.message : String(error) + }) return [] } } @@ -652,13 +673,16 @@ export class DiscreteMaterialPlanDAO { FROM ${tableName} ` - const result = this.dbType === 'sqlserver' - ? await (dbService as SqlServerService).query(sqlString) - : await (dbService as MySqlService).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] : {} } catch (error) { - console.error('[DiscreteMaterialPlanDAO] Get statistics error:', error) + log.error('Get statistics error', { + error: error instanceof Error ? error.message : String(error) + }) return {} } } diff --git a/src/main/services/database/entities/DiscreteMaterialPlan.ts b/src/main/services/database/entities/DiscreteMaterialPlan.ts new file mode 100644 index 0000000..5d9460c --- /dev/null +++ b/src/main/services/database/entities/DiscreteMaterialPlan.ts @@ -0,0 +1,143 @@ +/** + * TypeORM Entity for DiscreteMaterialPlanData table + */ + +import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm' + +@Entity('DiscreteMaterialPlanData') +export class DiscreteMaterialPlan { + @PrimaryGeneratedColumn() + id!: number + + @Column({ name: 'Factory', type: 'nvarchar', length: 100, nullable: true }) + factory!: string | null + + @Column({ name: 'MaterialStatus', type: 'nvarchar', length: 50, nullable: true }) + materialStatus!: string | null + + @Index() + @Column({ name: 'PlanNumber', type: 'nvarchar', length: 100, nullable: true }) + planNumber!: string | null + + @Index() + @Column({ name: 'SourceNumber', type: 'nvarchar', length: 100, nullable: true }) + sourceNumber!: string | null + + @Column({ name: 'MaterialType', type: 'nvarchar', length: 100, nullable: true }) + materialType!: string | null + + @Column({ name: 'ProductCode', type: 'nvarchar', length: 100, nullable: true }) + productCode!: string | null + + @Column({ name: 'ProductName', type: 'nvarchar', length: 255, nullable: true }) + productName!: string | null + + @Column({ name: 'ProductUnit', type: 'nvarchar', length: 50, nullable: true }) + productUnit!: string | null + + @Column({ name: 'ProductPlanQuantity', type: 'decimal', precision: 18, scale: 4, nullable: true }) + productPlanQuantity!: number | null + + @Column({ name: 'UseDepartment', type: 'nvarchar', length: 100, nullable: true }) + useDepartment!: string | null + + @Column({ name: 'Remark', type: 'nvarchar', length: 500, nullable: true }) + remark!: string | null + + @Column({ name: 'Creator', type: 'nvarchar', length: 100, nullable: true }) + creator!: string | null + + @Column({ name: 'CreateDate', type: 'datetime', nullable: true }) + createDate!: Date | null + + @Column({ name: 'Approver', type: 'nvarchar', length: 100, nullable: true }) + approver!: string | null + + @Column({ name: 'ApproveDate', type: 'datetime', nullable: true }) + approveDate!: Date | null + + @Column({ name: 'SequenceNumber', type: 'int', nullable: true }) + sequenceNumber!: number | null + + @Index() + @Column({ name: 'MaterialCode', type: 'nvarchar', length: 100, nullable: true }) + materialCode!: string | null + + @Column({ name: 'MaterialName', type: 'nvarchar', length: 255, nullable: true }) + materialName!: string | null + + @Column({ name: 'Specification', type: 'nvarchar', length: 255, nullable: true }) + specification!: string | null + + @Column({ name: 'Model', type: 'nvarchar', length: 255, nullable: true }) + model!: string | null + + @Column({ name: 'DrawingNumber', type: 'nvarchar', length: 100, nullable: true }) + drawingNumber!: string | null + + @Column({ name: 'MaterialQuality', type: 'nvarchar', length: 100, nullable: true }) + materialQuality!: string | null + + @Column({ name: 'PlanQuantity', type: 'decimal', precision: 18, scale: 4, nullable: true }) + planQuantity!: number | null + + @Column({ name: 'Unit', type: 'nvarchar', length: 50, nullable: true }) + unit!: string | null + + @Column({ name: 'RequiredDate', type: 'datetime', nullable: true }) + requiredDate!: Date | null + + @Column({ name: 'Warehouse', type: 'nvarchar', length: 100, nullable: true }) + warehouse!: string | null + + @Column({ name: 'UnitUsage', type: 'decimal', precision: 18, scale: 6, nullable: true }) + unitUsage!: number | null + + @Column({ + name: 'CumulativeOutputQuantity', + type: 'decimal', + precision: 18, + scale: 4, + nullable: true + }) + cumulativeOutputQuantity!: number | null + + @Column({ name: 'BOMVersion', type: 'nvarchar', length: 50, nullable: true }) + bomVersion!: string | null +} + +/** + * Material plan record interface for type-safe operations + */ +export interface MaterialPlanRecordData { + id?: number + factory?: string | null + materialStatus?: string | null + planNumber?: string | null + sourceNumber?: string | null + materialType?: string | null + productCode?: string | null + productName?: string | null + productUnit?: string | null + productPlanQuantity?: number | null + useDepartment?: string | null + remark?: string | null + creator?: string | null + createDate?: Date | null + approver?: string | null + approveDate?: Date | null + sequenceNumber?: number | null + materialCode?: string | null + materialName?: string | null + specification?: string | null + model?: string | null + drawingNumber?: string | null + materialQuality?: string | null + planQuantity?: number | null + unit?: string | null + requiredDate?: Date | null + warehouse?: string | null + unitUsage?: number | null + cumulativeOutputQuantity?: number | null + bomVersion?: string | null +} diff --git a/src/main/services/database/entities/MaterialsToBeDeleted.ts b/src/main/services/database/entities/MaterialsToBeDeleted.ts new file mode 100644 index 0000000..c39032e --- /dev/null +++ b/src/main/services/database/entities/MaterialsToBeDeleted.ts @@ -0,0 +1,27 @@ +/** + * TypeORM Entity for MaterialsToBeDeleted table + */ + +import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm' + +@Entity('MaterialsToBeDeleted') +export class MaterialsToBeDeleted { + @PrimaryGeneratedColumn() + id!: number + + @Index({ unique: true }) + @Column({ name: 'MaterialCode', type: 'nvarchar', length: 255, nullable: false }) + materialCode!: string + + @Column({ name: 'ManagerName', type: 'nvarchar', length: 255, nullable: true }) + managerName!: string | null +} + +/** + * Material record interface for type-safe operations + */ +export interface MaterialRecordData { + id?: number + materialCode: string + managerName: string | null +} diff --git a/src/main/services/database/materials-to-be-deleted-dao.ts b/src/main/services/database/materials-to-be-deleted-dao.ts index dae847c..b4f6a7e 100644 --- a/src/main/services/database/materials-to-be-deleted-dao.ts +++ b/src/main/services/database/materials-to-be-deleted-dao.ts @@ -11,6 +11,9 @@ import { MySqlService } from './mysql' import { SqlServerService } from './sql-server' import sql from 'mssql' +import { createLogger } from '../logger' + +const log = createLogger('MaterialsToBeDeletedDAO') /** * Material record interface @@ -132,7 +135,7 @@ export class MaterialsToBeDeletedDAO { */ async upsertMaterial(materialCode: string, managerName: string): Promise { if (!materialCode || !materialCode.trim()) { - console.error('[MaterialsToBeDeletedDAO] MaterialCode cannot be empty') + log.error('MaterialCode cannot be empty') return false } @@ -167,7 +170,9 @@ export class MaterialsToBeDeletedDAO { return true } catch (error) { - console.error('[MaterialsToBeDeletedDAO] Upsert material error:', error) + log.error('Upsert material error', { + error: error instanceof Error ? error.message : String(error) + }) return false } } @@ -177,7 +182,9 @@ export class MaterialsToBeDeletedDAO { * @param materials - List of materials with materialCode and managerName * @returns Statistics object */ - async upsertBatch(materials: { materialCode: string; managerName: string }[]): Promise { + async upsertBatch( + materials: { materialCode: string; managerName: string }[] + ): Promise { if (!materials || materials.length === 0) { return { total: 0, success: 0, failed: 0 } } @@ -227,12 +234,17 @@ export class MaterialsToBeDeletedDAO { stats.success++ } catch (error) { - console.error('[MaterialsToBeDeletedDAO] Error upserting material:', materialCode, error) + log.error('Error upserting material', { + materialCode, + error: error instanceof Error ? error.message : String(error) + }) stats.failed++ } } } catch (error) { - console.error('[MaterialsToBeDeletedDAO] Batch upsert error:', error) + log.error('Batch upsert error', { + error: error instanceof Error ? error.message : String(error) + }) stats.failed = stats.total - stats.success } @@ -256,13 +268,16 @@ 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 = + 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) { - console.error('[MaterialsToBeDeletedDAO] Get all material codes error:', error) + log.error('Get all material codes error', { + error: error instanceof Error ? error.message : String(error) + }) return new Set() } } @@ -283,17 +298,20 @@ 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 = + 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, materialCode: row.MaterialCode as string, managerName: row.ManagerName as string })) } catch (error) { - console.error('[MaterialsToBeDeletedDAO] Get all records error:', error) + log.error('Get all records error', { + error: error instanceof Error ? error.message : String(error) + }) return [] } } @@ -320,7 +338,7 @@ export class MaterialsToBeDeletedDAO { managerName: { value: managerName, type: sql.NVarChar } }) - return result.rows.map(row => ({ + return result.rows.map((row) => ({ id: row.ID as number, materialCode: row.MaterialCode as string, managerName: row.ManagerName as string @@ -335,14 +353,16 @@ export class MaterialsToBeDeletedDAO { const result = await (dbService as MySqlService).query(sqlString, [managerName]) - return result.rows.map(row => ({ + return result.rows.map((row) => ({ id: row.ID as number, materialCode: row.MaterialCode as string, managerName: row.ManagerName as string })) } } catch (error) { - console.error('[MaterialsToBeDeletedDAO] Get materials by manager error:', error) + log.error('Get materials by manager error', { + error: error instanceof Error ? error.message : String(error) + }) return [] } } @@ -363,13 +383,16 @@ 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 = + 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) { - console.error('[MaterialsToBeDeletedDAO] Get managers error:', error) + log.error('Get managers error', { + error: error instanceof Error ? error.message : String(error) + }) return [] } } @@ -427,7 +450,9 @@ export class MaterialsToBeDeletedDAO { } } } catch (error) { - console.error('[MaterialsToBeDeletedDAO] Get record by material code error:', error) + log.error('Get record by material code error', { + error: error instanceof Error ? error.message : String(error) + }) return null } } @@ -467,7 +492,9 @@ export class MaterialsToBeDeletedDAO { return result.rowCount > 0 } } catch (error) { - console.error('[MaterialsToBeDeletedDAO] Delete by material code error:', error) + log.error('Delete by material code error', { + error: error instanceof Error ? error.message : String(error) + }) return false } } @@ -504,7 +531,9 @@ export class MaterialsToBeDeletedDAO { return result.rowCount } } catch (error) { - console.error('[MaterialsToBeDeletedDAO] Delete by manager error:', error) + log.error('Delete by manager error', { + error: error instanceof Error ? error.message : String(error) + }) return 0 } } @@ -520,13 +549,16 @@ export class MaterialsToBeDeletedDAO { const sqlString = `DELETE FROM ${tableName}` - const result = this.dbType === 'sqlserver' - ? await (dbService as SqlServerService).query(sqlString) - : await (dbService as MySqlService).query(sqlString) + const result = + this.dbType === 'sqlserver' + ? await (dbService as SqlServerService).query(sqlString) + : await (dbService as MySqlService).query(sqlString) return result.rowCount } catch (error) { - console.error('[MaterialsToBeDeletedDAO] Delete all materials error:', error) + log.error('Delete all materials error', { + error: error instanceof Error ? error.message : String(error) + }) return 0 } } @@ -579,7 +611,9 @@ export class MaterialsToBeDeletedDAO { } } } catch (error) { - console.error('[MaterialsToBeDeletedDAO] Delete by material codes error:', error) + log.error('Delete by material codes error', { + error: error instanceof Error ? error.message : String(error) + }) } return totalDeleted @@ -622,7 +656,9 @@ export class MaterialsToBeDeletedDAO { return result.rows.length > 0 && (result.rows[0].count as number) > 0 } } catch (error) { - console.error('[MaterialsToBeDeletedDAO] Material exists error:', error) + log.error('Material exists error', { + error: error instanceof Error ? error.message : String(error) + }) return false } } @@ -638,13 +674,16 @@ export class MaterialsToBeDeletedDAO { 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 = + 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 } catch (error) { - console.error('[MaterialsToBeDeletedDAO] Count all error:', error) + log.error('Count all error', { + error: error instanceof Error ? error.message : String(error) + }) return 0 } } @@ -683,7 +722,9 @@ export class MaterialsToBeDeletedDAO { return result.rows.length > 0 ? (result.rows[0].count as number) : 0 } } catch (error) { - console.error('[MaterialsToBeDeletedDAO] Count by manager error:', error) + log.error('Count by manager error', { + error: error instanceof Error ? error.message : String(error) + }) return 0 } } @@ -706,9 +747,10 @@ 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 = + this.dbType === 'sqlserver' + ? await (dbService as SqlServerService).query(statsSql) + : await (dbService as MySqlService).query(statsSql) const stats = statsResult.rows[0] || {} @@ -721,11 +763,12 @@ 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 = + 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 })) @@ -735,7 +778,9 @@ export class MaterialsToBeDeletedDAO { materialsPerManager } } catch (error) { - console.error('[MaterialsToBeDeletedDAO] Get statistics error:', error) + log.error('Get statistics error', { + error: error instanceof Error ? error.message : String(error) + }) return { totalMaterials: 0, uniqueManagers: 0, diff --git a/src/main/services/database/repositories/DiscreteMaterialPlanRepository.ts b/src/main/services/database/repositories/DiscreteMaterialPlanRepository.ts new file mode 100644 index 0000000..ef39dcc --- /dev/null +++ b/src/main/services/database/repositories/DiscreteMaterialPlanRepository.ts @@ -0,0 +1,281 @@ +/** + * Repository for DiscreteMaterialPlan entity + * + * Provides type-safe database operations for discrete material plan data. + */ + +import { DataSource, Repository, In } from 'typeorm' +import { DiscreteMaterialPlan, MaterialPlanRecordData } from '../entities/DiscreteMaterialPlan' +import { getDataSource } from '../data-source' +import { createLogger } from '../../logger' + +const log = createLogger('DiscreteMaterialPlanRepository') + +/** + * DiscreteMaterialPlan Repository class + */ +export class DiscreteMaterialPlanRepository { + private repository: Repository | null = null + private dataSource: DataSource | null = null + + /** + * Get the repository instance + */ + private async getRepository(): Promise> { + if (!this.repository) { + this.dataSource = getDataSource() + if (!this.dataSource.isInitialized) { + await this.dataSource.initialize() + } + this.repository = this.dataSource.getRepository(DiscreteMaterialPlan) + } + return this.repository + } + + /** + * Query all records + */ + async queryAll(): Promise { + try { + const repo = await this.getRepository() + return await repo.find() + } catch (error) { + log.error('Query all failed', { + error: error instanceof Error ? error.message : String(error) + }) + return [] + } + } + + /** + * Query all records with deduplication by MaterialCode + */ + async queryAllDistinctByMaterialCode(): Promise { + try { + const repo = await this.getRepository() + + const query = ` + WITH RankedRecords AS ( + SELECT *, + ROW_NUMBER() OVER ( + PARTITION BY MaterialCode + ORDER BY CreateDate ASC, SequenceNumber ASC + ) AS rn + FROM DiscreteMaterialPlanData + WHERE MaterialCode IS NOT NULL + ) + SELECT * FROM RankedRecords WHERE rn = 1 + ` + + return await repo.query(query) + } catch (error) { + log.error('Query all distinct by material code failed', { + error: error instanceof Error ? error.message : String(error) + }) + return [] + } + } + + /** + * Query by source numbers (production order numbers) + */ + async queryBySourceNumbers(sourceNumbers: string[]): Promise { + if (!sourceNumbers.length) return [] + + try { + const repo = await this.getRepository() + const batchSize = 2000 + const allResults: DiscreteMaterialPlan[] = [] + + for (let i = 0; i < sourceNumbers.length; i += batchSize) { + const batch = sourceNumbers.slice(i, i + batchSize) + const results = await repo.find({ + where: { sourceNumber: In(batch) } + }) + allResults.push(...results) + } + + return allResults + } catch (error) { + log.error('Query by source numbers failed', { + count: sourceNumbers.length, + error: error instanceof Error ? error.message : String(error) + }) + return [] + } + } + + /** + * Query by source numbers with deduplication by MaterialCode + */ + async queryBySourceNumbersDistinct(sourceNumbers: string[]): Promise { + if (!sourceNumbers.length) return [] + + try { + const repo = await this.getRepository() + const batchSize = 2000 + const allResults: DiscreteMaterialPlan[] = [] + + for (let i = 0; i < sourceNumbers.length; i += batchSize) { + const batch = sourceNumbers.slice(i, i + batchSize) + + const query = ` + WITH RankedRecords AS ( + SELECT *, + ROW_NUMBER() OVER ( + PARTITION BY MaterialCode + ORDER BY CreateDate ASC, SequenceNumber ASC + ) AS rn + FROM DiscreteMaterialPlanData + WHERE SourceNumber IN (?) AND MaterialCode IS NOT NULL + ) + SELECT * FROM RankedRecords WHERE rn = 1 + ` + + const results = await repo.query(query, [batch]) + allResults.push(...results) + } + + return allResults + } catch (error) { + log.error('Query by source numbers distinct failed', { + count: sourceNumbers.length, + error: error instanceof Error ? error.message : String(error) + }) + return [] + } + } + + /** + * Query by single source number + */ + async queryBySourceNumber(sourceNumber: string): Promise { + try { + const repo = await this.getRepository() + return await repo.find({ where: { sourceNumber } }) + } catch (error) { + log.error('Query by source number failed', { + sourceNumber, + error: error instanceof Error ? error.message : String(error) + }) + return [] + } + } + + /** + * Query by plan number + */ + async queryByPlanNumber(planNumber: string): Promise { + try { + const repo = await this.getRepository() + return await repo.find({ where: { planNumber } }) + } catch (error) { + log.error('Query by plan number failed', { + planNumber, + error: error instanceof Error ? error.message : String(error) + }) + return [] + } + } + + /** + * Query by multiple plan numbers + */ + async queryByPlanNumbers(planNumbers: string[]): Promise { + if (!planNumbers.length) return [] + + try { + const repo = await this.getRepository() + return await repo.find({ + where: { planNumber: In(planNumbers) } + }) + } catch (error) { + log.error('Query by plan numbers failed', { + count: planNumbers.length, + error: error instanceof Error ? error.message : String(error) + }) + return [] + } + } + + /** + * Count all records + */ + async countAll(): Promise { + try { + const repo = await this.getRepository() + return await repo.count() + } catch { + return 0 + } + } + + /** + * Get unique material names + */ + async getUniqueMaterialNames(sourceNumbers?: string[]): Promise { + try { + const repo = await this.getRepository() + + let query = repo + .createQueryBuilder('m') + .select('DISTINCT m.materialName', 'materialName') + .where('m.materialName IS NOT NULL') + + if (sourceNumbers && sourceNumbers.length > 0) { + query = query.andWhere('m.sourceNumber IN (:...sourceNumbers)', { sourceNumbers }) + } + + const result = await query.orderBy('m.materialName', 'ASC').getRawMany() + return result.map((r) => r.materialName).filter(Boolean) + } catch (error) { + log.error('Get unique material names failed', { + error: error instanceof Error ? error.message : String(error) + }) + return [] + } + } + + /** + * Get statistics + */ + async getStatistics(): Promise<{ + totalRecords: number + uniquePlans: number + uniqueOrders: number + earliestRecord: Date | null + latestRecord: Date | null + }> { + try { + const repo = await this.getRepository() + + const result = await repo + .createQueryBuilder('m') + .select('COUNT(*)', 'totalRecords') + .addSelect('COUNT(DISTINCT m.planNumber)', 'uniquePlans') + .addSelect('COUNT(DISTINCT m.sourceNumber)', 'uniqueOrders') + .addSelect('MIN(m.createDate)', 'earliestRecord') + .addSelect('MAX(m.createDate)', 'latestRecord') + .getRawOne() + + return { + totalRecords: parseInt(result?.totalRecords || '0', 10), + uniquePlans: parseInt(result?.uniquePlans || '0', 10), + uniqueOrders: parseInt(result?.uniqueOrders || '0', 10), + earliestRecord: result?.earliestRecord || null, + latestRecord: result?.latestRecord || null + } + } catch (error) { + log.error('Get statistics failed', { + error: error instanceof Error ? error.message : String(error) + }) + return { + totalRecords: 0, + uniquePlans: 0, + uniqueOrders: 0, + earliestRecord: null, + latestRecord: null + } + } + } +} diff --git a/src/main/services/database/repositories/MaterialsToBeDeletedRepository.ts b/src/main/services/database/repositories/MaterialsToBeDeletedRepository.ts new file mode 100644 index 0000000..bb4e4a9 --- /dev/null +++ b/src/main/services/database/repositories/MaterialsToBeDeletedRepository.ts @@ -0,0 +1,266 @@ +/** + * Repository for MaterialsToBeDeleted entity + * + * Provides type-safe database operations for materials to be deleted. + */ + +import { DataSource, Repository, In } from 'typeorm' +import { MaterialsToBeDeleted, MaterialRecordData } from '../entities/MaterialsToBeDeleted' +import { getDataSource } from '../data-source' +import { createLogger } from '../../logger' + +const log = createLogger('MaterialsToBeDeletedRepository') + +/** + * Upsert statistics + */ +export interface UpsertStats { + total: number + success: number + failed: number +} + +/** + * MaterialsToBeDeleted Repository class + */ +export class MaterialsToBeDeletedRepository { + private repository: Repository | null = null + private dataSource: DataSource | null = null + + /** + * Get the repository instance + */ + private async getRepository(): Promise> { + if (!this.repository) { + this.dataSource = getDataSource() + if (!this.dataSource.isInitialized) { + await this.dataSource.initialize() + } + this.repository = this.dataSource.getRepository(MaterialsToBeDeleted) + } + return this.repository + } + + /** + * Insert or update a single material record + */ + async upsert(materialCode: string, managerName: string | null): Promise { + try { + const repo = await this.getRepository() + + // Use upsert pattern + let entity = await repo.findOne({ where: { materialCode } }) + + if (entity) { + entity.managerName = managerName + } else { + entity = repo.create({ materialCode, managerName }) + } + + await repo.save(entity) + log.debug('Upserted material', { materialCode }) + return true + } catch (error) { + log.error('Upsert material failed', { + materialCode, + error: error instanceof Error ? error.message : String(error) + }) + return false + } + } + + /** + * Insert or update multiple material records in batch + */ + async upsertBatch(materials: MaterialRecordData[]): Promise { + const stats: UpsertStats = { + total: materials.length, + success: 0, + failed: 0 + } + + try { + const repo = await this.getRepository() + + for (const material of materials) { + if (!material.materialCode?.trim()) { + stats.failed++ + continue + } + + try { + let entity = await repo.findOne({ where: { materialCode: material.materialCode } }) + + if (entity) { + entity.managerName = material.managerName + } else { + entity = repo.create({ + materialCode: material.materialCode, + managerName: material.managerName + }) + } + + await repo.save(entity) + stats.success++ + } catch { + stats.failed++ + } + } + + log.info('Batch upsert completed', stats) + return stats + } catch (error) { + log.error('Batch upsert failed', { + error: error instanceof Error ? error.message : String(error) + }) + stats.failed = stats.total - stats.success + return stats + } + } + + /** + * Get all material codes as a set + */ + async getAllMaterialCodes(): Promise> { + try { + const repo = await this.getRepository() + const records = await repo.find({ + select: ['materialCode'], + where: { materialCode: In([]) } // This will be overridden + }) + + // Use query builder for better performance + const result = await repo + .createQueryBuilder('m') + .select('m.materialCode') + .where('m.materialCode IS NOT NULL') + .getMany() + + return new Set(result.map((r) => r.materialCode).filter(Boolean)) + } catch (error) { + log.error('Get all material codes failed', { + error: error instanceof Error ? error.message : String(error) + }) + return new Set() + } + } + + /** + * Get all records + */ + async getAllRecords(): Promise { + try { + const repo = await this.getRepository() + return await repo.find({ + order: { managerName: 'ASC', materialCode: 'ASC' } + }) + } catch (error) { + log.error('Get all records failed', { + error: error instanceof Error ? error.message : String(error) + }) + return [] + } + } + + /** + * Get materials by manager name + */ + async getByManager(managerName: string): Promise { + try { + const repo = await this.getRepository() + return await repo.find({ + where: { managerName }, + order: { materialCode: 'ASC' } + }) + } catch (error) { + log.error('Get by manager failed', { + managerName, + error: error instanceof Error ? error.message : String(error) + }) + return [] + } + } + + /** + * Get unique manager names + */ + async getManagers(): Promise { + try { + const repo = await this.getRepository() + const result = await repo + .createQueryBuilder('m') + .select('DISTINCT m.managerName', 'managerName') + .where('m.managerName IS NOT NULL') + .orderBy('m.managerName', 'ASC') + .getRawMany() + + return result.map((r) => r.managerName).filter(Boolean) + } catch (error) { + log.error('Get managers failed', { + error: error instanceof Error ? error.message : String(error) + }) + return [] + } + } + + /** + * Delete by material code + */ + async deleteByMaterialCode(materialCode: string): Promise { + try { + const repo = await this.getRepository() + const result = await repo.delete({ materialCode }) + return (result.affected ?? 0) > 0 + } catch (error) { + log.error('Delete by material code failed', { + materialCode, + error: error instanceof Error ? error.message : String(error) + }) + return false + } + } + + /** + * Delete multiple materials by codes + */ + async deleteByMaterialCodes(materialCodes: string[]): Promise { + if (!materialCodes.length) return 0 + + try { + const repo = await this.getRepository() + const result = await repo.delete({ materialCode: In(materialCodes) }) + return result.affected ?? 0 + } catch (error) { + log.error('Delete by material codes failed', { + count: materialCodes.length, + error: error instanceof Error ? error.message : String(error) + }) + return 0 + } + } + + /** + * Check if a material exists + */ + async exists(materialCode: string): Promise { + try { + const repo = await this.getRepository() + const count = await repo.count({ where: { materialCode } }) + return count > 0 + } catch { + return false + } + } + + /** + * Count all records + */ + async countAll(): Promise { + try { + const repo = await this.getRepository() + return await repo.count() + } catch { + return 0 + } + } +} diff --git a/src/main/services/erp/ErpBrowserManager.ts b/src/main/services/erp/ErpBrowserManager.ts new file mode 100644 index 0000000..ec3bfe9 --- /dev/null +++ b/src/main/services/erp/ErpBrowserManager.ts @@ -0,0 +1,208 @@ +/** + * ERP Browser Manager + * + * Manages browser lifecycle for ERP automation. + * Separates browser management from authentication logic. + */ + +import { chromium, type Browser, type BrowserContext, type Page } from 'playwright' +import { createLogger } from '../logger' + +const log = createLogger('ErpBrowserManager') + +/** + * Browser configuration options + */ +export interface BrowserConfig { + headless?: boolean + slowMo?: number + viewport?: { width: number; height: number } + ignoreHTTPSErrors?: boolean + acceptDownloads?: boolean +} + +/** + * Default browser configuration + */ +const DEFAULT_CONFIG: Required = { + headless: false, + slowMo: 100, + viewport: { width: 1920, height: 1080 }, + ignoreHTTPSErrors: true, + acceptDownloads: true +} + +/** + * Browser session containing all browser-related objects + */ +export interface BrowserSession { + browser: Browser + context: BrowserContext + page: Page +} + +/** + * ErpBrowserManager class + * Manages browser lifecycle independently from ERP authentication + */ +export class ErpBrowserManager { + private config: Required + private session: BrowserSession | null = null + + constructor(config?: BrowserConfig) { + this.config = { ...DEFAULT_CONFIG, ...config } + } + + /** + * Launch a new browser instance + */ + async launch(): Promise { + if (this.session?.browser?.isConnected()) { + log.debug('Browser already running, returning existing instance') + return this.session.browser + } + + log.info('Launching browser', { headless: this.config.headless }) + + const browser = await chromium.launch({ + headless: this.config.headless, + slowMo: this.config.slowMo, + args: [ + '--ignore-certificate-errors', + '--ignore-ssl-errors', + '--ignore-certificate-errors-spki-list', + '--disable-web-security' + ] + }) + + log.info('Browser launched successfully') + return browser + } + + /** + * Create a new browser context + */ + async createContext(browser?: Browser): Promise { + const browserInstance = browser || (await this.launch()) + + log.debug('Creating browser context') + + const context = await browserInstance.newContext({ + acceptDownloads: this.config.acceptDownloads, + viewport: this.config.viewport, + ignoreHTTPSErrors: this.config.ignoreHTTPSErrors, + javaScriptEnabled: true + }) + + log.debug('Browser context created') + return context + } + + /** + * Create a new page in the context + */ + async createPage(context?: BrowserContext): Promise { + let contextInstance: BrowserContext + + if (context) { + contextInstance = context + } else if (this.session?.context) { + contextInstance = this.session.context + } else { + const browser = await this.launch() + contextInstance = await this.createContext(browser) + } + + log.debug('Creating new page') + const page = await contextInstance.newPage() + log.debug('Page created') + + return page + } + + /** + * Initialize a complete browser session + * This creates browser, context, and page in one call + */ + async initialize(): Promise { + if (this.session) { + log.debug('Returning existing browser session') + return this.session + } + + const browser = await this.launch() + const context = await this.createContext(browser) + const page = await this.createPage(context) + + this.session = { browser, context, page } + log.info('Browser session initialized') + + return this.session + } + + /** + * Get the current session + */ + getSession(): BrowserSession | null { + return this.session + } + + /** + * Check if browser is running + */ + isRunning(): boolean { + return this.session?.browser?.isConnected() ?? false + } + + /** + * Close the browser and cleanup + */ + async close(): Promise { + if (!this.session) { + log.debug('No browser session to close') + return + } + + log.info('Closing browser session') + + try { + if (this.session.context) { + await this.session.context.close() + } + } catch (error) { + log.warn('Error closing context', { + error: error instanceof Error ? error.message : String(error) + }) + } + + try { + if (this.session.browser) { + await this.session.browser.close() + } + } catch (error) { + log.warn('Error closing browser', { + error: error instanceof Error ? error.message : String(error) + }) + } + + this.session = null + log.info('Browser session closed') + } + + /** + * Navigate to a URL + */ + async navigate(url: string, options?: { timeout?: number }): Promise { + const page = this.session?.page + if (!page) { + throw new Error('No page available. Call initialize() first.') + } + + log.info('Navigating to URL', { url }) + await page.goto(url, { timeout: options?.timeout ?? 30000 }) + await page.waitForLoadState('domcontentloaded', { timeout: options?.timeout ?? 10000 }) + log.debug('Page loaded') + } +} + +export default ErpBrowserManager diff --git a/src/main/services/erp/erp-auth.ts b/src/main/services/erp/erp-auth.ts index d1bd161..9c979fd 100644 --- a/src/main/services/erp/erp-auth.ts +++ b/src/main/services/erp/erp-auth.ts @@ -1,5 +1,8 @@ import { chromium, type BrowserContext, type Page } from 'playwright' import type { ErpConfig, ErpSession } from '../../types/erp.types' +import { createLogger } from '../logger' + +const log = createLogger('ErpAuthService') /** * ERP Authentication Service @@ -91,7 +94,7 @@ export class ErpAuthService { try { await page.waitForLoadState('domcontentloaded', { timeout: 10000 }) } catch (e) { - console.log('Page load state check timed out, continuing...') + log.warn('Page load state check timed out, continuing') } // Handle force login confirmation dialog if present (Python: get_by_role("button", name="确定")) @@ -99,15 +102,15 @@ export class ErpAuthService { const confirmBtn = mainFrame.getByRole('button', { name: '确定' }) const count = await confirmBtn.count() if (count > 0) { - console.log('Force login detected, clicking confirm button') + log.info('Force login detected, clicking confirm button') await confirmBtn.first().click() await page.waitForTimeout(2000) } else { - console.log('Normal login, no confirmation dialog') + log.debug('Normal login, no confirmation dialog') } - } catch (e) { + } catch { // No force login dialog, continue - console.log('Normal login, no confirmation dialog') + log.debug('Normal login, no confirmation dialog') } // Create session with mainFrame (Python returns main_frame as part of login result) diff --git a/src/main/services/erp/order-resolver.ts b/src/main/services/erp/order-resolver.ts index 7855a92..bcaf5d4 100644 --- a/src/main/services/erp/order-resolver.ts +++ b/src/main/services/erp/order-resolver.ts @@ -156,7 +156,7 @@ export class OrderNumberResolver { // Update mappings with database results for (const mapping of mappings) { if (mapping.inputType === 'productionId') { - const dbResult = productionIdMappings.find(m => m.input === mapping.input) + const dbResult = productionIdMappings.find((m) => m.input === mapping.input) if (dbResult) { mapping.orderNumber = dbResult.orderNumber mapping.isValid = dbResult.isValid @@ -265,7 +265,7 @@ export class OrderNumberResolver { const message = error instanceof Error ? error.message : '未知数据库错误' // Return all as failed with error - return productionIds.map(pid => ({ + return productionIds.map((pid) => ({ input: pid, productionId: pid, isValid: false, @@ -294,7 +294,7 @@ export class OrderNumberResolver { ` const result = await this.mysqlService.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) { console.warn('[OrderResolver] Failed to verify order numbers:', error) return orderNumbers // Skip verification on error @@ -345,9 +345,7 @@ export class OrderNumberResolver { * @returns List of valid production order numbers */ getValidOrderNumbers(mappings: OrderMapping[]): string[] { - return mappings - .filter(m => m.isValid && m.orderNumber) - .map(m => m.orderNumber!) + return mappings.filter((m) => m.isValid && m.orderNumber).map((m) => m.orderNumber!) } /** @@ -356,8 +354,6 @@ export class OrderNumberResolver { * @returns List of warning messages */ getWarnings(mappings: OrderMapping[]): string[] { - return mappings - .filter(m => !m.isValid && m.error) - .map(m => m.error!) + return mappings.filter((m) => !m.isValid && m.error).map((m) => m.error!) } } diff --git a/src/main/services/logger/index.ts b/src/main/services/logger/index.ts new file mode 100644 index 0000000..4e7d1a3 --- /dev/null +++ b/src/main/services/logger/index.ts @@ -0,0 +1,97 @@ +/** + * Unified logging system using Winston + * Console + File transports with daily rotation + */ + +import winston from 'winston' +import DailyRotateFile from 'winston-daily-rotate-file' +import path from 'path' +import { app } from 'electron' +import fs from 'fs' + +// Get log directory - use app.getPath('logs') in production, or local logs dir in development +function getLogDir(): string { + if (app && app.isReady()) { + return app.getPath('logs') + } + // Fallback for development or before app is ready + const devLogDir = path.join(process.cwd(), 'logs') + if (!fs.existsSync(devLogDir)) { + fs.mkdirSync(devLogDir, { recursive: true }) + } + return devLogDir +} + +// Custom format for console output +const consoleFormat = winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + winston.format.colorize(), + winston.format.printf(({ timestamp, level, message, context, ...meta }) => { + const contextStr = context ? `[${context}]` : '' + const metaStr = Object.keys(meta).length > 0 ? ` ${JSON.stringify(meta)}` : '' + return `${timestamp} [${level}]${contextStr} ${message}${metaStr}` + }) +) + +// Custom format for file output +const fileFormat = winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + winston.format.json() +) + +// Daily rotate file transport configuration +const createFileTransport = (level?: string): DailyRotateFile => { + return new DailyRotateFile({ + filename: path.join(getLogDir(), 'app-%DATE%.log'), + datePattern: 'YYYY-MM-DD', + zippedArchive: true, + maxSize: '20m', + maxFiles: '14d', + level, + format: fileFormat + }) +} + +// Create the logger instance +const logger = winston.createLogger({ + level: process.env.LOG_LEVEL || 'info', + defaultMeta: { service: 'erpauto' }, + transports: [ + // Console transport - always enabled + new winston.transports.Console({ + format: consoleFormat + }), + // File transport for all levels + createFileTransport() + ] +}) + +// Add error-specific file transport in production +if (process.env.NODE_ENV === 'production') { + logger.add( + new DailyRotateFile({ + filename: path.join(getLogDir(), 'error-%DATE%.log'), + datePattern: 'YYYY-MM-DD', + zippedArchive: true, + maxSize: '20m', + maxFiles: '14d', + level: 'error', + format: fileFormat + }) + ) +} + +/** + * Create a child logger with a specific context + * @param context - The context/module name for the logger + * @returns A child logger instance + */ +export function createLogger(context: string): winston.Logger { + return logger.child({ context }) +} + +// Export the main logger for direct use +export default logger + +// Export log level types for convenience +export type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'verbose' diff --git a/src/main/services/user/bip-users-dao.ts b/src/main/services/user/bip-users-dao.ts index 49dff3c..9008e0e 100644 --- a/src/main/services/user/bip-users-dao.ts +++ b/src/main/services/user/bip-users-dao.ts @@ -229,11 +229,12 @@ export class BIPUsersDAO { ORDER BY UserName ` - const result = this.dbType === 'sqlserver' - ? await (dbService as SqlServerService).query(sqlString) - : await (dbService as MySqlService).query(sqlString) + const result = + 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, username: row.UserName as string, userType: row.UserType as 'Admin' | 'User' | 'Guest', diff --git a/src/main/types/errors.ts b/src/main/types/errors.ts new file mode 100644 index 0000000..d6b1c60 --- /dev/null +++ b/src/main/types/errors.ts @@ -0,0 +1,169 @@ +/** + * Custom error types for the application + * Provides structured error handling with codes and context + */ + +/** + * Base error class for all application errors + */ +export abstract class BaseError extends Error { + public readonly code: string + public readonly cause?: Error + + constructor(name: string, message: string, code: string, cause?: Error) { + super(message) + this.name = name + this.code = code + this.cause = cause + + // Maintains proper stack trace for where error was thrown (only in V8) + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor) + } + } + + /** + * Get a JSON representation of the error for logging/serialization + */ + toJSON(): Record { + return { + name: this.name, + message: this.message, + code: this.code, + cause: this.cause?.message + } + } +} + +/** + * Error codes for ERP connection errors + */ +export const ERP_ERROR_CODES = { + CONNECTION_FAILED: 'ERP_CONNECTION_FAILED', + LOGIN_FAILED: 'ERP_LOGIN_FAILED', + TIMEOUT: 'ERP_TIMEOUT', + NAVIGATION_ERROR: 'ERP_NAVIGATION_ERROR', + ELEMENT_NOT_FOUND: 'ERP_ELEMENT_NOT_FOUND', + SESSION_EXPIRED: 'ERP_SESSION_EXPIRED', + BROWSER_CRASH: 'ERP_BROWSER_CRASH' +} as const + +/** + * Error thrown when ERP connection, login, or browser automation fails + */ +export class ErpConnectionError extends BaseError { + constructor( + message: string, + code: (typeof ERP_ERROR_CODES)[keyof typeof ERP_ERROR_CODES] = ERP_ERROR_CODES.CONNECTION_FAILED, + cause?: Error + ) { + super('ErpConnectionError', message, code, cause) + } +} + +/** + * Error codes for database errors + */ +export const DATABASE_ERROR_CODES = { + CONNECTION_FAILED: 'DB_CONNECTION_FAILED', + QUERY_FAILED: 'DB_QUERY_FAILED', + TIMEOUT: 'DB_TIMEOUT', + INVALID_PARAMS: 'DB_INVALID_PARAMS', + RECORD_NOT_FOUND: 'DB_RECORD_NOT_FOUND', + TRANSACTION_FAILED: 'DB_TRANSACTION_FAILED' +} as const + +/** + * Error thrown when database operations fail + */ +export class DatabaseQueryError extends BaseError { + constructor( + message: string, + code: (typeof DATABASE_ERROR_CODES)[keyof typeof DATABASE_ERROR_CODES] = DATABASE_ERROR_CODES.QUERY_FAILED, + cause?: Error + ) { + super('DatabaseQueryError', message, code, cause) + } +} + +/** + * Error codes for validation errors + */ +export const VALIDATION_ERROR_CODES = { + INVALID_INPUT: 'VAL_INVALID_INPUT', + MISSING_REQUIRED: 'VAL_MISSING_REQUIRED', + INVALID_FORMAT: 'VAL_INVALID_FORMAT', + OUT_OF_RANGE: 'VAL_OUT_OF_RANGE', + INVALID_TYPE: 'VAL_INVALID_TYPE' +} as const + +/** + * Error thrown when input validation fails + */ +export class ValidationError extends BaseError { + constructor( + message: string, + code: (typeof VALIDATION_ERROR_CODES)[keyof typeof VALIDATION_ERROR_CODES] = VALIDATION_ERROR_CODES.INVALID_INPUT, + cause?: Error + ) { + super('ValidationError', message, code, cause) + } +} + +/** + * Type guard to check if an error is a BaseError + */ +export function isBaseError(error: unknown): error is BaseError { + return error instanceof BaseError +} + +/** + * Type guard to check if an error is an ErpConnectionError + */ +export function isErpConnectionError(error: unknown): error is ErpConnectionError { + return error instanceof ErpConnectionError +} + +/** + * Type guard to check if an error is a DatabaseQueryError + */ +export function isDatabaseQueryError(error: unknown): error is DatabaseQueryError { + return error instanceof DatabaseQueryError +} + +/** + * Type guard to check if an error is a ValidationError + */ +export function isValidationError(error: unknown): error is ValidationError { + return error instanceof ValidationError +} + +/** + * Get a user-friendly error message from any error type + * In production, generic errors are sanitized to avoid leaking sensitive info + */ +export function getErrorMessage(error: unknown): string { + if (isBaseError(error)) { + // BaseError messages are developer-controlled and safe + return error.message + } + if (error instanceof Error) { + // In production, return a generic message to avoid leaking sensitive info + // (e.g., database connection strings, file paths, server names) + if (process.env.NODE_ENV === 'production') { + return 'An unexpected error occurred' + } + return error.message + } + return 'An unknown error occurred' +} + +/** + * Get error code from any error type + */ +export function getErrorCode(error: unknown): string { + if (isBaseError(error)) { + return error.code + } + return 'UNKNOWN_ERROR' +} diff --git a/src/main/types/settings.types.ts b/src/main/types/settings.types.ts index 05d4933..2e6430e 100644 --- a/src/main/types/settings.types.ts +++ b/src/main/types/settings.types.ts @@ -22,7 +22,11 @@ export type MatchMode = 'substring' | 'exact' /** * Validation data source options */ -export type ValidationDataSource = 'database_full' | 'database_filtered' | 'excel_existing' | 'excel_full' +export type ValidationDataSource = + | 'database_full' + | 'database_filtered' + | 'excel_existing' + | 'excel_full' /** * ERP configuration diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 2e4ee23..d6cfcb5 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -9,7 +9,12 @@ import type { CurrentUserResponse } from '../main/ipc/auth-handler' import type { ValidationRequest, ValidationResponse } from '../main/types/validation.types' -import type { SettingsData, UserType, ConnectionTestResult, SaveSettingsResult } from '../main/types/settings.types' +import type { + SettingsData, + UserType, + ConnectionTestResult, + SaveSettingsResult +} from '../main/types/settings.types' /** * Order number resolver API diff --git a/src/preload/index.ts b/src/preload/index.ts index 211394f..e81a2c5 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -70,18 +70,15 @@ const api = { validate: (request: ValidationRequest) => ipcRenderer.invoke('validation:validate', request), setSharedProductionIds: (productionIds: string[]) => ipcRenderer.invoke('validation:setSharedProductionIds', productionIds), - getSharedProductionIds: () => - ipcRenderer.invoke('validation:getSharedProductionIds'), - getCleanerData: () => - ipcRenderer.invoke('validation:getCleanerData') + getSharedProductionIds: () => ipcRenderer.invoke('validation:getSharedProductionIds'), + getCleanerData: () => ipcRenderer.invoke('validation:getCleanerData') }, // Materials service materials: { upsertBatch: (materials: { materialCode: string; managerName: string }[]) => ipcRenderer.invoke('materials:upsertBatch', { materials }), - delete: (materialCodes: string[]) => - ipcRenderer.invoke('materials:delete', { materialCodes }), + delete: (materialCodes: string[]) => ipcRenderer.invoke('materials:delete', { materialCodes }), getManagers: () => ipcRenderer.invoke('materials:getManagers'), getByManager: (managerName: string) => ipcRenderer.invoke('materials:getByManager', managerName), diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 6489851..f8112a9 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -9,17 +9,11 @@ */ import React, { useState, useEffect } from 'react' -import { - LayoutDashboard, - Download, - Trash2, - Settings, - Database, - User, - LogOut -} from 'lucide-react' +import { LayoutDashboard, Download, Trash2, Settings, Database, User, LogOut } from 'lucide-react' import LoginDialog from './components/LoginDialog' -import UserSelectionDialog, { type UserInfo as SelectedUserInfo } from './components/UserSelectionDialog' +import UserSelectionDialog, { + type UserInfo as SelectedUserInfo +} from './components/UserSelectionDialog' import ExtractorPage from './pages/ExtractorPage' import CleanerPage from './pages/CleanerPage' import SettingsPage from './pages/SettingsPage' @@ -225,7 +219,12 @@ function App(): React.JSX.Element { // Show login dialog if not authenticated if (!isAuthenticated) { - console.log('Render: not authenticated, showLoginDialog:', showLoginDialog, 'computerName:', computerName) + console.log( + 'Render: not authenticated, showLoginDialog:', + showLoginDialog, + 'computerName:', + computerName + ) return ( <> - {errorMessage && ( -
{errorMessage}
- )} + {errorMessage &&
{errorMessage}
} {/* If showLoginDialog is false but not authenticated, show a message */} {!showLoginDialog && !showUserSelection && ( -
-
+
+

欢迎,{currentUser?.username}!

@@ -299,12 +300,11 @@ function App(): React.JSX.Element { const navItems = [ { id: 'extractor', label: '数据提取 (Extractor)', icon: }, { id: 'cleaner', label: '物料验证与清理 (Cleaner)', icon: }, - { id: 'settings', label: '系统设置 (Settings)', icon: }, - ]; + { id: 'settings', label: '系统设置 (Settings)', icon: } + ] return (
- {/* ================= 顶部导航与标题栏 ================= */}
-
setCurrentPage('home')} style={{ WebkitAppRegion: 'no-drag' } as any}> +
setCurrentPage('home')} + style={{ WebkitAppRegion: 'no-drag' } as any} + > ERP Auto
-
-
- v1.0 -
+
v1.0