UNPKG

cl-generate

Version:

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

686 lines (596 loc) 18 kB
#!/usr/bin/env node const fs = require("fs").promises; const path = require("path"); const createFile = require("../shares/createFile"); const createDir = require("../shares/createDir"); const { COLORS, CONFIG, INFRASTRUCTURE_DIR, DOMAIN_DIR, } = require("../constants/index"); CONFIG.MODULE_NAME_CAPITALIZED = CONFIG.MODULE_NAME.toUpperCase(); class ProjectSetup { async setupDomain() { const directories = ["config", "dtos", "logger", "repositories", "models"]; for (const dir of directories) { await createDir(path.join(DOMAIN_DIR, dir)); } await createDir(path.join(CONFIG.ROOT_DIR, "shared")); await createDir(path.join(CONFIG.ROOT_DIR, "_proto")); await createDir(path.join(CONFIG.ROOT_DIR, "shared", "utils")); await 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 mainDirs = [ "common", "config", "controllers", "entities", "logger", "repositories", "usecases-proxy", ]; const commonSubDirs = ["filter", "interceptors", "swagger"]; for (const dir of mainDirs) { await createDir(path.join(INFRASTRUCTURE_DIR, dir)); } for (const dir of commonSubDirs) { await createDir(path.join(INFRASTRUCTURE_DIR, "common", 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(), "logger/logger.module.ts": this.getLoggerModuleContent(), "logger/logger.service.ts": this.getLoggerServiceContent(), "usecases-proxy/usecases-proxy.module.ts": this.getUsecasesProxyModuleContent(), "usecases-proxy/usecases-proxy.ts": this.getUsecasesProxyContent(), "controllers/controllers.module.ts": this.getControllersModuleContent(), "repositories/repositories.module.ts": this.getRepositoriesModuleContent(), }; for (const [filePath, content] of Object.entries(files)) { await createFile(path.join(INFRASTRUCTURE_DIR, filePath), content); } } async setupRoot() { await createFile(path.join(".env"), this.getEnvContent()); await createFile( path.join(CONFIG.ROOT_DIR, "_proto", "common.proto"), this.getProtoCont() ); await createFile( path.join(CONFIG.ROOT_DIR, "app.module.ts"), this.getAppModuleContent() ); await createFile( path.join(CONFIG.ROOT_DIR, "main.ts"), this.getMaiContent() ); await createFile( path.join(CONFIG.ROOT_DIR, "shared", "utils", "env.util.ts"), this.getEnvUtilContent() ); await 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(); await this.updateTsConfig(); await this.updateNestCliConfig(); 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); } } getEnvContent() { return ` # TypeORM POSTGRES_HOST=walletservice-db POSTGRES_PORT=5432 POSTGRES_USER=<USER> POSTGRES_PASSWORD=<PASSWORD> POSTGRES_DATABASE=<DB_NAME> MODE=DEV RUN_MIGRATIONS=true # Database DATABASE_HOST=localhost DATABASE_DB=<DB_NAME> DATABASE_USER=<USER> DATABASE_PASSWORD=<PASSWORD> DATABASE_PORT=5220 # Redis REDIS_HOST=localhost REDIS_PORT=6220 # Server SERVER_PORT=20220 ENCRYPT_TEXT_KEY=jsje3j3,02.3j2jk # ISSUER ISSUER_WALLET_OWNERID=5ada082c-71d9-40ca-a2a9-359c35b66803 PROTO_WALLET_SVC_SYNC=false PROTO_URL=`; } getProtoCont() { return ` syntax = "proto3"; package common; message QueryString { string queryString = 1; string id = 2; } message Id { string id = 1; } message UserName { string userName = 1; } message Empty {} message PageInfo { string startCursor = 1; string endCursor = 2; bool hasNextPage = 3; bool hasPreviousPage = 4; } `; } getAppModuleContent() { return ` /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-call */ 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 {} `; } getMaiContent() { return ` /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-call */ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { MicroserviceOptions, Transport } from '@nestjs/microservices'; import { join } from 'path'; import * as dotenv from "dotenv"; dotenv.config(); async function bootstrap() { const port = process.env.SERVER_PORT; const app = await NestFactory.create(AppModule); app.connectMicroservice<MicroserviceOptions>({ transport: Transport.GRPC, options: { package: [], protoPath: [ join(__dirname, './_proto/common.proto'), ], url: '0.0.0.0:3000', }, }); await app.startAllMicroservices(); } bootstrap(); `; } getUsecasesProxyModuleContent() { return ` /* eslint-disable prettier/prettier */ import { DynamicModule, Module } from '@nestjs/common'; import { LoggerModule } from '../logger/logger.module'; import { EnvironmentConfigModule } from '../config/environment-config/environment-config.module'; import { RepositoriesModule } from '../repositories/repositories.module'; @Module({ imports: [LoggerModule, EnvironmentConfigModule, RepositoriesModule], }) export class UsecasesProxyModule { static register(): DynamicModule { return { module: UsecasesProxyModule, providers: [], exports: [], }; } } `; } getUsecasesProxyContent() { return ` export class UseCaseProxy<T> { constructor(private readonly useCase: T) {} getInstance(): T { return this.useCase; } } `; } getControllersModuleContent() { return ` /* eslint-disable prettier/prettier */ import { Module } from '@nestjs/common'; import { UsecasesProxyModule } from '../usecases-proxy/usecases-proxy.module'; @Module({ imports: [UsecasesProxyModule.register()], controllers: [], }) export class ControllersModule {}; `; } getRepositoriesModuleContent() { return ` /* eslint-disable prettier/prettier */ import { Module } from '@nestjs/common'; import { TypeOrmConfigModule } from '../config/typeorm/typeorm.module'; import { TypeOrmModule } from '@nestjs/typeorm'; @Module({ imports: [ TypeOrmConfigModule, TypeOrmModule.forFeature([]), ], providers: [], exports: [], }) export class RepositoriesModule {}\n `; } getEnvUtilContent() { return ` /* eslint-disable prettier/prettier */ 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 ` /* eslint-disable prettier/prettier */ interface IError { message: string; code_error: string; } import { Catch, ArgumentsHost, ExceptionFilter, HttpException, } from '@nestjs/common'; import { status } from '@grpc/grpc-js'; import { LoggerService } from '@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 ` /* eslint-disable prettier/prettier */ 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 ` /* eslint-disable prettier/prettier */ 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 ` /* eslint-disable prettier/prettier */ 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, }, }, }, ], }, }), ); }; `; } getLoggerModuleContent() { return ` /* eslint-disable prettier/prettier */ import { Module } from '@nestjs/common'; import { LoggerService } from './logger.service'; @Module({ providers: [LoggerService], exports: [LoggerService], }) export class LoggerModule {} `; } getLoggerServiceContent() { return ` /* eslint-disable prettier/prettier */ import { Injectable, Logger } from '@nestjs/common'; import { LoggerInterface } from '@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); } } } `; } async updateTsConfig() { const tsConfigPath = path.join(process.cwd(), "tsconfig.json"); try { if (!(await fs.stat(tsConfigPath).catch(() => false))) { throw new Error( `tsconfig.json not found at ${tsConfigPath}. Please create it first.` ); } else { const existingContent = await fs.readFile(tsConfigPath, "utf8"); const existingConfig = JSON.parse(existingContent); const requiredPaths = { "@/*": ["*"], "@domain/*": ["domain/*"], "@usecases/*": ["usecases/*"], "@infrastructure/*": ["infrastructure/*"], "@controllers/*": ["infrastructure/controllers/*"], "@entities/*": ["infrastructure/entities/*"], "@repositories/*": ["infrastructure/repositories/*"], "@common/*": ["common/*"], }; existingConfig.compilerOptions.baseUrl = "src"; existingConfig.compilerOptions.paths = requiredPaths; const updatedContent = JSON.stringify(existingConfig, null, 2); await fs.writeFile(tsConfigPath, updatedContent, "utf8"); console.log(`${COLORS.GREEN}✔ Updated ${tsConfigPath}${COLORS.NC}`); } } catch (error) { throw new Error(`Failed to update tsconfig.json: ${error.message}`); } } async updateNestCliConfig() { const nestCliConfigPath = path.join(process.cwd(), "nest-cli.json"); try { if (!(await fs.stat(nestCliConfigPath).catch(() => false))) { throw new Error( `nest-cli.json not found at ${nestCliConfigPath}. Please create it first.` ); } else { const existingContent = await fs.readFile(nestCliConfigPath, "utf8"); const existingConfig = JSON.parse(existingContent); existingConfig.compilerOptions.assets = ["**/*.proto"]; existingConfig.compilerOptions.watchAssets = true; const updatedContent = JSON.stringify(existingConfig, null, 2); await fs.writeFile(nestCliConfigPath, updatedContent, "utf8"); console.log( `${COLORS.GREEN}✔ Updated ${nestCliConfigPath}${COLORS.NC}` ); } } catch (error) { throw new Error(`Failed to update nest-cli.json: ${error.message}`); } } } module.exports = async function setup() { const projectSetup = new ProjectSetup(); await projectSetup.execute(); }; if (require.main === module) { module.exports(); }