From 605b5db37f563f034068826d492e8a03abcae6fd Mon Sep 17 00:00:00 2001 From: Savio629 Date: Mon, 15 Jul 2024 00:03:06 +0530 Subject: [PATCH 01/12] feat: New command to generate crud api --- actions/crud.action.ts | 186 ++++++++++++++++++++++++++ actions/index.ts | 1 + commands/command.loader.ts | 3 + commands/crud.command.ts | 14 ++ lib/templates/controller-template.ts | 36 +++++ lib/templates/dto-template.ts | 31 +++++ lib/templates/service-template.ts | 40 ++++++ package-lock.json | 191 ++++++++++++++++++++++----- package.json | 3 +- 9 files changed, 469 insertions(+), 36 deletions(-) create mode 100644 actions/crud.action.ts create mode 100644 commands/crud.command.ts create mode 100644 lib/templates/controller-template.ts create mode 100644 lib/templates/dto-template.ts create mode 100644 lib/templates/service-template.ts diff --git a/actions/crud.action.ts b/actions/crud.action.ts new file mode 100644 index 00000000..2a631901 --- /dev/null +++ b/actions/crud.action.ts @@ -0,0 +1,186 @@ +import * as chalk from 'chalk'; +import * as fs from 'fs'; +import { getDMMF } from '@prisma/internals'; +import { + AbstractPackageManager, + PackageManagerFactory, +} from '../lib/package-managers'; +import { AbstractAction } from './abstract.action'; +import { controllerTemplate } from '../lib/templates/controller-template'; +import { serviceTemplate } from '../lib/templates/service-template'; +import { dtoTemplate } from '../lib/templates/dto-template'; + +export class CrudAction extends AbstractAction { + private manager!: AbstractPackageManager; + + public async handle() { + this.manager = await PackageManagerFactory.find(); + await this.generateCrud(); + } + + private async generateCrud(): Promise { + try { + console.info(chalk.green('Generating CRUD API')); + + const dmmf = await this.generateDMMFJSON(); + if (dmmf) { + this.generateTypes(dmmf); + + this.createAPIs(dmmf); + + this.updateAppModule(dmmf); + } + + } catch (error) { + console.error(chalk.red('Error generating CRUD API'), error); + } + } + + private async generateDMMFJSON(): Promise { + try { + const datamodel = fs.readFileSync('./schema.prisma', 'utf-8'); + const dmmf = await getDMMF({ datamodel }); + fs.writeFileSync('./dmmf.json', JSON.stringify(dmmf, null, 2)); + return dmmf; + } catch (error) { + console.error(chalk.red('Error generating DMMF JSON'), error); + return null; + } + } + + private generateTypes(dmmf: any): void { + try { + const models = dmmf.datamodel.models; + + models.forEach((model: any) => { + const interfaceDir = './src/interfaces'; + const dtoDir = './src/dto'; + if (!fs.existsSync(interfaceDir)) { + fs.mkdirSync(interfaceDir, { recursive: true }); + } + if (!fs.existsSync(dtoDir)) { + fs.mkdirSync(dtoDir, { recursive: true }); + } + + const interfaceContent = this.interfaceTemplate(model); + fs.writeFileSync(`${interfaceDir}/${model.name.toLowerCase()}.interface.ts`, interfaceContent); + + const dtoContent = dtoTemplate(model); + fs.writeFileSync(`${dtoDir}/${model.name.toLowerCase()}.dto.ts`, dtoContent); + }); + + console.info(chalk.green('Types generated successfully')); + } catch (error) { + console.error(chalk.red('Error generating types'), error); + } + } + + private interfaceTemplate(model: any): string { + const fields = model.fields.map((field: any) => { + return `${field.name}${field.isRequired ? '' : '?'}: ${this.getType(field)};`; + }).join('\n '); + + return `export interface ${model.name} {\n ${fields}\n}`; + } + + private getType(field: any): string { + switch (field.type) { + case 'Int': return 'number'; + case 'String': return 'string'; + case 'Boolean': return 'boolean'; + case 'DateTime': return 'Date'; + case 'Json': return 'any'; + default: return 'any'; + } + } + + private createAPIs(dmmf: any): void { + try { + const models = dmmf.datamodel.models; + models.forEach((model: any) => { + this.createModelAPI(model); + }); + console.info(chalk.green('APIs created successfully')); + } catch (error) { + console.error(chalk.red('Error creating APIs'), error); + } + } + + private createModelAPI(model: any): void { + const controllerDir = `./src/controllers`; + const serviceDir = `./src/services`; + + if (!fs.existsSync(controllerDir)) { + fs.mkdirSync(controllerDir, { recursive: true }); + } + if (!fs.existsSync(serviceDir)) { + fs.mkdirSync(serviceDir, { recursive: true }); + } + + const controllerPath = `${controllerDir}/${model.name.toLowerCase()}.controller.ts`; + const servicePath = `${serviceDir}/${model.name.toLowerCase()}.service.ts`; + + if (!fs.existsSync(controllerPath)) { + const controllerContent = controllerTemplate(model); + fs.writeFileSync(controllerPath, controllerContent); + console.info(chalk.green(`${model.name.toLowerCase()}.controller.ts created successfully`)); + } else { + console.info(chalk.yellow(`${model.name.toLowerCase()}.controller.ts already exists`)); + } + + if (!fs.existsSync(servicePath)) { + const serviceContent = serviceTemplate(model); + fs.writeFileSync(servicePath, serviceContent); + console.info(chalk.green(`${model.name.toLowerCase()}.service.ts created successfully`)); + } else { + console.info(chalk.yellow(`${model.name.toLowerCase()}.service.ts already exists`)); + } + } + + private updateAppModule(dmmf: any): void { + try { + const models = dmmf.datamodel.models; + const imports = models.map((model: any) => { + const controllerImport = `import { ${model.name}Controller } from './controllers/${model.name.toLowerCase()}.controller';`; + const serviceImport = `import { ${model.name}Service } from './services/${model.name.toLowerCase()}.service';`; + + return `${serviceImport}\n${controllerImport}`; + }).join('\n'); + + const controllers = models.map((model: any) => `${model.name}Controller`).join(',\n '); + const providers = models.map((model: any) => `${model.name}Service`).join(',\n '); + + const appModulePath = './src/app.module.ts'; + let appModuleContent = fs.readFileSync(appModulePath, 'utf-8'); + + appModuleContent = appModuleContent.replace( + /(import {[^}]*} from '[@a-zA-Z0-9\/]*';\n*)+/, + `$&${imports}\n` + ); + appModuleContent = appModuleContent.replace( + /providers: \[\n([^]*)\n\]/, + `providers: [\n$1\n ${providers}]` + ); + + appModuleContent = appModuleContent.replace( + /controllers: \[\n([^]*)\n\]/, + `controllers: [\n$1\n ${controllers}]` + ); + const controllersIndex = appModuleContent.indexOf('controllers: [') + 'controllers: ['.length; + appModuleContent = + appModuleContent.slice(0, controllersIndex) + + ` ${controllers},` + + appModuleContent.slice(controllersIndex); + const providerIndex = appModuleContent.indexOf('providers: [') + 'providers: ['.length; + appModuleContent = + appModuleContent.slice(0, providerIndex) + + ` ${providers},` + + appModuleContent.slice(providerIndex); + + fs.writeFileSync(appModulePath, appModuleContent); + console.info(chalk.green('app.module.ts updated successfully')); + } catch (error) { + console.error(chalk.red('Error updating app.module.ts'), error); + } + } +} diff --git a/actions/index.ts b/actions/index.ts index 29083e82..2121a5dc 100644 --- a/actions/index.ts +++ b/actions/index.ts @@ -6,3 +6,4 @@ export * from './list.action'; export * from './new.action'; export * from './start.action'; export * from './add.action'; +export * from './crud.action'; \ No newline at end of file diff --git a/commands/command.loader.ts b/commands/command.loader.ts index deb98590..245ac860 100644 --- a/commands/command.loader.ts +++ b/commands/command.loader.ts @@ -8,6 +8,7 @@ import { ListAction, NewAction, StartAction, + CrudAction } from '../actions'; import { ERROR_PREFIX } from '../lib/ui'; import { AddCommand } from './add.command'; @@ -17,6 +18,7 @@ import { InfoCommand } from './info.command'; import { ListCommand } from './list.command'; import { NewCommand } from './new.command'; import { StartCommand } from './start.command'; +import { CrudCommand } from './crud.command'; export class CommandLoader { public static async load(program: CommanderStatic): Promise { new NewCommand(new NewAction()).load(program); @@ -25,6 +27,7 @@ export class CommandLoader { new InfoCommand(new InfoAction()).load(program); new ListCommand(new ListAction()).load(program); new AddCommand(new AddAction()).load(program); + new CrudCommand(new CrudAction()).load(program); await new GenerateCommand(new GenerateAction()).load(program); this.handleInvalidCommand(program); diff --git a/commands/crud.command.ts b/commands/crud.command.ts new file mode 100644 index 00000000..ae4c39f8 --- /dev/null +++ b/commands/crud.command.ts @@ -0,0 +1,14 @@ +import { CommanderStatic } from 'commander'; +import { AbstractCommand } from './abstract.command'; + +export class CrudCommand extends AbstractCommand{ + public load(program: CommanderStatic) { + program + .command('crud') + .alias('cr') + .description('Generate crud api.') + .action(async () => { + await this.action.handle(); + }); + } +} diff --git a/lib/templates/controller-template.ts b/lib/templates/controller-template.ts new file mode 100644 index 00000000..b4a31400 --- /dev/null +++ b/lib/templates/controller-template.ts @@ -0,0 +1,36 @@ +export const controllerTemplate = (model: any): string => ` +import { Controller, Get, Post, Body, Param, Put, Delete } from '@nestjs/common'; +import { ${model.name}Service } from '../services/${model.name.toLowerCase()}.service'; +import { ${model.name} } from '../interfaces/${model.name.toLowerCase()}.interface'; +import { Create${model.name}Dto, Update${model.name}Dto } from '../dto/${model.name.toLowerCase()}.dto'; + +@Controller('${model.name.toLowerCase()}') +export class ${model.name}Controller { + constructor(private readonly ${model.name.toLowerCase()}Service: ${model.name}Service) {} + + @Get() + async findAll(): Promise<${model.name}[]> { + return this.${model.name.toLowerCase()}Service.findAll(); + } + + @Get(':id') + async findOne(@Param('id') id: string): Promise<${model.name}> { + return this.${model.name.toLowerCase()}Service.findOne(+id); + } + + @Post() + async create(@Body() create${model.name}Dto: Create${model.name}Dto): Promise<${model.name}> { + return this.${model.name.toLowerCase()}Service.create(create${model.name}Dto); + } + + @Put(':id') + async update(@Param('id') id: string, @Body() update${model.name}Dto: Update${model.name}Dto): Promise<${model.name}> { + return this.${model.name.toLowerCase()}Service.update(+id, update${model.name}Dto); + } + + @Delete(':id') + async remove(@Param('id') id: string): Promise { + return this.${model.name.toLowerCase()}Service.remove(+id); + } +} +`; diff --git a/lib/templates/dto-template.ts b/lib/templates/dto-template.ts new file mode 100644 index 00000000..96703917 --- /dev/null +++ b/lib/templates/dto-template.ts @@ -0,0 +1,31 @@ +export const dtoTemplate = (model: any): string => { + const createDtoFields = model.fields.map((field: any) => { + return `${field.name}${field.isRequired ? '' : '?'}: ${getFieldType(field)};`; + }).join('\n '); + + const updateDtoFields = model.fields.map((field: any) => { + return `${field.name}?: ${getFieldType(field)};`; + }).join('\n '); + + return ` + export class Create${model.name}Dto { + ${createDtoFields} + } + + export class Update${model.name}Dto { + ${updateDtoFields} + } + `; + }; + + const getFieldType = (field: any): string => { + switch (field.type) { + case 'Int': return 'number'; + case 'String': return 'string'; + case 'Boolean': return 'boolean'; + case 'DateTime': return 'Date'; + case 'Json': return 'any'; + default: return 'any'; + } + }; + \ No newline at end of file diff --git a/lib/templates/service-template.ts b/lib/templates/service-template.ts new file mode 100644 index 00000000..43c3f2c8 --- /dev/null +++ b/lib/templates/service-template.ts @@ -0,0 +1,40 @@ +export const serviceTemplate = (model: any): string => ` +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma.service'; +import { ${model.name} } from '../interfaces/${model.name.toLowerCase()}.interface'; +import { Create${model.name}Dto, Update${model.name}Dto } from '../dto/${model.name.toLowerCase()}.dto'; + +@Injectable() +export class ${model.name}Service { + constructor(private prisma: PrismaService) {} + + async findAll(): Promise<${model.name}[]> { + return this.prisma.${model.name.toLowerCase()}.findMany(); + } + + async findOne(id: number): Promise<${model.name}> { + return this.prisma.${model.name.toLowerCase()}.findUnique({ + where: { id }, + }); + } + + async create(data: Create${model.name}Dto): Promise<${model.name}> { + return this.prisma.${model.name.toLowerCase()}.create({ + data, + }); + } + + async update(id: number, data: Update${model.name}Dto): Promise<${model.name}> { + return this.prisma.${model.name.toLowerCase()}.update({ + where: { id }, + data, + }); + } + + async remove(id: number): Promise { + await this.prisma.${model.name.toLowerCase()}.delete({ + where: { id }, + }); + } +} +`; diff --git a/package-lock.json b/package-lock.json index a6f50af5..29a3e5fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,8 +12,9 @@ "@angular-devkit/core": "16.2.3", "@angular-devkit/schematics": "16.2.3", "@angular-devkit/schematics-cli": "16.2.3", + "@prisma/internals": "^5.16.2", "@samagra-x/schematics": "^0.0.5", - "bun": "^1.0.35", + "bun": "^1.1.7", "chalk": "4.1.2", "chokidar": "3.5.3", "cli-table3": "0.6.3", @@ -1933,9 +1934,9 @@ } }, "node_modules/@oven/bun-darwin-aarch64": { - "version": "1.0.35", - "resolved": "https://registry.npmjs.org/@oven/bun-darwin-aarch64/-/bun-darwin-aarch64-1.0.35.tgz", - "integrity": "sha512-9bkgwxyp5OafSC9UqCx5LzXoojIT7XN9yGWb8G2+BIWETPCHYyrf5TKO/aaz6mgCRZv36bcjHSWG/c3dqYpGSg==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@oven/bun-darwin-aarch64/-/bun-darwin-aarch64-1.1.18.tgz", + "integrity": "sha512-2YMh1G+S5AxDqOEDh9i+9kc17887mkP/yzK/d5DQ0NyPt5uR2w5FKGaalPLDiu5w139y3LKBi+1eGba1oEJnyw==", "cpu": [ "arm64" ], @@ -1945,9 +1946,9 @@ ] }, "node_modules/@oven/bun-darwin-x64": { - "version": "1.0.35", - "resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64/-/bun-darwin-x64-1.0.35.tgz", - "integrity": "sha512-9VSiK8S00BN6Z2yGaO4urnFf/p5QzpgcvAaH1zOt+S2rPvoRJFPWTqNzQ7Izo6WOZTLMLL3OTMHmm2hlh6bgPg==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64/-/bun-darwin-x64-1.1.18.tgz", + "integrity": "sha512-ppeJpQqEXO6nfCneq2TXYFO/l1S/KYKTt3cintTiQxW0ISvj36vQcP/l0ln8BxEu46EnqulVKDrkTBAttv9sww==", "cpu": [ "x64" ], @@ -1957,9 +1958,9 @@ ] }, "node_modules/@oven/bun-darwin-x64-baseline": { - "version": "1.0.35", - "resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64-baseline/-/bun-darwin-x64-baseline-1.0.35.tgz", - "integrity": "sha512-rArqPS+mJbqOcXyPXZ4ACmLaMFdHi5rUTX+N4eGaZxsD5WFhM8cZfVhs6wYEixFS+BVlHcXPSPHsDE3DTPuf/Q==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64-baseline/-/bun-darwin-x64-baseline-1.1.18.tgz", + "integrity": "sha512-shwwfe9Yugpyr490FdjQ90O3JtETbszyUk4PBXQrbz3babPfhXGuVGewis8ORNYeb8zoWGo/adk4biby6kKwHA==", "cpu": [ "x64" ], @@ -1969,9 +1970,9 @@ ] }, "node_modules/@oven/bun-linux-aarch64": { - "version": "1.0.35", - "resolved": "https://registry.npmjs.org/@oven/bun-linux-aarch64/-/bun-linux-aarch64-1.0.35.tgz", - "integrity": "sha512-qDfGcjZyn/uwFIX+mNwJOeASqUNzrw625HBdUGoOU2HpqjocU3vermbYBFqDMDsLV9hoqsI7VG3Y1EcGPsuHBg==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-aarch64/-/bun-linux-aarch64-1.1.18.tgz", + "integrity": "sha512-cDwqcGA/PiiqM8pQkZSRW0HbSh3r1hMsS2ew61d6FjjEI7HP+bwTuu0n0rGdzQKWTtb3PzzXvOkiFZywKS5Gzg==", "cpu": [ "arm64" ], @@ -1981,9 +1982,9 @@ ] }, "node_modules/@oven/bun-linux-x64": { - "version": "1.0.35", - "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64/-/bun-linux-x64-1.0.35.tgz", - "integrity": "sha512-fAHdyilg7tRw4vp9lgxmkimKJom8Tfds/epcx8IFF8Oj1KXWmX5edbTfLnYvxI24IF/0IbqQ1Mw7bz9XwbB/Xw==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64/-/bun-linux-x64-1.1.18.tgz", + "integrity": "sha512-oce0pELxlVhRO7clQGAkbo8vfxaCmRpf7Tu/Swn+T/wqeA5tew02HmsZAnDQqgYx8Z2/QpCOfF1SvLsdg7hR+A==", "cpu": [ "x64" ], @@ -1993,9 +1994,9 @@ ] }, "node_modules/@oven/bun-linux-x64-baseline": { - "version": "1.0.35", - "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-baseline/-/bun-linux-x64-baseline-1.0.35.tgz", - "integrity": "sha512-LfJu69nqrxx5teuEXJZbCxedfNhVtkqdkyE7MUuHVAFLOBIuDHIoBBtW/Q3UxhdHe+lHwGBh4ePrnR4YCwJyCA==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-baseline/-/bun-linux-x64-baseline-1.1.18.tgz", + "integrity": "sha512-hxnFwssve6M9i4phusIn9swFvQKwLI+9i2taWSotshp1axLXQ5ruIIE9WPKJGR0i+yuw5Q8HBCnUDDh5ZMp9rA==", "cpu": [ "x64" ], @@ -2004,6 +2005,30 @@ "linux" ] }, + "node_modules/@oven/bun-windows-x64": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@oven/bun-windows-x64/-/bun-windows-x64-1.1.18.tgz", + "integrity": "sha512-d639p5g8hrXyvFX3FK9EpsaoVEhMRThftmkueljjpYnYjMvIiMQ2crHtI2zwZ6yLEHvecaFXVXlocu2+jxia7g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oven/bun-windows-x64-baseline": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@oven/bun-windows-x64-baseline/-/bun-windows-x64-baseline-1.1.18.tgz", + "integrity": "sha512-Wlb55q9QbayO+7NvfYMnU8oaTPz1k2xMr7mm9+JOnG/I6q82HMvIQEG181bAhU1kcm5YcZZ5E0WMp2gX3NFsEw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", @@ -2045,6 +2070,102 @@ "node": ">=12" } }, + "node_modules/@prisma/debug": { + "version": "5.16.2", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.16.2.tgz", + "integrity": "sha512-ItzB4nR4O8eLzuJiuP3WwUJfoIvewMHqpGCad+64gvThcKEVOtaUza9AEJo2DPqAOa/AWkFyK54oM4WwHeew+A==" + }, + "node_modules/@prisma/engines": { + "version": "5.16.2", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.16.2.tgz", + "integrity": "sha512-qUxwMtrwoG3byd4PbX6T7EjHJ8AUhzTuwniOGkh/hIznBfcE2QQnGakyEq4VnwNuttMqvh/GgPFapHQ3lCuRHg==", + "hasInstallScript": true, + "dependencies": { + "@prisma/debug": "5.16.2", + "@prisma/engines-version": "5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303", + "@prisma/fetch-engine": "5.16.2", + "@prisma/get-platform": "5.16.2" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303.tgz", + "integrity": "sha512-HkT2WbfmFZ9WUPyuJHhkiADxazHg8Y4gByrTSVeb3OikP6tjQ7txtSUGu9OBOBH0C13dPKN2qqH12xKtHu/Hiw==" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.16.2", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.16.2.tgz", + "integrity": "sha512-sq51lfHKfH2jjYSjBtMjP+AznFqOJzXpqmq6B9auWrlTJrMgZ7lPyhWUW7VU7LsQU48/TJ+DZeIz8s9bMYvcHg==", + "dependencies": { + "@prisma/debug": "5.16.2", + "@prisma/engines-version": "5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303", + "@prisma/get-platform": "5.16.2" + } + }, + "node_modules/@prisma/generator-helper": { + "version": "5.16.2", + "resolved": "https://registry.npmjs.org/@prisma/generator-helper/-/generator-helper-5.16.2.tgz", + "integrity": "sha512-ajdZ5OTKuLEYB7KQQPNYGPr4s56wD4+vH6KqIGiyQVw8ze8dPaxUB3MLzf0vCq2yYq6CZynSExf4InFXYBliTA==", + "dependencies": { + "@prisma/debug": "5.16.2" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.16.2", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.16.2.tgz", + "integrity": "sha512-cXiHPgNLNyj22vLouPVNegklpRL/iX2jxTeap5GRO3DmCoVyIHmJAV1CgUMUJhHlcol9yYy7EHvsnXTDJ/PKEA==", + "dependencies": { + "@prisma/debug": "5.16.2" + } + }, + "node_modules/@prisma/internals": { + "version": "5.16.2", + "resolved": "https://registry.npmjs.org/@prisma/internals/-/internals-5.16.2.tgz", + "integrity": "sha512-EyNy1A3V61buK4XEI3u8IpG/lYzJSWxGWxOuDeArEYkz5PbI0eN06MxzzIY/wRFK1Fa1rLbqtmnJQnMb6X7s1g==", + "dependencies": { + "@prisma/debug": "5.16.2", + "@prisma/engines": "5.16.2", + "@prisma/fetch-engine": "5.16.2", + "@prisma/generator-helper": "5.16.2", + "@prisma/get-platform": "5.16.2", + "@prisma/prisma-schema-wasm": "5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303", + "@prisma/schema-files-loader": "5.16.2", + "arg": "5.0.2", + "prompts": "2.4.2" + } + }, + "node_modules/@prisma/internals/node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + }, + "node_modules/@prisma/prisma-schema-wasm": { + "version": "5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303", + "resolved": "https://registry.npmjs.org/@prisma/prisma-schema-wasm/-/prisma-schema-wasm-5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303.tgz", + "integrity": "sha512-l2yUdEkR3eKEBKsEs18/ZjMNsS7IUMLFWZOvtylhHs2pMY6UaxJN1ho0x8IB2z54VsKUp0fhqPm5LSi9FWmeCA==" + }, + "node_modules/@prisma/schema-files-loader": { + "version": "5.16.2", + "resolved": "https://registry.npmjs.org/@prisma/schema-files-loader/-/schema-files-loader-5.16.2.tgz", + "integrity": "sha512-YuNphq5QYwVwFpLWUZpM800UU1Ejg5TUk39WJj1nfgGVUzT4J2Q/Jw3fQ+9XvyTVX+XwwmrLyvZb5N8KBYClZQ==", + "dependencies": { + "@prisma/prisma-schema-wasm": "5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303", + "fs-extra": "11.1.1" + } + }, + "node_modules/@prisma/schema-files-loader/node_modules/fs-extra": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/@samagra-x/schematics": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/@samagra-x/schematics/-/schematics-0.0.5.tgz", @@ -4279,9 +4400,9 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, "node_modules/bun": { - "version": "1.0.35", - "resolved": "https://registry.npmjs.org/bun/-/bun-1.0.35.tgz", - "integrity": "sha512-Or8sWSYSXBdcc7FgK+bPNIv2oOB3EVvDK7LGyNL5vBCbgyuUmPE4Mh+ZlInl3IF/HfkQ7REopx7B1jnbTw4OsQ==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/bun/-/bun-1.1.18.tgz", + "integrity": "sha512-bv1wLYtmkn6GCqYFsVO9xZzPvNaDlA3xHbtePGHMtXMqq8N/vo+L6b19LB4+I5RKXFAsSmgzonyh2oMExaaWcQ==", "cpu": [ "arm64", "x64" @@ -4289,19 +4410,22 @@ "hasInstallScript": true, "os": [ "darwin", - "linux" + "linux", + "win32" ], "bin": { - "bun": "bin/bun", - "bunx": "bin/bun" + "bun": "bin/bun.exe", + "bunx": "bin/bun.exe" }, "optionalDependencies": { - "@oven/bun-darwin-aarch64": "1.0.35", - "@oven/bun-darwin-x64": "1.0.35", - "@oven/bun-darwin-x64-baseline": "1.0.35", - "@oven/bun-linux-aarch64": "1.0.35", - "@oven/bun-linux-x64": "1.0.35", - "@oven/bun-linux-x64-baseline": "1.0.35" + "@oven/bun-darwin-aarch64": "1.1.18", + "@oven/bun-darwin-x64": "1.1.18", + "@oven/bun-darwin-x64-baseline": "1.1.18", + "@oven/bun-linux-aarch64": "1.1.18", + "@oven/bun-linux-x64": "1.1.18", + "@oven/bun-linux-x64-baseline": "1.1.18", + "@oven/bun-windows-x64": "1.1.18", + "@oven/bun-windows-x64-baseline": "1.1.18" } }, "node_modules/bundle-name": { @@ -10249,7 +10373,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, "engines": { "node": ">=6" } @@ -12767,7 +12890,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" @@ -14745,8 +14867,7 @@ "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" }, "node_modules/slash": { "version": "3.0.0", diff --git a/package.json b/package.json index c1b731de..06efb209 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,9 @@ "@angular-devkit/core": "16.2.3", "@angular-devkit/schematics": "16.2.3", "@angular-devkit/schematics-cli": "16.2.3", + "@prisma/internals": "^5.16.2", "@samagra-x/schematics": "^0.0.5", - "bun": "^1.0.35", + "bun": "^1.1.7", "chalk": "4.1.2", "chokidar": "3.5.3", "cli-table3": "0.6.3", From 2124330050b5403c7721b680b07b5711d47fe008 Mon Sep 17 00:00:00 2001 From: Savio629 Date: Mon, 15 Jul 2024 21:34:33 +0530 Subject: [PATCH 02/12] feat: single and multiple model generation --- actions/crud.action.ts | 68 +++++++++++++++++++++++++++------------- commands/crud.command.ts | 12 ++++--- 2 files changed, 53 insertions(+), 27 deletions(-) diff --git a/actions/crud.action.ts b/actions/crud.action.ts index 2a631901..8df29482 100644 --- a/actions/crud.action.ts +++ b/actions/crud.action.ts @@ -9,28 +9,39 @@ import { AbstractAction } from './abstract.action'; import { controllerTemplate } from '../lib/templates/controller-template'; import { serviceTemplate } from '../lib/templates/service-template'; import { dtoTemplate } from '../lib/templates/dto-template'; +import { Input } from '../commands/command.input'; export class CrudAction extends AbstractAction { private manager!: AbstractPackageManager; - public async handle() { + public async handle(inputs: Input[] = []) { this.manager = await PackageManagerFactory.find(); - await this.generateCrud(); + await this.generateCrud(inputs); } - private async generateCrud(): Promise { + private async generateCrud(inputs: Input[]): Promise { try { console.info(chalk.green('Generating CRUD API')); const dmmf = await this.generateDMMFJSON(); if (dmmf) { - this.generateTypes(dmmf); - - this.createAPIs(dmmf); + const existingModels = dmmf.datamodel.models.map((model: any) => model.name); + + if (inputs.length === 0) { + inputs = existingModels.map((modelName: any) => ({ name: modelName, value: modelName })); + } else { + const invalidInputs = inputs.filter(input => !existingModels.includes(input.name)); + + if (invalidInputs.length > 0) { + console.error(chalk.red('The following models do not exist:'), invalidInputs.map(input => input.name).join(', ')); + return; + } + } - this.updateAppModule(dmmf); + this.generateTypes(dmmf, inputs); + this.createAPIs(dmmf, inputs); + this.updateAppModule(dmmf, inputs); } - } catch (error) { console.error(chalk.red('Error generating CRUD API'), error); } @@ -48,9 +59,12 @@ export class CrudAction extends AbstractAction { } } - private generateTypes(dmmf: any): void { + private generateTypes(dmmf: any, inputs: Input[]): void { try { - const models = dmmf.datamodel.models; + const modelNames = inputs.map(input => input.name); + const models = dmmf.datamodel.models.filter((model: any) => + modelNames.includes(model.name) + ); models.forEach((model: any) => { const interfaceDir = './src/interfaces'; @@ -94,9 +108,12 @@ export class CrudAction extends AbstractAction { } } - private createAPIs(dmmf: any): void { + private createAPIs(dmmf: any, inputs: Input[]): void { try { - const models = dmmf.datamodel.models; + const modelNames = inputs.map(input => input.name); + const models = dmmf.datamodel.models.filter((model: any) => + modelNames.includes(model.name) + ); models.forEach((model: any) => { this.createModelAPI(model); }); @@ -137,9 +154,12 @@ export class CrudAction extends AbstractAction { } } - private updateAppModule(dmmf: any): void { + private updateAppModule(dmmf: any, inputs: Input[]): void { try { - const models = dmmf.datamodel.models; + const modelNames = inputs.map(input => input.name); + const models = dmmf.datamodel.models.filter((model: any) => + modelNames.includes(model.name) + ); const imports = models.map((model: any) => { const controllerImport = `import { ${model.name}Controller } from './controllers/${model.name.toLowerCase()}.controller';`; const serviceImport = `import { ${model.name}Service } from './services/${model.name.toLowerCase()}.service';`; @@ -152,11 +172,14 @@ export class CrudAction extends AbstractAction { const appModulePath = './src/app.module.ts'; let appModuleContent = fs.readFileSync(appModulePath, 'utf-8'); - + + const existingImports: string[] = appModuleContent.match(/import {[^}]*} from '[@a-zA-Z0-9\/]*';/g) || []; + const newImports: string[] = imports.split('\n').filter((importLine: string) => !existingImports.includes(importLine)); + appModuleContent = appModuleContent.replace( /(import {[^}]*} from '[@a-zA-Z0-9\/]*';\n*)+/, - `$&${imports}\n` - ); + `$&${newImports.join('\n')}\n` + ); appModuleContent = appModuleContent.replace( /providers: \[\n([^]*)\n\]/, `providers: [\n$1\n ${providers}]` @@ -166,17 +189,18 @@ export class CrudAction extends AbstractAction { /controllers: \[\n([^]*)\n\]/, `controllers: [\n$1\n ${controllers}]` ); + const controllersIndex = appModuleContent.indexOf('controllers: [') + 'controllers: ['.length; appModuleContent = appModuleContent.slice(0, controllersIndex) + ` ${controllers},` + appModuleContent.slice(controllersIndex); const providerIndex = appModuleContent.indexOf('providers: [') + 'providers: ['.length; - appModuleContent = - appModuleContent.slice(0, providerIndex) + - ` ${providers},` + - appModuleContent.slice(providerIndex); - + appModuleContent = + appModuleContent.slice(0, providerIndex) + + ` ${providers},` + + appModuleContent.slice(providerIndex); + fs.writeFileSync(appModulePath, appModuleContent); console.info(chalk.green('app.module.ts updated successfully')); } catch (error) { diff --git a/commands/crud.command.ts b/commands/crud.command.ts index ae4c39f8..8d66c8bb 100644 --- a/commands/crud.command.ts +++ b/commands/crud.command.ts @@ -1,14 +1,16 @@ import { CommanderStatic } from 'commander'; import { AbstractCommand } from './abstract.command'; +import { Input } from './command.input'; -export class CrudCommand extends AbstractCommand{ +export class CrudCommand extends AbstractCommand { public load(program: CommanderStatic) { program - .command('crud') + .command('crud [inputs...]') .alias('cr') - .description('Generate crud api.') - .action(async () => { - await this.action.handle(); + .description('Generate CRUD API for specified models.') + .action(async (inputArgs: string[] = []) => { + const inputs: Input[] = inputArgs.map(arg => ({ name: arg, value: arg })); + await this.action.handle(inputs); }); } } From 76ec685b9f3c426963e5dfa5b30d8f8272df746b Mon Sep 17 00:00:00 2001 From: Savio629 Date: Thu, 18 Jul 2024 17:51:03 +0530 Subject: [PATCH 03/12] fix: added swagger decorators --- actions/crud.action.ts | 127 +++++++++++++-------------- lib/templates/controller-template.ts | 65 ++++++++++---- lib/templates/service-template.ts | 4 +- 3 files changed, 110 insertions(+), 86 deletions(-) diff --git a/actions/crud.action.ts b/actions/crud.action.ts index 8df29482..2138aab6 100644 --- a/actions/crud.action.ts +++ b/actions/crud.action.ts @@ -14,7 +14,7 @@ import { Input } from '../commands/command.input'; export class CrudAction extends AbstractAction { private manager!: AbstractPackageManager; - public async handle(inputs: Input[] = []) { + public async handle(inputs: Input[]) { this.manager = await PackageManagerFactory.find(); await this.generateCrud(inputs); } @@ -26,21 +26,19 @@ export class CrudAction extends AbstractAction { const dmmf = await this.generateDMMFJSON(); if (dmmf) { const existingModels = dmmf.datamodel.models.map((model: any) => model.name); - - if (inputs.length === 0) { - inputs = existingModels.map((modelName: any) => ({ name: modelName, value: modelName })); - } else { - const invalidInputs = inputs.filter(input => !existingModels.includes(input.name)); - - if (invalidInputs.length > 0) { - console.error(chalk.red('The following models do not exist:'), invalidInputs.map(input => input.name).join(', ')); - return; - } + const inputModelNames = inputs.map(input => input.name); + const invalidInputs = inputModelNames.filter(name => name !== '*' && !existingModels.includes(name)); + + if (invalidInputs.length > 0) { + console.error(chalk.red('The following models do not exist:'), invalidInputs.join(', ')); + return; } - this.generateTypes(dmmf, inputs); - this.createAPIs(dmmf, inputs); - this.updateAppModule(dmmf, inputs); + const modelsToGenerate = inputModelNames.includes('*') ? existingModels : inputModelNames; + + this.generateTypes(dmmf, modelsToGenerate); + this.createAPIs(dmmf, modelsToGenerate); + this.updateAppModule(dmmf, modelsToGenerate); } } catch (error) { console.error(chalk.red('Error generating CRUD API'), error); @@ -59,25 +57,25 @@ export class CrudAction extends AbstractAction { } } - private generateTypes(dmmf: any, inputs: Input[]): void { + private generateTypes(dmmf: any, modelNames: string[]): void { try { - const modelNames = inputs.map(input => input.name); const models = dmmf.datamodel.models.filter((model: any) => modelNames.includes(model.name) ); models.forEach((model: any) => { - const interfaceDir = './src/interfaces'; - const dtoDir = './src/dto'; - if (!fs.existsSync(interfaceDir)) { - fs.mkdirSync(interfaceDir, { recursive: true }); + const modelDir = `./src/${model.name.toLowerCase()}`; + const dtoDir = `${modelDir}/dto`; + + if (!fs.existsSync(modelDir)) { + fs.mkdirSync(modelDir, { recursive: true }); } if (!fs.existsSync(dtoDir)) { fs.mkdirSync(dtoDir, { recursive: true }); } const interfaceContent = this.interfaceTemplate(model); - fs.writeFileSync(`${interfaceDir}/${model.name.toLowerCase()}.interface.ts`, interfaceContent); + fs.writeFileSync(`${modelDir}/${model.name.toLowerCase()}.interface.ts`, interfaceContent); const dtoContent = dtoTemplate(model); fs.writeFileSync(`${dtoDir}/${model.name.toLowerCase()}.dto.ts`, dtoContent); @@ -103,14 +101,13 @@ export class CrudAction extends AbstractAction { case 'String': return 'string'; case 'Boolean': return 'boolean'; case 'DateTime': return 'Date'; - case 'Json': return 'any'; + case 'Json': return 'any'; default: return 'any'; } } - private createAPIs(dmmf: any, inputs: Input[]): void { + private createAPIs(dmmf: any, modelNames: string[]): void { try { - const modelNames = inputs.map(input => input.name); const models = dmmf.datamodel.models.filter((model: any) => modelNames.includes(model.name) ); @@ -124,18 +121,10 @@ export class CrudAction extends AbstractAction { } private createModelAPI(model: any): void { - const controllerDir = `./src/controllers`; - const serviceDir = `./src/services`; - - if (!fs.existsSync(controllerDir)) { - fs.mkdirSync(controllerDir, { recursive: true }); - } - if (!fs.existsSync(serviceDir)) { - fs.mkdirSync(serviceDir, { recursive: true }); - } + const modelDir = `./src/${model.name.toLowerCase()}`; - const controllerPath = `${controllerDir}/${model.name.toLowerCase()}.controller.ts`; - const servicePath = `${serviceDir}/${model.name.toLowerCase()}.service.ts`; + const controllerPath = `${modelDir}/${model.name.toLowerCase()}.controller.ts`; + const servicePath = `${modelDir}/${model.name.toLowerCase()}.service.ts`; if (!fs.existsSync(controllerPath)) { const controllerContent = controllerTemplate(model); @@ -154,53 +143,59 @@ export class CrudAction extends AbstractAction { } } - private updateAppModule(dmmf: any, inputs: Input[]): void { + private updateAppModule(dmmf: any, modelNames: string[]): void { try { - const modelNames = inputs.map(input => input.name); const models = dmmf.datamodel.models.filter((model: any) => modelNames.includes(model.name) ); - const imports = models.map((model: any) => { - const controllerImport = `import { ${model.name}Controller } from './controllers/${model.name.toLowerCase()}.controller';`; - const serviceImport = `import { ${model.name}Service } from './services/${model.name.toLowerCase()}.service';`; + const newImports = models.map((model: any) => { + const controllerImport = `import { ${model.name}Controller } from './${model.name.toLowerCase()}/${model.name.toLowerCase()}.controller';`; + const serviceImport = `import { ${model.name}Service } from './${model.name.toLowerCase()}/${model.name.toLowerCase()}.service';`; return `${serviceImport}\n${controllerImport}`; }).join('\n'); - const controllers = models.map((model: any) => `${model.name}Controller`).join(',\n '); - const providers = models.map((model: any) => `${model.name}Service`).join(',\n '); + const newControllers = models.map((model: any) => `${model.name}Controller`); + const newProviders = models.map((model: any) => `${model.name}Service`); const appModulePath = './src/app.module.ts'; let appModuleContent = fs.readFileSync(appModulePath, 'utf-8'); - - const existingImports: string[] = appModuleContent.match(/import {[^}]*} from '[@a-zA-Z0-9\/]*';/g) || []; - const newImports: string[] = imports.split('\n').filter((importLine: string) => !existingImports.includes(importLine)); - - appModuleContent = appModuleContent.replace( - /(import {[^}]*} from '[@a-zA-Z0-9\/]*';\n*)+/, - `$&${newImports.join('\n')}\n` - ); + + // Avoid duplicate imports + const importRegex = /import {[^}]*} from '[@a-zA-Z0-9\/]*';/g; + const existingImports: string[] = appModuleContent.match(importRegex) || []; + const uniqueNewImports = newImports.split('\n').filter((importLine: string) => !existingImports.some(existingImport => existingImport === importLine)).join('\n'); + + if (uniqueNewImports) { + appModuleContent = appModuleContent.replace( + /(import {[^}]*} from '[@a-zA-Z0-9\/]*';\n*)+/, + `$&${uniqueNewImports}\n` + ); + } + + // Update controllers and providers arrays + const controllersRegex = /controllers: \[([^\]]*)\]/s; + const providersRegex = /providers: \[([^\]]*)\]/s; + + const controllersMatch = appModuleContent.match(controllersRegex); + const providersMatch = appModuleContent.match(providersRegex); + + const currentControllers = controllersMatch ? controllersMatch[1].split(',').map(controller => controller.trim()).filter(Boolean) : []; + const currentProviders = providersMatch ? providersMatch[1].split(',').map(provider => provider.trim()).filter(Boolean) : []; + + const updatedControllers = Array.from(new Set([...currentControllers, ...newControllers])).join(', '); + const updatedProviders = Array.from(new Set([...currentProviders, ...newProviders])).join(', '); + appModuleContent = appModuleContent.replace( - /providers: \[\n([^]*)\n\]/, - `providers: [\n$1\n ${providers}]` + controllersRegex, + `controllers: [${updatedControllers}]` ); appModuleContent = appModuleContent.replace( - /controllers: \[\n([^]*)\n\]/, - `controllers: [\n$1\n ${controllers}]` + providersRegex, + `providers: [${updatedProviders}]` ); - - const controllersIndex = appModuleContent.indexOf('controllers: [') + 'controllers: ['.length; - appModuleContent = - appModuleContent.slice(0, controllersIndex) + - ` ${controllers},` + - appModuleContent.slice(controllersIndex); - const providerIndex = appModuleContent.indexOf('providers: [') + 'providers: ['.length; - appModuleContent = - appModuleContent.slice(0, providerIndex) + - ` ${providers},` + - appModuleContent.slice(providerIndex); - + fs.writeFileSync(appModulePath, appModuleContent); console.info(chalk.green('app.module.ts updated successfully')); } catch (error) { diff --git a/lib/templates/controller-template.ts b/lib/templates/controller-template.ts index b4a31400..12f1a18f 100644 --- a/lib/templates/controller-template.ts +++ b/lib/templates/controller-template.ts @@ -1,36 +1,65 @@ -export const controllerTemplate = (model: any): string => ` -import { Controller, Get, Post, Body, Param, Put, Delete } from '@nestjs/common'; -import { ${model.name}Service } from '../services/${model.name.toLowerCase()}.service'; -import { ${model.name} } from '../interfaces/${model.name.toLowerCase()}.interface'; -import { Create${model.name}Dto, Update${model.name}Dto } from '../dto/${model.name.toLowerCase()}.dto'; +export function controllerTemplate(model: any): string { + const modelName = model.name; + const modelNameLowerCase = modelName.toLowerCase(); -@Controller('${model.name.toLowerCase()}') -export class ${model.name}Controller { - constructor(private readonly ${model.name.toLowerCase()}Service: ${model.name}Service) {} + const swaggerImports = ` +import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiBody } from '@nestjs/swagger'; +import { Controller, Get, Param, Post, Body, Put, Delete } from '@nestjs/common'; +import { ${modelName}Service } from './${modelNameLowerCase}.service'; +import { Create${modelName}Dto, Update${modelName}Dto } from './dto/${modelNameLowerCase}.dto'; +import { ${modelName} } from './${modelNameLowerCase}.interface'; +`; + + const controllerClass = ` +@ApiTags('${modelName}') +@Controller('${modelNameLowerCase}') +export class ${modelName}Controller { + constructor(private readonly ${modelNameLowerCase}Service: ${modelName}Service) {} + @ApiOperation({ summary: 'Get all ${modelNameLowerCase}s' }) + @ApiResponse({ status: 200, description: 'Return all ${modelNameLowerCase}s.' }) @Get() - async findAll(): Promise<${model.name}[]> { - return this.${model.name.toLowerCase()}Service.findAll(); + async findAll(): Promise<${modelName}[]> { + return this.${modelNameLowerCase}Service.findAll(); } + @ApiOperation({ summary: 'Get ${modelNameLowerCase} by ID' }) + @ApiParam({ name: 'id', type: 'number', description: 'ID of the ${modelNameLowerCase} to find' }) + @ApiResponse({ status: 200, description: 'Return the ${modelNameLowerCase}.' }) + @ApiResponse({ status: 404, description: '${modelName} not found.' }) @Get(':id') - async findOne(@Param('id') id: string): Promise<${model.name}> { - return this.${model.name.toLowerCase()}Service.findOne(+id); + async findOne(@Param('id') id: number): Promise<${modelName} | undefined> { + return this.${modelNameLowerCase}Service.findOne(id); } + @ApiOperation({ summary: 'Create ${modelNameLowerCase}' }) + @ApiBody({ type: Create${modelName}Dto }) + @ApiResponse({ status: 201, description: 'The ${modelNameLowerCase} has been successfully created.' }) @Post() - async create(@Body() create${model.name}Dto: Create${model.name}Dto): Promise<${model.name}> { - return this.${model.name.toLowerCase()}Service.create(create${model.name}Dto); + async create(@Body() ${modelNameLowerCase}Dto: Create${modelName}Dto): Promise<${modelName}> { + return this.${modelNameLowerCase}Service.create(${modelNameLowerCase}Dto); } + @ApiOperation({ summary: 'Update ${modelNameLowerCase} by ID' }) + @ApiParam({ name: 'id', type: 'number', description: 'ID of the ${modelNameLowerCase} to update' }) + @ApiBody({ type: Update${modelName}Dto }) + @ApiResponse({ status: 200, description: 'The ${modelNameLowerCase} has been successfully updated.' }) + @ApiResponse({ status: 404, description: '${modelName} not found.' }) @Put(':id') - async update(@Param('id') id: string, @Body() update${model.name}Dto: Update${model.name}Dto): Promise<${model.name}> { - return this.${model.name.toLowerCase()}Service.update(+id, update${model.name}Dto); + async update(@Param('id') id: number, @Body() ${modelNameLowerCase}Dto: Update${modelName}Dto): Promise<${modelName} | undefined> { + return this.${modelNameLowerCase}Service.update(id, ${modelNameLowerCase}Dto); } + @ApiOperation({ summary: 'Delete ${modelNameLowerCase} by ID' }) + @ApiParam({ name: 'id', type: 'number', description: 'ID of the ${modelNameLowerCase} to delete' }) + @ApiResponse({ status: 200, description: 'The ${modelNameLowerCase} has been successfully deleted.' }) + @ApiResponse({ status: 404, description: '${modelName} not found.' }) @Delete(':id') - async remove(@Param('id') id: string): Promise { - return this.${model.name.toLowerCase()}Service.remove(+id); + async remove(@Param('id') id: number): Promise { + return this.${modelNameLowerCase}Service.remove(id); } } `; + + return `${swaggerImports}${controllerClass}`; +} diff --git a/lib/templates/service-template.ts b/lib/templates/service-template.ts index 43c3f2c8..c789a534 100644 --- a/lib/templates/service-template.ts +++ b/lib/templates/service-template.ts @@ -1,8 +1,8 @@ export const serviceTemplate = (model: any): string => ` import { Injectable } from '@nestjs/common'; import { PrismaService } from '../prisma.service'; -import { ${model.name} } from '../interfaces/${model.name.toLowerCase()}.interface'; -import { Create${model.name}Dto, Update${model.name}Dto } from '../dto/${model.name.toLowerCase()}.dto'; +import { ${model.name} } from './${model.name.toLowerCase()}.interface'; +import { Create${model.name}Dto, Update${model.name}Dto } from './dto/${model.name.toLowerCase()}.dto'; @Injectable() export class ${model.name}Service { From d95f08716ccbfb6c8f6748b5fb67f15d215a821b Mon Sep 17 00:00:00 2001 From: Savio629 Date: Thu, 18 Jul 2024 18:56:00 +0530 Subject: [PATCH 04/12] fix: removed swagger decorators & updated service template --- lib/templates/controller-template.ts | 20 ----------- lib/templates/service-template.ts | 54 +++++++++++++++------------- 2 files changed, 30 insertions(+), 44 deletions(-) diff --git a/lib/templates/controller-template.ts b/lib/templates/controller-template.ts index 12f1a18f..768ec112 100644 --- a/lib/templates/controller-template.ts +++ b/lib/templates/controller-template.ts @@ -3,7 +3,6 @@ export function controllerTemplate(model: any): string { const modelNameLowerCase = modelName.toLowerCase(); const swaggerImports = ` -import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiBody } from '@nestjs/swagger'; import { Controller, Get, Param, Post, Body, Put, Delete } from '@nestjs/common'; import { ${modelName}Service } from './${modelNameLowerCase}.service'; import { Create${modelName}Dto, Update${modelName}Dto } from './dto/${modelNameLowerCase}.dto'; @@ -11,49 +10,30 @@ import { ${modelName} } from './${modelNameLowerCase}.interface'; `; const controllerClass = ` -@ApiTags('${modelName}') @Controller('${modelNameLowerCase}') export class ${modelName}Controller { constructor(private readonly ${modelNameLowerCase}Service: ${modelName}Service) {} - @ApiOperation({ summary: 'Get all ${modelNameLowerCase}s' }) - @ApiResponse({ status: 200, description: 'Return all ${modelNameLowerCase}s.' }) @Get() async findAll(): Promise<${modelName}[]> { return this.${modelNameLowerCase}Service.findAll(); } - @ApiOperation({ summary: 'Get ${modelNameLowerCase} by ID' }) - @ApiParam({ name: 'id', type: 'number', description: 'ID of the ${modelNameLowerCase} to find' }) - @ApiResponse({ status: 200, description: 'Return the ${modelNameLowerCase}.' }) - @ApiResponse({ status: 404, description: '${modelName} not found.' }) @Get(':id') async findOne(@Param('id') id: number): Promise<${modelName} | undefined> { return this.${modelNameLowerCase}Service.findOne(id); } - @ApiOperation({ summary: 'Create ${modelNameLowerCase}' }) - @ApiBody({ type: Create${modelName}Dto }) - @ApiResponse({ status: 201, description: 'The ${modelNameLowerCase} has been successfully created.' }) @Post() async create(@Body() ${modelNameLowerCase}Dto: Create${modelName}Dto): Promise<${modelName}> { return this.${modelNameLowerCase}Service.create(${modelNameLowerCase}Dto); } - @ApiOperation({ summary: 'Update ${modelNameLowerCase} by ID' }) - @ApiParam({ name: 'id', type: 'number', description: 'ID of the ${modelNameLowerCase} to update' }) - @ApiBody({ type: Update${modelName}Dto }) - @ApiResponse({ status: 200, description: 'The ${modelNameLowerCase} has been successfully updated.' }) - @ApiResponse({ status: 404, description: '${modelName} not found.' }) @Put(':id') async update(@Param('id') id: number, @Body() ${modelNameLowerCase}Dto: Update${modelName}Dto): Promise<${modelName} | undefined> { return this.${modelNameLowerCase}Service.update(id, ${modelNameLowerCase}Dto); } - @ApiOperation({ summary: 'Delete ${modelNameLowerCase} by ID' }) - @ApiParam({ name: 'id', type: 'number', description: 'ID of the ${modelNameLowerCase} to delete' }) - @ApiResponse({ status: 200, description: 'The ${modelNameLowerCase} has been successfully deleted.' }) - @ApiResponse({ status: 404, description: '${modelName} not found.' }) @Delete(':id') async remove(@Param('id') id: number): Promise { return this.${modelNameLowerCase}Service.remove(id); diff --git a/lib/templates/service-template.ts b/lib/templates/service-template.ts index c789a534..d5ee3999 100644 --- a/lib/templates/service-template.ts +++ b/lib/templates/service-template.ts @@ -1,40 +1,46 @@ -export const serviceTemplate = (model: any): string => ` +export const serviceTemplate = (model: any): string => { + const modelName = model.name; + const modelNameLowerCase = modelName.toLowerCase(); + + return ` import { Injectable } from '@nestjs/common'; -import { PrismaService } from '../prisma.service'; -import { ${model.name} } from './${model.name.toLowerCase()}.interface'; -import { Create${model.name}Dto, Update${model.name}Dto } from './dto/${model.name.toLowerCase()}.dto'; +import { ${modelName} } from './${modelNameLowerCase}.interface'; +import { Create${modelName}Dto, Update${modelName}Dto } from './dto/${modelNameLowerCase}.dto'; @Injectable() -export class ${model.name}Service { - constructor(private prisma: PrismaService) {} +export class ${modelName}Service { + private ${modelNameLowerCase}s: ${modelName}[] = []; + private idCounter: number = 1; - async findAll(): Promise<${model.name}[]> { - return this.prisma.${model.name.toLowerCase()}.findMany(); + async findAll(): Promise<${modelName}[]> { + return this.${modelNameLowerCase}s; } - async findOne(id: number): Promise<${model.name}> { - return this.prisma.${model.name.toLowerCase()}.findUnique({ - where: { id }, - }); + async findOne(id: number): Promise<${modelName}> { + return this.${modelNameLowerCase}s.find(${modelNameLowerCase} => ${modelNameLowerCase}.id === id); } - async create(data: Create${model.name}Dto): Promise<${model.name}> { - return this.prisma.${model.name.toLowerCase()}.create({ - data, - }); + async create(data: Create${modelName}Dto): Promise<${modelName}> { + const new${modelName}: ${modelName} = { + id: this.idCounter++, + ...data, + }; + this.${modelNameLowerCase}s.push(new${modelName}); + return new${modelName}; } - async update(id: number, data: Update${model.name}Dto): Promise<${model.name}> { - return this.prisma.${model.name.toLowerCase()}.update({ - where: { id }, - data, - }); + async update(id: number, data: Update${modelName}Dto): Promise<${modelName}> { + const ${modelNameLowerCase}Index = this.${modelNameLowerCase}s.findIndex(${modelNameLowerCase} => ${modelNameLowerCase}.id === id); + if (${modelNameLowerCase}Index === -1) { + return null; + } + this.${modelNameLowerCase}s[${modelNameLowerCase}Index] = { ...this.${modelNameLowerCase}s[${modelNameLowerCase}Index], ...data }; + return this.${modelNameLowerCase}s[${modelNameLowerCase}Index]; } async remove(id: number): Promise { - await this.prisma.${model.name.toLowerCase()}.delete({ - where: { id }, - }); + this.${modelNameLowerCase}s = this.${modelNameLowerCase}s.filter(${modelNameLowerCase} => ${modelNameLowerCase}.id !== id); } } `; +}; From 003291aef2f33a73822cec820baa10ed5e3819d6 Mon Sep 17 00:00:00 2001 From: Savio629 Date: Sun, 28 Jul 2024 22:21:54 +0530 Subject: [PATCH 05/12] fix: updated crud command --- actions/crud.action.ts | 2 - lib/templates/controller-template.ts | 28 +++++++--- lib/templates/dto-template.ts | 77 ++++++++++++++++++---------- lib/templates/service-template.ts | 9 +++- 4 files changed, 77 insertions(+), 39 deletions(-) diff --git a/actions/crud.action.ts b/actions/crud.action.ts index 2138aab6..d44ad0d8 100644 --- a/actions/crud.action.ts +++ b/actions/crud.action.ts @@ -161,7 +161,6 @@ export class CrudAction extends AbstractAction { const appModulePath = './src/app.module.ts'; let appModuleContent = fs.readFileSync(appModulePath, 'utf-8'); - // Avoid duplicate imports const importRegex = /import {[^}]*} from '[@a-zA-Z0-9\/]*';/g; const existingImports: string[] = appModuleContent.match(importRegex) || []; const uniqueNewImports = newImports.split('\n').filter((importLine: string) => !existingImports.some(existingImport => existingImport === importLine)).join('\n'); @@ -173,7 +172,6 @@ export class CrudAction extends AbstractAction { ); } - // Update controllers and providers arrays const controllersRegex = /controllers: \[([^\]]*)\]/s; const providersRegex = /providers: \[([^\]]*)\]/s; diff --git a/lib/templates/controller-template.ts b/lib/templates/controller-template.ts index 768ec112..be848b61 100644 --- a/lib/templates/controller-template.ts +++ b/lib/templates/controller-template.ts @@ -3,7 +3,7 @@ export function controllerTemplate(model: any): string { const modelNameLowerCase = modelName.toLowerCase(); const swaggerImports = ` -import { Controller, Get, Param, Post, Body, Put, Delete } from '@nestjs/common'; +import { Controller, Get, Param, Post, Body, Put, Delete, NotFoundException } from '@nestjs/common'; import { ${modelName}Service } from './${modelNameLowerCase}.service'; import { Create${modelName}Dto, Update${modelName}Dto } from './dto/${modelNameLowerCase}.dto'; import { ${modelName} } from './${modelNameLowerCase}.interface'; @@ -20,8 +20,13 @@ export class ${modelName}Controller { } @Get(':id') - async findOne(@Param('id') id: number): Promise<${modelName} | undefined> { - return this.${modelNameLowerCase}Service.findOne(id); + async findOne(@Param('id') id: string): Promise<${modelName}> { + const ${modelNameLowerCase}Id = parseInt(id, 10); + const ${modelNameLowerCase} = await this.${modelNameLowerCase}Service.findOne(${modelNameLowerCase}Id); + if (!${modelNameLowerCase}) { + throw new NotFoundException(\`${modelName} with ID \${id} not found\`); + } + return ${modelNameLowerCase}; } @Post() @@ -30,13 +35,22 @@ export class ${modelName}Controller { } @Put(':id') - async update(@Param('id') id: number, @Body() ${modelNameLowerCase}Dto: Update${modelName}Dto): Promise<${modelName} | undefined> { - return this.${modelNameLowerCase}Service.update(id, ${modelNameLowerCase}Dto); + async update(@Param('id') id: string, @Body() ${modelNameLowerCase}Dto: Update${modelName}Dto): Promise<${modelName}> { + const ${modelNameLowerCase}Id = parseInt(id, 10); + const updated${modelName} = await this.${modelNameLowerCase}Service.update(${modelNameLowerCase}Id, ${modelNameLowerCase}Dto); + if (!updated${modelName}) { + throw new NotFoundException(\`${modelName} with ID \${id} not found\`); + } + return updated${modelName}; } @Delete(':id') - async remove(@Param('id') id: number): Promise { - return this.${modelNameLowerCase}Service.remove(id); + async remove(@Param('id') id: string): Promise { + const ${modelNameLowerCase}Id = parseInt(id, 10); + const result = await this.${modelNameLowerCase}Service.remove(${modelNameLowerCase}Id); + if (!result) { + throw new NotFoundException(\`${modelName} with ID \${id} not found\`); + } } } `; diff --git a/lib/templates/dto-template.ts b/lib/templates/dto-template.ts index 96703917..248d7a35 100644 --- a/lib/templates/dto-template.ts +++ b/lib/templates/dto-template.ts @@ -1,31 +1,52 @@ export const dtoTemplate = (model: any): string => { - const createDtoFields = model.fields.map((field: any) => { - return `${field.name}${field.isRequired ? '' : '?'}: ${getFieldType(field)};`; - }).join('\n '); - - const updateDtoFields = model.fields.map((field: any) => { - return `${field.name}?: ${getFieldType(field)};`; - }).join('\n '); - - return ` - export class Create${model.name}Dto { - ${createDtoFields} + const createDtoFields = model.fields.map((field: any) => { + return `${getValidators(field, false)}\n ${field.name}${field.isRequired ? '' : '?'}: ${getFieldType(field)};`; + }).join('\n\n '); + + const updateDtoFields = model.fields.map((field: any) => { + return `${getValidators(field, true)}\n ${field.name}?: ${getFieldType(field)};`; + }).join('\n\n '); + + return ` +import { IsInt, IsString, IsBoolean, IsDate, IsOptional } from 'class-validator'; +import { Transform } from 'class-transformer'; + +export class Create${model.name}Dto { + ${createDtoFields} +} + +export class Update${model.name}Dto { + ${updateDtoFields} +} + `; +}; + +const getFieldType = (field: any): string => { + switch (field.type) { + case 'Int': return 'number'; + case 'String': return 'string'; + case 'Boolean': return 'boolean'; + case 'DateTime': return 'Date'; + case 'Json': return 'any'; + default: return 'any'; } - - export class Update${model.name}Dto { - ${updateDtoFields} +}; + +const getValidators = (field: any, isUpdate: boolean): string => { + const validators = []; + validators.push(getTypeValidator(field.type)); + if (!field!.isRequired! || isUpdate) { + validators.push('@IsOptional()'); } - `; - }; - - const getFieldType = (field: any): string => { - switch (field.type) { - case 'Int': return 'number'; - case 'String': return 'string'; - case 'Boolean': return 'boolean'; - case 'DateTime': return 'Date'; - case 'Json': return 'any'; - default: return 'any'; - } - }; - \ No newline at end of file + return validators.filter(Boolean).join('\n '); +}; + +const getTypeValidator = (type: string): string | null => { + switch (type) { + case 'Int': return '@IsInt()'; + case 'String': return '@IsString()'; + case 'Boolean': return '@IsBoolean()'; + case 'DateTime': return `@IsDate()\n @Transform(({ value }) => value ? new Date(value) : undefined)`; + default: return null; + } +}; diff --git a/lib/templates/service-template.ts b/lib/templates/service-template.ts index d5ee3999..fa7cf37b 100644 --- a/lib/templates/service-template.ts +++ b/lib/templates/service-template.ts @@ -38,8 +38,13 @@ export class ${modelName}Service { return this.${modelNameLowerCase}s[${modelNameLowerCase}Index]; } - async remove(id: number): Promise { - this.${modelNameLowerCase}s = this.${modelNameLowerCase}s.filter(${modelNameLowerCase} => ${modelNameLowerCase}.id !== id); + async remove(id: number): Promise { + const ${modelNameLowerCase}Index = this.${modelNameLowerCase}s.findIndex(${modelNameLowerCase} => ${modelNameLowerCase}.id === id); + if(${modelNameLowerCase}Index === -1) { + return false; + } + this.${modelNameLowerCase}s.splice(${modelNameLowerCase}Index, 1); + return true; } } `; From 41779df075ec0bf89477ea9e3a935cafedd1ed4c Mon Sep 17 00:00:00 2001 From: Savio629 Date: Mon, 29 Jul 2024 01:07:24 +0530 Subject: [PATCH 06/12] fix: updated crud command with e2e test --- package.json | 1 + test/e2e-test/crud.e2e-spec.ts | 81 ++++++++++++++++++++++++++++++++++ test/e2e-test/jest-e2e.json | 9 ++++ 3 files changed, 91 insertions(+) create mode 100644 test/e2e-test/crud.e2e-spec.ts create mode 100644 test/e2e-test/jest-e2e.json diff --git a/package.json b/package.json index 06efb209..e72b6316 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "prepublish:npm": "npm run build", "test": "jest --config test/jest-config.json", "test:dev": "npm run clean && jest --config test/jest-config.json --watchAll", + "test:e2e": "jest --config test/e2e-test/jest-e2e.json", "prerelease": "npm run build", "release": "release-it", "prepare": "husky install" diff --git a/test/e2e-test/crud.e2e-spec.ts b/test/e2e-test/crud.e2e-spec.ts new file mode 100644 index 00000000..401ab827 --- /dev/null +++ b/test/e2e-test/crud.e2e-spec.ts @@ -0,0 +1,81 @@ +import { exec } from 'child_process'; +import { join } from 'path'; +import { existsSync, readFileSync, writeFileSync, rmSync, rm } from 'fs'; + +describe('Stencil CLI e2e Test - CRUD command', () => { +// const testDir = join(__dirname, 'test-project'); + const schemaFilePath = join('schema.prisma'); + const schemaFileContent = ` + model Book { + id Int @id @default(autoincrement()) + title String + description String? + } + model Book1 { + id Int @id @default(autoincrement()) + title String + description String? + } + model Car { + id Int @id @default(autoincrement()) + title String + description String? + phone Int + add String + } + `; + + beforeAll(() => { + writeFileSync(schemaFilePath, schemaFileContent); + }); + + afterAll(() => { + rmSync(schemaFilePath); + ['book', 'book1', 'car'].forEach((model) => { + const modelDir = join('src', model); + if (existsSync(modelDir)) { + rmSync(modelDir, { recursive: true, force: true }); + } + rmSync('src', { recursive: true, force: true }); + rmSync('dmmf.json', { recursive: true, force: true }); + + }); + + }); + + it('should log an error when no model is provided', (done) => { + exec('npx stencil crud', (error, stdout, stderr) => { + expect(stderr).toContain('No model provided. Please specify a model or use "*" to generate all models.'); + done(); + }); + }); + + it('should generate files for the Book model', (done) => { + exec('npx stencil crud Book', (error, stdout, stderr) => { + expect(error).toBeNull(); + const modelDir = join('src', 'book'); + expect(existsSync(modelDir)).toBeTruthy(); + expect(existsSync(join(modelDir, 'book.controller.ts'))).toBeTruthy(); + expect(existsSync(join(modelDir, 'book.service.ts'))).toBeTruthy(); + expect(existsSync(join(modelDir, 'book.interface.ts'))).toBeTruthy(); + expect(existsSync(join(modelDir, 'dto', 'book.dto.ts'))).toBeTruthy(); + done(); + }); + }); + + it('should generate files for all models', (done) => { + exec('npx stencil crud *', (error, stdout, stderr) => { + expect(error).toBeNull(); + ['book', 'book1', 'car'].forEach((model) => { + const modelDir = join('src', model); + expect(existsSync(modelDir)).toBeTruthy(); + expect(existsSync(join(modelDir, `${model}.controller.ts`))).toBeTruthy(); + expect(existsSync(join(modelDir, `${model}.service.ts`))).toBeTruthy(); + expect(existsSync(join(modelDir, `${model}.interface.ts`))).toBeTruthy(); + expect(existsSync(join(modelDir, 'dto', `${model}.dto.ts`))).toBeTruthy(); + }); + done(); + }); + }); + +}); diff --git a/test/e2e-test/jest-e2e.json b/test/e2e-test/jest-e2e.json new file mode 100644 index 00000000..1a990c7c --- /dev/null +++ b/test/e2e-test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } + } \ No newline at end of file From 3afd0917a3b2c02bda55135138cc5f9c774630b6 Mon Sep 17 00:00:00 2001 From: Savio629 Date: Mon, 29 Jul 2024 20:30:21 +0530 Subject: [PATCH 07/12] fix: updated a condition of crud command --- commands/crud.command.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/commands/crud.command.ts b/commands/crud.command.ts index 8d66c8bb..c2d8978a 100644 --- a/commands/crud.command.ts +++ b/commands/crud.command.ts @@ -9,6 +9,10 @@ export class CrudCommand extends AbstractCommand { .alias('cr') .description('Generate CRUD API for specified models.') .action(async (inputArgs: string[] = []) => { + if (inputArgs.length === 0) { + console.error('No model provided. Please specify a model or use "*" to generate all models.'); + return; + } const inputs: Input[] = inputArgs.map(arg => ({ name: arg, value: arg })); await this.action.handle(inputs); }); From 4bf99fd9abf831fbd9c71cc8f4682df574850cbd Mon Sep 17 00:00:00 2001 From: Savio Dias Date: Sat, 24 Aug 2024 21:05:28 +0530 Subject: [PATCH 08/12] fix: fixed the repeating import bug --- actions/crud.action.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/actions/crud.action.ts b/actions/crud.action.ts index d44ad0d8..c0866a7a 100644 --- a/actions/crud.action.ts +++ b/actions/crud.action.ts @@ -148,23 +148,15 @@ export class CrudAction extends AbstractAction { const models = dmmf.datamodel.models.filter((model: any) => modelNames.includes(model.name) ); + const appModulePath = './src/app.module.ts'; + let appModuleContent = fs.readFileSync(appModulePath, 'utf-8'); const newImports = models.map((model: any) => { const controllerImport = `import { ${model.name}Controller } from './${model.name.toLowerCase()}/${model.name.toLowerCase()}.controller';`; const serviceImport = `import { ${model.name}Service } from './${model.name.toLowerCase()}/${model.name.toLowerCase()}.service';`; return `${serviceImport}\n${controllerImport}`; - }).join('\n'); - - const newControllers = models.map((model: any) => `${model.name}Controller`); - const newProviders = models.map((model: any) => `${model.name}Service`); - - const appModulePath = './src/app.module.ts'; - let appModuleContent = fs.readFileSync(appModulePath, 'utf-8'); - - const importRegex = /import {[^}]*} from '[@a-zA-Z0-9\/]*';/g; - const existingImports: string[] = appModuleContent.match(importRegex) || []; - const uniqueNewImports = newImports.split('\n').filter((importLine: string) => !existingImports.some(existingImport => existingImport === importLine)).join('\n'); - + }); + const uniqueNewImports = newImports.filter((importLine: string) => !appModuleContent.includes(importLine)).join('\n'); if (uniqueNewImports) { appModuleContent = appModuleContent.replace( /(import {[^}]*} from '[@a-zA-Z0-9\/]*';\n*)+/, @@ -181,6 +173,8 @@ export class CrudAction extends AbstractAction { const currentControllers = controllersMatch ? controllersMatch[1].split(',').map(controller => controller.trim()).filter(Boolean) : []; const currentProviders = providersMatch ? providersMatch[1].split(',').map(provider => provider.trim()).filter(Boolean) : []; + const newControllers = models.map((model: any) => `${model.name}Controller`); + const newProviders = models.map((model: any) => `${model.name}Service`); const updatedControllers = Array.from(new Set([...currentControllers, ...newControllers])).join(', '); const updatedProviders = Array.from(new Set([...currentProviders, ...newProviders])).join(', '); From 269aa4efe4b820e7c9d4062b282319652b12990f Mon Sep 17 00:00:00 2001 From: Savio Dias Date: Mon, 9 Sep 2024 15:18:37 +0530 Subject: [PATCH 09/12] feat: added swagger generation --- actions/add-utils/file-utils.ts | 21 +++ actions/add-utils/import-manager.ts | 32 ++++ actions/add-utils/swagger-decorator.ts | 117 ++++++++++++++ actions/add-utils/swagger-dto.ts | 51 ++++++ actions/add-utils/swagger-initializer.ts | 44 ++++++ actions/add.action.ts | 188 +++-------------------- commands/add.command.ts | 44 ++---- lib/templates/controller-template.ts | 10 +- lib/templates/swagger-init.ts | 47 ++++++ package.json | 2 +- 10 files changed, 355 insertions(+), 201 deletions(-) create mode 100644 actions/add-utils/file-utils.ts create mode 100644 actions/add-utils/import-manager.ts create mode 100644 actions/add-utils/swagger-decorator.ts create mode 100644 actions/add-utils/swagger-dto.ts create mode 100644 actions/add-utils/swagger-initializer.ts create mode 100644 lib/templates/swagger-init.ts diff --git a/actions/add-utils/file-utils.ts b/actions/add-utils/file-utils.ts new file mode 100644 index 00000000..20b99921 --- /dev/null +++ b/actions/add-utils/file-utils.ts @@ -0,0 +1,21 @@ +import { existsSync } from 'fs'; +import { join } from 'path'; + +export function getControllerFilePath(controllerFolder: string): string | null { + const controllerFilePath = join(process.cwd(), 'src', controllerFolder, `${controllerFolder}.controller.ts`); + if (existsSync(controllerFilePath)) { + return controllerFilePath; + } + return null; +} + +export function findProjectRoot(): string { + let dir = process.cwd(); + while (dir !== '/') { + if (existsSync(join(dir, 'package.json'))) { + return dir; + } + dir = join(dir, '..'); + } + throw new Error('Project root not found'); +} diff --git a/actions/add-utils/import-manager.ts b/actions/add-utils/import-manager.ts new file mode 100644 index 00000000..2d1cdf1e --- /dev/null +++ b/actions/add-utils/import-manager.ts @@ -0,0 +1,32 @@ +export function addImports(content: string, fileType: 'controller' | 'dto'): string { + const existingSwaggerImports = content.match(/import\s+{\s*([^}]+)\s*}\s+from\s+'@nestjs\/swagger';/); + + let swaggerDecorators: string[]; + + switch (fileType) { + case 'controller': + swaggerDecorators = ['ApiOperation', 'ApiResponse', 'ApiParam', 'ApiBody', 'ApiTags']; + break; + case 'dto': + swaggerDecorators = ['ApiProperty', 'ApiPropertyOptional']; + break; + default: + throw new Error('Unsupported file type'); + } + + const existingDecorators = existingSwaggerImports ? existingSwaggerImports[1].split(',').map((d) => d.trim()) : []; + const newDecorators = swaggerDecorators.filter((d) => !existingDecorators.includes(d)); + + if (newDecorators.length > 0) { + const newImportStatement = `import { ${[...existingDecorators, ...newDecorators].join(', ')} } from '@nestjs/swagger';`; + if (existingSwaggerImports) { + content = content.replace(existingSwaggerImports[0], newImportStatement); + } else { + content = content.replace(/import\s+[\w\{\}\s,]*\s+from\s+'@nestjs\/common';/, (match) => { + return `${match}\n${newImportStatement}`; + }); + } + } + + return content; +} diff --git a/actions/add-utils/swagger-decorator.ts b/actions/add-utils/swagger-decorator.ts new file mode 100644 index 00000000..3f3a811d --- /dev/null +++ b/actions/add-utils/swagger-decorator.ts @@ -0,0 +1,117 @@ +import * as fs from 'fs'; +import * as chalk from 'chalk'; +import { addImports } from './import-manager'; + +export function addSwaggerControllers(controllerPath: string) { + try { + const content = fs.readFileSync(controllerPath, 'utf-8'); + let updatedContent = content; + + const httpMethods = { + Get: /@Get\(['"]?[^'"]*['"]?\)?\s*\n\s*async\s+(\w+)\(([\w\s,@(){}:'"=]*)\)\s*:\s*Promise<([\w\[\]]+)>/g, + Post: /@Post\(['"]?[^'"]*['"]?\)?\s*\n\s*async\s+(\w+)\(([\w\s,@(){}:'"=]*)\)\s*:\s*Promise<([\w\[\]]+)>/g, + Put: /@Put\(['"]?[^'"]*['"]?\)?\s*\n\s*async\s+(\w+)\(([\w\s,@(){}:'"=]*)\)\s*:\s*Promise<([\w\[\]]+)>/g, + Delete: /@Delete\(['"]?[^'"]*['"]?\)?\s*\n\s*async\s+(\w+)\(([\w\s,@(){}:'"=]*)\)\s*:\s*Promise<([\w\[\]]+)>/g + }; + + let processedMethods: Set = new Set(); + + for (const [method, regex] of Object.entries(httpMethods)) { + let match; + while ((match = regex.exec(updatedContent)) !== null) { + const methodSignature = match[0]; + if (!processedMethods.has(methodSignature)) { + processedMethods.add(methodSignature); + const updatedMethod = replaceMethod(methodSignature, method); + updatedContent = updatedContent.replace(methodSignature, updatedMethod); + } + } + } + + if (!/@ApiTags\(/.test(updatedContent)) { + const classRegex = /@Controller\(['"]([\w-]+)['"]\)/; + updatedContent = updatedContent.replace(classRegex, (match, p1) => { + return `${match}\n@ApiTags('${p1}')`; + }); + } + + updatedContent = addImports(updatedContent,"controller"); + + fs.writeFileSync(controllerPath, updatedContent, 'utf-8'); + console.info(chalk.green(`Swagger decorators added to ${controllerPath}`)); + } catch (error) { + console.error(chalk.red(`Error adding Swagger decorators to ${controllerPath}`), error); + } +} + +function replaceMethod(methodSignature: string, method: string): string { + const decorators = generateSwaggerDecorators(methodSignature, method); + return decorators + methodSignature; +} + +function generateSwaggerDecorators(methodSignature: string, method: string): string { + let decorators = ''; + if (!/@ApiOperation\(/.test(methodSignature)) { + decorators += `@ApiOperation({ summary: '${getOperationSummary(method)}' })\n`; + } + + if (!/@ApiResponse\(/.test(methodSignature)) { + decorators += `@ApiResponse({ status: 200, description: 'Successful response', type: '${getReturnType(methodSignature)}' })\n`; + } + + if (/@Body\(/.test(methodSignature) && !/@ApiBody\(/.test(methodSignature)) { + decorators += `@ApiBody({ type: ${getBodyType(methodSignature)}, description: 'Item data' })\n`; + } + + if (/@Param\(['"]\w+['"]\)/.test(methodSignature) && !/@ApiParam\({ name: /.test(methodSignature)) { + const paramName = getParamName(methodSignature); + const paramType = getParamType(methodSignature); + decorators += `@ApiParam({ name: '${paramName}', description: 'ID of the item', type: '${paramType}' })\n`; + } + + if (method === 'Delete') { + if (!/@ApiResponse\({ status: 204/.test(methodSignature)) { + decorators += `@ApiResponse({ status: 204, description: 'Item deleted' })\n`; + } + if (!/@ApiResponse\({ status: 404/.test(methodSignature)) { + decorators += `@ApiResponse({ status: 404, description: 'Item not found' })\n`; + } + } + + return decorators; +} + +function getOperationSummary(method: string): string { + switch (method) { + case 'Get': + return 'Retrieve items'; + case 'Post': + return 'Create an item'; + case 'Put': + return 'Update an item'; + case 'Delete': + return 'Delete an item'; + default: + return ''; + } +} + +function getReturnType(methodSignature: string): string { + const match = methodSignature.match(/:\s*Promise<([\w\s@(){}]+)>/); + return match ? match[1].trim() : 'any'; +} + +function getBodyType(methodSignature: string): string { + const match = methodSignature.match(/@Body\(\)\s*([\w<>,]+):\s*([\w<>,]+)/); + return match ? match[2].trim() : 'any'; +} + +function getParamType(methodSignature: string): string { + const match = methodSignature.match(/@Param\(\s*['"]\w+['"]\s*\)\s*\w+:\s*(\w+)/); + return match ? match[1] : 'string'; +} + +function getParamName(methodSignature: string): string { + const match = methodSignature.match(/@Param\(\s*['"](\w+)['"]\s*\)/); + return match ? match[1] : 'id'; +} diff --git a/actions/add-utils/swagger-dto.ts b/actions/add-utils/swagger-dto.ts new file mode 100644 index 00000000..2a536b35 --- /dev/null +++ b/actions/add-utils/swagger-dto.ts @@ -0,0 +1,51 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { addImports } from './import-manager'; +import * as chalk from 'chalk'; +export function addSwaggerDto(controllerName: string) { + const dtoDir = path.join(process.cwd(), 'src', controllerName, 'dto'); + if (!fs.existsSync(dtoDir)) { + console.error(`DTO directory not found: ${dtoDir}`); + return; + } + + const dtoFiles = fs.readdirSync(dtoDir).filter(file => file.endsWith('.ts')); + + dtoFiles.forEach(file => { + const dtoPath = path.join(dtoDir, file); + const dtoContent = fs.readFileSync(dtoPath, 'utf-8'); + + let updatedContent = addSwaggerDecorators(dtoContent); + + updatedContent = addImports(updatedContent,"dto"); + + fs.writeFileSync(dtoPath, updatedContent, 'utf-8'); + console.info(chalk.green(`Swagger decorators added to ${dtoPath}`)); + }); +} + +const addSwaggerDecorators = (content: string): string => { + const lines = content.split('\n'); + +// const hasSwaggerImports = lines.some(line => line.includes('@nestjs/swagger')); +// if (!hasSwaggerImports) { +// lines.splice(1, 0, "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';"); +// } + + const updatedLines = lines.map(line => { + if (line.includes('export class Create') || line.includes('export class Update')) { + return line; + } + + if (line.includes(':')) { + const [name, type] = line.split(':'); + const isOptional = name.includes('?'); + const decorator = isOptional ? '@ApiPropertyOptional' : '@ApiProperty'; + return ` ${decorator}({ description: '${name.trim()}' })\n ${line}`; + } + + return line; + }); + + return updatedLines.join('\n'); +}; diff --git a/actions/add-utils/swagger-initializer.ts b/actions/add-utils/swagger-initializer.ts new file mode 100644 index 00000000..82af8901 --- /dev/null +++ b/actions/add-utils/swagger-initializer.ts @@ -0,0 +1,44 @@ +import * as fs from 'fs'; +import * as chalk from 'chalk'; +import { join } from 'path'; +import { findProjectRoot } from './file-utils'; + +export function addSwaggerInitialization() { + const projectRoot = findProjectRoot(); + const mainPath = join(projectRoot, 'src', 'main.ts'); + + try { + let mainContent = fs.readFileSync(mainPath, 'utf-8'); + + if (!/import\s+{\s*SwaggerModule\s*,\s*DocumentBuilder\s*}\s+from\s+'@nestjs\/swagger';/.test(mainContent)) { + mainContent = mainContent.replace( + 'import { NestFactory } from \'@nestjs/core\';', + `import { NestFactory } from '@nestjs/core'; +import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';` + ); + } + + if (!/const\s+config\s+=\s+new\s+DocumentBuilder\(\)/.test(mainContent)) { + mainContent = mainContent.replace( + /await\s+app\.listen\(\d+\);/, + `const config = new DocumentBuilder() + .setTitle('API Documentation') + .setDescription('The API description') + .setVersion('1.0') + .addTag('api') + .build(); + + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('api', app, document); + + await app.listen(3000);` + ); + } + + fs.writeFileSync(mainPath, mainContent, 'utf-8'); + console.info(chalk.green('Swagger initialized in main.ts')); + + } catch (error) { + console.error(chalk.red(`Error adding Swagger initialization to ${mainPath}`), error); + } +} diff --git a/actions/add.action.ts b/actions/add.action.ts index 5c5d1d09..801c9a15 100644 --- a/actions/add.action.ts +++ b/actions/add.action.ts @@ -1,180 +1,36 @@ import * as chalk from 'chalk'; -import { Input } from '../commands'; -import { getValueOrDefault } from '../lib/compiler/helpers/get-value-or-default'; -import { - AbstractPackageManager, - PackageManagerFactory, -} from '../lib/package-managers'; -import { - AbstractCollection, - CollectionFactory, - SchematicOption, -} from '../lib/schematics'; -import { MESSAGES } from '../lib/ui'; -import { loadConfiguration } from '../lib/utils/load-configuration'; -import { - askForProjectName, - hasValidOptionFlag, - moveDefaultProjectToStart, - shouldAskForProject, -} from '../lib/utils/project-utils'; import { AbstractAction } from './abstract.action'; - -const schematicName = 'nest-add'; - +import { Input } from '../commands/command.input'; +import { getControllerFilePath } from './add-utils/file-utils'; +import { addSwaggerInitialization } from './add-utils/swagger-initializer'; +import { addSwaggerControllers } from './add-utils/swagger-decorator'; +import { addSwaggerDto } from './add-utils/swagger-dto'; export class AddAction extends AbstractAction { - public async handle(inputs: Input[], options: Input[], extraFlags: string[]) { - const libraryName = this.getLibraryName(inputs); - const packageName = this.getPackageName(libraryName); - const collectionName = this.getCollectionName(libraryName, packageName); - const tagName = this.getTagName(packageName); - const skipInstall = hasValidOptionFlag('skip-install', options); - const packageInstallSuccess = - skipInstall || (await this.installPackage(collectionName, tagName)); - if (packageInstallSuccess) { - const sourceRootOption: Input = await this.getSourceRoot( - inputs.concat(options), - ); - options.push(sourceRootOption); - - await this.addLibrary(collectionName, options, extraFlags); - } else { - console.error( - chalk.red( - MESSAGES.LIBRARY_INSTALLATION_FAILED_BAD_PACKAGE(libraryName), - ), - ); - throw new Error( - MESSAGES.LIBRARY_INSTALLATION_FAILED_BAD_PACKAGE(libraryName), - ); - } - } - - private async getSourceRoot(inputs: Input[]): Promise { - const configuration = await loadConfiguration(); - const configurationProjects = configuration.projects; - - const appName = inputs.find((option) => option.name === 'project')! - .value as string; - - let sourceRoot = appName - ? getValueOrDefault(configuration, 'sourceRoot', appName) - : configuration.sourceRoot; - - const shouldAsk = shouldAskForProject( - schematicName, - configurationProjects, - appName, - ); - if (shouldAsk) { - const defaultLabel = ' [ Default ]'; - let defaultProjectName = configuration.sourceRoot + defaultLabel; - - for (const property in configurationProjects) { - if ( - configurationProjects[property].sourceRoot === - configuration.sourceRoot - ) { - defaultProjectName = property + defaultLabel; - break; - } + public async handle(inputs: Input[]) { + const subcommand = inputs.find((input) => input.name === 'subcommand')!.value as string; + const controllerName = inputs.find((input) => input.name === 'controllerPath')?.value as string; + const init = inputs.find((input) => input.name === 'init')?.value; + + if (subcommand === 'swagger') { + if (init) { + addSwaggerInitialization(); } - const projects = moveDefaultProjectToStart( - configuration, - defaultProjectName, - defaultLabel, - ); - - const answers = await askForProjectName( - MESSAGES.LIBRARY_PROJECT_SELECTION_QUESTION, - projects, - ); - const project = answers.appName.replace(defaultLabel, ''); - if (project !== configuration.sourceRoot) { - sourceRoot = configurationProjects[project].sourceRoot; + if (!controllerName) { + console.error(chalk.red('Controller path is required.')); + return; } - } - return { name: 'sourceRoot', value: sourceRoot }; - } - private async installPackage( - collectionName: string, - tagName: string, - ): Promise { - const manager: AbstractPackageManager = await PackageManagerFactory.find(); - tagName = tagName || 'latest'; - let installResult = false; - try { - installResult = await manager.addProduction([collectionName], tagName); - } catch (error) { - if (error && error.message) { - console.error(chalk.red(error.message)); - } - } - return installResult; - } + const controllerPath = getControllerFilePath(controllerName); - private async addLibrary( - collectionName: string, - options: Input[], - extraFlags: string[], - ) { - console.info(MESSAGES.LIBRARY_INSTALLATION_STARTS); - const schematicOptions: SchematicOption[] = []; - schematicOptions.push( - new SchematicOption( - 'sourceRoot', - options.find((option) => option.name === 'sourceRoot')!.value as string, - ), - ); - const extraFlagsString = extraFlags ? extraFlags.join(' ') : undefined; - - try { - const collection: AbstractCollection = - CollectionFactory.create(collectionName); - await collection.execute( - schematicName, - schematicOptions, - extraFlagsString, - ); - } catch (error) { - if (error && error.message) { - console.error(chalk.red(error.message)); - return Promise.reject(); + if (!controllerPath) { + console.error(chalk.red(`Controller file not found in path: ${controllerName}`)); + return; } - } - } - private getLibraryName(inputs: Input[]): string { - const libraryInput: Input = inputs.find( - (input) => input.name === 'library', - ) as Input; + addSwaggerControllers(controllerPath); + addSwaggerDto(controllerName); - if (!libraryInput) { - throw new Error('No library found in command input'); } - return libraryInput.value as string; - } - - private getPackageName(library: string): string { - return library.startsWith('@') - ? library.split('/', 2).join('/') - : library.split('/', 1)[0]; - } - - private getCollectionName(library: string, packageName: string): string { - return ( - (packageName.startsWith('@') - ? packageName.split('@', 2).join('@') - : packageName.split('@', 1).join('@')) + - library.slice(packageName.length) - ); - } - - private getTagName(packageName: string): string { - return packageName.startsWith('@') - ? packageName.split('@', 3)[2] - : packageName.split('@', 2)[1]; } } diff --git a/commands/add.command.ts b/commands/add.command.ts index a2006dbe..ff9f18c8 100644 --- a/commands/add.command.ts +++ b/commands/add.command.ts @@ -1,39 +1,25 @@ -import { Command, CommanderStatic } from 'commander'; -import { getRemainingFlags } from '../lib/utils/remaining-flags'; +import { CommanderStatic } from 'commander'; import { AbstractCommand } from './abstract.command'; import { Input } from './command.input'; export class AddCommand extends AbstractCommand { - public load(program: CommanderStatic): void { + public load(program: CommanderStatic) { program - .command('add ') - .allowUnknownOption() - .description('Adds support for an external library to your project.') - .option( - '-d, --dry-run', - 'Report actions that would be performed without writing out results.', - ) - .option('-s, --skip-install', 'Skip package installation.', false) - .option('-p, --project [project]', 'Project in which to generate files.') - .usage(' [options] [library-specific-options]') - .action(async (library: string, command: Command) => { - const options: Input[] = []; - options.push({ name: 'dry-run', value: !!command.dryRun }); - options.push({ name: 'skip-install', value: command.skipInstall }); - options.push({ - name: 'project', - value: command.project, - }); + .command('add [controllerPath]') + .alias('as') + .description('Add various functionalities to the specified controller.') + .option('--init', 'Initialize with default options') + .action(async (subcommand: string, controllerPath: string, options: any) => { + const inputs: Input[] = [ + { name: 'subcommand', value: subcommand }, + { name: 'controllerPath', value: controllerPath } + ]; - const inputs: Input[] = []; - inputs.push({ name: 'library', value: library }); - - const flags = getRemainingFlags(program); - try { - await this.action.handle(inputs, options, flags); - } catch (err) { - process.exit(1); + if (options.init) { + inputs.push({ name: 'init', value: options.init }); } + + await this.action.handle(inputs); }); } } diff --git a/lib/templates/controller-template.ts b/lib/templates/controller-template.ts index be848b61..c102c77e 100644 --- a/lib/templates/controller-template.ts +++ b/lib/templates/controller-template.ts @@ -14,12 +14,12 @@ import { ${modelName} } from './${modelNameLowerCase}.interface'; export class ${modelName}Controller { constructor(private readonly ${modelNameLowerCase}Service: ${modelName}Service) {} - @Get() +@Get() async findAll(): Promise<${modelName}[]> { return this.${modelNameLowerCase}Service.findAll(); } - @Get(':id') +@Get(':id') async findOne(@Param('id') id: string): Promise<${modelName}> { const ${modelNameLowerCase}Id = parseInt(id, 10); const ${modelNameLowerCase} = await this.${modelNameLowerCase}Service.findOne(${modelNameLowerCase}Id); @@ -29,12 +29,12 @@ export class ${modelName}Controller { return ${modelNameLowerCase}; } - @Post() +@Post() async create(@Body() ${modelNameLowerCase}Dto: Create${modelName}Dto): Promise<${modelName}> { return this.${modelNameLowerCase}Service.create(${modelNameLowerCase}Dto); } - @Put(':id') +@Put(':id') async update(@Param('id') id: string, @Body() ${modelNameLowerCase}Dto: Update${modelName}Dto): Promise<${modelName}> { const ${modelNameLowerCase}Id = parseInt(id, 10); const updated${modelName} = await this.${modelNameLowerCase}Service.update(${modelNameLowerCase}Id, ${modelNameLowerCase}Dto); @@ -44,7 +44,7 @@ export class ${modelName}Controller { return updated${modelName}; } - @Delete(':id') +@Delete(':id') async remove(@Param('id') id: string): Promise { const ${modelNameLowerCase}Id = parseInt(id, 10); const result = await this.${modelNameLowerCase}Service.remove(${modelNameLowerCase}Id); diff --git a/lib/templates/swagger-init.ts b/lib/templates/swagger-init.ts new file mode 100644 index 00000000..6da9d3f9 --- /dev/null +++ b/lib/templates/swagger-init.ts @@ -0,0 +1,47 @@ +import * as fs from 'fs'; +import * as chalk from 'chalk'; + + +export class addSwaggerInitialization{ + public async handle() { + + const mainPath = 'src/main.ts'; + const importStatement = `import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';`; + const setupCall = ` + const config = new DocumentBuilder() + .setTitle('API Documentation') + .setDescription('The API description') + .setVersion('1.0') + .addTag('api') + .build(); + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('api', app, document); + `; + + try { + let mainContent = fs.readFileSync(mainPath, 'utf-8'); + + if (!mainContent.includes(importStatement)) { + mainContent = mainContent.replace( + /import { ValidationPipe } from '@nestjs\/common';/, + `import { ValidationPipe } from '@nestjs/common';\n${importStatement}` + ); + console.info(chalk.green('Swagger import added to main.ts')); + } + + if (!mainContent.includes('SwaggerModule.setup')) { + mainContent = mainContent.replace( + /app\.useGlobalPipes\(new ValidationPipe\(\)\);/, + `app.useGlobalPipes(new ValidationPipe());\n${setupCall}` + ); + console.info(chalk.green('Swagger setup added to main.ts')); + } else { + console.info(chalk.yellow('Swagger already initialized in main.ts')); + } + + fs.writeFileSync(mainPath, mainContent, 'utf-8'); + } catch (error) { + console.error(chalk.red(`Error adding Swagger initialization to ${mainPath}`), error); + } + } +} diff --git a/package.json b/package.json index e72b6316..a745ef2a 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@angular-devkit/schematics-cli": "16.2.3", "@prisma/internals": "^5.16.2", "@samagra-x/schematics": "^0.0.5", - "bun": "^1.1.7", + "bun": "^1.1.26", "chalk": "4.1.2", "chokidar": "3.5.3", "cli-table3": "0.6.3", From fc1c2258217c2672d89de9a6e9f7dd1a88a19f1e Mon Sep 17 00:00:00 2001 From: Savio Dias Date: Tue, 10 Sep 2024 18:19:45 +0530 Subject: [PATCH 10/12] fix: updated swagger with path --- actions/add-utils/file-utils.ts | 14 +- actions/add-utils/import-manager.ts | 6 +- ...ger-decorator.ts => swagger-controller.ts} | 0 actions/add-utils/swagger-dto.ts | 27 +-- ...swagger-initializer.ts => swagger-init.ts} | 0 actions/add.action.ts | 21 +- commands/add.command.ts | 8 +- crudReadme.md | 47 ++++ lib/templates/swagger-init.ts | 47 ---- package-lock.json | 219 +++++++++++------- swaggerReadme.md | 46 ++++ 11 files changed, 268 insertions(+), 167 deletions(-) rename actions/add-utils/{swagger-decorator.ts => swagger-controller.ts} (100%) rename actions/add-utils/{swagger-initializer.ts => swagger-init.ts} (100%) create mode 100644 crudReadme.md delete mode 100644 lib/templates/swagger-init.ts create mode 100644 swaggerReadme.md diff --git a/actions/add-utils/file-utils.ts b/actions/add-utils/file-utils.ts index 20b99921..dac0b16a 100644 --- a/actions/add-utils/file-utils.ts +++ b/actions/add-utils/file-utils.ts @@ -1,14 +1,22 @@ import { existsSync } from 'fs'; -import { join } from 'path'; +import { join, basename } from 'path'; -export function getControllerFilePath(controllerFolder: string): string | null { - const controllerFilePath = join(process.cwd(), 'src', controllerFolder, `${controllerFolder}.controller.ts`); +export function getControllerFilePath(modelName: string): string | null { + const controllerFilePath = join(process.cwd(), modelName, `${basename(modelName)}.controller.ts`); if (existsSync(controllerFilePath)) { return controllerFilePath; } return null; } +export function getDtoFilePath(modelName: string): string | null { + const dtoFilePath = join(process.cwd(), modelName, 'dto',`${basename(modelName)}.dto.ts`); + if (existsSync(dtoFilePath)) { + return dtoFilePath; + } + return null; +} + export function findProjectRoot(): string { let dir = process.cwd(); while (dir !== '/') { diff --git a/actions/add-utils/import-manager.ts b/actions/add-utils/import-manager.ts index 2d1cdf1e..7b5eaf62 100644 --- a/actions/add-utils/import-manager.ts +++ b/actions/add-utils/import-manager.ts @@ -22,9 +22,9 @@ export function addImports(content: string, fileType: 'controller' | 'dto'): str if (existingSwaggerImports) { content = content.replace(existingSwaggerImports[0], newImportStatement); } else { - content = content.replace(/import\s+[\w\{\}\s,]*\s+from\s+'@nestjs\/common';/, (match) => { - return `${match}\n${newImportStatement}`; - }); + const lastImportIndex = content.lastIndexOf('import '); + const insertPosition = content.indexOf(';', lastImportIndex) + 1; + content = `${content.slice(0, insertPosition)}\n${newImportStatement}${content.slice(insertPosition)}`; } } diff --git a/actions/add-utils/swagger-decorator.ts b/actions/add-utils/swagger-controller.ts similarity index 100% rename from actions/add-utils/swagger-decorator.ts rename to actions/add-utils/swagger-controller.ts diff --git a/actions/add-utils/swagger-dto.ts b/actions/add-utils/swagger-dto.ts index 2a536b35..85daaa15 100644 --- a/actions/add-utils/swagger-dto.ts +++ b/actions/add-utils/swagger-dto.ts @@ -2,36 +2,21 @@ import * as fs from 'fs'; import * as path from 'path'; import { addImports } from './import-manager'; import * as chalk from 'chalk'; -export function addSwaggerDto(controllerName: string) { - const dtoDir = path.join(process.cwd(), 'src', controllerName, 'dto'); - if (!fs.existsSync(dtoDir)) { - console.error(`DTO directory not found: ${dtoDir}`); - return; - } - const dtoFiles = fs.readdirSync(dtoDir).filter(file => file.endsWith('.ts')); +export function addSwaggerDto(dtoFilePath: string) { - dtoFiles.forEach(file => { - const dtoPath = path.join(dtoDir, file); - const dtoContent = fs.readFileSync(dtoPath, 'utf-8'); + const dtoContent = fs.readFileSync(dtoFilePath, 'utf-8'); - let updatedContent = addSwaggerDecorators(dtoContent); + let updatedContent = addSwaggerDecorators(dtoContent); + updatedContent = addImports(updatedContent, 'dto'); - updatedContent = addImports(updatedContent,"dto"); - - fs.writeFileSync(dtoPath, updatedContent, 'utf-8'); - console.info(chalk.green(`Swagger decorators added to ${dtoPath}`)); - }); + fs.writeFileSync(dtoFilePath, updatedContent, 'utf-8'); + console.info(chalk.green(`Swagger decorators added to ${dtoFilePath}`)); } const addSwaggerDecorators = (content: string): string => { const lines = content.split('\n'); -// const hasSwaggerImports = lines.some(line => line.includes('@nestjs/swagger')); -// if (!hasSwaggerImports) { -// lines.splice(1, 0, "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';"); -// } - const updatedLines = lines.map(line => { if (line.includes('export class Create') || line.includes('export class Update')) { return line; diff --git a/actions/add-utils/swagger-initializer.ts b/actions/add-utils/swagger-init.ts similarity index 100% rename from actions/add-utils/swagger-initializer.ts rename to actions/add-utils/swagger-init.ts diff --git a/actions/add.action.ts b/actions/add.action.ts index 801c9a15..20e7f651 100644 --- a/actions/add.action.ts +++ b/actions/add.action.ts @@ -1,14 +1,14 @@ import * as chalk from 'chalk'; import { AbstractAction } from './abstract.action'; import { Input } from '../commands/command.input'; -import { getControllerFilePath } from './add-utils/file-utils'; +import { getControllerFilePath, getDtoFilePath } from './add-utils/file-utils'; import { addSwaggerInitialization } from './add-utils/swagger-initializer'; import { addSwaggerControllers } from './add-utils/swagger-decorator'; import { addSwaggerDto } from './add-utils/swagger-dto'; export class AddAction extends AbstractAction { public async handle(inputs: Input[]) { const subcommand = inputs.find((input) => input.name === 'subcommand')!.value as string; - const controllerName = inputs.find((input) => input.name === 'controllerPath')?.value as string; + const modelPath = inputs.find((input) => input.name === 'modelPath')?.value as string; const init = inputs.find((input) => input.name === 'init')?.value; if (subcommand === 'swagger') { @@ -16,20 +16,27 @@ export class AddAction extends AbstractAction { addSwaggerInitialization(); } - if (!controllerName) { - console.error(chalk.red('Controller path is required.')); + if (!modelPath) { + console.error(chalk.red('Model path is required.')); return; } - const controllerPath = getControllerFilePath(controllerName); + const controllerPath = getControllerFilePath(modelPath); if (!controllerPath) { - console.error(chalk.red(`Controller file not found in path: ${controllerName}`)); + console.error(chalk.red(`Controller file not found in path: ${modelPath}`)); return; } + const dtoFilePath = getDtoFilePath(modelPath); + + if (!dtoFilePath) { + console.error(chalk.red(`DTO file not found for model: ${modelPath}`)); + return; + } + addSwaggerControllers(controllerPath); - addSwaggerDto(controllerName); + addSwaggerDto(dtoFilePath); } } diff --git a/commands/add.command.ts b/commands/add.command.ts index ff9f18c8..fd47ab06 100644 --- a/commands/add.command.ts +++ b/commands/add.command.ts @@ -5,14 +5,14 @@ import { Input } from './command.input'; export class AddCommand extends AbstractCommand { public load(program: CommanderStatic) { program - .command('add [controllerPath]') + .command('add [modelName]') .alias('as') - .description('Add various functionalities to the specified controller.') + .description('Add various swagger methods to the specified model.') .option('--init', 'Initialize with default options') - .action(async (subcommand: string, controllerPath: string, options: any) => { + .action(async (subcommand: string, modelPath: string, options: any) => { const inputs: Input[] = [ { name: 'subcommand', value: subcommand }, - { name: 'controllerPath', value: controllerPath } + { name: 'modelPath', value: modelPath } ]; if (options.init) { diff --git a/crudReadme.md b/crudReadme.md new file mode 100644 index 00000000..d409d531 --- /dev/null +++ b/crudReadme.md @@ -0,0 +1,47 @@ +## CRUD Command Overview +When the crud command is invoked through the Stencil CLI, a folder is created for each model specified. This folder contains all necessary files, such as Controller, Service, Interface and DTOs. + +In addition to generating these files, the main module of the application is also updated to include the newly generated services and controllers. + + +## Crud Command + +``` +stencil crud [inputs...] +stencil cr [inputs...] +``` + +Example: `stencil crud model1` or `stencil crud *` + +**Description** + +Generate CRUD API for specified models. + +**Arguments** + +| Argument | Description | +|-----------|--------------| +| `[inputs...]` | The model name for which crud api needs to be generated | + +**Inputs** + +| Name | Description | +|---|---| +| `modelName` | Generates a crud api for modelName | +| `*` | Generates a crud api for all models present in schema.prisma | + +### Example structure of `schema.prisma` +``` +model Book { + id Int @id @default(autoincrement()) + title String + description String? +} +model Car { + id Int @id @default(autoincrement()) + title String + description String? + phone Int + add String +} +``` diff --git a/lib/templates/swagger-init.ts b/lib/templates/swagger-init.ts deleted file mode 100644 index 6da9d3f9..00000000 --- a/lib/templates/swagger-init.ts +++ /dev/null @@ -1,47 +0,0 @@ -import * as fs from 'fs'; -import * as chalk from 'chalk'; - - -export class addSwaggerInitialization{ - public async handle() { - - const mainPath = 'src/main.ts'; - const importStatement = `import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';`; - const setupCall = ` - const config = new DocumentBuilder() - .setTitle('API Documentation') - .setDescription('The API description') - .setVersion('1.0') - .addTag('api') - .build(); - const document = SwaggerModule.createDocument(app, config); - SwaggerModule.setup('api', app, document); - `; - - try { - let mainContent = fs.readFileSync(mainPath, 'utf-8'); - - if (!mainContent.includes(importStatement)) { - mainContent = mainContent.replace( - /import { ValidationPipe } from '@nestjs\/common';/, - `import { ValidationPipe } from '@nestjs/common';\n${importStatement}` - ); - console.info(chalk.green('Swagger import added to main.ts')); - } - - if (!mainContent.includes('SwaggerModule.setup')) { - mainContent = mainContent.replace( - /app\.useGlobalPipes\(new ValidationPipe\(\)\);/, - `app.useGlobalPipes(new ValidationPipe());\n${setupCall}` - ); - console.info(chalk.green('Swagger setup added to main.ts')); - } else { - console.info(chalk.yellow('Swagger already initialized in main.ts')); - } - - fs.writeFileSync(mainPath, mainContent, 'utf-8'); - } catch (error) { - console.error(chalk.red(`Error adding Swagger initialization to ${mainPath}`), error); - } - } -} diff --git a/package-lock.json b/package-lock.json index 29a3e5fe..2b9b710b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@angular-devkit/schematics-cli": "16.2.3", "@prisma/internals": "^5.16.2", "@samagra-x/schematics": "^0.0.5", - "bun": "^1.1.7", + "bun": "^1.1.26", "chalk": "4.1.2", "chokidar": "3.5.3", "cli-table3": "0.6.3", @@ -1096,6 +1096,52 @@ "node": ">=v14" } }, + "node_modules/@commitlint/top-level/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/top-level/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/top-level/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@commitlint/types": { "version": "17.8.1", "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-17.8.1.tgz", @@ -1934,96 +1980,104 @@ } }, "node_modules/@oven/bun-darwin-aarch64": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/@oven/bun-darwin-aarch64/-/bun-darwin-aarch64-1.1.18.tgz", - "integrity": "sha512-2YMh1G+S5AxDqOEDh9i+9kc17887mkP/yzK/d5DQ0NyPt5uR2w5FKGaalPLDiu5w139y3LKBi+1eGba1oEJnyw==", + "version": "1.1.26", + "resolved": "https://registry.npmjs.org/@oven/bun-darwin-aarch64/-/bun-darwin-aarch64-1.1.26.tgz", + "integrity": "sha512-E8/3i0RIvsIWS+kyeIlbwBh+4qB5DsQIfcO6xr4p3t7tEzvRWnrFkJrbJthru/eB1UsVV9PJ/hsxTrp3m3za4A==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@oven/bun-darwin-x64": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64/-/bun-darwin-x64-1.1.18.tgz", - "integrity": "sha512-ppeJpQqEXO6nfCneq2TXYFO/l1S/KYKTt3cintTiQxW0ISvj36vQcP/l0ln8BxEu46EnqulVKDrkTBAttv9sww==", + "version": "1.1.26", + "resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64/-/bun-darwin-x64-1.1.26.tgz", + "integrity": "sha512-ENRAAGBr2zh0VfETZXqcNPO3ZnnKDX3U6E/oWY+J70uWa9dJqRlRaj1oLB63AGoYJBNdhEcsSmTAk7toCJ+PGQ==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@oven/bun-darwin-x64-baseline": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64-baseline/-/bun-darwin-x64-baseline-1.1.18.tgz", - "integrity": "sha512-shwwfe9Yugpyr490FdjQ90O3JtETbszyUk4PBXQrbz3babPfhXGuVGewis8ORNYeb8zoWGo/adk4biby6kKwHA==", + "version": "1.1.26", + "resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64-baseline/-/bun-darwin-x64-baseline-1.1.26.tgz", + "integrity": "sha512-36HQlQfbrwP//xOS5VFN9AR/iH6BDQo3y8j5282DmRO+h6jylwlg+2+Sfz+1uXDOLDQWCbnNv3Mpl8+Ltso6cQ==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@oven/bun-linux-aarch64": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/@oven/bun-linux-aarch64/-/bun-linux-aarch64-1.1.18.tgz", - "integrity": "sha512-cDwqcGA/PiiqM8pQkZSRW0HbSh3r1hMsS2ew61d6FjjEI7HP+bwTuu0n0rGdzQKWTtb3PzzXvOkiFZywKS5Gzg==", + "version": "1.1.26", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-aarch64/-/bun-linux-aarch64-1.1.26.tgz", + "integrity": "sha512-MqE/ClaEMW6B5i5UIYJnHbadWLt6QQQHV3NBlXd78Mhx1OiZY0YmARQmAItPUp9mxIEgGuA2QyrKvgGD3pzWPQ==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@oven/bun-linux-x64": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64/-/bun-linux-x64-1.1.18.tgz", - "integrity": "sha512-oce0pELxlVhRO7clQGAkbo8vfxaCmRpf7Tu/Swn+T/wqeA5tew02HmsZAnDQqgYx8Z2/QpCOfF1SvLsdg7hR+A==", + "version": "1.1.26", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64/-/bun-linux-x64-1.1.26.tgz", + "integrity": "sha512-sD/ZegJpnBg93qsKsiGnJgTROc68CWONwZpvtL65cBROLBqKb965ofhPUaM5oV8HckfaTDmT37cks59hG+tHvw==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@oven/bun-linux-x64-baseline": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-baseline/-/bun-linux-x64-baseline-1.1.18.tgz", - "integrity": "sha512-hxnFwssve6M9i4phusIn9swFvQKwLI+9i2taWSotshp1axLXQ5ruIIE9WPKJGR0i+yuw5Q8HBCnUDDh5ZMp9rA==", + "version": "1.1.26", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-baseline/-/bun-linux-x64-baseline-1.1.26.tgz", + "integrity": "sha512-jQeSLodwfQu5pG529jYG73VSFq26hdrTspxo9E/1B1WvwKrs2Vtz3w32zv+JWH+gvZqc28A/yK6pAmzQMiscNg==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@oven/bun-windows-x64": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/@oven/bun-windows-x64/-/bun-windows-x64-1.1.18.tgz", - "integrity": "sha512-d639p5g8hrXyvFX3FK9EpsaoVEhMRThftmkueljjpYnYjMvIiMQ2crHtI2zwZ6yLEHvecaFXVXlocu2+jxia7g==", + "version": "1.1.26", + "resolved": "https://registry.npmjs.org/@oven/bun-windows-x64/-/bun-windows-x64-1.1.26.tgz", + "integrity": "sha512-EkyW6JYnZPFxD9XsdEDqFxVCnWnAoyacUAiOEUYAiz8LsnbHLMlOfbdw7KYzvm7UPFoEkUZKD78eSdpg6q6c+Q==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@oven/bun-windows-x64-baseline": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/@oven/bun-windows-x64-baseline/-/bun-windows-x64-baseline-1.1.18.tgz", - "integrity": "sha512-Wlb55q9QbayO+7NvfYMnU8oaTPz1k2xMr7mm9+JOnG/I6q82HMvIQEG181bAhU1kcm5YcZZ5E0WMp2gX3NFsEw==", + "version": "1.1.26", + "resolved": "https://registry.npmjs.org/@oven/bun-windows-x64-baseline/-/bun-windows-x64-baseline-1.1.26.tgz", + "integrity": "sha512-qb593xu9WIKBCHd47z7ZaZTC9h8r4T6qDbBV/XGLhxdZEJb24ePWdhW8WoHxa9hsATio9SByozqwblXb2tJncw==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -4400,14 +4454,15 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, "node_modules/bun": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/bun/-/bun-1.1.18.tgz", - "integrity": "sha512-bv1wLYtmkn6GCqYFsVO9xZzPvNaDlA3xHbtePGHMtXMqq8N/vo+L6b19LB4+I5RKXFAsSmgzonyh2oMExaaWcQ==", + "version": "1.1.26", + "resolved": "https://registry.npmjs.org/bun/-/bun-1.1.26.tgz", + "integrity": "sha512-dWSewAqE7sVbYmflJxgG47dW4vmsbar7VAnQ4ao45y3ulr3n7CwdsMLFnzd28jhPRtF+rsaVK2y4OLIkP3OD4A==", "cpu": [ "arm64", "x64" ], "hasInstallScript": true, + "license": "MIT", "os": [ "darwin", "linux", @@ -4418,14 +4473,14 @@ "bunx": "bin/bun.exe" }, "optionalDependencies": { - "@oven/bun-darwin-aarch64": "1.1.18", - "@oven/bun-darwin-x64": "1.1.18", - "@oven/bun-darwin-x64-baseline": "1.1.18", - "@oven/bun-linux-aarch64": "1.1.18", - "@oven/bun-linux-x64": "1.1.18", - "@oven/bun-linux-x64-baseline": "1.1.18", - "@oven/bun-windows-x64": "1.1.18", - "@oven/bun-windows-x64-baseline": "1.1.18" + "@oven/bun-darwin-aarch64": "1.1.26", + "@oven/bun-darwin-x64": "1.1.26", + "@oven/bun-darwin-x64-baseline": "1.1.26", + "@oven/bun-linux-aarch64": "1.1.26", + "@oven/bun-linux-x64": "1.1.26", + "@oven/bun-linux-x64-baseline": "1.1.26", + "@oven/bun-windows-x64": "1.1.26", + "@oven/bun-windows-x64-baseline": "1.1.26" } }, "node_modules/bundle-name": { @@ -6260,6 +6315,22 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint/node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -6278,6 +6349,36 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -6830,22 +6931,6 @@ "node": ">=8" } }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/find-versions": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-5.1.0.tgz", @@ -10818,21 +10903,6 @@ "node": ">=6.11.5" } }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -12208,21 +12278,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", diff --git a/swaggerReadme.md b/swaggerReadme.md new file mode 100644 index 00000000..826304bb --- /dev/null +++ b/swaggerReadme.md @@ -0,0 +1,46 @@ +## Swagger Command Overview +The `stencil add swagger` command helps developers to automatically generate Swagger decorators for controllers and DTOs. This ensures that the generated APIs are well-documented and aligned with the Swagger/OpenAPI specification. It can be used to initialize Swagger for a project or update existing files with the necessary decorators. + +## Swagger Command + +``` +stencil add [modelName...] [options] + +stencil as [modelName...] [options] +``` + +Example: `stencil add swagger modelName` or `stencil as swagger modelName --init` + +**Description** + +The Swagger subcommand automates the process of adding Swagger decorators to existing controllers and DTOs. It initializes Swagger setup for the project and adds necessary `@nestjs/swagger` imports and decorators like `ApiProperty`, `ApiPropertyOptional`, `ApiOperation`, `ApiResponse`, etc., for DTOs and controllers. + + +**Arguments** + +| Argument | Description | +|-----------|--------------| +| `[modelName...]` | The model name for which Swagger decorators need to be added + | + +**Inputs** + +| Name | Description | +|---|---| +| `modelName` | Adds Swagger decorators for the specified model's controller and DTO files + | +| `*` | Adds Swagger decorators for all models present in the schema.prisma + | + +**Options** + +| Option | Description | +|---|---| +| `--init` | Initializes Swagger in the project (adds configuration to main.ts) + | + +**Supported Swagger Decorators:** + +For Controllers: `ApiOperation`, `ApiResponse`, `ApiParam`, `ApiBody`, `ApiTags` + +For DTOs: `ApiProperty`, `ApiPropertyOptional` From 6342c71638cbbc3d917030b9ef73a5baf709c0f7 Mon Sep 17 00:00:00 2001 From: Savio Dias Date: Tue, 10 Sep 2024 18:24:28 +0530 Subject: [PATCH 11/12] fix: updated readme --- swaggerReadme.md | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/swaggerReadme.md b/swaggerReadme.md index 826304bb..d1ded8f3 100644 --- a/swaggerReadme.md +++ b/swaggerReadme.md @@ -4,12 +4,12 @@ The `stencil add swagger` command helps developers to automatically generate Swa ## Swagger Command ``` -stencil add [modelName...] [options] +stencil add [modelPath] [options] -stencil as [modelName...] [options] +stencil as [modelPath] [options] ``` -Example: `stencil add swagger modelName` or `stencil as swagger modelName --init` +Example: `stencil add swagger modelPath` or `stencil as swagger modelPath --init` **Description** @@ -20,17 +20,9 @@ The Swagger subcommand automates the process of adding Swagger decorators to exi | Argument | Description | |-----------|--------------| -| `[modelName...]` | The model name for which Swagger decorators need to be added +| `[modelPath]` | The model path for which Swagger decorators need to be added | -**Inputs** - -| Name | Description | -|---|---| -| `modelName` | Adds Swagger decorators for the specified model's controller and DTO files - | -| `*` | Adds Swagger decorators for all models present in the schema.prisma - | **Options** From b5d2fae4bd8119bd8fb894f7c2cd3383247946aa Mon Sep 17 00:00:00 2001 From: Savio Dias Date: Wed, 11 Sep 2024 11:06:16 +0530 Subject: [PATCH 12/12] fix: updated e2e test and crud action --- actions/crud.action.ts | 6 +- test/e2e-test/crud.e2e-spec.ts | 149 ++++++++++++++++++++++++++------- 2 files changed, 122 insertions(+), 33 deletions(-) diff --git a/actions/crud.action.ts b/actions/crud.action.ts index c0866a7a..d452dfbb 100644 --- a/actions/crud.action.ts +++ b/actions/crud.action.ts @@ -21,8 +21,6 @@ export class CrudAction extends AbstractAction { private async generateCrud(inputs: Input[]): Promise { try { - console.info(chalk.green('Generating CRUD API')); - const dmmf = await this.generateDMMFJSON(); if (dmmf) { const existingModels = dmmf.datamodel.models.map((model: any) => model.name); @@ -33,7 +31,7 @@ export class CrudAction extends AbstractAction { console.error(chalk.red('The following models do not exist:'), invalidInputs.join(', ')); return; } - + console.info(chalk.green('Generating CRUD API')); const modelsToGenerate = inputModelNames.includes('*') ? existingModels : inputModelNames; this.generateTypes(dmmf, modelsToGenerate); @@ -52,7 +50,7 @@ export class CrudAction extends AbstractAction { fs.writeFileSync('./dmmf.json', JSON.stringify(dmmf, null, 2)); return dmmf; } catch (error) { - console.error(chalk.red('Error generating DMMF JSON'), error); + console.error(chalk.red('Error generating DMMF JSON')); return null; } } diff --git a/test/e2e-test/crud.e2e-spec.ts b/test/e2e-test/crud.e2e-spec.ts index 401ab827..8ef3f588 100644 --- a/test/e2e-test/crud.e2e-spec.ts +++ b/test/e2e-test/crud.e2e-spec.ts @@ -2,8 +2,7 @@ import { exec } from 'child_process'; import { join } from 'path'; import { existsSync, readFileSync, writeFileSync, rmSync, rm } from 'fs'; -describe('Stencil CLI e2e Test - CRUD command', () => { -// const testDir = join(__dirname, 'test-project'); +describe('Stencil CLI e2e Test - CRUD & Swagger commands', () => { const schemaFilePath = join('schema.prisma'); const schemaFileContent = ` model Book { @@ -25,57 +24,149 @@ describe('Stencil CLI e2e Test - CRUD command', () => { } `; - beforeAll(() => { - writeFileSync(schemaFilePath, schemaFileContent); - }); - - afterAll(() => { - rmSync(schemaFilePath); - ['book', 'book1', 'car'].forEach((model) => { - const modelDir = join('src', model); - if (existsSync(modelDir)) { - rmSync(modelDir, { recursive: true, force: true }); - } - rmSync('src', { recursive: true, force: true }); - rmSync('dmmf.json', { recursive: true, force: true }); - + beforeAll((done) => { + exec('stencil new test-project --prisma no --user-service no --monitoring no --monitoringService no --temporal no --logging no --fileUpload no --package-manager npm', (newError, newStdout, newStderr) => { + expect(newError).toBeNull(); + process.chdir('test-project'); + + writeFileSync(schemaFilePath, schemaFileContent); + done(); }); + },60000); + + afterAll(() => { + process.chdir('..'); + rmSync('test-project', { recursive: true, force: true }); }); + it('should log an error when no model is provided', (done) => { - exec('npx stencil crud', (error, stdout, stderr) => { + exec('stencil crud', (error, stdout, stderr) => { expect(stderr).toContain('No model provided. Please specify a model or use "*" to generate all models.'); done(); }); }); - it('should generate files for the Book model', (done) => { - exec('npx stencil crud Book', (error, stdout, stderr) => { + it('should log an error for an empty or missing Prisma schema file', (done) => { + rmSync(schemaFilePath); + + exec('stencil crud Book', (error, stdout, stderr) => { + expect(stderr).toContain('Error generating DMMF JSON'); + writeFileSync(schemaFilePath, schemaFileContent); + done(); + }); + }); + + it('should generate files for a single model', (done) => { + exec('stencil crud Book', (error, stdout, stderr) => { expect(error).toBeNull(); const modelDir = join('src', 'book'); expect(existsSync(modelDir)).toBeTruthy(); - expect(existsSync(join(modelDir, 'book.controller.ts'))).toBeTruthy(); - expect(existsSync(join(modelDir, 'book.service.ts'))).toBeTruthy(); - expect(existsSync(join(modelDir, 'book.interface.ts'))).toBeTruthy(); - expect(existsSync(join(modelDir, 'dto', 'book.dto.ts'))).toBeTruthy(); + expect( + [`book.controller.ts`, `book.service.ts`, `book.interface.ts`, `dto/book.dto.ts`] + .map(file => existsSync(join(modelDir, file))) + .every(exists => exists) + ).toBeTruthy(); + }); done(); + }); + + it('should generate files for non-existing model', (done) => { + exec('stencil crud Random', (error, stdout, stderr) => { + expect(stderr).toContain('The following models do not exist: Random'); done(); }); }); + it('should generate files for all models', (done) => { - exec('npx stencil crud *', (error, stdout, stderr) => { + exec('stencil crud *', (error, stdout, stderr) => { expect(error).toBeNull(); ['book', 'book1', 'car'].forEach((model) => { const modelDir = join('src', model); expect(existsSync(modelDir)).toBeTruthy(); - expect(existsSync(join(modelDir, `${model}.controller.ts`))).toBeTruthy(); - expect(existsSync(join(modelDir, `${model}.service.ts`))).toBeTruthy(); - expect(existsSync(join(modelDir, `${model}.interface.ts`))).toBeTruthy(); - expect(existsSync(join(modelDir, 'dto', `${model}.dto.ts`))).toBeTruthy(); - }); + expect( + [`${model}.controller.ts`, `${model}.service.ts`, `${model}.interface.ts`, `dto/${model}.dto.ts`] + .map(file => existsSync(join(modelDir, file))) + .every(exists => exists) + ).toBeTruthy(); + }); done(); }); }); + it('should log error for wrong/missing model while adding Swagger decorators', (done) => { + exec('stencil crud Book', (crudError, crudStdout, crudStderr) => { + expect(crudError).toBeNull(); + exec('stencil add swagger src/random', (swaggerError, swaggerStdout, swaggerStderr) => { + expect(swaggerStderr).toContain('Controller file not found in path: src/random'); + done(); + }); + }); + }); + + it('should add Swagger decorators for the Book model', (done) => { + exec('stencil crud Book', (crudError, crudStdout, crudStderr) => { + expect(crudError).toBeNull(); + const modelDir = join('src', 'book'); + expect(existsSync(modelDir)).toBeTruthy(); + exec('stencil add swagger src/book', (swaggerError, swaggerStdout, swaggerStderr) => { + expect(swaggerError).toBeNull(); + + const controllerPath = join('src', 'book', 'book.controller.ts'); + const controllerContent = readFileSync(controllerPath, 'utf-8'); + + expect(existsSync(controllerPath)).toBeTruthy(); + + const controllerDecorators = ['@ApiOperation', '@ApiResponse', '@ApiParam', '@ApiBody', '@ApiTags']; + controllerDecorators.forEach(decorator => { + expect(controllerContent).toContain(decorator); + }); + + const dtoPath = join('src', 'book', 'dto', 'book.dto.ts'); + const dtoContent = readFileSync(dtoPath, 'utf-8'); + + expect(existsSync(dtoPath)).toBeTruthy(); + + const dtoDecorators = ['@ApiProperty', '@ApiPropertyOptional']; + dtoDecorators.forEach(decorator => { + expect(dtoContent).toContain(decorator); + }); + + done(); + }); + }); + }); + + + it('should log error for missing dto while adding Swagger decorators', (done) => { + exec('stencil crud Book', (crudError, crudStdout, crudStderr) => { + rmSync(join('src', 'book', 'dto', 'book.dto.ts')); + expect(crudError).toBeNull(); + + exec('stencil add swagger src/book', (swaggerError, swaggerStdout, swaggerStderr) => { + expect(swaggerStderr).toContain('DTO file not found for model: src/book'); + done(); + }); + }); + }); + + + it('should check Swagger initialization in main.ts', (done) => { + exec('stencil crud Book', (crudError, crudStdout, crudStderr) => { + expect(crudError).toBeNull(); + const modelDir = join('src', 'book'); + expect(existsSync(modelDir)).toBeTruthy(); + exec('stencil add swagger src/book --init', (swaggerError, swaggerStdout, swaggerStderr) => { + expect(swaggerError).toBeNull(); + + const mainTsPath = join('src', 'main.ts'); + const mainTsContent = readFileSync(mainTsPath, 'utf-8'); + + expect(mainTsContent).toContain("SwaggerModule.setup('api', app, document);"); + + done(); + }); + }); + }); });