feat(plop): 模板支持关联关系和多对多生成
- service.hbs: 支持三种服务类型,生成 @CrudOptions 关联配置和 DTO 转换方法 - dto.hbs: 生成 BriefDto、DetailResponseDto、AssignDto - controller.hbs: 支持关联查询端点和多对多分配端点 - types.hbs: 添加关联类型接口定义 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Patch,
|
||||
{{#if (eq serviceType 'CrudService')}}Patch{{else}}Put{{/if}},
|
||||
Delete,
|
||||
Param,
|
||||
Body,
|
||||
@@ -16,16 +16,25 @@ import {
|
||||
ApiOkResponse,
|
||||
ApiCreatedResponse,
|
||||
} from '@nestjs/swagger';
|
||||
{{#if (eq serviceType 'CrudService')}}
|
||||
import { Prisma } from '@prisma/client';
|
||||
{{/if}}
|
||||
|
||||
import {
|
||||
Create{{pascalCase name}}Dto,
|
||||
Update{{pascalCase name}}Dto,
|
||||
{{pascalCase name}}ResponseDto,
|
||||
{{#if needsDetailDto}}
|
||||
{{pascalCase name}}DetailResponseDto,
|
||||
{{/if}}
|
||||
Paginated{{pascalCase name}}ResponseDto,
|
||||
{{#if hasQueryDto}}
|
||||
{{pascalCase name}}QueryDto,
|
||||
{{/if}}
|
||||
{{#each manyToMany}}
|
||||
Assign{{pascalCase name}}Dto,
|
||||
{{targetModel}}BriefDto,
|
||||
{{/each}}
|
||||
} from './dto/{{kebabCase name}}.dto';
|
||||
import { {{pascalCase name}}Service } from './{{kebabCase name}}.service';
|
||||
|
||||
@@ -53,6 +62,7 @@ export class {{pascalCase name}}Controller {
|
||||
@ApiOkResponse({ type: Paginated{{pascalCase name}}ResponseDto, description: '{{chineseName}}列表' })
|
||||
{{#if hasQueryDto}}
|
||||
findAll(@Query() query: {{pascalCase name}}QueryDto) {
|
||||
{{#if (eq serviceType 'CrudService')}}
|
||||
const { {{#each queryFields}}{{name}}, {{/each}}...pagination } = query;
|
||||
const where: Prisma.{{pascalCase name}}WhereInput = {};
|
||||
{{#each queryFields}}
|
||||
@@ -65,10 +75,17 @@ export class {{pascalCase name}}Controller {
|
||||
}
|
||||
{{/each}}
|
||||
return this.{{camelCase name}}Service.findAll({ ...pagination, where });
|
||||
{{else}}
|
||||
return this.{{camelCase name}}Service.findAllWithRelations(query);
|
||||
{{/if}}
|
||||
}
|
||||
{{else}}
|
||||
findAll(@Query() query: PaginationQueryDto) {
|
||||
{{#if (eq serviceType 'CrudService')}}
|
||||
return this.{{camelCase name}}Service.findAll(query);
|
||||
{{else}}
|
||||
return this.{{camelCase name}}Service.findAllWithRelations(query);
|
||||
{{/if}}
|
||||
}
|
||||
{{/if}}
|
||||
|
||||
@@ -100,12 +117,24 @@ export class {{pascalCase name}}Controller {
|
||||
{{/if}}
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: '根据 ID 获取{{chineseName}}' })
|
||||
{{#if needsDetailDto}}
|
||||
@ApiOkResponse({ type: {{pascalCase name}}DetailResponseDto, description: '{{chineseName}}详情' })
|
||||
findById(@Param('id') id: string) {
|
||||
return this.{{camelCase name}}Service.findByIdWithRelations(id);
|
||||
}
|
||||
{{else if needsResponseDto}}
|
||||
@ApiOkResponse({ type: {{pascalCase name}}ResponseDto, description: '{{chineseName}}详情' })
|
||||
findById(@Param('id') id: string) {
|
||||
return this.{{camelCase name}}Service.findByIdWithRelations(id);
|
||||
}
|
||||
{{else}}
|
||||
@ApiOkResponse({ type: {{pascalCase name}}ResponseDto, description: '{{chineseName}}详情' })
|
||||
findById(@Param('id') id: string) {
|
||||
return this.{{camelCase name}}Service.findById(id);
|
||||
}
|
||||
{{/if}}
|
||||
|
||||
@Patch(':id')
|
||||
@{{#if (eq serviceType 'CrudService')}}Patch{{else}}Put{{/if}}(':id')
|
||||
@ApiOperation({ summary: '更新{{chineseName}}信息' })
|
||||
@ApiOkResponse({ type: {{pascalCase name}}ResponseDto, description: '更新后的{{chineseName}}信息' })
|
||||
update(@Param('id') id: string, @Body() dto: Update{{pascalCase name}}Dto) {
|
||||
@@ -120,11 +149,29 @@ export class {{pascalCase name}}Controller {
|
||||
}
|
||||
|
||||
{{#if softDelete}}
|
||||
@Patch(':id/restore')
|
||||
@{{#if (eq serviceType 'CrudService')}}Patch{{else}}Put{{/if}}(':id/restore')
|
||||
@ApiOperation({ summary: '恢复已删除的{{chineseName}}' })
|
||||
@ApiOkResponse({ type: {{pascalCase name}}ResponseDto, description: '恢复后的{{chineseName}}信息' })
|
||||
restore(@Param('id') id: string) {
|
||||
return this.{{camelCase name}}Service.restore(id);
|
||||
}
|
||||
{{/if}}
|
||||
{{#if hasManyToMany}}
|
||||
{{#each manyToMany}}
|
||||
|
||||
@Get(':id/{{name}}')
|
||||
@ApiOperation({ summary: '获取{{../chineseName}}的{{name}}列表' })
|
||||
@ApiOkResponse({ type: [{{targetModel}}BriefDto], description: '{{name}}列表' })
|
||||
get{{pascalCase name}}(@Param('id') id: string) {
|
||||
return this.{{camelCase ../name}}Service.getManyToManyTargets(id, '{{name}}');
|
||||
}
|
||||
|
||||
@Put(':id/{{name}}')
|
||||
@ApiOperation({ summary: '分配{{../chineseName}}的{{name}}' })
|
||||
@ApiOkResponse({ type: {{pascalCase ../name}}DetailResponseDto, description: '分配成功' })
|
||||
assign{{pascalCase name}}(@Param('id') id: string, @Body() dto: Assign{{pascalCase name}}Dto) {
|
||||
return this.{{camelCase ../name}}Service.assignManyToMany(id, '{{name}}', { targetIds: dto.{{target}}Ids });
|
||||
}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { ApiProperty, ApiPropertyOptional{{#if needsDetailDto}}, PartialType{{/if}} } from '@nestjs/swagger';
|
||||
{{#if (hasValidation fields)}}
|
||||
import { {{validationImports fields}}, IsOptional } from 'class-validator';
|
||||
import { {{validationImports fields}}, IsOptional{{#if hasManyToMany}}, IsArray, IsString{{/if}} } from 'class-validator';
|
||||
{{else}}
|
||||
import { IsOptional } from 'class-validator';
|
||||
import { IsOptional{{#if hasManyToMany}}, IsArray, IsString{{/if}} } from 'class-validator';
|
||||
{{/if}}
|
||||
{{#if (hasTransform fields)}}
|
||||
{{#if (or (hasTransform fields) hasManyToMany)}}
|
||||
import { Type } from 'class-transformer';
|
||||
{{/if}}
|
||||
import type {
|
||||
@@ -47,6 +47,51 @@ export class Update{{pascalCase name}}Dto implements IUpdate{{pascalCase name}}D
|
||||
{{/each}}
|
||||
}
|
||||
|
||||
{{#if hasRelations}}
|
||||
{{#each relations}}
|
||||
/** {{model}}简要信息 */
|
||||
export class {{model}}BriefDto {
|
||||
@ApiProperty({ example: 'clxxx123', description: '{{model}} ID' })
|
||||
id: string;
|
||||
|
||||
{{#each selectFields}}
|
||||
{{#unless (eq this 'id')}}
|
||||
@ApiProperty({ description: '{{this}}' })
|
||||
{{this}}: string;
|
||||
|
||||
{{/unless}}
|
||||
{{/each}}
|
||||
}
|
||||
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{#if hasManyToMany}}
|
||||
{{#each manyToMany}}
|
||||
/** {{targetModel}}简要信息 */
|
||||
export class {{targetModel}}BriefDto {
|
||||
@ApiProperty({ example: 'clxxx123', description: '{{targetModel}} ID' })
|
||||
id: string;
|
||||
|
||||
{{#each selectFields}}
|
||||
{{#unless (eq this 'id')}}
|
||||
@ApiProperty({ description: '{{this}}' })
|
||||
{{this}}?: string;
|
||||
|
||||
{{/unless}}
|
||||
{{/each}}
|
||||
}
|
||||
|
||||
/** 分配{{name}} DTO */
|
||||
export class Assign{{pascalCase name}}Dto {
|
||||
@ApiProperty({ description: '{{targetModel}} ID 列表', type: [String] })
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@Type(() => String)
|
||||
{{target}}Ids: string[];
|
||||
}
|
||||
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
/** {{chineseName}}响应 DTO */
|
||||
export class {{pascalCase name}}ResponseDto implements {{pascalCase name}}Response {
|
||||
@ApiProperty({ example: 'clxxx123', description: '{{chineseName}} ID' })
|
||||
@@ -61,6 +106,13 @@ export class {{pascalCase name}}ResponseDto implements {{pascalCase name}}Respon
|
||||
{{name}}: {{tsResponseType this}};
|
||||
{{/if}}
|
||||
|
||||
{{/each}}
|
||||
{{#each relations}}
|
||||
{{#if includeInList}}
|
||||
@ApiPropertyOptional({ type: {{model}}BriefDto, description: '{{name}}信息' })
|
||||
{{name}}?: {{model}}BriefDto;
|
||||
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
@ApiProperty({ example: '2026-01-16T10:00:00.000Z', description: '创建时间' })
|
||||
createdAt: string;
|
||||
@@ -74,6 +126,29 @@ export class {{pascalCase name}}ResponseDto implements {{pascalCase name}}Respon
|
||||
{{/if}}
|
||||
}
|
||||
|
||||
{{#if needsDetailDto}}
|
||||
/** {{chineseName}}详情响应 DTO */
|
||||
export class {{pascalCase name}}DetailResponseDto extends {{pascalCase name}}ResponseDto {
|
||||
{{#each relations}}
|
||||
{{#unless includeInList}}
|
||||
@ApiPropertyOptional({ type: {{model}}BriefDto, description: '{{name}}信息' })
|
||||
{{name}}?: {{model}}BriefDto;
|
||||
|
||||
{{/unless}}
|
||||
{{/each}}
|
||||
{{#each manyToMany}}
|
||||
@ApiProperty({ type: [{{targetModel}}BriefDto], description: '{{name}}列表' })
|
||||
{{name}}: {{targetModel}}BriefDto[];
|
||||
|
||||
{{/each}}
|
||||
{{#each countRelations}}
|
||||
@ApiProperty({ example: 0, description: '{{this}}数量' })
|
||||
{{this}}Count: number;
|
||||
|
||||
{{/each}}
|
||||
}
|
||||
{{/if}}
|
||||
|
||||
/** 分页{{chineseName}}响应 DTO */
|
||||
export class Paginated{{pascalCase name}}ResponseDto extends createPaginatedResponseDto(
|
||||
{{pascalCase name}}ResponseDto,
|
||||
|
||||
@@ -1,14 +1,45 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma, {{pascalCase name}} } from '@prisma/client';
|
||||
import type { Prisma, {{pascalCase name}} } from '@prisma/client';
|
||||
|
||||
import { Create{{pascalCase name}}Dto, Update{{pascalCase name}}Dto } from './dto/{{kebabCase name}}.dto';
|
||||
{{#if needsResponseDto}}
|
||||
import { {{pascalCase name}}ResponseDto{{#if needsDetailDto}}, {{pascalCase name}}DetailResponseDto{{/if}} } from './dto/{{kebabCase name}}.dto';
|
||||
{{else}}
|
||||
import { Update{{pascalCase name}}Dto } from './dto/{{kebabCase name}}.dto';
|
||||
{{/if}}
|
||||
|
||||
import { CrudOptions, CrudService } from '@/common/crud';
|
||||
import { CrudOptions } from '@/common/crud/crud.decorator';
|
||||
{{#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}}
|
||||
import { PrismaService } from '@/prisma/prisma.service';
|
||||
|
||||
{{#if hasRelations}}
|
||||
// 带关联的实体类型
|
||||
type {{pascalCase name}}WithRelations = {{pascalCase name}} & {
|
||||
{{#each relations}}
|
||||
{{name}}?: { {{#each selectFields}}{{this}}: string{{#unless @last}}; {{/unless}}{{/each}} } | null;
|
||||
{{/each}}
|
||||
};
|
||||
{{/if}}
|
||||
|
||||
{{#if needsDetailDto}}
|
||||
// 详情实体类型(包含多对多和统计)
|
||||
type {{pascalCase name}}WithDetails = {{#if hasRelations}}{{pascalCase name}}WithRelations{{else}}{{pascalCase name}}{{/if}} & {
|
||||
{{#each manyToMany}}
|
||||
{{name}}?: Array<{ {{target}}: { {{#each selectFields}}{{this}}: string{{#unless @last}}; {{/unless}}{{/each}} } }>;
|
||||
{{/each}}
|
||||
{{#if hasCountRelations}}
|
||||
_count?: { {{#each countRelations}}{{this}}: number{{#unless @last}}; {{/unless}}{{/each}} };
|
||||
{{/if}}
|
||||
};
|
||||
{{/if}}
|
||||
|
||||
@Injectable()
|
||||
@CrudOptions({
|
||||
softDelete: {{softDelete}},
|
||||
defaultPageSize: {{defaultPageSize}},
|
||||
maxPageSize: {{maxPageSize}},
|
||||
defaultSortBy: '{{defaultSortBy}}',
|
||||
@@ -24,13 +55,58 @@ import { PrismaService } from '@/prisma/prisma.service';
|
||||
deletedAt: true,
|
||||
{{/if}}
|
||||
},
|
||||
{{#if hasQueryDto}}
|
||||
filterableFields: [
|
||||
{{#each queryFields}}
|
||||
{{#if (eq type 'string')}}
|
||||
{ field: '{{name}}', operator: 'contains' },
|
||||
{{else}}
|
||||
'{{name}}',
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
],
|
||||
{{/if}}
|
||||
{{#if hasRelations}}
|
||||
relations: {
|
||||
{{#each relations}}
|
||||
{{name}}: {
|
||||
select: { {{#each selectFields}}{{this}}: true{{#unless @last}}, {{/unless}}{{/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{{#unless @last}}, {{/unless}}{{/each}} },
|
||||
},
|
||||
{{/each}}
|
||||
},
|
||||
{{/if}}
|
||||
{{#if hasCountRelations}}
|
||||
countRelations: [{{#each countRelations}}'{{this}}'{{#unless @last}}, {{/unless}}{{/each}}],
|
||||
{{/if}}
|
||||
})
|
||||
export class {{pascalCase name}}Service extends CrudService<
|
||||
export class {{pascalCase name}}Service extends {{serviceType}}<
|
||||
{{pascalCase name}},
|
||||
Create{{pascalCase name}}Dto,
|
||||
Prisma.{{pascalCase name}}CreateInput,
|
||||
{{#if needsResponseDto}}
|
||||
Prisma.{{pascalCase name}}UpdateInput,
|
||||
{{else}}
|
||||
Update{{pascalCase name}}Dto,
|
||||
{{/if}}
|
||||
Prisma.{{pascalCase name}}WhereInput,
|
||||
Prisma.{{pascalCase name}}WhereUniqueInput
|
||||
Prisma.{{pascalCase name}}WhereUniqueInput{{#if needsResponseDto}},
|
||||
{{pascalCase name}}ResponseDto{{#if needsDetailDto}},
|
||||
{{pascalCase name}}DetailResponseDto{{/if}}{{/if}}
|
||||
> {
|
||||
constructor(prisma: PrismaService) {
|
||||
super(prisma, '{{camelCase name}}');
|
||||
@@ -49,4 +125,54 @@ export class {{pascalCase name}}Service extends CrudService<
|
||||
return '已删除的{{chineseName}}不存在';
|
||||
}
|
||||
{{/if}}
|
||||
|
||||
{{#if needsResponseDto}}
|
||||
/**
|
||||
* 转换为响应 DTO
|
||||
*/
|
||||
protected toResponseDto = (entity: {{#if hasRelations}}{{pascalCase name}}WithRelations{{else}}{{pascalCase name}}{{/if}}): {{pascalCase name}}ResponseDto => ({
|
||||
id: entity.id,
|
||||
{{#each responseFields}}
|
||||
{{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 ../softDelete}}
|
||||
deletedAt: entity.deletedAt?.toISOString() ?? undefined,
|
||||
{{/if}}
|
||||
});
|
||||
{{/if}}
|
||||
|
||||
{{#if needsDetailDto}}
|
||||
/**
|
||||
* 转换为详情响应 DTO
|
||||
*/
|
||||
protected override toDetailDto(entity: {{pascalCase name}}WithDetails): {{pascalCase name}}DetailResponseDto {
|
||||
return {
|
||||
id: entity.id,
|
||||
{{#each responseFields}}
|
||||
{{name}}: entity.{{name}}{{#if nullable}} ?? undefined{{/if}},
|
||||
{{/each}}
|
||||
{{#each relations}}
|
||||
{{name}}: entity.{{name}} ?? undefined,
|
||||
{{/each}}
|
||||
{{#each manyToMany}}
|
||||
{{name}}: entity.{{name}}?.map((item) => item.{{target}}) ?? [],
|
||||
{{/each}}
|
||||
{{#each countRelations}}
|
||||
{{this}}Count: entity._count?.{{this}} ?? 0,
|
||||
{{/each}}
|
||||
createdAt: entity.createdAt.toISOString(),
|
||||
updatedAt: entity.updatedAt.toISOString(),
|
||||
{{#if ../softDelete}}
|
||||
deletedAt: entity.deletedAt?.toISOString() ?? undefined,
|
||||
{{/if}}
|
||||
};
|
||||
}
|
||||
{{/if}}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,43 @@
|
||||
// ==================== {{chineseName}}相关类型 ====================
|
||||
{{#if hasRelations}}
|
||||
|
||||
{{#each relations}}
|
||||
/** {{model}}简要信息 */
|
||||
export interface {{model}}Brief {
|
||||
id: string;
|
||||
{{#each selectFields}}
|
||||
{{#unless (eq this 'id')}}
|
||||
{{this}}: string;
|
||||
{{/unless}}
|
||||
{{/each}}
|
||||
}
|
||||
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{#if hasManyToMany}}
|
||||
{{#each manyToMany}}
|
||||
/** {{targetModel}}简要信息 */
|
||||
export interface {{targetModel}}Brief {
|
||||
id: string;
|
||||
{{#each selectFields}}
|
||||
{{#unless (eq this 'id')}}
|
||||
{{this}}?: string;
|
||||
{{/unless}}
|
||||
{{/each}}
|
||||
}
|
||||
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
/** {{chineseName}}响应(API 返回) */
|
||||
export interface {{pascalCase name}}Response {
|
||||
id: string;
|
||||
{{#each responseFields}}
|
||||
{{name}}: {{tsResponseType this}}{{#if nullable}} | null{{/if}};
|
||||
{{/each}}
|
||||
{{#each relations}}
|
||||
{{#if includeInList}}
|
||||
{{name}}?: {{model}}Brief;
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@@ -12,6 +45,23 @@ export interface {{pascalCase name}}Response {
|
||||
deletedAt?: string | null;
|
||||
{{/if}}
|
||||
}
|
||||
{{#if needsDetailDto}}
|
||||
|
||||
/** {{chineseName}}详情响应 */
|
||||
export interface {{pascalCase name}}DetailResponse extends {{pascalCase name}}Response {
|
||||
{{#each relations}}
|
||||
{{#unless includeInList}}
|
||||
{{name}}?: {{model}}Brief;
|
||||
{{/unless}}
|
||||
{{/each}}
|
||||
{{#each manyToMany}}
|
||||
{{name}}: {{targetModel}}Brief[];
|
||||
{{/each}}
|
||||
{{#each countRelations}}
|
||||
{{this}}Count: number;
|
||||
{{/each}}
|
||||
}
|
||||
{{/if}}
|
||||
|
||||
/** 创建{{chineseName}}请求 */
|
||||
export interface Create{{pascalCase name}}Dto {
|
||||
@@ -26,3 +76,12 @@ export interface Update{{pascalCase name}}Dto {
|
||||
{{name}}?: {{tsType this}};
|
||||
{{/each}}
|
||||
}
|
||||
{{#if hasManyToMany}}
|
||||
{{#each manyToMany}}
|
||||
|
||||
/** 分配{{name}}请求 */
|
||||
export interface Assign{{pascalCase name}}Dto {
|
||||
{{target}}Ids: string[];
|
||||
}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
Reference in New Issue
Block a user