Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[93] feat: New command to generate crud api #38

Open
wants to merge 12 commits into
base: dev
Choose a base branch
from
29 changes: 29 additions & 0 deletions actions/add-utils/file-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { existsSync } from 'fs';
import { join, basename } from 'path';

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 !== '/') {
if (existsSync(join(dir, 'package.json'))) {
return dir;
}
dir = join(dir, '..');
}
throw new Error('Project root not found');
}
32 changes: 32 additions & 0 deletions actions/add-utils/import-manager.ts
Original file line number Diff line number Diff line change
@@ -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 {
const lastImportIndex = content.lastIndexOf('import ');
const insertPosition = content.indexOf(';', lastImportIndex) + 1;
content = `${content.slice(0, insertPosition)}\n${newImportStatement}${content.slice(insertPosition)}`;
}
}

return content;
}
117 changes: 117 additions & 0 deletions actions/add-utils/swagger-controller.ts
Original file line number Diff line number Diff line change
@@ -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<string> = 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';
}
36 changes: 36 additions & 0 deletions actions/add-utils/swagger-dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as fs from 'fs';
import * as path from 'path';
import { addImports } from './import-manager';
import * as chalk from 'chalk';

export function addSwaggerDto(dtoFilePath: string) {

const dtoContent = fs.readFileSync(dtoFilePath, 'utf-8');

let updatedContent = addSwaggerDecorators(dtoContent);
updatedContent = addImports(updatedContent, 'dto');

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 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');
};
44 changes: 44 additions & 0 deletions actions/add-utils/swagger-init.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading