feat(plop): 代码生成器支持 CrudService 分层架构
- 新增 relation-parser.ts 关联关系 DSL 解析器 - 生成器支持三种服务类型选择:CrudService/RelationCrudService/ManyToManyCrudService - 添加关联关系、多对多关系、统计关系配置问题 - 修复 helpers 导入路径扩展名问题 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
340
docs/workflow/250119-plop-crud-generator-upgrade.md
Normal file
340
docs/workflow/250119-plop-crud-generator-upgrade.md
Normal file
@@ -0,0 +1,340 @@
|
||||
# Plop CRUD 代码生成器升级计划
|
||||
|
||||
## 目标
|
||||
|
||||
根据 CrudService 分层架构(CrudService → RelationCrudService → ManyToManyCrudService),调整 plop 代码生成器,支持生成带关联关系的模块。
|
||||
|
||||
## 现状分析
|
||||
|
||||
### 当前生成器能力
|
||||
- 生成单表 CRUD 模块(继承 CrudService)
|
||||
- 支持软删除配置
|
||||
- 支持字段 DSL 定义
|
||||
- 支持过滤字段配置
|
||||
|
||||
### 不支持的场景
|
||||
- 带关联查询的模块(RelationCrudService)
|
||||
- 多对多关系的模块(ManyToManyCrudService)
|
||||
- 生成 toResponseDto / toDetailDto 方法
|
||||
- 生成关联类型定义
|
||||
|
||||
## 升级方案
|
||||
|
||||
### 新增交互问题
|
||||
|
||||
#### 1. 服务类型选择
|
||||
```
|
||||
? 选择服务类型:
|
||||
○ CrudService(单表 CRUD)
|
||||
○ RelationCrudService(带关联查询)
|
||||
○ ManyToManyCrudService(多对多关系)
|
||||
```
|
||||
|
||||
#### 2. 关联关系配置(当选择 RelationCrudService 或 ManyToManyCrudService)
|
||||
使用编辑器输入 DSL 格式:
|
||||
```
|
||||
# 关联关系定义(每行一个关联)
|
||||
# 格式: 关联名:目标模型 字段1,字段2,... [noList]
|
||||
# noList: 不在列表中包含
|
||||
#
|
||||
# 示例:
|
||||
# class:Class id,code,name
|
||||
# headTeacher:Teacher id,teacherNo,name,subject
|
||||
```
|
||||
|
||||
#### 3. 多对多关系配置(当选择 ManyToManyCrudService)
|
||||
使用编辑器输入 DSL 格式:
|
||||
```
|
||||
# 多对多关系定义(每行一个关系)
|
||||
# 格式: 关系名:中间表:外键:目标键:目标模型 字段1,字段2,...
|
||||
#
|
||||
# 示例:
|
||||
# teachers:classTeacher:classId:teacherId:Teacher id,teacherNo,name,subject
|
||||
```
|
||||
|
||||
#### 4. 统计关系配置(当选择 RelationCrudService 或 ManyToManyCrudService)
|
||||
```
|
||||
? 需要在详情中统计数量的关系(逗号分隔,如 students,orders):
|
||||
```
|
||||
|
||||
### 新增模板数据字段
|
||||
|
||||
```typescript
|
||||
interface TemplateData {
|
||||
// 现有字段...
|
||||
|
||||
// 服务类型
|
||||
serviceType: 'CrudService' | 'RelationCrudService' | 'ManyToManyCrudService';
|
||||
|
||||
// 关联配置
|
||||
relations: Array<{
|
||||
name: string; // 关联名,如 'class'
|
||||
model: string; // 目标模型,如 'Class'
|
||||
selectFields: string[]; // select 字段,如 ['id', 'code', 'name']
|
||||
includeInList: boolean; // 是否在列表中包含
|
||||
}>;
|
||||
|
||||
// 多对多配置
|
||||
manyToMany: Array<{
|
||||
name: string; // 关系名,如 'teachers'
|
||||
through: string; // 中间表,如 'classTeacher'
|
||||
foreignKey: string; // 当前实体外键,如 'classId'
|
||||
targetKey: string; // 目标实体外键,如 'teacherId'
|
||||
target: string; // 目标模型,如 'teacher'
|
||||
targetModel: string; // 目标模型 PascalCase,如 'Teacher'
|
||||
selectFields: string[]; // 目标实体 select 字段
|
||||
}>;
|
||||
|
||||
// 统计关系
|
||||
countRelations: string[];
|
||||
|
||||
// 派生标志
|
||||
hasRelations: boolean;
|
||||
hasManyToMany: boolean;
|
||||
hasCountRelations: boolean;
|
||||
needsResponseDto: boolean; // RelationCrudService 或 ManyToManyCrudService 时为 true
|
||||
needsDetailDto: boolean; // ManyToManyCrudService 时为 true
|
||||
}
|
||||
```
|
||||
|
||||
### 模板调整
|
||||
|
||||
#### service.hbs 调整
|
||||
|
||||
**调整前**:只支持 CrudService
|
||||
```typescript
|
||||
export class {{pascalCase name}}Service extends CrudService<...>
|
||||
```
|
||||
|
||||
**调整后**:根据 serviceType 生成不同的代码
|
||||
|
||||
```handlebars
|
||||
{{#if (eq serviceType 'CrudService')}}
|
||||
import { CrudService } from '@/common/crud/crud.service';
|
||||
{{else if (eq serviceType 'RelationCrudService')}}
|
||||
import { RelationCrudService } from '@/common/crud/relation-crud.service';
|
||||
{{else}}
|
||||
import { ManyToManyCrudService } from '@/common/crud/many-to-many-crud.service';
|
||||
{{/if}}
|
||||
|
||||
// 生成关联类型定义
|
||||
{{#if hasRelations}}
|
||||
type {{pascalCase name}}WithRelations = {{pascalCase name}} & {
|
||||
{{#each relations}}
|
||||
{{name}}?: { {{#each selectFields}}{{this}}: {{../typeForField this}}; {{/each}}} | null;
|
||||
{{/each}}
|
||||
};
|
||||
{{/if}}
|
||||
|
||||
// 生成详情类型定义(多对多)
|
||||
{{#if hasManyToMany}}
|
||||
type {{pascalCase name}}WithDetails = {{pascalCase name}}WithRelations & {
|
||||
{{#each manyToMany}}
|
||||
{{name}}?: Array<{ {{target}}: { {{#each selectFields}}{{this}}: {{../typeForField this}}; {{/each}}} }>;
|
||||
{{/each}}
|
||||
{{#if hasCountRelations}}
|
||||
_count?: { {{#each countRelations}}{{this}}: number; {{/each}} };
|
||||
{{/if}}
|
||||
};
|
||||
{{/if}}
|
||||
|
||||
// 继承对应的基类
|
||||
export class {{pascalCase name}}Service extends {{serviceType}}<
|
||||
{{pascalCase name}},
|
||||
...
|
||||
{{#if needsResponseDto}}
|
||||
{{pascalCase name}}ResponseDto{{#if needsDetailDto}},
|
||||
{{pascalCase name}}DetailResponseDto{{/if}}
|
||||
{{/if}}
|
||||
>
|
||||
|
||||
// 生成 @CrudOptions 配置
|
||||
@CrudOptions({
|
||||
filterableFields: [...],
|
||||
{{#if hasRelations}}
|
||||
relations: {
|
||||
{{#each relations}}
|
||||
{{name}}: { select: { {{#each selectFields}}{{this}}: true, {{/each}} }{{#unless includeInList}}, includeInList: false{{/unless}} },
|
||||
{{/each}}
|
||||
},
|
||||
{{/if}}
|
||||
{{#if hasManyToMany}}
|
||||
manyToMany: {
|
||||
{{#each manyToMany}}
|
||||
{{name}}: {
|
||||
through: '{{through}}',
|
||||
foreignKey: '{{foreignKey}}',
|
||||
targetKey: '{{targetKey}}',
|
||||
target: '{{target}}',
|
||||
targetSelect: { {{#each selectFields}}{{this}}: true, {{/each}} },
|
||||
},
|
||||
{{/each}}
|
||||
},
|
||||
{{/if}}
|
||||
{{#if hasCountRelations}}
|
||||
countRelations: [{{#each countRelations}}'{{this}}', {{/each}}],
|
||||
{{/if}}
|
||||
})
|
||||
|
||||
// 生成 toResponseDto(RelationCrudService/ManyToManyCrudService 需要)
|
||||
{{#if needsResponseDto}}
|
||||
protected toResponseDto = (entity: {{pascalCase name}}WithRelations): {{pascalCase name}}ResponseDto => ({
|
||||
id: entity.id,
|
||||
{{#each fields}}
|
||||
{{name}}: entity.{{name}}{{#if nullable}} ?? undefined{{/if}},
|
||||
{{/each}}
|
||||
{{#each relations}}
|
||||
{{#if includeInList}}
|
||||
{{name}}: entity.{{name}} ?? undefined,
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
createdAt: entity.createdAt.toISOString(),
|
||||
updatedAt: entity.updatedAt.toISOString(),
|
||||
});
|
||||
{{/if}}
|
||||
|
||||
// 生成 toDetailDto(ManyToManyCrudService 需要)
|
||||
{{#if needsDetailDto}}
|
||||
protected override toDetailDto(entity: {{pascalCase name}}WithDetails): {{pascalCase name}}DetailResponseDto {
|
||||
return {
|
||||
...this.toResponseDto(entity),
|
||||
{{#each manyToMany}}
|
||||
{{name}}: entity.{{name}}?.map((item) => item.{{target}}) ?? [],
|
||||
{{/each}}
|
||||
{{#each countRelations}}
|
||||
{{this}}Count: entity._count?.{{this}} ?? 0,
|
||||
{{/each}}
|
||||
};
|
||||
}
|
||||
{{/if}}
|
||||
```
|
||||
|
||||
#### dto.hbs 调整
|
||||
|
||||
新增详情响应 DTO(当有多对多关系时):
|
||||
|
||||
```handlebars
|
||||
{{#if needsDetailDto}}
|
||||
/** {{chineseName}}详情响应 DTO */
|
||||
export class {{pascalCase name}}DetailResponseDto extends {{pascalCase name}}ResponseDto {
|
||||
{{#each manyToMany}}
|
||||
@ApiProperty({ type: [{{targetModel}}BriefDto], description: '{{../chineseName}}的{{name}}' })
|
||||
{{name}}: {{targetModel}}BriefDto[];
|
||||
|
||||
{{/each}}
|
||||
{{#each countRelations}}
|
||||
@ApiProperty({ example: 0, description: '{{this}}数量' })
|
||||
{{this}}Count: number;
|
||||
|
||||
{{/each}}
|
||||
}
|
||||
{{/if}}
|
||||
```
|
||||
|
||||
#### controller.hbs 调整
|
||||
|
||||
根据服务类型调整端点:
|
||||
|
||||
```handlebars
|
||||
{{#if (or (eq serviceType 'RelationCrudService') (eq serviceType 'ManyToManyCrudService'))}}
|
||||
@Get()
|
||||
findAll(@Query() query: ...) {
|
||||
return this.{{camelCase name}}Service.findAllWithRelations(query);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findById(@Param('id') id: string) {
|
||||
return this.{{camelCase name}}Service.findByIdWithRelations(id);
|
||||
}
|
||||
{{/if}}
|
||||
|
||||
{{#if hasManyToMany}}
|
||||
{{#each manyToMany}}
|
||||
@Patch(':id/{{name}}')
|
||||
@ApiOperation({ summary: '分配{{../chineseName}}的{{name}}' })
|
||||
assign{{pascalCase name}}(@Param('id') id: string, @Body() dto: Assign{{pascalCase name}}Dto) {
|
||||
return this.{{camelCase ../name}}Service.assignManyToMany(id, '{{name}}', { targetIds: dto.{{singular name}}Ids });
|
||||
}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
```
|
||||
|
||||
### 实施步骤
|
||||
|
||||
#### 阶段 1:扩展生成器逻辑
|
||||
- 文件:`plop/generators/crud.ts`
|
||||
- 内容:
|
||||
1. 添加 serviceType 选择问题
|
||||
2. 添加关联配置问题(条件显示)
|
||||
3. 添加多对多配置问题(条件显示)
|
||||
4. 添加统计关系配置问题(条件显示)
|
||||
5. 扩展 TemplateData 类型
|
||||
6. 添加关联/多对多 DSL 解析函数
|
||||
|
||||
#### 阶段 2:新增辅助函数
|
||||
- 文件:`plop/utils/relation-parser.ts`(新建)
|
||||
- 内容:
|
||||
1. `parseRelations(dsl)` - 解析关联配置
|
||||
2. `parseManyToMany(dsl)` - 解析多对多配置
|
||||
|
||||
#### 阶段 3:调整模板
|
||||
- 文件:`plop/templates/api/service.hbs`
|
||||
- 文件:`plop/templates/api/dto.hbs`
|
||||
- 文件:`plop/templates/api/controller.hbs`
|
||||
- 文件:`plop/templates/shared/types.hbs`
|
||||
|
||||
#### 阶段 4:测试验证
|
||||
1. 生成单表 CRUD 模块(如 Product)
|
||||
2. 生成带关联查询的模块(模拟 Student)
|
||||
3. 生成多对多关系的模块(模拟 Class)
|
||||
|
||||
## 预期效果
|
||||
|
||||
### 生成单表模块示例
|
||||
```bash
|
||||
$ pnpm plop crud
|
||||
? 模块名称: product
|
||||
? 服务类型: CrudService(单表 CRUD)
|
||||
# 其他问题...
|
||||
```
|
||||
|
||||
### 生成关联查询模块示例
|
||||
```bash
|
||||
$ pnpm plop crud
|
||||
? 模块名称: order
|
||||
? 服务类型: RelationCrudService(带关联查询)
|
||||
? 关联关系配置:
|
||||
user:User id,name,email
|
||||
product:Product id,name,price
|
||||
? 统计关系: items
|
||||
```
|
||||
|
||||
### 生成多对多模块示例
|
||||
```bash
|
||||
$ pnpm plop crud
|
||||
? 模块名称: course
|
||||
? 服务类型: ManyToManyCrudService(多对多关系)
|
||||
? 关联关系配置:
|
||||
teacher:Teacher id,name
|
||||
? 多对多关系配置:
|
||||
students:courseStudent:courseId:studentId:Student id,name,studentNo
|
||||
? 统计关系: lessons
|
||||
```
|
||||
|
||||
## 向后兼容性
|
||||
|
||||
- 现有的单表 CRUD 模块生成方式保持不变
|
||||
- 选择 CrudService 时,行为与调整前完全一致
|
||||
- 新增的关联配置问题仅在选择 RelationCrudService 或 ManyToManyCrudService 时显示
|
||||
|
||||
## 文件清单
|
||||
|
||||
| 文件 | 操作 | 说明 |
|
||||
|------|------|------|
|
||||
| `plop/generators/crud.ts` | 修改 | 添加服务类型选择和关联配置问题 |
|
||||
| `plop/utils/relation-parser.ts` | 新建 | 关联/多对多 DSL 解析器 |
|
||||
| `plop/helpers/index.ts` | 修改 | 添加新的 helper(如果需要)|
|
||||
| `plop/templates/api/service.hbs` | 修改 | 支持三种服务类型 |
|
||||
| `plop/templates/api/dto.hbs` | 修改 | 支持关联字段和详情 DTO |
|
||||
| `plop/templates/api/controller.hbs` | 修改 | 支持关联查询端点 |
|
||||
| `plop/templates/shared/types.hbs` | 修改 | 添加关联类型定义 |
|
||||
Reference in New Issue
Block a user