style: apply formatter to docs, types, and test files
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,20 +11,20 @@
|
|||||||
|
|
||||||
### 失败测试分布
|
### 失败测试分布
|
||||||
|
|
||||||
| 测试文件 | 失败数量 | 根因分类 | 预计工时 |
|
| 测试文件 | 失败数量 | 根因分类 | 预计工时 |
|
||||||
|---------|---------|---------|---------|
|
| -------------------------- | --------------- | ------------------------------ | --------- |
|
||||||
| `logger.test.ts` | 11 failures | Winston format mock + 循环依赖 | 2-3h |
|
| `logger.test.ts` | 11 failures | Winston format mock + 循环依赖 | 2-3h |
|
||||||
| `update-service.test.ts` | 1 failure | Mock 参数不匹配 | 30min |
|
| `update-service.test.ts` | 1 failure | Mock 参数不匹配 | 30min |
|
||||||
| `update-installer.test.ts` | 1 failure | 路径断言错误 | 15min |
|
| `update-installer.test.ts` | 1 failure | 路径断言错误 | 15min |
|
||||||
| **总计** | **13 failures** | - | **~3-4h** |
|
| **总计** | **13 failures** | - | **~3-4h** |
|
||||||
|
|
||||||
### 测试通过率
|
### 测试通过率
|
||||||
|
|
||||||
| 指标 | 当前 | 修复后 |
|
| 指标 | 当前 | 修复后 |
|
||||||
|------|------|--------|
|
| -------- | ------------- | -------------- |
|
||||||
| 失败套件 | 3 suites | 0 suites |
|
| 失败套件 | 3 suites | 0 suites |
|
||||||
| 失败测试 | 13 tests | 0 tests |
|
| 失败测试 | 13 tests | 0 tests |
|
||||||
| 通过率 | 95% (311/327) | 100% (327/327) |
|
| 通过率 | 95% (311/327) | 100% (327/327) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -42,6 +42,7 @@
|
|||||||
#### 问题诊断
|
#### 问题诊断
|
||||||
|
|
||||||
**失败模式**:
|
**失败模式**:
|
||||||
|
|
||||||
```
|
```
|
||||||
TypeError: __vite_ssr_import_0__.default.format(...) is not a function
|
TypeError: __vite_ssr_import_0__.default.format(...) is not a function
|
||||||
at src/main/services/logger/index.ts:114:4
|
at src/main/services/logger/index.ts:114:4
|
||||||
@@ -54,6 +55,7 @@ TypeError: __vite_ssr_import_0__.default.format(...) is not a function
|
|||||||
3. **具体表现**: 第 114 行的 `winston.format()` 链式调用在 mock 环境中返回 undefined
|
3. **具体表现**: 第 114 行的 `winston.format()` 链式调用在 mock 环境中返回 undefined
|
||||||
|
|
||||||
**调用栈**:
|
**调用栈**:
|
||||||
|
|
||||||
```
|
```
|
||||||
logger.test.ts
|
logger.test.ts
|
||||||
→ imports logger.ts
|
→ imports logger.ts
|
||||||
@@ -63,6 +65,7 @@ logger.test.ts
|
|||||||
```
|
```
|
||||||
|
|
||||||
**文件位置**:
|
**文件位置**:
|
||||||
|
|
||||||
- 测试文件:`tests/unit/logger.test.ts`
|
- 测试文件:`tests/unit/logger.test.ts`
|
||||||
- 被 mock 文件:`src/main/services/logger/index.ts:100-116`
|
- 被 mock 文件:`src/main/services/logger/index.ts:100-116`
|
||||||
- Setup mock: `tests/setup.ts` (无 winston mock 冲突)
|
- Setup mock: `tests/setup.ts` (无 winston mock 冲突)
|
||||||
@@ -135,11 +138,13 @@ vi.mock('winston', () => ({
|
|||||||
**方案 B: 将 logger.test.ts 转为集成测试 (2 小时)**
|
**方案 B: 将 logger.test.ts 转为集成测试 (2 小时)**
|
||||||
|
|
||||||
如果 mock 过于复杂,可以考虑:
|
如果 mock 过于复杂,可以考虑:
|
||||||
|
|
||||||
- 使用 vi.resetModules() 确保每次测试都重新加载
|
- 使用 vi.resetModules() 确保每次测试都重新加载
|
||||||
- 使用 vi.mock(importOriginal) 混合真实模块
|
- 使用 vi.mock(importOriginal) 混合真实模块
|
||||||
- 或完全重写测试,只测试 logger 的公共 API
|
- 或完全重写测试,只测试 logger 的公共 API
|
||||||
|
|
||||||
**预期结果**:
|
**预期结果**:
|
||||||
|
|
||||||
- ✅ 18/18 tests passing
|
- ✅ 18/18 tests passing
|
||||||
- ✅ format().combine().timestamp().printf() 链式调用正常工作
|
- ✅ format().combine().timestamp().printf() 链式调用正常工作
|
||||||
- ✅ logger 创建、子 logger、日志输出测试全部通过
|
- ✅ logger 创建、子 logger、日志输出测试全部通过
|
||||||
@@ -165,8 +170,9 @@ vi.mock('winston', () => ({
|
|||||||
**失败测试**: `checks updates for user and auto-downloads available recommendation`
|
**失败测试**: `checks updates for user and auto-downloads available recommendation`
|
||||||
|
|
||||||
**错误信息**:
|
**错误信息**:
|
||||||
|
|
||||||
```
|
```
|
||||||
AssertionError: expected "vi.fn()" to be called with arguments:
|
AssertionError: expected "vi.fn()" to be called with arguments:
|
||||||
['stable/1.1.0.exe', 'preview/1.1.0.exe']
|
['stable/1.1.0.exe', 'preview/1.1.0.exe']
|
||||||
|
|
||||||
Number of calls: 0
|
Number of calls: 0
|
||||||
@@ -175,6 +181,7 @@ Number of calls: 0
|
|||||||
**根因**: Mock 调用参数与实际调用不匹配
|
**根因**: Mock 调用参数与实际调用不匹配
|
||||||
|
|
||||||
**代码位置**:
|
**代码位置**:
|
||||||
|
|
||||||
- 测试文件:`tests/unit/update-service.test.ts:165-175`
|
- 测试文件:`tests/unit/update-service.test.ts:165-175`
|
||||||
- 被测文件:`src/main/services/update/update-service.ts`
|
- 被测文件:`src/main/services/update/update-service.ts`
|
||||||
|
|
||||||
@@ -200,12 +207,14 @@ it('checks updates for user and auto-downloads available recommendation', async
|
|||||||
**步骤 2.2.3**: 更新测试断言
|
**步骤 2.2.3**: 更新测试断言
|
||||||
|
|
||||||
**选项 A: 匹配实际调用**
|
**选项 A: 匹配实际调用**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 如果实际只调用了一个参数
|
// 如果实际只调用了一个参数
|
||||||
expect(mockDownload).toHaveBeenCalledWith('stable/1.1.0.exe')
|
expect(mockDownload).toHaveBeenCalledWith('stable/1.1.0.exe')
|
||||||
```
|
```
|
||||||
|
|
||||||
**选项 B: 使用更松散的断言**
|
**选项 B: 使用更松散的断言**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 如果参数顺序或数量有变化
|
// 如果参数顺序或数量有变化
|
||||||
expect(mockDownload).toHaveBeenCalled()
|
expect(mockDownload).toHaveBeenCalled()
|
||||||
@@ -213,14 +222,12 @@ expect(mockDownload.mock.calls[0]).toContain('stable/1.1.0.exe')
|
|||||||
```
|
```
|
||||||
|
|
||||||
**选项 C: 调整 mock 设置**
|
**选项 C: 调整 mock 设置**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 确保 mock 正确设置
|
// 确保 mock 正确设置
|
||||||
mockDownload.mockClear()
|
mockDownload.mockClear()
|
||||||
// ... 触发动作 ...
|
// ... 触发动作 ...
|
||||||
expect(mockDownload).toHaveBeenCalledWith(
|
expect(mockDownload).toHaveBeenCalledWith(expect.stringContaining('stable'), expect.any(String))
|
||||||
expect.stringContaining('stable'),
|
|
||||||
expect.any(String)
|
|
||||||
)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 成功标准
|
#### 成功标准
|
||||||
@@ -243,8 +250,9 @@ expect(mockDownload).toHaveBeenCalledWith(
|
|||||||
**失败测试**: `builds downloaded package path under userData pending-update`
|
**失败测试**: `builds downloaded package path under userData pending-update`
|
||||||
|
|
||||||
**错误信息**:
|
**错误信息**:
|
||||||
|
|
||||||
```
|
```
|
||||||
AssertionError: expected 'D:\...\test-user-data\pending-update\stable-1.2.3.exe'
|
AssertionError: expected 'D:\...\test-user-data\pending-update\stable-1.2.3.exe'
|
||||||
to contain 'logs\pending-update'
|
to contain 'logs\pending-update'
|
||||||
|
|
||||||
Expected: "logs\pending-update"
|
Expected: "logs\pending-update"
|
||||||
@@ -254,6 +262,7 @@ Received: "D:\...\test-user-data\pending-update\stable-1.2.3.exe"
|
|||||||
**根因**: Electron mock 的 `app.getPath('userData')` 返回 `test-user-data`,但测试期望路径包含 `logs`
|
**根因**: Electron mock 的 `app.getPath('userData')` 返回 `test-user-data`,但测试期望路径包含 `logs`
|
||||||
|
|
||||||
**代码位置**:
|
**代码位置**:
|
||||||
|
|
||||||
- 测试文件:`tests/unit/update-installer.test.ts:13-16`
|
- 测试文件:`tests/unit/update-installer.test.ts:13-16`
|
||||||
- Setup mock: `tests/setup.ts:17-24`
|
- Setup mock: `tests/setup.ts:17-24`
|
||||||
|
|
||||||
@@ -286,6 +295,7 @@ userData: path.join(process.cwd(), 'logs')
|
|||||||
```
|
```
|
||||||
|
|
||||||
**推荐**: 方案 2.3.1 (测试适应 mock)
|
**推荐**: 方案 2.3.1 (测试适应 mock)
|
||||||
|
|
||||||
- 理由:mock 是为了测试隔离,测试应该适应 mock 环境
|
- 理由:mock 是为了测试隔离,测试应该适应 mock 环境
|
||||||
|
|
||||||
#### 成功标准
|
#### 成功标准
|
||||||
@@ -310,6 +320,7 @@ npm run test:run tests/unit/logger.test.ts
|
|||||||
```
|
```
|
||||||
|
|
||||||
**失败时排查**:
|
**失败时排查**:
|
||||||
|
|
||||||
1. 检查 vi.mock 是否在文件顶部 (hoisted)
|
1. 检查 vi.mock 是否在文件顶部 (hoisted)
|
||||||
2. 清除 vitest 缓存:`npx vitest --clearCache`
|
2. 清除 vitest 缓存:`npx vitest --clearCache`
|
||||||
3. 检查是否有多个 winston mock 冲突
|
3. 检查是否有多个 winston mock 冲突
|
||||||
@@ -354,11 +365,11 @@ npm run test:run 2>&1 | Select-String "Test Files.*failed"
|
|||||||
|
|
||||||
### 技术指标
|
### 技术指标
|
||||||
|
|
||||||
| 指标 | 修复前 | 修复后 | 验证命令 |
|
| 指标 | 修复前 | 修复后 | 验证命令 |
|
||||||
|------|-------|-------|---------|
|
| -------- | ------------- | ------------------ | ------------------ |
|
||||||
| 失败套件 | 3 suites | 0 suites | `npm run test:run` |
|
| 失败套件 | 3 suites | 0 suites | `npm run test:run` |
|
||||||
| 失败测试 | 13 tests | 0 tests | `npm run test:run` |
|
| 失败测试 | 13 tests | 0 tests | `npm run test:run` |
|
||||||
| 通过率 | 95% (311/327) | **100%** (327/327) | 测试报告 |
|
| 通过率 | 95% (311/327) | **100%** (327/327) | 测试报告 |
|
||||||
|
|
||||||
### 验收条件
|
### 验收条件
|
||||||
|
|
||||||
@@ -373,11 +384,11 @@ npm run test:run 2>&1 | Select-String "Test Files.*failed"
|
|||||||
|
|
||||||
### 技术风险
|
### 技术风险
|
||||||
|
|
||||||
| 风险 | 可能性 | 影响 | 缓解措施 |
|
| 风险 | 可能性 | 影响 | 缓解措施 |
|
||||||
|------|--------|------|---------|
|
| -------------------- | ------ | ---- | ------------------------------- |
|
||||||
| logger mock 实现复杂 | 中 | 高 | 采用延迟 mock,先跑通一部分测试 |
|
| logger mock 实现复杂 | 中 | 高 | 采用延迟 mock,先跑通一部分测试 |
|
||||||
| 链式调用 mock 不完整 | 高 | 中 | 使用 createFormatFn 工厂函数 |
|
| 链式调用 mock 不完整 | 高 | 中 | 使用 createFormatFn 工厂函数 |
|
||||||
| 循环依赖难解耦 | 低 | 高 | 只修复 mock,不重构依赖关系 |
|
| 循环依赖难解耦 | 低 | 高 | 只修复 mock,不重构依赖关系 |
|
||||||
|
|
||||||
### 时间风险
|
### 时间风险
|
||||||
|
|
||||||
@@ -386,6 +397,7 @@ npm run test:run 2>&1 | Select-String "Test Files.*failed"
|
|||||||
- **保守估计**: 6 小时 (遇到意外问题)
|
- **保守估计**: 6 小时 (遇到意外问题)
|
||||||
|
|
||||||
**风险缓解**: 如果 logger mock 问题超过 3 小时无法解决,考虑:
|
**风险缓解**: 如果 logger mock 问题超过 3 小时无法解决,考虑:
|
||||||
|
|
||||||
1. 暂时跳过 logger.test.ts (保持 95% 通过率)
|
1. 暂时跳过 logger.test.ts (保持 95% 通过率)
|
||||||
2. 先修复简单的 update 测试 (13 failures → 2 failures)
|
2. 先修复简单的 update 测试 (13 failures → 2 failures)
|
||||||
3. 记录问题,后续专门花精力解决
|
3. 记录问题,后续专门花精力解决
|
||||||
@@ -401,6 +413,7 @@ npm run test:run 2>&1 | Select-String "Test Files.*failed"
|
|||||||
**实际工时**: X 小时
|
**实际工时**: X 小时
|
||||||
|
|
||||||
**修复步骤**:
|
**修复步骤**:
|
||||||
|
|
||||||
1. [ ] 诊断 mock 问题
|
1. [ ] 诊断 mock 问题
|
||||||
2. [ ] 实现 formatFn 工厂
|
2. [ ] 实现 formatFn 工厂
|
||||||
3. [ ] 添加所有链式方法
|
3. [ ] 添加所有链式方法
|
||||||
@@ -408,10 +421,12 @@ npm run test:run 2>&1 | Select-String "Test Files.*failed"
|
|||||||
5. [ ] 验证测试通过
|
5. [ ] 验证测试通过
|
||||||
|
|
||||||
**遇到的问题**:
|
**遇到的问题**:
|
||||||
|
|
||||||
- 问题 1: [描述] → 解决方案: [方案]
|
- 问题 1: [描述] → 解决方案: [方案]
|
||||||
- 问题 2: [描述] → 解决方案: [方案]
|
- 问题 2: [描述] → 解决方案: [方案]
|
||||||
|
|
||||||
**关键代码**:
|
**关键代码**:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 最终有效的 mock 实现
|
// 最终有效的 mock 实现
|
||||||
```
|
```
|
||||||
@@ -424,7 +439,8 @@ npm run test:run 2>&1 | Select-String "Test Files.*failed"
|
|||||||
**结束时间**: HH:MM
|
**结束时间**: HH:MM
|
||||||
**实际工时**: X 分钟
|
**实际工时**: X 分钟
|
||||||
|
|
||||||
**修复方式**:
|
**修复方式**:
|
||||||
|
|
||||||
- [ ] 修改断言
|
- [ ] 修改断言
|
||||||
- [ ] 修改 mock 参数
|
- [ ] 修改 mock 参数
|
||||||
- [ ] 其他: [描述]
|
- [ ] 其他: [描述]
|
||||||
@@ -439,7 +455,8 @@ npm run test:run 2>&1 | Select-String "Test Files.*failed"
|
|||||||
**结束时间**: HH:MM
|
**结束时间**: HH:MM
|
||||||
**实际工时**: X 分钟
|
**实际工时**: X 分钟
|
||||||
|
|
||||||
**修复方式**:
|
**修复方式**:
|
||||||
|
|
||||||
- [ ] 修改断言
|
- [ ] 修改断言
|
||||||
- [ ] 修改 mock
|
- [ ] 修改 mock
|
||||||
- [ ] 其他: [描述]
|
- [ ] 其他: [描述]
|
||||||
|
|||||||
@@ -44,12 +44,10 @@ export interface SqlDialect {
|
|||||||
}): string
|
}): string
|
||||||
|
|
||||||
// 分页
|
// 分页
|
||||||
paginate(p: {
|
paginate(p: { sql: string; limit: number; offset?: number; paramIndex: number }): {
|
||||||
sql: string
|
sql: string
|
||||||
limit: number
|
|
||||||
offset?: number
|
|
||||||
paramIndex: number
|
paramIndex: number
|
||||||
}): { sql: string; paramIndex: number }
|
}
|
||||||
|
|
||||||
// 批量限制
|
// 批量限制
|
||||||
maxBatchRows(columnsPerRow: number): number
|
maxBatchRows(columnsPerRow: number): number
|
||||||
@@ -60,13 +58,14 @@ export interface SqlDialect {
|
|||||||
|
|
||||||
新建 `src/main/services/database/dialects/` 目录:
|
新建 `src/main/services/database/dialects/` 目录:
|
||||||
|
|
||||||
| 文件 | 数据库 | param(n) | quoteTableName | currentTimestamp | upsert | paginate |
|
| 文件 | 数据库 | param(n) | quoteTableName | currentTimestamp | upsert | paginate |
|
||||||
|------|--------|----------|---------------|-----------------|--------|----------|
|
| ----------------------- | ---------- | -------- | --------------- | ------------------- | ------------------ | ------------------ |
|
||||||
| `mysql-dialect.ts` | MySQL | `?` | `dbo_Table` | `NOW()` | `ON DUPLICATE KEY` | `LIMIT x OFFSET y` |
|
| `mysql-dialect.ts` | MySQL | `?` | `dbo_Table` | `NOW()` | `ON DUPLICATE KEY` | `LIMIT x OFFSET y` |
|
||||||
| `sqlserver-dialect.ts` | SQL Server | `@p{n}` | `[dbo].[Table]` | `GETDATE()` | `MERGE` | `OFFSET/FETCH` |
|
| `sqlserver-dialect.ts` | SQL Server | `@p{n}` | `[dbo].[Table]` | `GETDATE()` | `MERGE` | `OFFSET/FETCH` |
|
||||||
| `postgresql-dialect.ts` | PostgreSQL | `${n+1}` | `"dbo"."Table"` | `CURRENT_TIMESTAMP` | `ON CONFLICT` | `LIMIT x OFFSET y` |
|
| `postgresql-dialect.ts` | PostgreSQL | `${n+1}` | `"dbo"."Table"` | `CURRENT_TIMESTAMP` | `ON CONFLICT` | `LIMIT x OFFSET y` |
|
||||||
|
|
||||||
方言工厂 `dialects/index.ts`:
|
方言工厂 `dialects/index.ts`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export function createDialect(type: DatabaseType): SqlDialect
|
export function createDialect(type: DatabaseType): SqlDialect
|
||||||
```
|
```
|
||||||
@@ -76,16 +75,19 @@ export function createDialect(type: DatabaseType): SqlDialect
|
|||||||
每个 DAO 新增 `dialect` 成员,替代原有的 `getTableName()`、`buildPlaceholders()` 和所有 `isSqlServer` 分支:
|
每个 DAO 新增 `dialect` 成员,替代原有的 `getTableName()`、`buildPlaceholders()` 和所有 `isSqlServer` 分支:
|
||||||
|
|
||||||
**删除:**
|
**删除:**
|
||||||
|
|
||||||
- `getTableName()` 私有方法
|
- `getTableName()` 私有方法
|
||||||
- `buildPlaceholders()` 私有方法
|
- `buildPlaceholders()` 私有方法
|
||||||
- 所有 `isSqlServer` 局部变量和条件分支
|
- 所有 `isSqlServer` 局部变量和条件分支
|
||||||
- `*_CONFIG` 中的 `TABLE_NAME_SQLSERVER` / `TABLE_NAME_MYSQL` → 合并为 `TABLE_SCHEMA` + `TABLE_NAME`
|
- `*_CONFIG` 中的 `TABLE_NAME_SQLSERVER` / `TABLE_NAME_MYSQL` → 合并为 `TABLE_SCHEMA` + `TABLE_NAME`
|
||||||
|
|
||||||
**新增:**
|
**新增:**
|
||||||
|
|
||||||
- `private dialect: SqlDialect | null = null`
|
- `private dialect: SqlDialect | null = null`
|
||||||
- `private getDialect(): SqlDialect`
|
- `private getDialect(): SqlDialect`
|
||||||
|
|
||||||
**涉及 DAO:**
|
**涉及 DAO:**
|
||||||
|
|
||||||
- `DiscreteMaterialPlanDAO` — 占位符、表名、批量大小
|
- `DiscreteMaterialPlanDAO` — 占位符、表名、批量大小
|
||||||
- `MaterialsToBeDeletedDAO` — 占位符、表名、MERGE/ON DUPLICATE KEY → `upsert()`
|
- `MaterialsToBeDeletedDAO` — 占位符、表名、MERGE/ON DUPLICATE KEY → `upsert()`
|
||||||
- `MaterialsTypeToBeDeletedDAO` — 同上
|
- `MaterialsTypeToBeDeletedDAO` — 同上
|
||||||
@@ -94,6 +96,7 @@ export function createDialect(type: DatabaseType): SqlDialect
|
|||||||
### 4. PostgreSQL 服务层
|
### 4. PostgreSQL 服务层
|
||||||
|
|
||||||
新建 `src/main/services/database/postgresql.ts`:
|
新建 `src/main/services/database/postgresql.ts`:
|
||||||
|
|
||||||
- 使用 `pg` 驱动,`Pool` 连接池
|
- 使用 `pg` 驱动,`Pool` 连接池
|
||||||
- 实现 `IDatabaseService` 接口
|
- 实现 `IDatabaseService` 接口
|
||||||
- `query()` 直接传递参数数组给 `pg`
|
- `query()` 直接传递参数数组给 `pg`
|
||||||
@@ -113,22 +116,22 @@ export function createDialect(type: DatabaseType): SqlDialect
|
|||||||
|
|
||||||
## 改动范围
|
## 改动范围
|
||||||
|
|
||||||
| 层 | 文件 | 动作 |
|
| 层 | 文件 | 动作 |
|
||||||
|---|---|---|
|
| ------- | ---------------------------------------------- | ---- |
|
||||||
| 类型 | `types/database.types.ts` | 修改 |
|
| 类型 | `types/database.types.ts` | 修改 |
|
||||||
| 方言 | `database/dialects/index.ts` | 新建 |
|
| 方言 | `database/dialects/index.ts` | 新建 |
|
||||||
| 方言 | `database/dialects/mysql-dialect.ts` | 新建 |
|
| 方言 | `database/dialects/mysql-dialect.ts` | 新建 |
|
||||||
| 方言 | `database/dialects/sqlserver-dialect.ts` | 新建 |
|
| 方言 | `database/dialects/sqlserver-dialect.ts` | 新建 |
|
||||||
| 方言 | `database/dialects/postgresql-dialect.ts` | 新建 |
|
| 方言 | `database/dialects/postgresql-dialect.ts` | 新建 |
|
||||||
| 服务 | `database/postgresql.ts` | 新建 |
|
| 服务 | `database/postgresql.ts` | 新建 |
|
||||||
| 工厂 | `database/index.ts` | 修改 |
|
| 工厂 | `database/index.ts` | 修改 |
|
||||||
| TypeORM | `database/data-source.ts` | 修改 |
|
| TypeORM | `database/data-source.ts` | 修改 |
|
||||||
| DAO | `database/discrete-material-plan-dao.ts` | 重构 |
|
| DAO | `database/discrete-material-plan-dao.ts` | 重构 |
|
||||||
| DAO | `database/materials-to-be-deleted-dao.ts` | 重构 |
|
| DAO | `database/materials-to-be-deleted-dao.ts` | 重构 |
|
||||||
| DAO | `database/materials-type-to-be-deleted-dao.ts` | 重构 |
|
| DAO | `database/materials-type-to-be-deleted-dao.ts` | 重构 |
|
||||||
| DAO | `database/extractor-operation-history-dao.ts` | 重构 |
|
| DAO | `database/extractor-operation-history-dao.ts` | 重构 |
|
||||||
| 配置 | `config.template.yaml` | 修改 |
|
| 配置 | `config.template.yaml` | 修改 |
|
||||||
| 依赖 | `package.json` | 修改 |
|
| 依赖 | `package.json` | 修改 |
|
||||||
|
|
||||||
共 **4 个新文件 + 10 个修改文件**。
|
共 **4 个新文件 + 10 个修改文件**。
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
## Task 1: SqlDialect Interface & Type Definitions
|
## Task 1: SqlDialect Interface & Type Definitions
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
|
|
||||||
- Create: `src/main/types/sql-dialect.types.ts`
|
- Create: `src/main/types/sql-dialect.types.ts`
|
||||||
- Modify: `src/main/types/database.types.ts:11` (extend `DatabaseType`)
|
- Modify: `src/main/types/database.types.ts:11` (extend `DatabaseType`)
|
||||||
- Modify: `src/main/types/config.schema.ts:15` (extend Zod enum)
|
- Modify: `src/main/types/config.schema.ts:15` (extend Zod enum)
|
||||||
@@ -20,11 +21,13 @@
|
|||||||
**Step 1: Add `'postgresql'` to `DatabaseType`**
|
**Step 1: Add `'postgresql'` to `DatabaseType`**
|
||||||
|
|
||||||
In `src/main/types/database.types.ts:11`, change:
|
In `src/main/types/database.types.ts:11`, change:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export type DatabaseType = 'mysql' | 'sqlserver' | 'postgresql'
|
export type DatabaseType = 'mysql' | 'sqlserver' | 'postgresql'
|
||||||
```
|
```
|
||||||
|
|
||||||
Add after line 104 (after `SqlServerConfig`):
|
Add after line 104 (after `SqlServerConfig`):
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
/**
|
/**
|
||||||
* PostgreSQL-specific configuration
|
* PostgreSQL-specific configuration
|
||||||
@@ -44,11 +47,13 @@ export interface PostgreSqlConfig extends DatabaseConfig {
|
|||||||
In `src/main/types/config.schema.ts`:
|
In `src/main/types/config.schema.ts`:
|
||||||
|
|
||||||
Change line 15:
|
Change line 15:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export const databaseTypeSchema = z.enum(['mysql', 'sqlserver', 'postgresql'])
|
export const databaseTypeSchema = z.enum(['mysql', 'sqlserver', 'postgresql'])
|
||||||
```
|
```
|
||||||
|
|
||||||
Add after `sqlServerConfigSchema` (after line 58):
|
Add after `sqlServerConfigSchema` (after line 58):
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
/**
|
/**
|
||||||
* PostgreSQL 配置 Schema
|
* PostgreSQL 配置 Schema
|
||||||
@@ -65,6 +70,7 @@ export type PostgreSqlConfig = z.infer<typeof postgresqlConfigSchema>
|
|||||||
```
|
```
|
||||||
|
|
||||||
Change `databaseConfigSchema` (line 63):
|
Change `databaseConfigSchema` (line 63):
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export const databaseConfigSchema = z.object({
|
export const databaseConfigSchema = z.object({
|
||||||
activeType: databaseTypeSchema.default('mysql'),
|
activeType: databaseTypeSchema.default('mysql'),
|
||||||
@@ -77,6 +83,7 @@ export const databaseConfigSchema = z.object({
|
|||||||
**Step 3: Create `SqlDialect` interface**
|
**Step 3: Create `SqlDialect` interface**
|
||||||
|
|
||||||
Create `src/main/types/sql-dialect.types.ts`:
|
Create `src/main/types/sql-dialect.types.ts`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
/**
|
/**
|
||||||
* SQL Dialect Abstraction
|
* SQL Dialect Abstraction
|
||||||
@@ -152,12 +159,10 @@ export interface SqlDialect {
|
|||||||
*
|
*
|
||||||
* @returns Object with modified sql and next param index
|
* @returns Object with modified sql and next param index
|
||||||
*/
|
*/
|
||||||
paginate(params: {
|
paginate(params: { sql: string; limit: number; offset?: number; paramIndex: number }): {
|
||||||
sql: string
|
sql: string
|
||||||
limit: number
|
nextParamIndex: number
|
||||||
offset?: number
|
}
|
||||||
paramIndex: number
|
|
||||||
}): { sql: string; nextParamIndex: number }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maximum rows per batch given columns per row
|
* Maximum rows per batch given columns per row
|
||||||
@@ -185,6 +190,7 @@ git commit -m "feat(db): add SqlDialect interface and PostgreSQL type definition
|
|||||||
## Task 2: Implement Three Dialect Classes
|
## Task 2: Implement Three Dialect Classes
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
|
|
||||||
- Create: `src/main/services/database/dialects/mysql-dialect.ts`
|
- Create: `src/main/services/database/dialects/mysql-dialect.ts`
|
||||||
- Create: `src/main/services/database/dialects/sqlserver-dialect.ts`
|
- Create: `src/main/services/database/dialects/sqlserver-dialect.ts`
|
||||||
- Create: `src/main/services/database/dialects/postgresql-dialect.ts`
|
- Create: `src/main/services/database/dialects/postgresql-dialect.ts`
|
||||||
@@ -196,6 +202,7 @@ git commit -m "feat(db): add SqlDialect interface and PostgreSQL type definition
|
|||||||
**Step 1: Write tests for all three dialects**
|
**Step 1: Write tests for all three dialects**
|
||||||
|
|
||||||
Create `tests/unit/dialects/mysql-dialect.test.ts`:
|
Create `tests/unit/dialects/mysql-dialect.test.ts`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { describe, it, expect } from 'vitest'
|
import { describe, it, expect } from 'vitest'
|
||||||
import { MySqlDialect } from '@main/services/database/dialects/mysql-dialect'
|
import { MySqlDialect } from '@main/services/database/dialects/mysql-dialect'
|
||||||
@@ -255,6 +262,7 @@ describe('MySqlDialect', () => {
|
|||||||
```
|
```
|
||||||
|
|
||||||
Create `tests/unit/dialects/sqlserver-dialect.test.ts`:
|
Create `tests/unit/dialects/sqlserver-dialect.test.ts`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { describe, it, expect } from 'vitest'
|
import { describe, it, expect } from 'vitest'
|
||||||
import { SqlServerDialect } from '@main/services/database/dialects/sqlserver-dialect'
|
import { SqlServerDialect } from '@main/services/database/dialects/sqlserver-dialect'
|
||||||
@@ -327,6 +335,7 @@ describe('SqlServerDialect', () => {
|
|||||||
```
|
```
|
||||||
|
|
||||||
Create `tests/unit/dialects/postgresql-dialect.test.ts`:
|
Create `tests/unit/dialects/postgresql-dialect.test.ts`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { describe, it, expect } from 'vitest'
|
import { describe, it, expect } from 'vitest'
|
||||||
import { PostgreSqlDialect } from '@main/services/database/dialects/postgresql-dialect'
|
import { PostgreSqlDialect } from '@main/services/database/dialects/postgresql-dialect'
|
||||||
@@ -393,6 +402,7 @@ Expected: FAIL — modules not found
|
|||||||
**Step 3: Implement MySqlDialect**
|
**Step 3: Implement MySqlDialect**
|
||||||
|
|
||||||
Create `src/main/services/database/dialects/mysql-dialect.ts`:
|
Create `src/main/services/database/dialects/mysql-dialect.ts`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import type { SqlDialect } from '../../../types/sql-dialect.types'
|
import type { SqlDialect } from '../../../types/sql-dialect.types'
|
||||||
import type { DatabaseType } from '../../../types/database.types'
|
import type { DatabaseType } from '../../../types/database.types'
|
||||||
@@ -437,12 +447,10 @@ export class MySqlDialect implements SqlDialect {
|
|||||||
return { sql, nextParamIndex: p.startParamIndex + p.allColumns.length }
|
return { sql, nextParamIndex: p.startParamIndex + p.allColumns.length }
|
||||||
}
|
}
|
||||||
|
|
||||||
paginate(p: {
|
paginate(p: { sql: string; limit: number; offset?: number; paramIndex: number }): {
|
||||||
sql: string
|
sql: string
|
||||||
limit: number
|
nextParamIndex: number
|
||||||
offset?: number
|
} {
|
||||||
paramIndex: number
|
|
||||||
}): { sql: string; nextParamIndex: number } {
|
|
||||||
let sql = p.sql
|
let sql = p.sql
|
||||||
if (p.offset !== undefined) {
|
if (p.offset !== undefined) {
|
||||||
sql += ` LIMIT ${p.limit} OFFSET ${p.offset}`
|
sql += ` LIMIT ${p.limit} OFFSET ${p.offset}`
|
||||||
@@ -461,6 +469,7 @@ export class MySqlDialect implements SqlDialect {
|
|||||||
**Step 4: Implement SqlServerDialect**
|
**Step 4: Implement SqlServerDialect**
|
||||||
|
|
||||||
Create `src/main/services/database/dialects/sqlserver-dialect.ts`:
|
Create `src/main/services/database/dialects/sqlserver-dialect.ts`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import type { SqlDialect } from '../../../types/sql-dialect.types'
|
import type { SqlDialect } from '../../../types/sql-dialect.types'
|
||||||
import type { DatabaseType } from '../../../types/database.types'
|
import type { DatabaseType } from '../../../types/database.types'
|
||||||
@@ -490,21 +499,15 @@ export class SqlServerDialect implements SqlDialect {
|
|||||||
allColumns: string[]
|
allColumns: string[]
|
||||||
startParamIndex: number
|
startParamIndex: number
|
||||||
}): { sql: string; nextParamIndex: number } {
|
}): { sql: string; nextParamIndex: number } {
|
||||||
const sourceValues = p.allColumns
|
const sourceValues = p.allColumns.map((_, i) => `@p${p.startParamIndex + i}`).join(', ')
|
||||||
.map((_, i) => `@p${p.startParamIndex + i}`)
|
|
||||||
.join(', ')
|
|
||||||
const sourceColumns = p.allColumns.join(', ')
|
const sourceColumns = p.allColumns.join(', ')
|
||||||
const keyMatch = p.keyColumns
|
const keyMatch = p.keyColumns.map((col) => `target.${col} = source.${col}`).join(' AND ')
|
||||||
.map((col) => `target.${col} = source.${col}`)
|
|
||||||
.join(' AND ')
|
|
||||||
const updateSet = p.allColumns
|
const updateSet = p.allColumns
|
||||||
.filter((col) => !p.keyColumns.includes(col))
|
.filter((col) => !p.keyColumns.includes(col))
|
||||||
.map((col) => `${col} = source.${col}`)
|
.map((col) => `${col} = source.${col}`)
|
||||||
.join(', ')
|
.join(', ')
|
||||||
const insertColumns = p.allColumns.join(', ')
|
const insertColumns = p.allColumns.join(', ')
|
||||||
const insertValues = p.allColumns
|
const insertValues = p.allColumns.map((col) => `source.${col}`).join(', ')
|
||||||
.map((col) => `source.${col}`)
|
|
||||||
.join(', ')
|
|
||||||
|
|
||||||
const sql = `
|
const sql = `
|
||||||
MERGE ${p.table} AS target
|
MERGE ${p.table} AS target
|
||||||
@@ -516,12 +519,10 @@ export class SqlServerDialect implements SqlDialect {
|
|||||||
return { sql, nextParamIndex: p.startParamIndex + p.allColumns.length }
|
return { sql, nextParamIndex: p.startParamIndex + p.allColumns.length }
|
||||||
}
|
}
|
||||||
|
|
||||||
paginate(p: {
|
paginate(p: { sql: string; limit: number; offset?: number; paramIndex: number }): {
|
||||||
sql: string
|
sql: string
|
||||||
limit: number
|
nextParamIndex: number
|
||||||
offset?: number
|
} {
|
||||||
paramIndex: number
|
|
||||||
}): { sql: string; nextParamIndex: number } {
|
|
||||||
let sql = p.sql
|
let sql = p.sql
|
||||||
let nextIndex = p.paramIndex
|
let nextIndex = p.paramIndex
|
||||||
|
|
||||||
@@ -547,6 +548,7 @@ export class SqlServerDialect implements SqlDialect {
|
|||||||
**Step 5: Implement PostgreSqlDialect**
|
**Step 5: Implement PostgreSqlDialect**
|
||||||
|
|
||||||
Create `src/main/services/database/dialects/postgresql-dialect.ts`:
|
Create `src/main/services/database/dialects/postgresql-dialect.ts`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import type { SqlDialect } from '../../../types/sql-dialect.types'
|
import type { SqlDialect } from '../../../types/sql-dialect.types'
|
||||||
import type { DatabaseType } from '../../../types/database.types'
|
import type { DatabaseType } from '../../../types/database.types'
|
||||||
@@ -577,9 +579,7 @@ export class PostgreSqlDialect implements SqlDialect {
|
|||||||
allColumns: string[]
|
allColumns: string[]
|
||||||
startParamIndex: number
|
startParamIndex: number
|
||||||
}): { sql: string; nextParamIndex: number } {
|
}): { sql: string; nextParamIndex: number } {
|
||||||
const placeholders = p.allColumns
|
const placeholders = p.allColumns.map((_, i) => `$${p.startParamIndex + i + 1}`).join(', ')
|
||||||
.map((_, i) => `$${p.startParamIndex + i + 1}`)
|
|
||||||
.join(', ')
|
|
||||||
const columns = p.allColumns.join(', ')
|
const columns = p.allColumns.join(', ')
|
||||||
const conflictKeys = p.keyColumns.map((k) => `"${k}"`).join(', ')
|
const conflictKeys = p.keyColumns.map((k) => `"${k}"`).join(', ')
|
||||||
const updateSet = p.allColumns
|
const updateSet = p.allColumns
|
||||||
@@ -595,12 +595,10 @@ export class PostgreSqlDialect implements SqlDialect {
|
|||||||
return { sql, nextParamIndex: p.startParamIndex + p.allColumns.length }
|
return { sql, nextParamIndex: p.startParamIndex + p.allColumns.length }
|
||||||
}
|
}
|
||||||
|
|
||||||
paginate(p: {
|
paginate(p: { sql: string; limit: number; offset?: number; paramIndex: number }): {
|
||||||
sql: string
|
sql: string
|
||||||
limit: number
|
nextParamIndex: number
|
||||||
offset?: number
|
} {
|
||||||
paramIndex: number
|
|
||||||
}): { sql: string; nextParamIndex: number } {
|
|
||||||
let sql = p.sql
|
let sql = p.sql
|
||||||
if (p.offset !== undefined) {
|
if (p.offset !== undefined) {
|
||||||
sql += ` LIMIT ${p.limit} OFFSET ${p.offset}`
|
sql += ` LIMIT ${p.limit} OFFSET ${p.offset}`
|
||||||
@@ -619,6 +617,7 @@ export class PostgreSqlDialect implements SqlDialect {
|
|||||||
**Step 6: Create dialect factory index**
|
**Step 6: Create dialect factory index**
|
||||||
|
|
||||||
Create `src/main/services/database/dialects/index.ts`:
|
Create `src/main/services/database/dialects/index.ts`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export { MySqlDialect } from './mysql-dialect'
|
export { MySqlDialect } from './mysql-dialect'
|
||||||
export { SqlServerDialect } from './sqlserver-dialect'
|
export { SqlServerDialect } from './sqlserver-dialect'
|
||||||
@@ -659,12 +658,14 @@ git commit -m "feat(db): implement SqlDialect abstraction with MySQL, SQL Server
|
|||||||
## Task 3: PostgreSqlService Implementation
|
## Task 3: PostgreSqlService Implementation
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
|
|
||||||
- Create: `src/main/services/database/postgresql.ts`
|
- Create: `src/main/services/database/postgresql.ts`
|
||||||
- Test: `tests/unit/postgresql.test.ts`
|
- Test: `tests/unit/postgresql.test.ts`
|
||||||
|
|
||||||
**Step 1: Write tests**
|
**Step 1: Write tests**
|
||||||
|
|
||||||
Create `tests/unit/postgresql.test.ts`:
|
Create `tests/unit/postgresql.test.ts`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { describe, it, expect, beforeEach } from 'vitest'
|
import { describe, it, expect, beforeEach } from 'vitest'
|
||||||
import { PostgreSqlService } from '@main/services/database/postgresql'
|
import { PostgreSqlService } from '@main/services/database/postgresql'
|
||||||
@@ -743,6 +744,7 @@ Run: `npm install pg && npm install -D @types/pg`
|
|||||||
**Step 4: Implement PostgreSqlService**
|
**Step 4: Implement PostgreSqlService**
|
||||||
|
|
||||||
Create `src/main/services/database/postgresql.ts`:
|
Create `src/main/services/database/postgresql.ts`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Pool } from 'pg'
|
import { Pool } from 'pg'
|
||||||
import type {
|
import type {
|
||||||
@@ -902,6 +904,7 @@ git commit -m "feat(db): add PostgreSqlService with pg driver"
|
|||||||
## Task 4: Refactor DiscreteMaterialPlanDAO
|
## Task 4: Refactor DiscreteMaterialPlanDAO
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
|
|
||||||
- Modify: `src/main/services/database/discrete-material-plan-dao.ts`
|
- Modify: `src/main/services/database/discrete-material-plan-dao.ts`
|
||||||
- Modify: `src/main/services/database/materials-to-be-deleted-dao.ts`
|
- Modify: `src/main/services/database/materials-to-be-deleted-dao.ts`
|
||||||
- Modify: `src/main/services/database/materials-type-to-be-deleted-dao.ts`
|
- Modify: `src/main/services/database/materials-type-to-be-deleted-dao.ts`
|
||||||
@@ -914,11 +917,13 @@ This is the largest task. Each DAO follows the same pattern — replace `isSqlSe
|
|||||||
**Step 1: Add dialect import and member**
|
**Step 1: Add dialect import and member**
|
||||||
|
|
||||||
At top of file, add import:
|
At top of file, add import:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { createDialect, type SqlDialect } from './dialects'
|
import { createDialect, type SqlDialect } from './dialects'
|
||||||
```
|
```
|
||||||
|
|
||||||
In class body, add:
|
In class body, add:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
private dialect: SqlDialect | null = null
|
private dialect: SqlDialect | null = null
|
||||||
|
|
||||||
@@ -933,6 +938,7 @@ private getDialect(): SqlDialect {
|
|||||||
**Step 2: Remove `getTableName()` and `buildPlaceholders()` methods**
|
**Step 2: Remove `getTableName()` and `buildPlaceholders()` methods**
|
||||||
|
|
||||||
Replace with:
|
Replace with:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
private getTableName(): string {
|
private getTableName(): string {
|
||||||
return this.getDialect().quoteTableName('dbo', 'DiscreteMaterialPlanData')
|
return this.getDialect().quoteTableName('dbo', 'DiscreteMaterialPlanData')
|
||||||
@@ -944,11 +950,14 @@ Delete `buildPlaceholders()` entirely — replaced by `dialect.params()`.
|
|||||||
**Step 3: Replace all `isSqlServer` local variables**
|
**Step 3: Replace all `isSqlServer` local variables**
|
||||||
|
|
||||||
Replace patterns like:
|
Replace patterns like:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const isSqlServer = dbService.type === 'sqlserver'
|
const isSqlServer = dbService.type === 'sqlserver'
|
||||||
const placeholder = isSqlServer ? '@p0' : '?'
|
const placeholder = isSqlServer ? '@p0' : '?'
|
||||||
```
|
```
|
||||||
|
|
||||||
With:
|
With:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const dialect = this.getDialect()
|
const dialect = this.getDialect()
|
||||||
const placeholder = dialect.param(0)
|
const placeholder = dialect.param(0)
|
||||||
@@ -961,6 +970,7 @@ Replace `isSqlServer ? '@p0' : '?'` single placeholders with `dialect.param(0)`.
|
|||||||
**Step 4: Refactor `buildRowValues()`**
|
**Step 4: Refactor `buildRowValues()`**
|
||||||
|
|
||||||
Change from:
|
Change from:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
if (isSqlServer) {
|
if (isSqlServer) {
|
||||||
return `@p${values.length - 1}`
|
return `@p${values.length - 1}`
|
||||||
@@ -968,7 +978,9 @@ if (isSqlServer) {
|
|||||||
return '?'
|
return '?'
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
To:
|
To:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
return this.getDialect().param(values.length - 1)
|
return this.getDialect().param(values.length - 1)
|
||||||
```
|
```
|
||||||
@@ -976,13 +988,16 @@ return this.getDialect().param(values.length - 1)
|
|||||||
**Step 5: Refactor `batchInsert()` batch size**
|
**Step 5: Refactor `batchInsert()` batch size**
|
||||||
|
|
||||||
Change from:
|
Change from:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const isSqlServer = dbService.type === 'sqlserver'
|
const isSqlServer = dbService.type === 'sqlserver'
|
||||||
const effectiveBatchSize = isSqlServer
|
const effectiveBatchSize = isSqlServer
|
||||||
? Math.min(batchSize, Math.floor(sqlServerMaxParams / columnsPerRow))
|
? Math.min(batchSize, Math.floor(sqlServerMaxParams / columnsPerRow))
|
||||||
: batchSize
|
: batchSize
|
||||||
```
|
```
|
||||||
|
|
||||||
To:
|
To:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const dialect = this.getDialect()
|
const dialect = this.getDialect()
|
||||||
const effectiveBatchSize = Math.min(batchSize, dialect.maxBatchRows(columnsPerRow))
|
const effectiveBatchSize = Math.min(batchSize, dialect.maxBatchRows(columnsPerRow))
|
||||||
@@ -995,6 +1010,7 @@ Same pattern as above, plus refactor UPSERT methods:
|
|||||||
**Step 6: Replace MERGE/ON DUPLICATE KEY with `dialect.upsert()`**
|
**Step 6: Replace MERGE/ON DUPLICATE KEY with `dialect.upsert()`**
|
||||||
|
|
||||||
In `upsertMaterial()`, replace the entire if/else block:
|
In `upsertMaterial()`, replace the entire if/else block:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
if (isSqlServer) {
|
if (isSqlServer) {
|
||||||
// MERGE ...
|
// MERGE ...
|
||||||
@@ -1002,7 +1018,9 @@ if (isSqlServer) {
|
|||||||
// ON DUPLICATE KEY UPDATE ...
|
// ON DUPLICATE KEY UPDATE ...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
With:
|
With:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const dialect = this.getDialect()
|
const dialect = this.getDialect()
|
||||||
const { sql: sqlString } = dialect.upsert({
|
const { sql: sqlString } = dialect.upsert({
|
||||||
@@ -1034,6 +1052,7 @@ Additionally, refactor `updateMaterial()` — the entire if/else block that dupl
|
|||||||
Plus specific changes:
|
Plus specific changes:
|
||||||
|
|
||||||
In `insertBatchRecords()`, replace the if/else with:
|
In `insertBatchRecords()`, replace the if/else with:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const dialect = this.getDialect()
|
const dialect = this.getDialect()
|
||||||
const sqlString = `
|
const sqlString = `
|
||||||
@@ -1045,6 +1064,7 @@ const sqlString = `
|
|||||||
```
|
```
|
||||||
|
|
||||||
In `getBatches()`, replace pagination logic with:
|
In `getBatches()`, replace pagination logic with:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
if (options?.limit) {
|
if (options?.limit) {
|
||||||
const result = dialect.paginate({
|
const result = dialect.paginate({
|
||||||
@@ -1075,6 +1095,7 @@ git commit -m "refactor(db): replace isSqlServer checks with SqlDialect abstract
|
|||||||
## Task 5: Database Factory & Config Integration
|
## Task 5: Database Factory & Config Integration
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
|
|
||||||
- Modify: `src/main/services/database/index.ts`
|
- Modify: `src/main/services/database/index.ts`
|
||||||
- Modify: `src/main/services/config/config-manager.ts:47-69,302-308`
|
- Modify: `src/main/services/config/config-manager.ts:47-69,302-308`
|
||||||
- Modify: `src/main/services/database/data-source.ts`
|
- Modify: `src/main/services/database/data-source.ts`
|
||||||
@@ -1085,12 +1106,14 @@ git commit -m "refactor(db): replace isSqlServer checks with SqlDialect abstract
|
|||||||
In `src/main/services/database/index.ts`:
|
In `src/main/services/database/index.ts`:
|
||||||
|
|
||||||
Add imports:
|
Add imports:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { PostgreSqlService } from './postgresql'
|
import { PostgreSqlService } from './postgresql'
|
||||||
import type { PostgreSqlConfig } from '../../types/database.types'
|
import type { PostgreSqlConfig } from '../../types/database.types'
|
||||||
```
|
```
|
||||||
|
|
||||||
Add config factory:
|
Add config factory:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export function createPostgreSqlConfig(): PostgreSqlConfig {
|
export function createPostgreSqlConfig(): PostgreSqlConfig {
|
||||||
const configManager = ConfigManager.getInstance()
|
const configManager = ConfigManager.getInstance()
|
||||||
@@ -1107,6 +1130,7 @@ export function createPostgreSqlConfig(): PostgreSqlConfig {
|
|||||||
```
|
```
|
||||||
|
|
||||||
Update `create()` function (line 89):
|
Update `create()` function (line 89):
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
if (dbType === 'postgresql') {
|
if (dbType === 'postgresql') {
|
||||||
log.info('Creating PostgreSQL database service')
|
log.info('Creating PostgreSQL database service')
|
||||||
@@ -1121,6 +1145,7 @@ if (dbType === 'postgresql') {
|
|||||||
```
|
```
|
||||||
|
|
||||||
Add to re-exports:
|
Add to re-exports:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export { PostgreSqlService } from './postgresql'
|
export { PostgreSqlService } from './postgresql'
|
||||||
```
|
```
|
||||||
@@ -1128,6 +1153,7 @@ export { PostgreSqlService } from './postgresql'
|
|||||||
**Step 2: Update ConfigManager defaults**
|
**Step 2: Update ConfigManager defaults**
|
||||||
|
|
||||||
In `src/main/services/config/config-manager.ts`, add to `DEFAULT_CONFIG.database` (after line 69):
|
In `src/main/services/config/config-manager.ts`, add to `DEFAULT_CONFIG.database` (after line 69):
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
postgresql: {
|
postgresql: {
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
@@ -1140,6 +1166,7 @@ postgresql: {
|
|||||||
```
|
```
|
||||||
|
|
||||||
Update `getActiveDatabaseConfig()` (line 302-308):
|
Update `getActiveDatabaseConfig()` (line 302-308):
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
public getActiveDatabaseConfig(): MySqlConfig | SqlServerConfig | PostgreSqlConfig {
|
public getActiveDatabaseConfig(): MySqlConfig | SqlServerConfig | PostgreSqlConfig {
|
||||||
const { activeType, mysql, sqlserver, postgresql } = this.config.database
|
const { activeType, mysql, sqlserver, postgresql } = this.config.database
|
||||||
@@ -1156,19 +1183,24 @@ public getActiveDatabaseConfig(): MySqlConfig | SqlServerConfig | PostgreSqlConf
|
|||||||
In `src/main/services/database/data-source.ts`:
|
In `src/main/services/database/data-source.ts`:
|
||||||
|
|
||||||
Update `getDatabaseType()`:
|
Update `getDatabaseType()`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function getDatabaseType(): 'mysql' | 'mssql' | 'postgres' {
|
function getDatabaseType(): 'mysql' | 'mssql' | 'postgres' {
|
||||||
const configManager = ConfigManager.getInstance()
|
const configManager = ConfigManager.getInstance()
|
||||||
const dbType = configManager.getDatabaseType()
|
const dbType = configManager.getDatabaseType()
|
||||||
switch (dbType) {
|
switch (dbType) {
|
||||||
case 'sqlserver': return 'mssql'
|
case 'sqlserver':
|
||||||
case 'postgresql': return 'postgres'
|
return 'mssql'
|
||||||
default: return 'mysql'
|
case 'postgresql':
|
||||||
|
return 'postgres'
|
||||||
|
default:
|
||||||
|
return 'mysql'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Add postgres branch in `buildDataSourceOptions()`:
|
Add postgres branch in `buildDataSourceOptions()`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
if (type === 'mssql') {
|
if (type === 'mssql') {
|
||||||
// ... existing mssql config
|
// ... existing mssql config
|
||||||
@@ -1191,17 +1223,19 @@ if (type === 'mssql') {
|
|||||||
**Step 4: Update config template**
|
**Step 4: Update config template**
|
||||||
|
|
||||||
In `config.template.yaml`, add after sqlserver section:
|
In `config.template.yaml`, add after sqlserver section:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
postgresql:
|
postgresql:
|
||||||
host: <PG_HOST>
|
host: <PG_HOST>
|
||||||
port: 5432
|
port: 5432
|
||||||
database: <DATABASE_NAME>
|
database: <DATABASE_NAME>
|
||||||
username: <USERNAME>
|
username: <USERNAME>
|
||||||
password: <PASSWORD>
|
password: <PASSWORD>
|
||||||
maxPoolSize: 10
|
maxPoolSize: 10
|
||||||
```
|
```
|
||||||
|
|
||||||
Update header comment to mention postgresql:
|
Update header comment to mention postgresql:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# 3. 设置 database.activeType 为 mysql、sqlserver 或 postgresql
|
# 3. 设置 database.activeType 为 mysql、sqlserver 或 postgresql
|
||||||
```
|
```
|
||||||
@@ -1228,6 +1262,7 @@ git commit -m "feat(db): integrate PostgreSQL into factory, config, and TypeORM
|
|||||||
## Task 6: Verification & Smoke Test
|
## Task 6: Verification & Smoke Test
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
|
|
||||||
- Test: Manual integration verification
|
- Test: Manual integration verification
|
||||||
|
|
||||||
**Step 1: Run full typecheck**
|
**Step 1: Run full typecheck**
|
||||||
@@ -1252,6 +1287,7 @@ Run: `npm run format`
|
|||||||
**Step 5: Verify config.yaml can be parsed with postgresql section**
|
**Step 5: Verify config.yaml can be parsed with postgresql section**
|
||||||
|
|
||||||
Create a temporary test that validates the config schema accepts postgresql:
|
Create a temporary test that validates the config schema accepts postgresql:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// In a scratch test file
|
// In a scratch test file
|
||||||
import { validateConfig } from '../../src/main/types/config.schema'
|
import { validateConfig } from '../../src/main/types/config.schema'
|
||||||
@@ -1262,7 +1298,13 @@ const config = {
|
|||||||
activeType: 'postgresql',
|
activeType: 'postgresql',
|
||||||
mysql: { host: 'localhost', port: 3306, database: 'test', username: 'root', password: '' },
|
mysql: { host: 'localhost', port: 3306, database: 'test', username: 'root', password: '' },
|
||||||
sqlserver: { server: 'localhost', port: 1433, database: 'test', username: 'sa', password: '' },
|
sqlserver: { server: 'localhost', port: 1433, database: 'test', username: 'sa', password: '' },
|
||||||
postgresql: { host: '192.168.31.83', port: 5432, database: 'CompanyDB', username: 'admin', password: 'test' }
|
postgresql: {
|
||||||
|
host: '192.168.31.83',
|
||||||
|
port: 5432,
|
||||||
|
database: 'CompanyDB',
|
||||||
|
username: 'admin',
|
||||||
|
password: 'test'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
paths: { dataDir: './data/' },
|
paths: { dataDir: './data/' },
|
||||||
extraction: {},
|
extraction: {},
|
||||||
@@ -1287,13 +1329,13 @@ git commit -m "chore: format and verify PostgreSQL integration"
|
|||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
| Task | Description | New Files | Modified Files |
|
| Task | Description | New Files | Modified Files |
|
||||||
|------|-------------|-----------|---------------|
|
| ---- | ------------------------------------- | ----------- | ---------------- |
|
||||||
| 1 | Types & SqlDialect interface | 1 | 2 |
|
| 1 | Types & SqlDialect interface | 1 | 2 |
|
||||||
| 2 | Three dialect implementations + tests | 4 + 3 tests | 0 |
|
| 2 | Three dialect implementations + tests | 4 + 3 tests | 0 |
|
||||||
| 3 | PostgreSqlService + test | 1 + 1 test | 1 (package.json) |
|
| 3 | PostgreSqlService + test | 1 + 1 test | 1 (package.json) |
|
||||||
| 4 | Refactor all 4 DAOs | 0 | 4 |
|
| 4 | Refactor all 4 DAOs | 0 | 4 |
|
||||||
| 5 | Factory, config, TypeORM integration | 0 | 4 |
|
| 5 | Factory, config, TypeORM integration | 0 | 4 |
|
||||||
| 6 | Verification | 0 | 0 |
|
| 6 | Verification | 0 | 0 |
|
||||||
|
|
||||||
**Total:** 6 tasks, ~14 files touched (9 new, 10 modified), 5 commits
|
**Total:** 6 tasks, ~14 files touched (9 new, 10 modified), 5 commits
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ export function setupProcessGuards(): void {
|
|||||||
|
|
||||||
process.on('unhandledRejection', (reason) => {
|
process.on('unhandledRejection', (reason) => {
|
||||||
const errorMeta =
|
const errorMeta =
|
||||||
reason instanceof Error
|
reason instanceof Error ? { error: serializeError(reason) } : { reason: String(reason) }
|
||||||
? { error: serializeError(reason) }
|
|
||||||
: { reason: String(reason) }
|
|
||||||
logger.error('Unhandled Rejection', errorMeta)
|
logger.error('Unhandled Rejection', errorMeta)
|
||||||
logAudit('SYSTEM_ERROR', 'system', {
|
logAudit('SYSTEM_ERROR', 'system', {
|
||||||
username: 'system',
|
username: 'system',
|
||||||
|
|||||||
@@ -64,12 +64,10 @@ export interface SqlDialect {
|
|||||||
* MySQL/PostgreSQL: LIMIT x OFFSET y
|
* MySQL/PostgreSQL: LIMIT x OFFSET y
|
||||||
* SQL Server: OFFSET x ROWS FETCH NEXT y ROWS ONLY
|
* SQL Server: OFFSET x ROWS FETCH NEXT y ROWS ONLY
|
||||||
*/
|
*/
|
||||||
paginate(params: {
|
paginate(params: { sql: string; limit: number; offset?: number; paramIndex: number }): {
|
||||||
sql: string
|
sql: string
|
||||||
limit: number
|
nextParamIndex: number
|
||||||
offset?: number
|
}
|
||||||
paramIndex: number
|
|
||||||
}): { sql: string; nextParamIndex: number }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maximum rows per batch given columns per row
|
* Maximum rows per batch given columns per row
|
||||||
|
|||||||
@@ -21,16 +21,20 @@ describe('Cleaner Service (Integration)', () => {
|
|||||||
const hasCredentials = !!(config.url && config.username && config.password)
|
const hasCredentials = !!(config.url && config.username && config.password)
|
||||||
|
|
||||||
describe('Dry-run mode', () => {
|
describe('Dry-run mode', () => {
|
||||||
it.skipIf(!hasCredentials)('should initialize with dry-run mode', async () => {
|
it.skipIf(!hasCredentials)(
|
||||||
const authService = new ErpAuthService(config)
|
'should initialize with dry-run mode',
|
||||||
await authService.login()
|
async () => {
|
||||||
|
const authService = new ErpAuthService(config)
|
||||||
|
await authService.login()
|
||||||
|
|
||||||
const cleaner = new CleanerService(authService, { dryRun: true })
|
const cleaner = new CleanerService(authService, { dryRun: true })
|
||||||
|
|
||||||
expect(cleaner.isDryRun()).toBe(true)
|
expect(cleaner.isDryRun()).toBe(true)
|
||||||
|
|
||||||
await authService.close()
|
await authService.close()
|
||||||
}, 30000)
|
},
|
||||||
|
30000
|
||||||
|
)
|
||||||
|
|
||||||
it.skipIf(!hasCredentials)(
|
it.skipIf(!hasCredentials)(
|
||||||
'should track materials to delete without actually deleting (dry-run)',
|
'should track materials to delete without actually deleting (dry-run)',
|
||||||
@@ -83,115 +87,131 @@ describe('Cleaner Service (Integration)', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('Order processing', () => {
|
describe('Order processing', () => {
|
||||||
it.skipIf(!hasCredentials)('should process single order and return details', async () => {
|
it.skipIf(!hasCredentials)(
|
||||||
const authService = new ErpAuthService(config)
|
'should process single order and return details',
|
||||||
await authService.login()
|
async () => {
|
||||||
|
const authService = new ErpAuthService(config)
|
||||||
|
await authService.login()
|
||||||
|
|
||||||
const orderContent = await fs.readFile(productionIdFile, 'utf-8')
|
const orderContent = await fs.readFile(productionIdFile, 'utf-8')
|
||||||
const orderNumbers = orderContent
|
const orderNumbers = orderContent
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map((line) => line.trim())
|
.map((line) => line.trim())
|
||||||
.filter((line) => line.length > 0)
|
.filter((line) => line.length > 0)
|
||||||
.slice(0, 1) // Test single order
|
.slice(0, 1) // Test single order
|
||||||
|
|
||||||
const cleaner = new CleanerService(authService, { dryRun: true })
|
const cleaner = new CleanerService(authService, { dryRun: true })
|
||||||
|
|
||||||
const result = await cleaner.clean({
|
const result = await cleaner.clean({
|
||||||
orderNumbers,
|
orderNumbers,
|
||||||
materialCodes: [], // Empty list - nothing to delete
|
materialCodes: [], // Empty list - nothing to delete
|
||||||
dryRun: true
|
dryRun: true
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(result.ordersProcessed).toBe(1)
|
expect(result.ordersProcessed).toBe(1)
|
||||||
expect(result.details).toHaveLength(1)
|
expect(result.details).toHaveLength(1)
|
||||||
expect(result.details[0].orderNumber).toBe(orderNumbers[0])
|
expect(result.details[0].orderNumber).toBe(orderNumbers[0])
|
||||||
|
|
||||||
await authService.close()
|
await authService.close()
|
||||||
}, 60000)
|
},
|
||||||
|
60000
|
||||||
|
)
|
||||||
|
|
||||||
it.skipIf(!hasCredentials)('should handle order with "审批通过" status', async () => {
|
it.skipIf(!hasCredentials)(
|
||||||
const authService = new ErpAuthService(config)
|
'should handle order with "审批通过" status',
|
||||||
await authService.login()
|
async () => {
|
||||||
|
const authService = new ErpAuthService(config)
|
||||||
|
await authService.login()
|
||||||
|
|
||||||
const orderContent = await fs.readFile(productionIdFile, 'utf-8')
|
const orderContent = await fs.readFile(productionIdFile, 'utf-8')
|
||||||
const orderNumbers = orderContent
|
const orderNumbers = orderContent
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map((line) => line.trim())
|
.map((line) => line.trim())
|
||||||
.filter((line) => line.length > 0)
|
.filter((line) => line.length > 0)
|
||||||
.slice(0, 1)
|
.slice(0, 1)
|
||||||
|
|
||||||
const cleaner = new CleanerService(authService, { dryRun: true })
|
const cleaner = new CleanerService(authService, { dryRun: true })
|
||||||
|
|
||||||
const result = await cleaner.clean({
|
const result = await cleaner.clean({
|
||||||
orderNumbers,
|
orderNumbers,
|
||||||
materialCodes: [],
|
materialCodes: [],
|
||||||
dryRun: true
|
dryRun: true
|
||||||
})
|
})
|
||||||
|
|
||||||
// Order details should include status information
|
// Order details should include status information
|
||||||
const detail = result.details[0]
|
const detail = result.details[0]
|
||||||
console.log(
|
console.log(
|
||||||
`Order ${detail.orderNumber} - Materials deleted: ${detail.materialsDeleted}, Skipped: ${detail.materialsSkipped}`
|
`Order ${detail.orderNumber} - Materials deleted: ${detail.materialsDeleted}, Skipped: ${detail.materialsSkipped}`
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(detail).toBeDefined()
|
expect(detail).toBeDefined()
|
||||||
|
|
||||||
await authService.close()
|
await authService.close()
|
||||||
}, 60000)
|
},
|
||||||
|
60000
|
||||||
|
)
|
||||||
|
|
||||||
it.skipIf(!hasCredentials)('should handle multiple orders with progress callback', async () => {
|
it.skipIf(!hasCredentials)(
|
||||||
const authService = new ErpAuthService(config)
|
'should handle multiple orders with progress callback',
|
||||||
await authService.login()
|
async () => {
|
||||||
|
const authService = new ErpAuthService(config)
|
||||||
|
await authService.login()
|
||||||
|
|
||||||
const orderContent = await fs.readFile(productionIdFile, 'utf-8')
|
const orderContent = await fs.readFile(productionIdFile, 'utf-8')
|
||||||
const orderNumbers = orderContent
|
const orderNumbers = orderContent
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map((line) => line.trim())
|
.map((line) => line.trim())
|
||||||
.filter((line) => line.length > 0)
|
.filter((line) => line.length > 0)
|
||||||
.slice(0, 3) // Test 3 orders
|
.slice(0, 3) // Test 3 orders
|
||||||
|
|
||||||
const progressMessages: string[] = []
|
const progressMessages: string[] = []
|
||||||
|
|
||||||
const cleaner = new CleanerService(authService, { dryRun: true })
|
const cleaner = new CleanerService(authService, { dryRun: true })
|
||||||
|
|
||||||
const result = await cleaner.clean({
|
const result = await cleaner.clean({
|
||||||
orderNumbers,
|
orderNumbers,
|
||||||
materialCodes: [],
|
materialCodes: [],
|
||||||
dryRun: true,
|
dryRun: true,
|
||||||
onProgress: (message, progress) => {
|
onProgress: (message, progress) => {
|
||||||
progressMessages.push(`${progress?.toFixed(0)}%: ${message}`)
|
progressMessages.push(`${progress?.toFixed(0)}%: ${message}`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(result.ordersProcessed).toBe(3)
|
expect(result.ordersProcessed).toBe(3)
|
||||||
expect(progressMessages.length).toBeGreaterThan(0)
|
expect(progressMessages.length).toBeGreaterThan(0)
|
||||||
|
|
||||||
console.log('Progress messages:', progressMessages.slice(0, 5))
|
console.log('Progress messages:', progressMessages.slice(0, 5))
|
||||||
|
|
||||||
await authService.close()
|
await authService.close()
|
||||||
}, 180000)
|
},
|
||||||
|
180000
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Error handling', () => {
|
describe('Error handling', () => {
|
||||||
it.skipIf(!hasCredentials)('should continue processing after order error', async () => {
|
it.skipIf(!hasCredentials)(
|
||||||
const authService = new ErpAuthService(config)
|
'should continue processing after order error',
|
||||||
await authService.login()
|
async () => {
|
||||||
|
const authService = new ErpAuthService(config)
|
||||||
|
await authService.login()
|
||||||
|
|
||||||
const orderNumbers = ['INVALID_ORDER_12345', 'INVALID_ORDER_67890']
|
const orderNumbers = ['INVALID_ORDER_12345', 'INVALID_ORDER_67890']
|
||||||
|
|
||||||
const cleaner = new CleanerService(authService, { dryRun: true })
|
const cleaner = new CleanerService(authService, { dryRun: true })
|
||||||
|
|
||||||
const result = await cleaner.clean({
|
const result = await cleaner.clean({
|
||||||
orderNumbers,
|
orderNumbers,
|
||||||
materialCodes: [],
|
materialCodes: [],
|
||||||
dryRun: true
|
dryRun: true
|
||||||
})
|
})
|
||||||
|
|
||||||
// Should still process (even if with errors)
|
// Should still process (even if with errors)
|
||||||
expect(result.details.length).toBeGreaterThan(0)
|
expect(result.details.length).toBeGreaterThan(0)
|
||||||
|
|
||||||
await authService.close()
|
await authService.close()
|
||||||
}, 120000)
|
},
|
||||||
|
120000
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Navigation', () => {
|
describe('Navigation', () => {
|
||||||
|
|||||||
@@ -19,22 +19,30 @@ describe('ERP Authentication Service (Integration)', () => {
|
|||||||
authService = new ErpAuthService(config)
|
authService = new ErpAuthService(config)
|
||||||
})
|
})
|
||||||
|
|
||||||
it.skipIf(!hasCredentials)('should login successfully', async () => {
|
it.skipIf(!hasCredentials)(
|
||||||
const session = await authService.login()
|
'should login successfully',
|
||||||
|
async () => {
|
||||||
|
const session = await authService.login()
|
||||||
|
|
||||||
expect(session).toBeDefined()
|
expect(session).toBeDefined()
|
||||||
expect(session.browser).toBeDefined()
|
expect(session.browser).toBeDefined()
|
||||||
expect(session.context).toBeDefined()
|
expect(session.context).toBeDefined()
|
||||||
expect(session.page).toBeDefined()
|
expect(session.page).toBeDefined()
|
||||||
expect(session.isLoggedIn).toBe(true)
|
expect(session.isLoggedIn).toBe(true)
|
||||||
}, 30000)
|
},
|
||||||
|
30000
|
||||||
|
)
|
||||||
|
|
||||||
it.skipIf(!hasCredentials)('should navigate to main page after login', async () => {
|
it.skipIf(!hasCredentials)(
|
||||||
const session = await authService.login()
|
'should navigate to main page after login',
|
||||||
|
async () => {
|
||||||
|
const session = await authService.login()
|
||||||
|
|
||||||
const url = session.page.url()
|
const url = session.page.url()
|
||||||
expect(url).toContain(config.url)
|
expect(url).toContain(config.url)
|
||||||
}, 30000)
|
},
|
||||||
|
30000
|
||||||
|
)
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
if (hasCredentials && authService) {
|
if (hasCredentials && authService) {
|
||||||
|
|||||||
@@ -18,117 +18,129 @@ describe('Extractor Service (Integration)', () => {
|
|||||||
// Check if we have ERP credentials
|
// Check if we have ERP credentials
|
||||||
const hasCredentials = !!(config.url && config.username && config.password)
|
const hasCredentials = !!(config.url && config.username && config.password)
|
||||||
|
|
||||||
it.skipIf(!hasCredentials)('should extract data for single order number', async () => {
|
it.skipIf(!hasCredentials)(
|
||||||
// Create fresh auth service for this test
|
'should extract data for single order number',
|
||||||
const authService = new ErpAuthService(config)
|
async () => {
|
||||||
await authService.login()
|
// Create fresh auth service for this test
|
||||||
|
const authService = new ErpAuthService(config)
|
||||||
|
await authService.login()
|
||||||
|
|
||||||
const extractor = new ExtractorService(authService)
|
const extractor = new ExtractorService(authService)
|
||||||
|
|
||||||
const result = await extractor.extract({
|
const result = await extractor.extract({
|
||||||
orderNumbers: [testOrderNumber]
|
orderNumbers: [testOrderNumber]
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(result.downloadedFiles).toHaveLength(1)
|
expect(result.downloadedFiles).toHaveLength(1)
|
||||||
expect(result.errors).toHaveLength(0)
|
expect(result.errors).toHaveLength(0)
|
||||||
|
|
||||||
// Verify file exists
|
// Verify file exists
|
||||||
const filePath = result.downloadedFiles[0]
|
const filePath = result.downloadedFiles[0]
|
||||||
const stats = await fs.stat(filePath)
|
|
||||||
expect(stats.size).toBeGreaterThan(0)
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
await authService.close()
|
|
||||||
}, 60000)
|
|
||||||
|
|
||||||
it.skipIf(!hasCredentials)('should extract data for multiple order numbers', async () => {
|
|
||||||
// Create fresh auth service for this test
|
|
||||||
const authService = new ErpAuthService(config)
|
|
||||||
await authService.login()
|
|
||||||
|
|
||||||
const extractor = new ExtractorService(authService)
|
|
||||||
|
|
||||||
// Read order numbers from productionID.txt file
|
|
||||||
const productionIdFile = path.join(process.cwd(), '../references/demo/productionID.txt')
|
|
||||||
const content = await fs.readFile(productionIdFile, 'utf-8')
|
|
||||||
const orderNumbers = content
|
|
||||||
.split('\n')
|
|
||||||
.map((line) => line.trim())
|
|
||||||
.filter((line) => line.length > 0)
|
|
||||||
.slice(0, 5) // Test first 5 orders
|
|
||||||
|
|
||||||
console.log(`Testing with ${orderNumbers.length} order numbers:`, orderNumbers)
|
|
||||||
|
|
||||||
const result = await extractor.extract({
|
|
||||||
orderNumbers,
|
|
||||||
batchSize: 100 // Process all in one batch
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log(`Downloaded ${result.downloadedFiles.length} files`)
|
|
||||||
if (result.errors.length > 0) {
|
|
||||||
console.log('Errors:', result.errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(result.downloadedFiles.length).toBeGreaterThanOrEqual(1)
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
await authService.close()
|
|
||||||
}, 120000) // Increase timeout to 2 minutes
|
|
||||||
|
|
||||||
it.skipIf(!hasCredentials)('should extract data for 300 orders with batch size 70', async () => {
|
|
||||||
// Create fresh auth service for this test
|
|
||||||
const authService = new ErpAuthService(config)
|
|
||||||
await authService.login()
|
|
||||||
|
|
||||||
const extractor = new ExtractorService(authService)
|
|
||||||
|
|
||||||
// Read all order numbers from productionID.txt file
|
|
||||||
const productionIdFile = path.join(process.cwd(), '../references/demo/productionID.txt')
|
|
||||||
const content = await fs.readFile(productionIdFile, 'utf-8')
|
|
||||||
const orderNumbers = content
|
|
||||||
.split('\n')
|
|
||||||
.map((line) => line.trim())
|
|
||||||
.filter((line) => line.length > 0)
|
|
||||||
|
|
||||||
console.log(`Testing with ${orderNumbers.length} order numbers`)
|
|
||||||
console.log(`Batch size: 70, Expected batches: ${Math.ceil(orderNumbers.length / 70)}`)
|
|
||||||
|
|
||||||
const startTime = Date.now()
|
|
||||||
|
|
||||||
const result = await extractor.extract({
|
|
||||||
orderNumbers,
|
|
||||||
batchSize: 70 // Process 70 orders per batch
|
|
||||||
})
|
|
||||||
|
|
||||||
const endTime = Date.now()
|
|
||||||
const duration = ((endTime - startTime) / 1000).toFixed(2)
|
|
||||||
|
|
||||||
console.log(`\n=== Extraction Summary ===`)
|
|
||||||
console.log(`Total orders: ${orderNumbers.length}`)
|
|
||||||
console.log(`Batch size: 70`)
|
|
||||||
console.log(`Expected batches: ${Math.ceil(orderNumbers.length / 70)}`)
|
|
||||||
console.log(`Downloaded files: ${result.downloadedFiles.length}`)
|
|
||||||
console.log(`Total duration: ${duration}s`)
|
|
||||||
console.log(
|
|
||||||
`Average time per batch: ${(duration / result.downloadedFiles.length).toFixed(2)}s`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (result.errors.length > 0) {
|
|
||||||
console.log(`\nErrors encountered: ${result.errors.length}`)
|
|
||||||
result.errors.forEach((err, idx) => console.log(` ${idx + 1}. ${err}`))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify results
|
|
||||||
expect(result.downloadedFiles.length).toBeGreaterThanOrEqual(1)
|
|
||||||
|
|
||||||
// Verify each downloaded file exists and has content
|
|
||||||
for (const filePath of result.downloadedFiles) {
|
|
||||||
const stats = await fs.stat(filePath)
|
const stats = await fs.stat(filePath)
|
||||||
console.log(` - ${path.basename(filePath)}: ${(stats.size / 1024).toFixed(2)} KB`)
|
|
||||||
expect(stats.size).toBeGreaterThan(0)
|
expect(stats.size).toBeGreaterThan(0)
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
await authService.close()
|
await authService.close()
|
||||||
}, 600000) // 10 minutes timeout for large batch test
|
},
|
||||||
|
60000
|
||||||
|
)
|
||||||
|
|
||||||
|
it.skipIf(!hasCredentials)(
|
||||||
|
'should extract data for multiple order numbers',
|
||||||
|
async () => {
|
||||||
|
// Create fresh auth service for this test
|
||||||
|
const authService = new ErpAuthService(config)
|
||||||
|
await authService.login()
|
||||||
|
|
||||||
|
const extractor = new ExtractorService(authService)
|
||||||
|
|
||||||
|
// Read order numbers from productionID.txt file
|
||||||
|
const productionIdFile = path.join(process.cwd(), '../references/demo/productionID.txt')
|
||||||
|
const content = await fs.readFile(productionIdFile, 'utf-8')
|
||||||
|
const orderNumbers = content
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => line.length > 0)
|
||||||
|
.slice(0, 5) // Test first 5 orders
|
||||||
|
|
||||||
|
console.log(`Testing with ${orderNumbers.length} order numbers:`, orderNumbers)
|
||||||
|
|
||||||
|
const result = await extractor.extract({
|
||||||
|
orderNumbers,
|
||||||
|
batchSize: 100 // Process all in one batch
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(`Downloaded ${result.downloadedFiles.length} files`)
|
||||||
|
if (result.errors.length > 0) {
|
||||||
|
console.log('Errors:', result.errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.downloadedFiles.length).toBeGreaterThanOrEqual(1)
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
await authService.close()
|
||||||
|
},
|
||||||
|
120000
|
||||||
|
) // Increase timeout to 2 minutes
|
||||||
|
|
||||||
|
it.skipIf(!hasCredentials)(
|
||||||
|
'should extract data for 300 orders with batch size 70',
|
||||||
|
async () => {
|
||||||
|
// Create fresh auth service for this test
|
||||||
|
const authService = new ErpAuthService(config)
|
||||||
|
await authService.login()
|
||||||
|
|
||||||
|
const extractor = new ExtractorService(authService)
|
||||||
|
|
||||||
|
// Read all order numbers from productionID.txt file
|
||||||
|
const productionIdFile = path.join(process.cwd(), '../references/demo/productionID.txt')
|
||||||
|
const content = await fs.readFile(productionIdFile, 'utf-8')
|
||||||
|
const orderNumbers = content
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => line.length > 0)
|
||||||
|
|
||||||
|
console.log(`Testing with ${orderNumbers.length} order numbers`)
|
||||||
|
console.log(`Batch size: 70, Expected batches: ${Math.ceil(orderNumbers.length / 70)}`)
|
||||||
|
|
||||||
|
const startTime = Date.now()
|
||||||
|
|
||||||
|
const result = await extractor.extract({
|
||||||
|
orderNumbers,
|
||||||
|
batchSize: 70 // Process 70 orders per batch
|
||||||
|
})
|
||||||
|
|
||||||
|
const endTime = Date.now()
|
||||||
|
const duration = ((endTime - startTime) / 1000).toFixed(2)
|
||||||
|
|
||||||
|
console.log(`\n=== Extraction Summary ===`)
|
||||||
|
console.log(`Total orders: ${orderNumbers.length}`)
|
||||||
|
console.log(`Batch size: 70`)
|
||||||
|
console.log(`Expected batches: ${Math.ceil(orderNumbers.length / 70)}`)
|
||||||
|
console.log(`Downloaded files: ${result.downloadedFiles.length}`)
|
||||||
|
console.log(`Total duration: ${duration}s`)
|
||||||
|
console.log(
|
||||||
|
`Average time per batch: ${(duration / result.downloadedFiles.length).toFixed(2)}s`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result.errors.length > 0) {
|
||||||
|
console.log(`\nErrors encountered: ${result.errors.length}`)
|
||||||
|
result.errors.forEach((err, idx) => console.log(` ${idx + 1}. ${err}`))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify results
|
||||||
|
expect(result.downloadedFiles.length).toBeGreaterThanOrEqual(1)
|
||||||
|
|
||||||
|
// Verify each downloaded file exists and has content
|
||||||
|
for (const filePath of result.downloadedFiles) {
|
||||||
|
const stats = await fs.stat(filePath)
|
||||||
|
console.log(` - ${path.basename(filePath)}: ${(stats.size / 1024).toFixed(2)} KB`)
|
||||||
|
expect(stats.size).toBeGreaterThan(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
await authService.close()
|
||||||
|
},
|
||||||
|
600000
|
||||||
|
) // 10 minutes timeout for large batch test
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -689,11 +689,12 @@ export function createMockRepository(options?: {
|
|||||||
return {
|
return {
|
||||||
find: vi.fn().mockResolvedValue(mockFindResult),
|
find: vi.fn().mockResolvedValue(mockFindResult),
|
||||||
findOne: vi.fn().mockResolvedValue(mockFindResult[0] ?? null),
|
findOne: vi.fn().mockResolvedValue(mockFindResult[0] ?? null),
|
||||||
create: vi.fn((plainObject?: Record<string, unknown>) => plainObject ?? ({})),
|
create: vi.fn((plainObject?: Record<string, unknown>) => plainObject ?? {}),
|
||||||
save: vi.fn().mockImplementation((entity: Record<string, unknown>) => Promise.resolve(entity)),
|
save: vi.fn().mockImplementation((entity: Record<string, unknown>) => Promise.resolve(entity)),
|
||||||
delete: vi.fn().mockResolvedValue({ affected: 1 }),
|
delete: vi.fn().mockResolvedValue({ affected: 1 }),
|
||||||
count: vi.fn().mockResolvedValue(mockFindResult.length),
|
count: vi.fn().mockResolvedValue(mockFindResult.length),
|
||||||
createQueryBuilder: vi .fn()
|
createQueryBuilder: vi
|
||||||
|
.fn()
|
||||||
.mockImplementation(() => createMockQueryBuilder({ result: mockFindResult }))
|
.mockImplementation(() => createMockQueryBuilder({ result: mockFindResult }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -724,9 +725,12 @@ export function createMockDataSource(
|
|||||||
destroy: vi.fn().mockResolvedValue(undefined),
|
destroy: vi.fn().mockResolvedValue(undefined),
|
||||||
isInitialized,
|
isInitialized,
|
||||||
getRepository: vi.fn().mockReturnValue(mockRepo),
|
getRepository: vi.fn().mockReturnValue(mockRepo),
|
||||||
create: vi.fn().mockImplementation(
|
create: vi
|
||||||
(_entityClass: unknown, plainObject?: Record<string, unknown>) => plainObject ?? ({} as Record<string, unknown>)
|
.fn()
|
||||||
),
|
.mockImplementation(
|
||||||
|
(_entityClass: unknown, plainObject?: Record<string, unknown>) =>
|
||||||
|
plainObject ?? ({} as Record<string, unknown>)
|
||||||
|
),
|
||||||
save: vi.fn().mockImplementation((entity: Record<string, unknown>) => Promise.resolve(entity)),
|
save: vi.fn().mockImplementation((entity: Record<string, unknown>) => Promise.resolve(entity)),
|
||||||
createQueryBuilder: vi
|
createQueryBuilder: vi
|
||||||
.fn()
|
.fn()
|
||||||
|
|||||||
@@ -20,9 +20,7 @@ describe('MySqlDialect', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should handle arbitrary schema and table names', () => {
|
it('should handle arbitrary schema and table names', () => {
|
||||||
expect(dialect.quoteTableName('my_schema', 'my_table')).toBe(
|
expect(dialect.quoteTableName('my_schema', 'my_table')).toBe('my_schema_my_table')
|
||||||
'my_schema_my_table'
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,7 @@ describe('PostgreSqlDialect', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should handle arbitrary names', () => {
|
it('should handle arbitrary names', () => {
|
||||||
expect(dialect.quoteTableName('my_schema', 'my_table')).toBe(
|
expect(dialect.quoteTableName('my_schema', 'my_table')).toBe('"my_schema"."my_table"')
|
||||||
'"my_schema"."my_table"'
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,7 @@ describe('SqlServerDialect', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should handle arbitrary names', () => {
|
it('should handle arbitrary names', () => {
|
||||||
expect(dialect.quoteTableName('my_schema', 'my_table')).toBe(
|
expect(dialect.quoteTableName('my_schema', 'my_table')).toBe('[my_schema].[my_table]')
|
||||||
'[my_schema].[my_table]'
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -74,16 +74,13 @@ vi.mock('../../src/main/services/database/data-source', () => ({
|
|||||||
// MaterialsToBeDeletedRepository
|
// MaterialsToBeDeletedRepository
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
describe('MaterialsToBeDeletedRepository', () => {
|
describe('MaterialsToBeDeletedRepository', () => {
|
||||||
let MaterialsToBeDeletedRepository: typeof import(
|
let MaterialsToBeDeletedRepository: typeof import('../../src/main/services/database/repositories/MaterialsToBeDeletedRepository').MaterialsToBeDeletedRepository
|
||||||
'../../src/main/services/database/repositories/MaterialsToBeDeletedRepository'
|
|
||||||
).MaterialsToBeDeletedRepository
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
mockRepo = createMockRepository()
|
mockRepo = createMockRepository()
|
||||||
const mod = await import(
|
const mod =
|
||||||
'../../src/main/services/database/repositories/MaterialsToBeDeletedRepository'
|
await import('../../src/main/services/database/repositories/MaterialsToBeDeletedRepository')
|
||||||
)
|
|
||||||
MaterialsToBeDeletedRepository = mod.MaterialsToBeDeletedRepository
|
MaterialsToBeDeletedRepository = mod.MaterialsToBeDeletedRepository
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -384,16 +381,13 @@ describe('MaterialsToBeDeletedRepository', () => {
|
|||||||
// DiscreteMaterialPlanRepository
|
// DiscreteMaterialPlanRepository
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
describe('DiscreteMaterialPlanRepository', () => {
|
describe('DiscreteMaterialPlanRepository', () => {
|
||||||
let DiscreteMaterialPlanRepository: typeof import(
|
let DiscreteMaterialPlanRepository: typeof import('../../src/main/services/database/repositories/DiscreteMaterialPlanRepository').DiscreteMaterialPlanRepository
|
||||||
'../../src/main/services/database/repositories/DiscreteMaterialPlanRepository'
|
|
||||||
).DiscreteMaterialPlanRepository
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
mockRepo = createMockRepository()
|
mockRepo = createMockRepository()
|
||||||
const mod = await import(
|
const mod =
|
||||||
'../../src/main/services/database/repositories/DiscreteMaterialPlanRepository'
|
await import('../../src/main/services/database/repositories/DiscreteMaterialPlanRepository')
|
||||||
)
|
|
||||||
DiscreteMaterialPlanRepository = mod.DiscreteMaterialPlanRepository
|
DiscreteMaterialPlanRepository = mod.DiscreteMaterialPlanRepository
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -430,9 +424,8 @@ describe('DiscreteMaterialPlanRepository', () => {
|
|||||||
mockRepo = { ...createMockRepository(), query: vi.fn().mockResolvedValue(mockQueryResult) }
|
mockRepo = { ...createMockRepository(), query: vi.fn().mockResolvedValue(mockQueryResult) }
|
||||||
// Re-import to pick up new mockRepo
|
// Re-import to pick up new mockRepo
|
||||||
vi.resetModules()
|
vi.resetModules()
|
||||||
const mod = await import(
|
const mod =
|
||||||
'../../src/main/services/database/repositories/DiscreteMaterialPlanRepository'
|
await import('../../src/main/services/database/repositories/DiscreteMaterialPlanRepository')
|
||||||
)
|
|
||||||
const freshRepo = new mod.DiscreteMaterialPlanRepository()
|
const freshRepo = new mod.DiscreteMaterialPlanRepository()
|
||||||
|
|
||||||
const result = await freshRepo.queryAllDistinctByMaterialCode()
|
const result = await freshRepo.queryAllDistinctByMaterialCode()
|
||||||
@@ -447,9 +440,8 @@ describe('DiscreteMaterialPlanRepository', () => {
|
|||||||
query: vi.fn().mockRejectedValue(new Error('db fail'))
|
query: vi.fn().mockRejectedValue(new Error('db fail'))
|
||||||
}
|
}
|
||||||
vi.resetModules()
|
vi.resetModules()
|
||||||
const mod = await import(
|
const mod =
|
||||||
'../../src/main/services/database/repositories/DiscreteMaterialPlanRepository'
|
await import('../../src/main/services/database/repositories/DiscreteMaterialPlanRepository')
|
||||||
)
|
|
||||||
const freshRepo = new mod.DiscreteMaterialPlanRepository()
|
const freshRepo = new mod.DiscreteMaterialPlanRepository()
|
||||||
|
|
||||||
const result = await freshRepo.queryAllDistinctByMaterialCode()
|
const result = await freshRepo.queryAllDistinctByMaterialCode()
|
||||||
@@ -512,9 +504,8 @@ describe('DiscreteMaterialPlanRepository', () => {
|
|||||||
it('queryBySourceNumbersDistinct: calls repo.query() per batch', async () => {
|
it('queryBySourceNumbersDistinct: calls repo.query() per batch', async () => {
|
||||||
mockRepo = { ...createMockRepository(), query: vi.fn().mockResolvedValue([{ M: 'X' }]) }
|
mockRepo = { ...createMockRepository(), query: vi.fn().mockResolvedValue([{ M: 'X' }]) }
|
||||||
vi.resetModules()
|
vi.resetModules()
|
||||||
const mod = await import(
|
const mod =
|
||||||
'../../src/main/services/database/repositories/DiscreteMaterialPlanRepository'
|
await import('../../src/main/services/database/repositories/DiscreteMaterialPlanRepository')
|
||||||
)
|
|
||||||
const repo = new mod.DiscreteMaterialPlanRepository()
|
const repo = new mod.DiscreteMaterialPlanRepository()
|
||||||
|
|
||||||
const result = await repo.queryBySourceNumbersDistinct(['S1', 'S2'])
|
const result = await repo.queryBySourceNumbersDistinct(['S1', 'S2'])
|
||||||
@@ -529,9 +520,8 @@ describe('DiscreteMaterialPlanRepository', () => {
|
|||||||
query: vi.fn().mockRejectedValue(new Error('db fail'))
|
query: vi.fn().mockRejectedValue(new Error('db fail'))
|
||||||
}
|
}
|
||||||
vi.resetModules()
|
vi.resetModules()
|
||||||
const mod = await import(
|
const mod =
|
||||||
'../../src/main/services/database/repositories/DiscreteMaterialPlanRepository'
|
await import('../../src/main/services/database/repositories/DiscreteMaterialPlanRepository')
|
||||||
)
|
|
||||||
const repo = new mod.DiscreteMaterialPlanRepository()
|
const repo = new mod.DiscreteMaterialPlanRepository()
|
||||||
|
|
||||||
const result = await repo.queryBySourceNumbersDistinct(['S1'])
|
const result = await repo.queryBySourceNumbersDistinct(['S1'])
|
||||||
|
|||||||
Reference in New Issue
Block a user