feat(types): 添加类型检查脚本和迁移指南
添加 check-types.ps1 脚本用于检查项目各模块的类型定义 添加 migrate-types.ps1 脚本用于自动迁移类型导入路径 添加类型迁移指南文档说明迁移步骤和最佳实践 添加共享类型测试用例验证 schema 定义
This commit is contained in:
376
docs/05_AI/08_Type_Migration_Guide.md
Normal file
376
docs/05_AI/08_Type_Migration_Guide.md
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
# 类型导入迁移指南
|
||||||
|
|
||||||
|
> **目的**: 帮助开发者将旧的类型导入方式迁移到统一类型中心
|
||||||
|
> **更新日期**: 2026-03-20
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 迁移概览
|
||||||
|
|
||||||
|
### 1.1 迁移目标
|
||||||
|
|
||||||
|
```
|
||||||
|
旧方式(分散) 新方式(统一)
|
||||||
|
─────────────────────────────────────────────────────
|
||||||
|
各模块独立定义类型 → 从类型中心导入
|
||||||
|
手动维护类型一致性 → Schema 自动推导
|
||||||
|
无运行时验证 → Zod 运行时验证
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 迁移原则
|
||||||
|
|
||||||
|
1. **渐进式迁移**: 不要求一次性全部迁移
|
||||||
|
2. **向后兼容**: 保留旧的类型导出路径
|
||||||
|
3. **验证优先**: 迁移后必须通过类型检查
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 后端迁移
|
||||||
|
|
||||||
|
### 2.1 旧导入方式
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ 旧方式:从本地文件导入
|
||||||
|
import { User } from '../models/User';
|
||||||
|
import { Product } from '../models/Product';
|
||||||
|
import { Order } from '../models/Order';
|
||||||
|
|
||||||
|
// ❌ 旧方式:在文件中直接定义
|
||||||
|
interface CreateUserRequest {
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 新导入方式
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 新方式:从统一类型中心导入
|
||||||
|
import { User, Product, Order } from '@shared/types';
|
||||||
|
import { CreateUserDTO, UpdateUserDTO } from '@shared/types';
|
||||||
|
import { CreateUserSchema } from '@shared/schemas';
|
||||||
|
|
||||||
|
// ✅ 新方式:使用 Schema 验证
|
||||||
|
const validated = CreateUserSchema.parse(requestBody);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 迁移步骤
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 查找需要迁移的文件
|
||||||
|
grep -r "from '../models" server/src --include="*.ts"
|
||||||
|
|
||||||
|
# 2. 替换导入语句
|
||||||
|
# 将 '../models/User' 替换为 '@shared/types'
|
||||||
|
|
||||||
|
# 3. 运行类型检查
|
||||||
|
cd server && npx tsc --noEmit
|
||||||
|
|
||||||
|
# 4. 修复类型错误
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 前端迁移
|
||||||
|
|
||||||
|
### 3.1 旧导入方式
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ 旧方式:从本地 types 导入
|
||||||
|
import { User } from '../types/user';
|
||||||
|
import { Product } from '../types/product';
|
||||||
|
|
||||||
|
// ❌ 旧方式:在组件中定义类型
|
||||||
|
interface UserCardProps {
|
||||||
|
user: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 新导入方式
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 新方式:从 server 类型中心导入
|
||||||
|
import { User, Product } from '@shared/types';
|
||||||
|
import { UserDTO, ProductDTO } from '@shared/types';
|
||||||
|
|
||||||
|
// ✅ 新方式:使用类型化的 Props
|
||||||
|
import { User } from '@shared/types';
|
||||||
|
|
||||||
|
interface UserCardProps {
|
||||||
|
user: User;
|
||||||
|
onEdit?: (user: User) => void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 迁移步骤
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 查找需要迁移的文件
|
||||||
|
grep -r "from '../types" dashboard/src --include="*.ts" --include="*.tsx"
|
||||||
|
|
||||||
|
# 2. 替换导入语句
|
||||||
|
# 将 '../types/user' 替换为 '@shared/types'
|
||||||
|
|
||||||
|
# 3. 运行类型检查
|
||||||
|
cd dashboard && npx tsc --noEmit
|
||||||
|
|
||||||
|
# 4. 修复类型错误
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 插件端迁移
|
||||||
|
|
||||||
|
### 4.1 旧导入方式
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ 旧方式:在插件中重复定义类型
|
||||||
|
interface Message {
|
||||||
|
type: string;
|
||||||
|
payload: any;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 新导入方式
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 新方式:从 server 类型中心导入
|
||||||
|
import { BaseMessage, MessageResponse } from '../../server/src/shared/types';
|
||||||
|
import { BaseMessageSchema } from '../../server/src/shared/schemas';
|
||||||
|
|
||||||
|
// ✅ 新方式:使用 Schema 验证
|
||||||
|
const validated = BaseMessageSchema.parse(message);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 常见迁移场景
|
||||||
|
|
||||||
|
### 5.1 类型扩展
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ 旧方式:扩展本地类型
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserWithRole extends User {
|
||||||
|
role: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 新方式:从 Schema 扩展
|
||||||
|
import { User } from '@shared/types';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const UserWithRoleSchema = UserSchema.extend({
|
||||||
|
role: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
type UserWithRole = z.infer<typeof UserWithRoleSchema>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 类型组合
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ 旧方式:手动组合类型
|
||||||
|
interface CreateUserRequest {
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateUserRequest {
|
||||||
|
username?: string;
|
||||||
|
email?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 新方式:使用 Schema 工具
|
||||||
|
import { UserSchema } from '@shared/schemas';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const CreateUserSchema = UserSchema.pick({
|
||||||
|
username: true,
|
||||||
|
email: true,
|
||||||
|
password: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const UpdateUserSchema = UserSchema.partial().pick({
|
||||||
|
username: true,
|
||||||
|
email: true
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 API 响应类型
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ 旧方式:手动定义响应类型
|
||||||
|
interface ApiResponse<T> {
|
||||||
|
success: boolean;
|
||||||
|
data?: T;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 新方式:使用统一响应类型
|
||||||
|
import { ApiResponse, PaginatedResult } from '@shared/types';
|
||||||
|
import { SuccessResponseSchema, ErrorResponseSchema } from '@shared/schemas';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 迁移检查清单
|
||||||
|
|
||||||
|
### 6.1 后端检查
|
||||||
|
|
||||||
|
- [ ] 所有 Service 层使用 `@shared/types` 导入
|
||||||
|
- [ ] 所有 Controller 层使用 `@shared/schemas` 验证
|
||||||
|
- [ ] 所有 Model 层类型从 Schema 推导
|
||||||
|
- [ ] 运行 `npx tsc --noEmit` 无错误
|
||||||
|
|
||||||
|
### 6.2 前端检查
|
||||||
|
|
||||||
|
- [ ] 所有组件使用 `@shared/types` 导入
|
||||||
|
- [ ] 所有 API 调用使用类型化的 DTO
|
||||||
|
- [ ] 所有 Props 使用类型中心定义
|
||||||
|
- [ ] 运行 `npx tsc --noEmit` 无错误
|
||||||
|
|
||||||
|
### 6.3 插件端检查
|
||||||
|
|
||||||
|
- [ ] 所有消息类型从 server 导入
|
||||||
|
- [ ] 所有消息使用 Schema 验证
|
||||||
|
- [ ] 运行 `npx tsc --noEmit` 无错误
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 迁移脚本
|
||||||
|
|
||||||
|
### 7.1 自动替换脚本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# migrate-types.sh
|
||||||
|
|
||||||
|
# 后端迁移
|
||||||
|
find server/src -name "*.ts" -type f -exec sed -i \
|
||||||
|
-e "s|from '../models/User'|from '@shared/types'|g" \
|
||||||
|
-e "s|from '../models/Product'|from '@shared/types'|g" \
|
||||||
|
-e "s|from '../models/Order'|from '@shared/types'|g" \
|
||||||
|
{} +
|
||||||
|
|
||||||
|
# 前端迁移
|
||||||
|
find dashboard/src -name "*.ts" -o -name "*.tsx" -type f -exec sed -i \
|
||||||
|
-e "s|from '../types/user'|from '@shared/types'|g" \
|
||||||
|
-e "s|from '../types/product'|from '@shared/types'|g" \
|
||||||
|
{} +
|
||||||
|
|
||||||
|
echo "Migration complete. Please run: npx tsc --noEmit"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 类型检查脚本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# check-types.sh
|
||||||
|
|
||||||
|
echo "Checking server types..."
|
||||||
|
cd server && npx tsc --noEmit --skipLibCheck
|
||||||
|
SERVER_EXIT=$?
|
||||||
|
|
||||||
|
echo "Checking dashboard types..."
|
||||||
|
cd ../dashboard && npx tsc --noEmit --skipLibCheck
|
||||||
|
DASHBOARD_EXIT=$?
|
||||||
|
|
||||||
|
echo "Checking extension types..."
|
||||||
|
cd ../extension && npx tsc --noEmit --skipLibCheck
|
||||||
|
EXTENSION_EXIT=$?
|
||||||
|
|
||||||
|
if [ $SERVER_EXIT -ne 0 ] || [ $DASHBOARD_EXIT -ne 0 ] || [ $EXTENSION_EXIT -ne 0 ]; then
|
||||||
|
echo "❌ Type check failed"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "✅ All type checks passed"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 常见问题
|
||||||
|
|
||||||
|
### Q1: 导入路径报错怎么办?
|
||||||
|
|
||||||
|
**A**: 检查 `tsconfig.json` 中的 `paths` 配置是否正确:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": {
|
||||||
|
"@shared/types": ["../server/src/shared/types"],
|
||||||
|
"@shared/schemas": ["../server/src/shared/schemas"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q2: 类型不匹配怎么办?
|
||||||
|
|
||||||
|
**A**: 确保使用 Schema 推导类型,而不是手动定义:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ 手动定义可能与 Schema 不一致
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 从 Schema 推导,保证一致
|
||||||
|
import { User } from '@shared/types';
|
||||||
|
// User 类型来自 UserSchema
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q3: 运行时验证失败怎么办?
|
||||||
|
|
||||||
|
**A**: 使用 Schema 的 `parse` 或 `safeParse` 方法:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { CreateUserSchema } from '@shared/schemas';
|
||||||
|
|
||||||
|
// 安全解析
|
||||||
|
const result = CreateUserSchema.safeParse(data);
|
||||||
|
if (!result.success) {
|
||||||
|
console.error(result.error.issues);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const validated = result.data;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 版本兼容
|
||||||
|
|
||||||
|
### 9.1 向后兼容
|
||||||
|
|
||||||
|
旧的导入路径仍然可用,但建议迁移:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 仍然可用(向后兼容)
|
||||||
|
import { User } from '../types/domain/User';
|
||||||
|
|
||||||
|
// 推荐使用(新方式)
|
||||||
|
import { User } from '@shared/types';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 废弃计划
|
||||||
|
|
||||||
|
| 版本 | 状态 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 1.0.0 | 当前 | 新旧方式并存 |
|
||||||
|
| 1.1.0 | 计划 | 标记旧方式为 deprecated |
|
||||||
|
| 2.0.0 | 计划 | 移除旧的类型定义文件 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*迁移完成后,请更新 `server/src/shared/types/version.ts` 中的版本号*
|
||||||
85
scripts/check-types.ps1
Normal file
85
scripts/check-types.ps1
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
# check-types.ps1 - 类型检查脚本
|
||||||
|
# 用法: ./scripts/check-types.ps1
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$ProjectRoot = Split-Path -Parent $PSScriptRoot
|
||||||
|
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " TypeScript Type Check Script" -ForegroundColor Cyan
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
$ServerExit = 0
|
||||||
|
$DashboardExit = 0
|
||||||
|
$ExtensionExit = 0
|
||||||
|
$NodeAgentExit = 0
|
||||||
|
|
||||||
|
# 检查 server
|
||||||
|
Write-Host "[1/4] Checking server types..." -ForegroundColor Yellow
|
||||||
|
Set-Location "$ProjectRoot/server"
|
||||||
|
try {
|
||||||
|
npx tsc --noEmit --skipLibCheck 2>&1 | Out-Null
|
||||||
|
Write-Host " ✅ Server types OK" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
Write-Host " ❌ Server types FAILED" -ForegroundColor Red
|
||||||
|
npx tsc --noEmit --skipLibCheck 2>&1
|
||||||
|
$ServerExit = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查 dashboard
|
||||||
|
Write-Host "[2/4] Checking dashboard types..." -ForegroundColor Yellow
|
||||||
|
Set-Location "$ProjectRoot/dashboard"
|
||||||
|
try {
|
||||||
|
npx tsc --noEmit --skipLibCheck 2>&1 | Out-Null
|
||||||
|
Write-Host " ✅ Dashboard types OK" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
Write-Host " ❌ Dashboard types FAILED" -ForegroundColor Red
|
||||||
|
npx tsc --noEmit --skipLibCheck 2>&1
|
||||||
|
$DashboardExit = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查 extension
|
||||||
|
Write-Host "[3/4] Checking extension types..." -ForegroundColor Yellow
|
||||||
|
Set-Location "$ProjectRoot/extension"
|
||||||
|
try {
|
||||||
|
npx tsc --noEmit --skipLibCheck 2>&1 | Out-Null
|
||||||
|
Write-Host " ✅ Extension types OK" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
Write-Host " ❌ Extension types FAILED" -ForegroundColor Red
|
||||||
|
npx tsc --noEmit --skipLibCheck 2>&1
|
||||||
|
$ExtensionExit = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查 node-agent
|
||||||
|
Write-Host "[4/4] Checking node-agent types..." -ForegroundColor Yellow
|
||||||
|
if (Test-Path "$ProjectRoot/node-agent/tsconfig.json") {
|
||||||
|
Set-Location "$ProjectRoot/node-agent"
|
||||||
|
try {
|
||||||
|
npx tsc --noEmit --skipLibCheck 2>&1 | Out-Null
|
||||||
|
Write-Host " ✅ Node-agent types OK" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
Write-Host " ❌ Node-agent types FAILED" -ForegroundColor Red
|
||||||
|
npx tsc --noEmit --skipLibCheck 2>&1
|
||||||
|
$NodeAgentExit = 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host " ⏭️ Node-agent skipped (no tsconfig.json)" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
$TotalErrors = $ServerExit + $DashboardExit + $ExtensionExit + $NodeAgentExit
|
||||||
|
|
||||||
|
if ($TotalErrors -eq 0) {
|
||||||
|
Write-Host " ✅ All type checks passed!" -ForegroundColor Green
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Set-Location $ProjectRoot
|
||||||
|
exit 0
|
||||||
|
} else {
|
||||||
|
Write-Host " ❌ Type check failed with $TotalErrors error(s)" -ForegroundColor Red
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Set-Location $ProjectRoot
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
129
scripts/migrate-types.ps1
Normal file
129
scripts/migrate-types.ps1
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
# migrate-types.ps1 - 类型导入迁移脚本
|
||||||
|
# 用法: ./scripts/migrate-types.ps1 [-DryRun]
|
||||||
|
|
||||||
|
param(
|
||||||
|
[switch]$DryRun
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$ProjectRoot = Split-Path -Parent $PSScriptRoot
|
||||||
|
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " Type Import Migration Script" -ForegroundColor Cyan
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
if ($DryRun) {
|
||||||
|
Write-Host "🔍 DRY RUN MODE - No files will be modified" -ForegroundColor Yellow
|
||||||
|
Write-Host ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# 定义迁移规则
|
||||||
|
$MigrationRules = @(
|
||||||
|
@{
|
||||||
|
Pattern = "from ['`"]\.\.\/models\/User['`"]"
|
||||||
|
Replacement = "from '@shared/types'"
|
||||||
|
Description = "User model import"
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
Pattern = "from ['`"]\.\.\/models\/Product['`"]"
|
||||||
|
Replacement = "from '@shared/types'"
|
||||||
|
Description = "Product model import"
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
Pattern = "from ['`"]\.\.\/models\/Order['`"]"
|
||||||
|
Replacement = "from '@shared/types'"
|
||||||
|
Description = "Order model import"
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
Pattern = "from ['`"]\.\.\/types\/user['`"]"
|
||||||
|
Replacement = "from '@shared/types'"
|
||||||
|
Description = "User type import (frontend)"
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
Pattern = "from ['`"]\.\.\/types\/product['`"]"
|
||||||
|
Replacement = "from '@shared/types'"
|
||||||
|
Description = "Product type import (frontend)"
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
Pattern = "from ['`"]\.\.\/types\/order['`"]"
|
||||||
|
Replacement = "from '@shared/types'"
|
||||||
|
Description = "Order type import (frontend)"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
$TotalFiles = 0
|
||||||
|
$TotalChanges = 0
|
||||||
|
|
||||||
|
# 迁移后端文件
|
||||||
|
Write-Host "[1/2] Migrating server files..." -ForegroundColor Yellow
|
||||||
|
$ServerFiles = Get-ChildItem -Path "$ProjectRoot/server/src" -Include "*.ts" -Recurse
|
||||||
|
|
||||||
|
foreach ($File in $ServerFiles) {
|
||||||
|
$Content = Get-Content $File.FullName -Raw
|
||||||
|
$OriginalContent = $Content
|
||||||
|
$FileChanged = $false
|
||||||
|
|
||||||
|
foreach ($Rule in $MigrationRules) {
|
||||||
|
if ($Content -match $Rule.Pattern) {
|
||||||
|
$Content = $Content -replace $Rule.Pattern, $Rule.Replacement
|
||||||
|
Write-Host " 📝 $($File.Name): $($Rule.Description)" -ForegroundColor Gray
|
||||||
|
$FileChanged = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($FileChanged) {
|
||||||
|
$TotalFiles++
|
||||||
|
$TotalChanges++
|
||||||
|
|
||||||
|
if (-not $DryRun) {
|
||||||
|
Set-Content -Path $File.FullName -Value $Content -NoNewline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 迁移前端文件
|
||||||
|
Write-Host "[2/2] Migrating dashboard files..." -ForegroundColor Yellow
|
||||||
|
$DashboardFiles = Get-ChildItem -Path "$ProjectRoot/dashboard/src" -Include "*.ts", "*.tsx" -Recurse
|
||||||
|
|
||||||
|
foreach ($File in $DashboardFiles) {
|
||||||
|
$Content = Get-Content $File.FullName -Raw
|
||||||
|
$OriginalContent = $Content
|
||||||
|
$FileChanged = $false
|
||||||
|
|
||||||
|
foreach ($Rule in $MigrationRules) {
|
||||||
|
if ($Content -match $Rule.Pattern) {
|
||||||
|
$Content = $Content -replace $Rule.Pattern, $Rule.Replacement
|
||||||
|
Write-Host " 📝 $($File.Name): $($Rule.Description)" -ForegroundColor Gray
|
||||||
|
$FileChanged = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($FileChanged) {
|
||||||
|
$TotalFiles++
|
||||||
|
$TotalChanges++
|
||||||
|
|
||||||
|
if (-not $DryRun) {
|
||||||
|
Set-Content -Path $File.FullName -Value $Content -NoNewline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " Migration Summary" -ForegroundColor Cyan
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " Files processed: $TotalFiles" -ForegroundColor White
|
||||||
|
Write-Host " Total changes: $TotalChanges" -ForegroundColor White
|
||||||
|
|
||||||
|
if ($DryRun) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " This was a DRY RUN. Run without -DryRun to apply changes." -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " ✅ Migration complete!" -ForegroundColor Green
|
||||||
|
Write-Host " Please run: ./scripts/check-types.ps1" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
247
server/src/shared/schemas/__tests__/schemas.test.ts
Normal file
247
server/src/shared/schemas/__tests__/schemas.test.ts
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import {
|
||||||
|
UserSchema,
|
||||||
|
UserRoleSchema,
|
||||||
|
UserStatusSchema,
|
||||||
|
CreateUserSchema,
|
||||||
|
UpdateUserSchema
|
||||||
|
} from '@shared/schemas/user.schema';
|
||||||
|
import {
|
||||||
|
ProductSchema,
|
||||||
|
ProductStatusSchema
|
||||||
|
} from '@shared/schemas/product.schema';
|
||||||
|
import {
|
||||||
|
OrderSchema,
|
||||||
|
OrderStatusSchema
|
||||||
|
} from '@shared/schemas/order.schema';
|
||||||
|
import {
|
||||||
|
BaseMessageSchema,
|
||||||
|
MessageTypeSchema,
|
||||||
|
MessageResponseSchema
|
||||||
|
} from '@shared/schemas/message.schema';
|
||||||
|
|
||||||
|
describe('User Schema', () => {
|
||||||
|
it('should validate valid user data', () => {
|
||||||
|
const validUser = {
|
||||||
|
id: '123',
|
||||||
|
name: 'John Doe',
|
||||||
|
email: 'john@example.com',
|
||||||
|
role: 'admin',
|
||||||
|
status: 'active',
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = UserSchema.safeParse(validUser);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject invalid email', () => {
|
||||||
|
const invalidUser = {
|
||||||
|
id: '123',
|
||||||
|
name: 'John Doe',
|
||||||
|
email: 'invalid-email',
|
||||||
|
role: 'admin',
|
||||||
|
status: 'active',
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = UserSchema.safeParse(invalidUser);
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject invalid role', () => {
|
||||||
|
const invalidUser = {
|
||||||
|
id: '123',
|
||||||
|
name: 'John Doe',
|
||||||
|
email: 'john@example.com',
|
||||||
|
role: 'invalid_role',
|
||||||
|
status: 'active',
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = UserSchema.safeParse(invalidUser);
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Product Schema', () => {
|
||||||
|
it('should validate valid product data', () => {
|
||||||
|
const validProduct = {
|
||||||
|
id: '123',
|
||||||
|
name: 'Test Product',
|
||||||
|
sku: 'SKU-001',
|
||||||
|
price: 99.99,
|
||||||
|
stock: 100,
|
||||||
|
status: 'active',
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = ProductSchema.safeParse(validProduct);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject negative price', () => {
|
||||||
|
const invalidProduct = {
|
||||||
|
id: '123',
|
||||||
|
name: 'Test Product',
|
||||||
|
sku: 'SKU-001',
|
||||||
|
price: -10,
|
||||||
|
stock: 100,
|
||||||
|
status: 'active',
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = ProductSchema.safeParse(invalidProduct);
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Order Schema', () => {
|
||||||
|
it('should validate valid order data', () => {
|
||||||
|
const validOrder = {
|
||||||
|
id: '123',
|
||||||
|
orderNumber: 'ORD-001',
|
||||||
|
status: 'pending',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
productId: 'p1',
|
||||||
|
productName: 'Product 1',
|
||||||
|
quantity: 2,
|
||||||
|
unitPrice: 50,
|
||||||
|
totalPrice: 100
|
||||||
|
}
|
||||||
|
],
|
||||||
|
totalAmount: 100,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = OrderSchema.safeParse(validOrder);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Message Schema', () => {
|
||||||
|
it('should validate valid message', () => {
|
||||||
|
const validMessage = {
|
||||||
|
type: 'COLLECT_ORDERS',
|
||||||
|
payload: { shopId: 'shop1' },
|
||||||
|
traceId: 'trace-123'
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = BaseMessageSchema.safeParse(validMessage);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject invalid message type', () => {
|
||||||
|
const invalidMessage = {
|
||||||
|
type: 'INVALID_TYPE',
|
||||||
|
payload: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = BaseMessageSchema.safeParse(invalidMessage);
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate message response', () => {
|
||||||
|
const validResponse = {
|
||||||
|
success: true,
|
||||||
|
data: { orders: [] },
|
||||||
|
traceId: 'trace-123'
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = MessageResponseSchema.safeParse(validResponse);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate error response', () => {
|
||||||
|
const errorResponse = {
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
code: 'ERROR_001',
|
||||||
|
message: 'Something went wrong'
|
||||||
|
},
|
||||||
|
traceId: 'trace-123'
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = MessageResponseSchema.safeParse(errorResponse);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Type Inference', () => {
|
||||||
|
it('should infer correct types from schema', () => {
|
||||||
|
type User = z.infer<typeof UserSchema>;
|
||||||
|
type UserRole = z.infer<typeof UserRoleSchema>;
|
||||||
|
type UserStatus = z.infer<typeof UserStatusSchema>;
|
||||||
|
|
||||||
|
const user: User = {
|
||||||
|
id: '123',
|
||||||
|
name: 'Test',
|
||||||
|
email: 'test@example.com',
|
||||||
|
role: 'admin',
|
||||||
|
status: 'active',
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(user.role).toBe('admin');
|
||||||
|
expect(user.status).toBe('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Schema Extensions', () => {
|
||||||
|
it('should extend schema correctly', () => {
|
||||||
|
const ExtendedUserSchema = UserSchema.extend({
|
||||||
|
department: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
const validExtendedUser = {
|
||||||
|
id: '123',
|
||||||
|
name: 'John',
|
||||||
|
email: 'john@example.com',
|
||||||
|
role: 'admin',
|
||||||
|
status: 'active',
|
||||||
|
department: 'Engineering',
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = ExtendedUserSchema.safeParse(validExtendedUser);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pick fields from schema', () => {
|
||||||
|
const UserPreviewSchema = UserSchema.pick({
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const validPreview = {
|
||||||
|
id: '123',
|
||||||
|
name: 'John',
|
||||||
|
email: 'john@example.com'
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = UserPreviewSchema.safeParse(validPreview);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should make schema partial', () => {
|
||||||
|
const PartialUserSchema = UserSchema.partial();
|
||||||
|
|
||||||
|
const validPartial = {
|
||||||
|
name: 'John'
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = PartialUserSchema.safeParse(validPartial);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user