diff --git a/docs/workflow/250119-plop-crud-generator-upgrade.md b/docs/workflow/250119-plop-crud-generator-upgrade.md new file mode 100644 index 0000000..5cab36b --- /dev/null +++ b/docs/workflow/250119-plop-crud-generator-upgrade.md @@ -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` | 修改 | 添加关联类型定义 | diff --git a/plop/generators/crud.ts b/plop/generators/crud.ts index d361704..c14fcc7 100644 --- a/plop/generators/crud.ts +++ b/plop/generators/crud.ts @@ -1,10 +1,25 @@ /** * CRUD 生成器 * 交互式提问和文件生成逻辑 + * + * 支持三种服务类型: + * - CrudService: 单表 CRUD + * - RelationCrudService: 带关联查询 + * - ManyToManyCrudService: 多对多关系 */ import type { NodePlopAPI, ActionType } from 'plop'; -import { parseFields, type FieldDefinition } from '../utils/field-parser.ts'; +import { parseFields, type FieldDefinition } from '../utils/field-parser'; +import { + parseRelations, + parseManyToMany, + parseCountRelations, + type RelationConfig, + type ManyToManyConfig, +} from '../utils/relation-parser'; + +// 服务类型 +type ServiceType = 'CrudService' | 'RelationCrudService' | 'ManyToManyCrudService'; // 模板数据类型 interface TemplateData { @@ -28,6 +43,19 @@ interface TemplateData { hasTextarea: boolean; hasSelect: boolean; hasSwitch: boolean; + + // 服务类型相关 + serviceType: ServiceType; + relations: RelationConfig[]; + manyToMany: ManyToManyConfig[]; + countRelations: string[]; + + // 派生标志 + hasRelations: boolean; + hasManyToMany: boolean; + hasCountRelations: boolean; + needsResponseDto: boolean; + needsDetailDto: boolean; } /** @@ -71,6 +99,18 @@ export function crudGenerator(plop: NodePlopAPI) { { name: 'Prisma Model', value: 'prisma', checked: true }, ], }, + // 服务类型选择 + { + type: 'list', + name: 'serviceType', + message: '选择服务类型:', + choices: [ + { name: 'CrudService(单表 CRUD)', value: 'CrudService' }, + { name: 'RelationCrudService(带关联查询)', value: 'RelationCrudService' }, + { name: 'ManyToManyCrudService(多对多关系)', value: 'ManyToManyCrudService' }, + ], + default: 'CrudService', + }, { type: 'confirm', name: 'softDelete', @@ -98,6 +138,49 @@ export function crudGenerator(plop: NodePlopAPI) { name:string 名称 "示例名称" min:2 max:100 `, }, + // 关联关系配置(RelationCrudService 或 ManyToManyCrudService 时显示) + { + type: 'editor', + name: 'relationsRaw', + message: '定义关联关系(见下方示例):', + when: (answers: { serviceType: ServiceType }) => + answers.serviceType === 'RelationCrudService' || + answers.serviceType === 'ManyToManyCrudService', + default: `# 关联关系定义(每行一个关联) +# 格式: 关联名:目标模型 字段1,字段2,... [noList] +# noList: 不在列表中包含 +# +# 示例: +# class:Class id,code,name +# headTeacher:Teacher id,teacherNo,name,subject noList + +`, + }, + // 多对多关系配置(ManyToManyCrudService 时显示) + { + type: 'editor', + name: 'manyToManyRaw', + message: '定义多对多关系(见下方示例):', + when: (answers: { serviceType: ServiceType }) => + answers.serviceType === 'ManyToManyCrudService', + default: `# 多对多关系定义(每行一个关系) +# 格式: 关系名:中间表:外键:目标键:目标模型 字段1,字段2,... +# +# 示例: +# teachers:classTeacher:classId:teacherId:Teacher id,teacherNo,name,subject + +`, + }, + // 统计关系配置(RelationCrudService 或 ManyToManyCrudService 时显示) + { + type: 'input', + name: 'countRelationsRaw', + message: '统计关系(逗号分隔,如 students,orders,留空跳过):', + when: (answers: { serviceType: ServiceType }) => + answers.serviceType === 'RelationCrudService' || + answers.serviceType === 'ManyToManyCrudService', + default: '', + }, { type: 'checkbox', name: 'searchFieldNames', @@ -172,6 +255,28 @@ name:string 名称 "示例名称" min:2 max:100 return []; } + // 解析关联配置 + const serviceType = data.serviceType as ServiceType; + const relations = + serviceType !== 'CrudService' && data.relationsRaw + ? parseRelations(data.relationsRaw) + : []; + const manyToMany = + serviceType === 'ManyToManyCrudService' && data.manyToManyRaw + ? parseManyToMany(data.manyToManyRaw) + : []; + const countRelations = + serviceType !== 'CrudService' && data.countRelationsRaw + ? parseCountRelations(data.countRelationsRaw) + : []; + + // 派生标志 + const hasRelations = relations.length > 0; + const hasManyToMany = manyToMany.length > 0; + const hasCountRelations = countRelations.length > 0; + const needsResponseDto = serviceType !== 'CrudService'; + const needsDetailDto = serviceType === 'ManyToManyCrudService' && (hasManyToMany || hasCountRelations); + // 准备模板数据 const templateData: TemplateData = { name: data.name, @@ -200,6 +305,17 @@ name:string 名称 "示例名称" min:2 max:100 }), hasSelect: fields.some((f) => f.type === 'enum'), hasSwitch: fields.some((f) => f.type === 'boolean'), + + // 服务类型相关 + serviceType, + relations, + manyToMany, + countRelations, + hasRelations, + hasManyToMany, + hasCountRelations, + needsResponseDto, + needsDetailDto, }; const actions: ActionType[] = []; @@ -349,7 +465,17 @@ name:string 名称 "示例名称" min:2 max:100 // 打印生成信息 actions.push(() => { console.log('\n✨ 生成完成!\n'); - console.log('后续步骤:'); + console.log(`服务类型: ${serviceType}`); + if (hasRelations) { + console.log(`关联关系: ${relations.map((r) => r.name).join(', ')}`); + } + if (hasManyToMany) { + console.log(`多对多关系: ${manyToMany.map((m) => m.name).join(', ')}`); + } + if (hasCountRelations) { + console.log(`统计关系: ${countRelations.join(', ')}`); + } + console.log('\n后续步骤:'); if (data.generateTargets.includes('prisma')) { console.log('1. 运行 pnpm db:generate && pnpm db:push'); } diff --git a/plop/helpers/index.ts b/plop/helpers/index.ts index 37e44ba..951b596 100644 --- a/plop/helpers/index.ts +++ b/plop/helpers/index.ts @@ -16,13 +16,15 @@ import { getFormControl, getCellRenderer, getWhereCondition, -} from '../utils/field-parser.ts'; +} from '../utils/field-parser'; /** * 命名转换工具函数 */ function toCamelCase(str: string): string { - return str.replace(/[-_](.)/g, (_, c) => c.toUpperCase()).replace(/^(.)/, (_, c) => c.toLowerCase()); + return str + .replace(/[-_](.)/g, (_, c) => c.toUpperCase()) + .replace(/^(.)/, (_, c) => c.toLowerCase()); } function toPascalCase(str: string): string { @@ -64,56 +66,44 @@ export function registerHelpers(plop: NodePlopAPI) { plop.setHelper('tsType', (field: FieldDefinition) => getTsType(field)); - plop.setHelper('tsResponseType', (field: FieldDefinition) => - getTsResponseType(field), - ); + plop.setHelper('tsResponseType', (field: FieldDefinition) => getTsResponseType(field)); - plop.setHelper('prismaType', (field: FieldDefinition) => - getPrismaType(field), - ); + plop.setHelper('prismaType', (field: FieldDefinition) => getPrismaType(field)); // ===== 验证相关 helpers ===== plop.setHelper('validationDecorators', (field: FieldDefinition) => - getValidationDecorators(field), + getValidationDecorators(field) ); plop.setHelper('validationImports', (fields: FieldDefinition[]) => - getValidationImports(fields).join(', '), + getValidationImports(fields).join(', ') ); // ===== Zod 验证 helpers ===== - plop.setHelper('zodValidation', (field: FieldDefinition) => - getZodValidation(field), - ); + plop.setHelper('zodValidation', (field: FieldDefinition) => getZodValidation(field)); // ===== 表单控件 helpers ===== - plop.setHelper('formControl', (field: FieldDefinition) => - getFormControl(field), - ); + plop.setHelper('formControl', (field: FieldDefinition) => getFormControl(field)); // ===== 表格渲染 helpers ===== - plop.setHelper('cellRenderer', (field: FieldDefinition) => - getCellRenderer(field), - ); + plop.setHelper('cellRenderer', (field: FieldDefinition) => getCellRenderer(field)); // ===== 查询条件 helpers ===== - plop.setHelper('whereCondition', (field: FieldDefinition) => - getWhereCondition(field), - ); + plop.setHelper('whereCondition', (field: FieldDefinition) => getWhereCondition(field)); // ===== 条件判断 helpers ===== plop.setHelper('hasValidation', (fields: FieldDefinition[]) => - fields.some((f) => f.validations.length > 0), + fields.some((f) => f.validations.length > 0) ); plop.setHelper('hasTransform', (fields: FieldDefinition[]) => - fields.some((f) => f.type === 'date' || f.type === 'datetime'), + fields.some((f) => f.type === 'date' || f.type === 'datetime') ); plop.setHelper('hasTextarea', (fields: FieldDefinition[]) => @@ -121,22 +111,18 @@ export function registerHelpers(plop: NodePlopAPI) { if (f.type !== 'string') return false; const maxLen = f.validations.find((v) => v.type === 'max')?.value; return maxLen && Number(maxLen) > 100; - }), + }) ); - plop.setHelper('hasSelect', (fields: FieldDefinition[]) => - fields.some((f) => f.type === 'enum'), - ); + plop.setHelper('hasSelect', (fields: FieldDefinition[]) => fields.some((f) => f.type === 'enum')); plop.setHelper('hasSwitch', (fields: FieldDefinition[]) => - fields.some((f) => f.type === 'boolean'), + fields.some((f) => f.type === 'boolean') ); // ===== 字符串处理 helpers ===== - plop.setHelper('join', (arr: string[], separator: string) => - arr.join(separator), - ); + plop.setHelper('join', (arr: string[], separator: string) => arr.join(separator)); plop.setHelper('jsonStringify', (obj: unknown) => JSON.stringify(obj)); @@ -149,9 +135,7 @@ export function registerHelpers(plop: NodePlopAPI) { plop.setHelper('not', (a: unknown) => !a); // 判断数组是否包含某值 - plop.setHelper('includes', (arr: unknown[], value: unknown) => - arr?.includes(value), - ); + plop.setHelper('includes', (arr: unknown[], value: unknown) => arr?.includes(value)); // ===== 默认值 helpers ===== diff --git a/plop/utils/relation-parser.ts b/plop/utils/relation-parser.ts new file mode 100644 index 0000000..dd68ffd --- /dev/null +++ b/plop/utils/relation-parser.ts @@ -0,0 +1,216 @@ +/** + * 关联关系 DSL 解析器 + * + * DSL 格式: + * - 关联关系: 关联名:目标模型 字段1,字段2,... [noList] + * - 多对多: 关系名:中间表:外键:目标键:目标模型 字段1,字段2,... + */ + +/** + * 关联关系配置 + */ +export interface RelationConfig { + /** 关联名,如 'class' */ + name: string; + /** 目标模型 PascalCase,如 'Class' */ + model: string; + /** 目标模型 camelCase,如 'class' */ + modelCamel: string; + /** select 字段列表 */ + selectFields: string[]; + /** 是否在列表中包含(默认 true) */ + includeInList: boolean; +} + +/** + * 多对多关系���置 + */ +export interface ManyToManyConfig { + /** 关系名,如 'teachers' */ + name: string; + /** 中间表名,如 'classTeacher' */ + through: string; + /** 当前实体外键,如 'classId' */ + foreignKey: string; + /** 目标实体外键,如 'teacherId' */ + targetKey: string; + /** 目标模型 camelCase,如 'teacher' */ + target: string; + /** 目标模型 PascalCase,如 'Teacher' */ + targetModel: string; + /** 目标实体 select 字段列表 */ + selectFields: string[]; +} + +/** + * 解析关联关系 DSL + * + * @example + * 输入: + * ``` + * class:Class id,code,name + * headTeacher:Teacher id,teacherNo,name,subject noList + * ``` + * + * 输出: + * [ + * { name: 'class', model: 'Class', selectFields: ['id', 'code', 'name'], includeInList: true }, + * { name: 'headTeacher', model: 'Teacher', selectFields: ['id', 'teacherNo', 'name', 'subject'], includeInList: false }, + * ] + */ +export function parseRelations(dsl: string): RelationConfig[] { + const lines = dsl + .split('\n') + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith('#')); + + const relations: RelationConfig[] = []; + + for (const line of lines) { + // 匹配格式: 关联名:目标模型 字段1,字段2,... [noList] + const match = line.match(/^(\w+):(\w+)\s+([\w,]+)(?:\s+(noList))?$/); + if (!match) { + console.warn(`无法解析关联配置行: ${line}`); + continue; + } + + const [, name, model, fieldsStr, noListFlag] = match; + const selectFields = fieldsStr.split(',').map((f) => f.trim()); + + relations.push({ + name, + model, + modelCamel: model.charAt(0).toLowerCase() + model.slice(1), + selectFields, + includeInList: !noListFlag, + }); + } + + return relations; +} + +/** + * 解析多对多关系 DSL + * + * @example + * 输入: + * ``` + * teachers:classTeacher:classId:teacherId:Teacher id,teacherNo,name,subject + * ``` + * + * 输出: + * [ + * { + * name: 'teachers', + * through: 'classTeacher', + * foreignKey: 'classId', + * targetKey: 'teacherId', + * target: 'teacher', + * targetModel: 'Teacher', + * selectFields: ['id', 'teacherNo', 'name', 'subject'], + * }, + * ] + */ +export function parseManyToMany(dsl: string): ManyToManyConfig[] { + const lines = dsl + .split('\n') + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith('#')); + + const configs: ManyToManyConfig[] = []; + + for (const line of lines) { + // 匹配格式: 关系名:中间表:外键:目标键:目标模型 字段1,字段2,... + const match = line.match(/^(\w+):(\w+):(\w+):(\w+):(\w+)\s+([\w,]+)$/); + if (!match) { + console.warn(`无法解析多对多配置行: ${line}`); + continue; + } + + const [, name, through, foreignKey, targetKey, targetModel, fieldsStr] = match; + const selectFields = fieldsStr.split(',').map((f) => f.trim()); + + configs.push({ + name, + through, + foreignKey, + targetKey, + target: targetModel.charAt(0).toLowerCase() + targetModel.slice(1), + targetModel, + selectFields, + }); + } + + return configs; +} + +/** + * 解析统计关系(逗号分隔的字符串) + * + * @example + * 输入: "students, orders" + * 输出: ['students', 'orders'] + */ +export function parseCountRelations(input: string): string[] { + if (!input || !input.trim()) { + return []; + } + return input + .split(',') + .map((s) => s.trim()) + .filter(Boolean); +} + +/** + * 获取字段的 TypeScript 类型(用于生成关联类型) + * 简化版本,假设大多数字段是 string + */ +export function getFieldType(fieldName: string): string { + // 常见的非字符串字段 + const numberFields = ['id', 'count', 'amount', 'price', 'quantity', 'sort', 'order']; + const booleanFields = ['is', 'has', 'can', 'should', 'enabled', 'active', 'visible']; + + if (numberFields.some((f) => fieldName.toLowerCase().includes(f))) { + return 'number'; + } + if (booleanFields.some((f) => fieldName.toLowerCase().startsWith(f))) { + return 'boolean'; + } + return 'string'; +} + +/** + * 生成关联类型的 select 对象字符串 + * + * @example + * 输入: ['id', 'code', 'name'] + * 输出: '{ id: true, code: true, name: true }' + */ +export function generateSelectObject(fields: string[]): string { + const pairs = fields.map((f) => `${f}: true`); + return `{ ${pairs.join(', ')} }`; +} + +/** + * 生成关联类型定义字符串 + * + * @example + * 输入: { name: 'class', selectFields: ['id', 'code', 'name'] } + * 输出: 'class?: { id: string; code: string; name: string } | null' + */ +export function generateRelationTypeField(config: RelationConfig): string { + const fieldTypes = config.selectFields.map((f) => `${f}: ${getFieldType(f)}`); + return `${config.name}?: { ${fieldTypes.join('; ')} } | null`; +} + +/** + * 生成多对多类型定义字符串 + * + * @example + * 输入: { name: 'teachers', target: 'teacher', selectFields: ['id', 'name'] } + * 输出: 'teachers?: Array<{ teacher: { id: string; name: string } }>' + */ +export function generateManyToManyTypeField(config: ManyToManyConfig): string { + const fieldTypes = config.selectFields.map((f) => `${f}: ${getFieldType(f)}`); + return `${config.name}?: Array<{ ${config.target}: { ${fieldTypes.join('; ')} } }>`; +}