UNPKG

clean-gen

Version:

A cross-platform CLI tool to generate NestJS clean architecture modules

567 lines (506 loc) 15.4 kB
#!/usr/bin/env node const fs = require("fs").promises; const path = require("path"); // Colors for output (ANSI escape codes) const COLORS = { GREEN: "\x1b[32m", YELLOW: "\x1b[33m", RED: "\x1b[31m", NC: "\x1b[0m", }; // Configuration const CONFIG = { MODULE_NAME: process.argv[2] || "myapp", ROOT_DIR: "src", }; CONFIG.MODULE_NAME_CAPITALIZED = CONFIG.MODULE_NAME.toUpperCase(); class ProjectSetup { async createDir(dirPath) { try { await fs.mkdir(dirPath, { recursive: true }); console.log(`${COLORS.GREEN}Created directory:${COLORS.NC} ${dirPath}`); } catch (error) { if (error.code === "EEXIST") { console.log( `${COLORS.YELLOW}Directory already exists:${COLORS.NC} ${dirPath}` ); } else { throw new Error( `Failed to create directory: ${dirPath} - ${error.message}` ); } } } async createFile(filePath, content) { try { if (!(await fs.stat(filePath).catch(() => false))) { await fs.writeFile(filePath, content.trim(), "utf8"); console.log(`${COLORS.GREEN}Created file:${COLORS.NC} ${filePath}`); } else { console.log( `${COLORS.YELLOW}File already exists:${COLORS.NC} ${filePath}` ); } } catch (error) { throw new Error(`Failed to create file: ${filePath} - ${error.message}`); } } async setupDomain() { const DOMAIN_DIR = path.join(CONFIG.ROOT_DIR, "domain"); const directories = ["config", "dtos", "logger", "repositories", "models"]; for (const dir of directories) { await this.createDir(path.join(DOMAIN_DIR, dir)); } await this.createDir(path.join(CONFIG.ROOT_DIR, "shared")); await this.createDir(path.join(CONFIG.ROOT_DIR, "shared", "utils")); await this.createFile( path.join(DOMAIN_DIR, "config", "databaseConfig.interface.ts"), `export interface DatabaseConfigInterface { getDBHost(): string; getDBName(): string; getDBPort(): number; getDBUser(): string; getDBPass(): string; }` ); await this.createFile( path.join(DOMAIN_DIR, "logger", "logger.interface.ts"), `export interface LoggerInterface { debug(context: string, message: string): void; log(context: string, message: string): void; error(context: string, message: string, trace?: string): void; warn(context: string, message: string): void; verbose(context: string, message: string): void; }` ); } async setupInfrastructure() { const INFRASTRUCTURE_DIR = path.join(CONFIG.ROOT_DIR, "infrastructure"); const mainDirs = [ "common", "config", "controllers", "entities", "logger", "repositories", "usecases-proxy", ]; const commonSubDirs = ["filter", "interceptors", "swagger"]; const configSubDirs = ["environment-config", "typeorm"]; for (const dir of mainDirs) { await this.createDir(path.join(INFRASTRUCTURE_DIR, dir)); } for (const dir of commonSubDirs) { await this.createDir(path.join(INFRASTRUCTURE_DIR, "common", dir)); } for (const dir of configSubDirs) { await this.createDir(path.join(INFRASTRUCTURE_DIR, "config", dir)); } const files = { "common/filter/exception.filter.ts": this.getExceptionFilterContent(), "common/interceptors/logger.interceptor.ts": this.getLoggerInterceptorContent(), "common/interceptors/response.interceptor.ts": this.getResponseInterceptorContent(), "common/swagger/response.decorator.ts": this.getResponseDecoratorContent(), "config/environment-config/environment-config.module.ts": this.getEnvConfigModuleContent(), "config/environment-config/environment-config.service.ts": this.getEnvConfigServiceContent(), "config/typeorm/typeorm.module.ts": this.getTypeOrmModuleContent(), "logger/logger.module.ts": this.getLoggerModuleContent(), "logger/logger.service.ts": this.getLoggerServiceContent(), }; for (const [filePath, content] of Object.entries(files)) { await this.createFile(path.join(INFRASTRUCTURE_DIR, filePath), content); } } async setupRoot() { await this.createFile( path.join(CONFIG.ROOT_DIR, "app.module.ts"), this.getAppModuleContent() ); await this.createFile( path.join(CONFIG.ROOT_DIR, "shared", "utils", "env.util.ts"), this.getEnvUtilContent() ); await this.createDir(path.join(CONFIG.ROOT_DIR, "usecases")); } async execute() { try { console.log(`${COLORS.YELLOW}Starting project setup...${COLORS.NC}`); await this.setupDomain(); await this.setupInfrastructure(); await this.setupRoot(); console.log( `${COLORS.GREEN}Project setup completed successfully!${COLORS.NC}` ); } catch (error) { console.error( `${COLORS.RED}Error during setup:${COLORS.NC} ${error.message}` ); process.exit(1); } } getAppModuleContent() { return ` import { Module } from '@nestjs/common'; import { TypeOrmConfigModule } from './infrastructure/config/typeorm/typeorm.module'; import { LoggerModule } from './infrastructure/logger/logger.module'; import { UsecasesProxyModule } from './infrastructure/usecases-proxy/usecases-proxy.module'; import { ControllersModule } from './infrastructure/controllers/controllers.module'; import { EnvironmentConfigModule } from './infrastructure/config/environment-config/environment-config.module'; @Module({ imports: [ TypeOrmConfigModule, LoggerModule, UsecasesProxyModule.register(), ControllersModule, EnvironmentConfigModule, ], providers: [], }) export class AppModule {} `; } getEnvUtilContent() { return ` import * as dotenv from 'dotenv'; dotenv.config(); export const ISSUER_WALLET_OWNERID = process.env.ISSUER_WALLET_OWNERID; export const ISSUER_WALLET_OWNERID_CATEGORY = process.env.ISSUER_WALLET_OWNERID_CATEGORY; export const ENCRYPT_TEXT_KEY = process.env.ENCRYPT_TEXT_KEY; export const PROTO_WALLET_SVC_SYNC = process.env.PROTO_WALLET_SVC_SYNC; `; } getExceptionFilterContent() { return ` interface IError { message: string; code_error: string; } import { Catch, ArgumentsHost, ExceptionFilter, HttpException, } from '@nestjs/common'; import { status } from '@grpc/grpc-js'; import { LoggerService } from 'src/infrastructure/logger/logger.service'; import { FastifyRequest } from 'fastify'; @Catch() export class GrpcExceptionFilter implements ExceptionFilter { constructor(private readonly logger: LoggerService) {} catch(exception: any, host: ArgumentsHost) { console.log(\`error der\`); const ctx = host.switchToRpc(); const call = ctx.getContext(); // gRPC call context const metadata = ctx.getData(); // Metadata from the gRPC call console.log(\`Filter call\`, call); console.log(\`Filter metadata\`, metadata); if (exception instanceof HttpException) { const errorResponse = exception.getResponse(); let message: string = errorResponse['message']; if ( errorResponse['message'] != undefined && typeof errorResponse['message'] === 'object' ) { message = errorResponse['message'].find((item) => item); } const error = { code: status.INVALID_ARGUMENT, message: message || 'Invalid argument', details: errorResponse, }; throw error; } const error = { code: status.INVALID_ARGUMENT, message: exception?.message || 'Internal server error', details: exception, }; throw error; } private logMessage( request: FastifyRequest, message: IError, status: number, exception: any, ) { if (status === 500) { this.logger.error( \`End Request for \${request.url}\`, \`method=\${request.method} status=\${status} code_error=\${ message.code_error ? message.code_error : null } message=\${message.message ? message.message : null}\`, status >= 500 ? exception.stack : '', ); } else { this.logger.warn( \`End Request for \${request.url}\`, \`method=\${request.method} status=\${status} code_error=\${ message.code_error ? message.code_error : null } message=\${message.message ? message.message : null}\`, ); } } } `; } getLoggerInterceptorContent() { return ` import { CallHandler, ExecutionContext, Injectable, NestInterceptor, } from '@nestjs/common'; import { FastifyRequest } from 'fastify'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; import { LoggerService } from '../../logger/logger.service'; @Injectable() export class LoggingInterceptor implements NestInterceptor { constructor(private readonly logger: LoggerService) {} intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const now = Date.now(); const httpContext = context.switchToHttp(); const request = httpContext.getRequest<FastifyRequest>(); const ip = this.getIP(request); this.logger.log( \`Incoming Request on \${request.url}\`, \`method=\${request.method} ip=\${ip}\`, ); return next.handle().pipe( tap(() => { this.logger.log( \`End Request for \${request.url}\`, \`method=\${request.method} ip=\${ip} duration=\${Date.now() - now}ms\`, ); }), ); } private getIP(request: FastifyRequest): string { let ip: string; const ipAddr = request?.headers?.['x-forwarded-for']; if (ipAddr) { ip = ipAddr[ipAddr.length - 1]; } else { ip = request?.socket?.remoteAddress; } return ip?.replace('::ffff:', ''); } } `; } getResponseInterceptorContent() { return ` import { Injectable, NestInterceptor, ExecutionContext, CallHandler, } from '@nestjs/common'; import { FastifyRequest } from 'fastify'; import { ApiProperty } from '@nestjs/swagger'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; export class ResponseFormat<T> { @ApiProperty() isArray?: boolean; @ApiProperty() path?: string; @ApiProperty() duration?: string; @ApiProperty() method?: string; data: T; } @Injectable() export class ResponseInterceptor<T> implements NestInterceptor<T, ResponseFormat<T>> { intercept( context: ExecutionContext, next: CallHandler, ): Observable<ResponseFormat<T>> { const now = Date.now(); const isGrpc = context.getType() === 'rpc'; if (isGrpc) { return next.handle().pipe( map((data) => { return data; }), ); } else { const httpContext = context.switchToHttp(); const request = httpContext.getRequest<FastifyRequest>(); return next.handle().pipe( map((data) => { return { data, isArray: Array.isArray(data), path: request.url, duration: \`\${Date.now() - now}ms\`, method: request.method, }; }), ); } } } `; } getResponseDecoratorContent() { return ` import { applyDecorators, Type } from '@nestjs/common'; import { ApiOkResponse, getSchemaPath } from '@nestjs/swagger'; import { ResponseFormat } from '../../common/interceptors/response.interceptor'; export const ApiResponseType = <TModel extends Type<any>>( model: TModel, isArray: boolean, ) => { return applyDecorators( ApiOkResponse({ isArray: isArray, schema: { allOf: [ { $ref: getSchemaPath(ResponseFormat) }, { properties: { data: { $ref: getSchemaPath(model), }, isArray: { type: 'boolean', default: isArray, }, }, }, ], }, }), ); }; `; } getEnvConfigModuleContent() { return ` import { Module } from '@nestjs/common'; import { EnvironmentConfigService } from './environment-config.service'; import { ConfigService } from '@nestjs/config'; @Module({ providers: [EnvironmentConfigService, ConfigService], exports: [EnvironmentConfigService], }) export class EnvironmentConfigModule {} `; } getEnvConfigServiceContent() { return ` import { Injectable } from '@nestjs/common'; import { DatabaseConfigInterface } from 'src/domain/config/databaseConfig.interface'; import { ConfigService } from '@nestjs/config'; import * as dotenv from 'dotenv'; dotenv.config(); @Injectable() export class EnvironmentConfigService implements DatabaseConfigInterface { constructor(private readonly configService: ConfigService) {} getDBHost(): string { return this.configService.get<string>('DATABASE_HOST'); } getDBName(): string { return this.configService.get<string>('DATABASE_DB'); } getDBPort(): number { return this.configService.get<number>('DATABASE_PORT'); } getDBUser(): string { return this.configService.get<string>('DATABASE_USER'); } getDBPass(): string { return this.configService.get<string>('DATABASE_PASSWORD'); } } `; } getTypeOrmModuleContent() { return ` import { Module } from '@nestjs/common'; import { EnvironmentConfigService } from '../environment-config/environment-config.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { EnvironmentConfigModule } from '../environment-config/environment-config.module'; import * as dotenv from 'dotenv'; dotenv.config(); export const getTypeOrmModuleOptions = ( config: EnvironmentConfigService, ): any => ({ type: 'postgres', host: config.getDBHost(), port: config.getDBPort(), username: config.getDBUser(), password: config.getDBPass(), database: config.getDBName(), entities: [], synchronize: true, }); @Module({ imports: [ TypeOrmModule.forRootAsync({ imports: [EnvironmentConfigModule], inject: [EnvironmentConfigService], useFactory: getTypeOrmModuleOptions, }), ], }) export class TypeOrmConfigModule {} `; } getLoggerModuleContent() { return ` import { Module } from '@nestjs/common'; import { LoggerService } from './logger.service'; @Module({ providers: [LoggerService], exports: [LoggerService], }) export class LoggerModule {} `; } getLoggerServiceContent() { return ` import { Injectable, Logger } from '@nestjs/common'; import { LoggerInterface } from 'src/domain/logger/logger.interface'; @Injectable() export class LoggerService extends Logger implements LoggerInterface { debug(context: string, message: string) { if (process.env.NODE_ENV !== 'production') { super.debug(\`[DEBUG] \${message}\`, context); } } log(context: string, message: string) { super.log(\`[INFO] \${message}\`, context); } error(context: string, message: string, trace?: string) { super.error(\`[ERROR] \${message}\`, trace, context); } warn(context: string, message: string) { super.warn(\`[WARN] \${message}\`, context); } verbose(context: string, message: string) { if (process.env.NODE_ENV !== 'production') { super.verbose(\`[VERBOSE] \${message}\`, context); } } } `; } } module.exports = async function setup() { const projectSetup = new ProjectSetup(); await projectSetup.execute(); }; if (require.main === module) { module.exports(); }