Skip to content

Commit

Permalink
✨ feat: add nest gen
Browse files Browse the repository at this point in the history
  • Loading branch information
gylove1994 committed Sep 25, 2024
1 parent aa4c0fa commit a0db24c
Show file tree
Hide file tree
Showing 16 changed files with 580 additions and 35 deletions.
10 changes: 7 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@
"prepublish": "npm run build",
"publish": "npm publish --access public"
},
"files": ["dist", "README.md", "LICENSE", "bin"],
"files": [
"dist",
"README.md",
"LICENSE",
"bin"
],
"bin": {
"npg": "./bin/npg"
},
Expand All @@ -36,8 +41,7 @@
"commander": "^12.1.0",
"dotenv": "^16.4.5",
"fs-extra": "^11.2.0",
"inquirer": "^11.0.2",
"rxjs": "^7.8.1"
"inquirer": "^11.0.2"
},
"devDependencies": {
"@biomejs/biome": "1.9.2",
Expand Down
3 changes: 0 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

110 changes: 106 additions & 4 deletions src/generateEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@ import { strings } from "@angular-devkit/core";
import type { Model, Schema } from "@mrleebo/prisma-ast";
import chalk from "chalk";
import { getRelation } from "./utils/getRelation";
import { importApiProperty, importFile } from "./utils/import";
import {
importApiProperty,
importFile,
importJsonValue,
importPickType,
} from "./utils/import";
import { mkFile } from "./utils/mkFile";
import { propertyMap } from "./utils/propertyMap";
import { swaggerMap } from "./utils/swaggerMap";
import { classify } from "@angular-devkit/core/src/utils/strings";

export function generateEntity(model: Model) {
const propertiesContent = model.properties
Expand All @@ -14,9 +20,13 @@ export function generateEntity(model: Model) {
return swaggerMap(prop) + propertyMap(prop);
})
.join("\n");
const hasJson = model.properties.some(
(prop) => prop.type === "field" && prop.fieldType === "Json",
);
const imports = getRelation(model)
.map(importFile)
.map((v) => importFile(v))
.concat(importApiProperty())
.concat(hasJson ? importJsonValue() : "")
.join("");
const content = `${imports}\nexport class ${strings.classify(
model.name,
Expand All @@ -27,10 +37,102 @@ export function generateEntity(model: Model) {
};
}

export function generateEntityFile(prisma: Schema, outputPath: string) {
export function generateEntityFile(
prisma: Schema,
outputPath: string,
dryRun: boolean,
) {
const entityList = prisma.list.filter((item) => item.type === "model");
for (const entity of entityList) {
const { name, content } = generateEntity(entity);
mkFile(outputPath, name, content);
mkFile(outputPath, name, content, dryRun);
}
}

export function generatePickEntity(model: Model) {
// 从模型中提取字段属性的名称
const properties = model.properties
.filter((v) => v.type === "field")
.map((prop) => prop.name);

// 生成所有可能的属性组合
function generateCombinations(
arr: string[],
): { combination: string; originalNames: string[] }[] {
const result: { combination: string; originalNames: string[] }[] = [];

function backtrack(
start: number,
current: string[],
originalCurrent: string[],
) {
if (current.length > 0) {
result.push({
combination: current.join("_"),
originalNames: [...originalCurrent],
});
}

for (let i = start; i < arr.length; i++) {
current.push(classify(arr[i]));
originalCurrent.push(arr[i]);
backtrack(i + 1, current, originalCurrent);
current.pop();
originalCurrent.pop();
}
}

backtrack(0, [], []);
return result;
}

// 获取所有属性的组合
const combinations = generateCombinations(properties);

// 获取实体名称并首字母大写
const entityName = strings.classify(model.name);

const imports = importPickType() + importFile(entityName);

// 为每个组合生成PickType
const c = combinations
.map(
({ combination, originalNames }) =>
`class ${entityName}Pick_${combination} extends PickType(${entityName}, [${originalNames.map((v) => `"${v}"`).join(", ")}]) {}\n`,
)
.join("\n");

// 添加新的函数,使用泛型来确保返回类型的正确性
const pickTypeFunction = `
export function ${entityName}PickType<T extends keyof ${entityName}>(keys: T[]) {
const sortedKeys = keys.sort().join('_');
switch (sortedKeys) {
${combinations
.map(
({ combination, originalNames }) => `
case '${originalNames.sort().join("_")}':
return ${entityName}Pick_${combination};`,
)
.join("")}
default:
throw new Error('未找到匹配的PickType类');
}
}`;

const content = `${imports}\n${c}\n${pickTypeFunction}`;

// 返回生成的文件名和内容
return { name: `${entityName}PickTypeEntities.ts`, content };
}

export function generatePickEntityFile(
prisma: Schema,
outputPath: string,
dryRun: boolean,
) {
const entityList = prisma.list.filter((item) => item.type === "model");
for (const entity of entityList) {
const { name, content } = generatePickEntity(entity);
mkFile(outputPath, name, content, dryRun);
}
}
8 changes: 6 additions & 2 deletions src/generateEnum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,14 @@ export function generateEnum(enumModel: Enum) {
return res;
}

export function generateEnumFile(prisma: Schema, outputPath: string) {
export function generateEnumFile(
prisma: Schema,
outputPath: string,
dryRun: boolean,
) {
const enumList = prisma.list.filter((item) => item.type === "enum");
for (const e of enumList) {
const { name, content } = generateEnum(e);
mkFile(outputPath, name, content);
mkFile(outputPath, name, content, dryRun);
}
}
129 changes: 129 additions & 0 deletions src/generateNestController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import type { Model, Schema } from "@mrleebo/prisma-ast";
import { strings } from "@angular-devkit/core";
import { mkFile } from "./utils/mkFile";

const controllerTemplate = `
import { Controller } from '@nestjs/common';
import { {_@modelNameCapitalize@_}Service } from './{_@modelName@_}.service';
import { {_@modelNameCapitalize@_}IdExistDto, {_@modelNameCapitalize@_}CreateDto, {_@modelNameCapitalize@_}UpdateDto, Pagination{_@modelNameCapitalize@_}Dto } from './{_@modelName@_}.dtos';
import { ApiResponse, ApiTags, ApiOperation, ApiExtraModels } from '@nestjs/swagger';
import { HttpCode, Query, Body, Post, Get } from '@nestjs/common';
import { {_@modelNameCapitalize@_}FindAllResponse, {_@modelNameCapitalize@_}FindOneResponse, {_@modelNameCapitalize@_}CreateResponse, {_@modelNameCapitalize@_}UpdateResponse, {_@modelNameCapitalize@_}DeleteResponse, {_@modelNameCapitalize@_}ListResponse } from './{_@modelName@_}.types';
@ApiTags('{_@modelNameCapitalize@_}')
@Controller('{_@modelName@_}')
@ApiExtraModels({_@modelNameCapitalize@_}FindAllResponse, {_@modelNameCapitalize@_}FindOneResponse, {_@modelNameCapitalize@_}CreateResponse, {_@modelNameCapitalize@_}UpdateResponse, {_@modelNameCapitalize@_}DeleteResponse, {_@modelNameCapitalize@_}ListResponse)
export class {_@modelNameCapitalize@_}Controller {
constructor(private readonly {_@modelName@_}Service: {_@modelNameCapitalize@_}Service) {}
@Get('all')
@HttpCode(200)
@ApiOperation({ summary: '获取所有{_@modelName@_}' })
@ApiResponse({
status: 200,
type: {_@modelNameCapitalize@_}FindAllResponse,
})
findAll() {
return this.{_@modelName@_}Service.findAll();
}
@Get('detail')
@HttpCode(200)
@ApiOperation({ summary: '获取{_@modelName@_}详情' })
@ApiResponse({
status: 200,
type: {_@modelNameCapitalize@_}FindOneResponse,
})
findOne(@Query() dto: {_@modelNameCapitalize@_}IdExistDto) {
return this.{_@modelName@_}Service.findOne(dto);
}
@Post('create')
@HttpCode(200)
@ApiOperation({ summary: '创建{_@modelName@_}' })
@ApiResponse({
status: 200,
type: {_@modelNameCapitalize@_}CreateResponse,
})
create(@Body() dto: {_@modelNameCapitalize@_}CreateDto) {
return this.{_@modelName@_}Service.create(dto);
}
@Post('update')
@HttpCode(200)
@ApiOperation({ summary: '更新{_@modelName@_}' })
@ApiResponse({
status: 200,
type: {_@modelNameCapitalize@_}UpdateResponse,
})
update(@Body() dto: {_@modelNameCapitalize@_}UpdateDto) {
return this.{_@modelName@_}Service.update(dto);
}
@Post('delete')
@HttpCode(200)
@ApiOperation({ summary: '删除{_@modelName@_}' })
@ApiResponse({
status: 200,
type: {_@modelNameCapitalize@_}DeleteResponse,
})
delete(@Body() dto: {_@modelNameCapitalize@_}IdExistDto) {
return this.{_@modelName@_}Service.delete(dto);
}
@Get('list')
@ApiOperation({ summary: '分页获取{_@modelName@_}列表' })
@ApiResponse({
status: 200,
type: {_@modelNameCapitalize@_}ListResponse,
})
@HttpCode(200)
async list(@Query() dto: Pagination{_@modelNameCapitalize@_}Dto) {
return await this.{_@modelName@_}Service.list(dto)
}
// for CORS
// @Options('create')
// createOp() {}
// @Options('update')
// updateOp() {}
// @Options('delete')
// deleteOp() {}
}
`;

export function generateNestController(prisma: Schema) {
const entityList = prisma.list
.filter((item) => item.type === "model")
.map((item) => {
const entity = item as Model;
const modelNameCamelize = strings.camelize(entity.name);
const modelNameCapitalize = strings.capitalize(modelNameCamelize);
return {
name: modelNameCamelize,
content: controllerTemplate
.replaceAll(/{_@modelName@_}/g, modelNameCamelize)
.replaceAll(/{_@modelNameCapitalize@_}/g, modelNameCapitalize),
};
});
return entityList;
}

export function generateNestControllerFile(
prisma: Schema,
outputPath: string,
dryRun: boolean,
) {
const entityList = generateNestController(prisma);
for (const entity of entityList) {
mkFile(
`${outputPath}/${entity.name}`,
`${entity.name}.controller.ts`,
entity.content,
dryRun,
);
}
}
80 changes: 80 additions & 0 deletions src/generateNestDto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { strings } from "@angular-devkit/core";
import type { Model, Schema } from "@mrleebo/prisma-ast";
import { propertyMap } from "./utils/propertyMap";
import { mkFile } from "./utils/mkFile";
import { getRelation } from "./utils/getRelation";
import { importFile } from "./utils/import";

const dtoTemplate = `
import { ApiProperty, IntersectionType, OmitType, PartialType } from '@nestjs/swagger';
import { IsNotEmpty, IsString, IsNumber, IsDate, IsBoolean, IsArray, IsObject, IsOptional } from 'class-validator';
{_@imports@_}
export class Pagination{_@modelNameCapitalize@_}Dto {
@ApiProperty({ description: '分页大小' })
pageSize: number;
@ApiProperty({ description: '当前页' })
page: number;
}
export class {_@modelNameCapitalize@_}IdExistDto {
@ApiProperty({ description: '{_@modelName@_}ID' })
@IsNotEmpty({ message: '{_@modelName@_}ID不能为空' })
// @IsUUID('4', { message: '{_@modelName@_}ID类型错误, 正确类型为uuid' })
@IsString({ message: '{_@modelName@_}ID类型错误, 正确类型为string' })
// @IsExistInDataBase({_@modelNameCapitalize@_},'id')
id: string;
}
export class {_@modelNameCapitalize@_}CreateDto {
{_@CreateDtoFields@_}
}
export class {_@modelNameCapitalize@_}UpdateDto extends IntersectionType({_@modelNameCapitalize@_}IdExistDto, OmitType(PartialType({_@modelNameCapitalize@_}CreateDto), [])) {}
`;

export function generateNestDto(model: Schema) {
const fields = model.list
.filter((field) => field.type === "model")
.map((v) => {
const field = v as Model;
const modelNameCamelize = strings.camelize(field.name);
const modelNameCapitalize = strings.capitalize(modelNameCamelize);
const imports = getRelation(field)
.map((v) => importFile(v, true))
.join("\n");
const createdDtoField = v.properties
.filter((v) => v.type === "field")
.map((v) => {
return propertyMap(v);
})
.join("\n");
return {
name: `${modelNameCamelize}`,
content: dtoTemplate
.replaceAll("{_@modelName@_}", modelNameCamelize)
.replaceAll("{_@modelNameCapitalize@_}", modelNameCapitalize)
.replaceAll("{_@CreateDtoFields@_}", createdDtoField)
.replaceAll("{_@imports@_}", imports),
};
});
return fields;
}

export function generateNestDtoFile(
prisma: Schema,
outputPath: string,
dryRun: boolean,
) {
const entityList = generateNestDto(prisma);
for (const entity of entityList) {
mkFile(
`${outputPath}/${entity.name}`,
`${entity.name}.dtos.ts`,
entity.content,
dryRun,
);
}
}
Loading

0 comments on commit a0db24c

Please sign in to comment.