cl-generate
Version:
A cross-platform CLI tool to generate NestJS clean architecture modules
686 lines (596 loc) • 18 kB
JavaScript
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';
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';
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';
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';
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';
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';
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> {
isArray?: boolean;
path?: string;
duration?: string;
method?: string;
data: T;
}
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';
export class LoggerModule {}
`;
}
getLoggerServiceContent() {
return `
/* eslint-disable prettier/prettier */
import { Injectable, Logger } from '@nestjs/common';
import { LoggerInterface } from '@domain/logger/logger.interface';
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();
}