clean-gen
Version:
A cross-platform CLI tool to generate NestJS clean architecture modules
567 lines (506 loc) • 15.4 kB
JavaScript
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';
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';
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';
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> {
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 `
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';
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();
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,
});
export class TypeOrmConfigModule {}
`;
}
getLoggerModuleContent() {
return `
import { Module } from '@nestjs/common';
import { LoggerService } from './logger.service';
export class LoggerModule {}
`;
}
getLoggerServiceContent() {
return `
import { Injectable, Logger } from '@nestjs/common';
import { LoggerInterface } from 'src/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);
}
}
}
`;
}
}
module.exports = async function setup() {
const projectSetup = new ProjectSetup();
await projectSetup.execute();
};
if (require.main === module) {
module.exports();
}