archforge-x 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +156 -56
- package/dist/cli/commands/sync.js +22 -0
- package/dist/cli/interactive.js +41 -3
- package/dist/core/architecture/schema.js +23 -2
- package/dist/core/architecture/validator.js +112 -0
- package/dist/generators/base.js +166 -0
- package/dist/generators/generator.js +35 -332
- package/dist/generators/go/gin.js +327 -0
- package/dist/generators/node/express.js +920 -0
- package/dist/generators/node/nestjs.js +770 -0
- package/dist/generators/node/nextjs.js +252 -0
- package/dist/generators/python/django.js +327 -0
- package/dist/generators/python/fastapi.js +309 -0
- package/dist/generators/registry.js +25 -0
- package/dist/index.js +29 -15
- package/package.json +3 -1
- package/dist/cli/init.js +0 -74
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NestJSGenerator = void 0;
|
|
4
|
+
const base_1 = require("../base");
|
|
5
|
+
class NestJSGenerator extends base_1.BaseGenerator {
|
|
6
|
+
getGeneratorName() {
|
|
7
|
+
return "NestJS";
|
|
8
|
+
}
|
|
9
|
+
async generateProjectStructure(root, arch, options) {
|
|
10
|
+
const archStyle = options.architecture || "clean";
|
|
11
|
+
this.generateCommonFiles(root, options);
|
|
12
|
+
if (archStyle === "clean") {
|
|
13
|
+
this.generateCleanArchitecture(root, options);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
this.generateStandardArchitecture(root, options);
|
|
17
|
+
}
|
|
18
|
+
this.generateDocker(root, options);
|
|
19
|
+
this.generateCI(root, options);
|
|
20
|
+
}
|
|
21
|
+
generateCommonFiles(root, options) {
|
|
22
|
+
const projectName = options.projectName || "nestjs-app";
|
|
23
|
+
this.writeFile(root, "package.json", JSON.stringify({
|
|
24
|
+
name: projectName,
|
|
25
|
+
version: "0.0.1",
|
|
26
|
+
description: "",
|
|
27
|
+
author: "",
|
|
28
|
+
private: true,
|
|
29
|
+
license: "UNLICENSED",
|
|
30
|
+
scripts: {
|
|
31
|
+
"build": "nest build",
|
|
32
|
+
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
33
|
+
"start": "nest start",
|
|
34
|
+
"start:dev": "nest start --watch",
|
|
35
|
+
"start:debug": "nest start --debug --watch",
|
|
36
|
+
"start:prod": "node dist/main",
|
|
37
|
+
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
|
38
|
+
"test": "jest",
|
|
39
|
+
"test:watch": "jest --watch",
|
|
40
|
+
"test:cov": "jest --coverage",
|
|
41
|
+
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
|
42
|
+
"test:e2e": "jest --config ./test/jest-e2e.json"
|
|
43
|
+
},
|
|
44
|
+
dependencies: {
|
|
45
|
+
"@nestjs/common": "^10.0.0",
|
|
46
|
+
"@nestjs/core": "^10.0.0",
|
|
47
|
+
"@nestjs/platform-express": "^10.0.0",
|
|
48
|
+
"@nestjs/config": "^3.0.0",
|
|
49
|
+
"@nestjs/terminus": "^10.0.0",
|
|
50
|
+
"reflect-metadata": "^0.1.13",
|
|
51
|
+
"rxjs": "^7.8.1",
|
|
52
|
+
"nestjs-pino": "^3.3.0",
|
|
53
|
+
"pino-http": "^8.3.3",
|
|
54
|
+
"zod": "^3.22.2",
|
|
55
|
+
"ioredis": "^5.3.2",
|
|
56
|
+
"class-validator": "^0.14.3",
|
|
57
|
+
"class-transformer": "^0.5.1",
|
|
58
|
+
...(options.orm === "prisma" ? { "@prisma/client": "^5.0.0" } : {})
|
|
59
|
+
},
|
|
60
|
+
devDependencies: {
|
|
61
|
+
"@nestjs/cli": "^10.0.0",
|
|
62
|
+
"@nestjs/schematics": "^10.0.0",
|
|
63
|
+
"@nestjs/testing": "^10.0.0",
|
|
64
|
+
"@types/express": "^4.17.17",
|
|
65
|
+
"@types/jest": "^29.5.2",
|
|
66
|
+
"@types/node": "^20.3.1",
|
|
67
|
+
"@types/supertest": "^2.0.12",
|
|
68
|
+
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
|
69
|
+
"@typescript-eslint/parser": "^6.0.0",
|
|
70
|
+
"eslint": "^8.42.0",
|
|
71
|
+
"eslint-config-prettier": "^9.0.0",
|
|
72
|
+
"eslint-plugin-prettier": "^5.0.0",
|
|
73
|
+
"eslint-plugin-import": "^2.27.5",
|
|
74
|
+
"jest": "^29.5.0",
|
|
75
|
+
"prettier": "^3.0.0",
|
|
76
|
+
"pino-pretty": "^10.0.0",
|
|
77
|
+
"source-map-support": "^0.5.21",
|
|
78
|
+
"supertest": "^6.3.3",
|
|
79
|
+
"ts-jest": "^29.1.0",
|
|
80
|
+
"ts-loader": "^9.4.3",
|
|
81
|
+
"ts-node": "^10.9.1",
|
|
82
|
+
"tsconfig-paths": "^4.2.0",
|
|
83
|
+
"typescript": "^5.1.3",
|
|
84
|
+
...(options.orm === "prisma" ? { "prisma": "^5.0.0" } : {})
|
|
85
|
+
},
|
|
86
|
+
jest: {
|
|
87
|
+
"moduleFileExtensions": ["js", "json", "ts"],
|
|
88
|
+
"rootDir": "src",
|
|
89
|
+
"testRegex": ".*\\.spec\\.ts$",
|
|
90
|
+
"transform": { "^.+\\.(t|j)s$": "ts-jest" },
|
|
91
|
+
"collectCoverageFrom": ["**/*.(t|j)s"],
|
|
92
|
+
"coverageDirectory": "../coverage",
|
|
93
|
+
"testEnvironment": "node"
|
|
94
|
+
}
|
|
95
|
+
}, null, 2));
|
|
96
|
+
this.writeFile(root, "tsconfig.json", JSON.stringify({
|
|
97
|
+
compilerOptions: {
|
|
98
|
+
module: "commonjs",
|
|
99
|
+
declaration: true,
|
|
100
|
+
removeComments: true,
|
|
101
|
+
emitDecoratorMetadata: true,
|
|
102
|
+
experimentalDecorators: true,
|
|
103
|
+
allowSyntheticDefaultImports: true,
|
|
104
|
+
target: "ES2021",
|
|
105
|
+
sourceMap: true,
|
|
106
|
+
outDir: "./dist",
|
|
107
|
+
baseUrl: "./",
|
|
108
|
+
incremental: true,
|
|
109
|
+
skipLibCheck: true,
|
|
110
|
+
strictNullChecks: false,
|
|
111
|
+
noImplicitAny: false,
|
|
112
|
+
strictBindCallApply: false,
|
|
113
|
+
forceConsistentCasingInFileNames: false,
|
|
114
|
+
noFallthroughCasesInSwitch: false
|
|
115
|
+
}
|
|
116
|
+
}, null, 2));
|
|
117
|
+
this.writeFile(root, "nest-cli.json", JSON.stringify({
|
|
118
|
+
"$schema": "https://json.schemastore.org/nest-cli",
|
|
119
|
+
"collection": "@nestjs/schematics",
|
|
120
|
+
"sourceRoot": "src",
|
|
121
|
+
"compilerOptions": {
|
|
122
|
+
"deleteOutDir": true
|
|
123
|
+
}
|
|
124
|
+
}, null, 2));
|
|
125
|
+
// .eslintrc.json (Architecture Guardrails)
|
|
126
|
+
const archStyle = options.architecture || "clean";
|
|
127
|
+
let restrictedPaths = [];
|
|
128
|
+
if (archStyle === "clean") {
|
|
129
|
+
restrictedPaths = [
|
|
130
|
+
{
|
|
131
|
+
target: "./src/domain",
|
|
132
|
+
from: "./src/application",
|
|
133
|
+
message: "Domain layer cannot import from Application layer"
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
target: "./src/domain",
|
|
137
|
+
from: "./src/infrastructure",
|
|
138
|
+
message: "Domain layer cannot import from Infrastructure layer"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
target: "./src/domain",
|
|
142
|
+
from: "./src/presentation",
|
|
143
|
+
message: "Domain layer cannot import from Presentation layer"
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
target: "./src/application",
|
|
147
|
+
from: "./src/infrastructure",
|
|
148
|
+
message: "Application layer cannot import from Infrastructure layer"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
target: "./src/application",
|
|
152
|
+
from: "./src/presentation",
|
|
153
|
+
message: "Application layer cannot import from Presentation layer"
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
target: "./src/infrastructure",
|
|
157
|
+
from: "./src/presentation",
|
|
158
|
+
message: "Infrastructure layer cannot import from Presentation layer"
|
|
159
|
+
}
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
this.writeFile(root, ".eslintrc.json", JSON.stringify({
|
|
163
|
+
"parser": "@typescript-eslint/parser",
|
|
164
|
+
"parserOptions": {
|
|
165
|
+
"project": "tsconfig.json",
|
|
166
|
+
"tsconfigRootDir": "__dirname",
|
|
167
|
+
"sourceType": "module"
|
|
168
|
+
},
|
|
169
|
+
"plugins": ["@typescript-eslint/eslint-plugin", "import"],
|
|
170
|
+
"extends": [
|
|
171
|
+
"plugin:@typescript-eslint/recommended",
|
|
172
|
+
"plugin:prettier/recommended"
|
|
173
|
+
],
|
|
174
|
+
"root": true,
|
|
175
|
+
"env": {
|
|
176
|
+
"node": true,
|
|
177
|
+
"jest": true
|
|
178
|
+
},
|
|
179
|
+
"ignorePatterns": [".eslintrc.js", ".eslintrc.json"],
|
|
180
|
+
"rules": {
|
|
181
|
+
"@typescript-eslint/interface-name-prefix": "off",
|
|
182
|
+
"@typescript-eslint/explicit-function-return-type": "off",
|
|
183
|
+
"@typescript-eslint/explicit-module-boundary-types": "off",
|
|
184
|
+
"@typescript-eslint/no-explicit-any": "off",
|
|
185
|
+
"import/no-restricted-paths": ["error", {
|
|
186
|
+
"zones": restrictedPaths
|
|
187
|
+
}]
|
|
188
|
+
}
|
|
189
|
+
}, null, 2));
|
|
190
|
+
this.writeFile(root, ".env", `PORT=3000\nNODE_ENV=development\nDATABASE_URL="postgresql://user:password@localhost:5432/appdb?schema=public"\nREDIS_URL="redis://localhost:6379"\n`);
|
|
191
|
+
this.writeFile(root, ".env.example", `PORT=3000\nNODE_ENV=development\nDATABASE_URL="postgresql://user:password@localhost:5432/appdb?schema=public"\nREDIS_URL="redis://localhost:6379"\n`);
|
|
192
|
+
this.writeFile(root, ".gitignore", `node_modules/\ndist/\n.env\n`);
|
|
193
|
+
this.generateConfigModule(root, options);
|
|
194
|
+
this.generateCacheModule(root, options);
|
|
195
|
+
this.generateErrorFilter(root, options);
|
|
196
|
+
this.generateHealthModule(root, options);
|
|
197
|
+
if (options.orm === "prisma") {
|
|
198
|
+
this.generatePrismaSchema(root, options);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
generateHealthModule(root, options) {
|
|
202
|
+
this.writeFile(root, "src/infrastructure/health/health.controller.ts", `
|
|
203
|
+
import { Controller, Get } from '@nestjs/common';
|
|
204
|
+
import { HealthCheckService, HealthCheck, TypeOrmHealthIndicator, MemoryHealthIndicator } from '@nestjs/terminus';
|
|
205
|
+
|
|
206
|
+
@Controller('health')
|
|
207
|
+
export class HealthController {
|
|
208
|
+
constructor(
|
|
209
|
+
private health: HealthCheckService,
|
|
210
|
+
private memory: MemoryHealthIndicator,
|
|
211
|
+
) {}
|
|
212
|
+
|
|
213
|
+
@Get()
|
|
214
|
+
@HealthCheck()
|
|
215
|
+
check() {
|
|
216
|
+
return this.health.check([
|
|
217
|
+
() => this.memory.checkHeap('memory_heap', 150 * 1024 * 1024),
|
|
218
|
+
]);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
`);
|
|
222
|
+
this.writeFile(root, "src/infrastructure/health/health.module.ts", `
|
|
223
|
+
import { Module } from '@nestjs/common';
|
|
224
|
+
import { TerminusModule } from '@nestjs/terminus';
|
|
225
|
+
import { HealthController } from './health.controller';
|
|
226
|
+
|
|
227
|
+
@Module({
|
|
228
|
+
imports: [TerminusModule],
|
|
229
|
+
controllers: [HealthController],
|
|
230
|
+
})
|
|
231
|
+
export class HealthModule {}
|
|
232
|
+
`);
|
|
233
|
+
}
|
|
234
|
+
generateConfigModule(root, options) {
|
|
235
|
+
this.writeFile(root, "src/infrastructure/config/env.schema.ts", `
|
|
236
|
+
import { z } from "zod";
|
|
237
|
+
|
|
238
|
+
export const envSchema = z.object({
|
|
239
|
+
PORT: z.string().default("3000"),
|
|
240
|
+
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
|
|
241
|
+
DATABASE_URL: z.string(),
|
|
242
|
+
REDIS_URL: z.string().default("redis://localhost:6379"),
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
export type Env = z.infer<typeof envSchema>;
|
|
246
|
+
`);
|
|
247
|
+
this.writeFile(root, "src/infrastructure/config/config.module.ts", `
|
|
248
|
+
import { Module } from '@nestjs/common';
|
|
249
|
+
import { ConfigModule as NestConfigModule } from '@nestjs/config';
|
|
250
|
+
import { envSchema } from './env.schema';
|
|
251
|
+
|
|
252
|
+
@Module({
|
|
253
|
+
imports: [
|
|
254
|
+
NestConfigModule.forRoot({
|
|
255
|
+
isGlobal: true,
|
|
256
|
+
validate: (config) => {
|
|
257
|
+
const parsed = envSchema.safeParse(config);
|
|
258
|
+
if (!parsed.success) {
|
|
259
|
+
console.error("❌ Invalid environment variables:", parsed.error.format());
|
|
260
|
+
throw new Error("Invalid environment variables");
|
|
261
|
+
}
|
|
262
|
+
return parsed.data;
|
|
263
|
+
},
|
|
264
|
+
}),
|
|
265
|
+
],
|
|
266
|
+
})
|
|
267
|
+
export class ConfigModule {}
|
|
268
|
+
`);
|
|
269
|
+
}
|
|
270
|
+
generateCacheModule(root, options) {
|
|
271
|
+
this.writeFile(root, "src/infrastructure/cache/cache.interface.ts", `
|
|
272
|
+
export interface ICache {
|
|
273
|
+
get<T>(key: string): Promise<T | null>;
|
|
274
|
+
set(key: string, value: any, ttlSeconds?: number): Promise<void>;
|
|
275
|
+
delete(key: string): Promise<void>;
|
|
276
|
+
}
|
|
277
|
+
`);
|
|
278
|
+
this.writeFile(root, "src/infrastructure/cache/redis.cache.ts", `
|
|
279
|
+
import { Injectable, OnModuleDestroy } from '@nestjs/common';
|
|
280
|
+
import { ConfigService } from '@nestjs/config';
|
|
281
|
+
import Redis from 'ioredis';
|
|
282
|
+
import { ICache } from './cache.interface';
|
|
283
|
+
|
|
284
|
+
@Injectable()
|
|
285
|
+
export class RedisCache implements ICache, OnModuleDestroy {
|
|
286
|
+
private readonly redis: Redis;
|
|
287
|
+
|
|
288
|
+
constructor(private configService: ConfigService) {
|
|
289
|
+
this.redis = new Redis(this.configService.get<string>('REDIS_URL'));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async get<T>(key: string): Promise<T | null> {
|
|
293
|
+
const data = await this.redis.get(key);
|
|
294
|
+
return data ? JSON.parse(data) : null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async set(key: string, value: any, ttlSeconds?: number): Promise<void> {
|
|
298
|
+
const data = JSON.stringify(value);
|
|
299
|
+
if (ttlSeconds) {
|
|
300
|
+
await this.redis.setex(key, ttlSeconds, data);
|
|
301
|
+
} else {
|
|
302
|
+
await this.redis.set(key, data);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async delete(key: string): Promise<void> {
|
|
307
|
+
await this.redis.del(key);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
onModuleDestroy() {
|
|
311
|
+
this.redis.disconnect();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
`);
|
|
315
|
+
this.writeFile(root, "src/infrastructure/cache/cache.module.ts", `
|
|
316
|
+
import { Global, Module } from '@nestjs/common';
|
|
317
|
+
import { RedisCache } from './redis.cache';
|
|
318
|
+
|
|
319
|
+
@Global()
|
|
320
|
+
@Module({
|
|
321
|
+
providers: [
|
|
322
|
+
{
|
|
323
|
+
provide: 'ICache',
|
|
324
|
+
useClass: RedisCache,
|
|
325
|
+
},
|
|
326
|
+
],
|
|
327
|
+
exports: ['ICache'],
|
|
328
|
+
})
|
|
329
|
+
export class CacheModule {}
|
|
330
|
+
`);
|
|
331
|
+
}
|
|
332
|
+
generateErrorFilter(root, options) {
|
|
333
|
+
this.writeFile(root, "src/infrastructure/filters/http-exception.filter.ts", `
|
|
334
|
+
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
|
|
335
|
+
import { Request, Response } from 'express';
|
|
336
|
+
import { PinoLogger } from 'nestjs-pino';
|
|
337
|
+
|
|
338
|
+
@Catch()
|
|
339
|
+
export class HttpExceptionFilter implements ExceptionFilter {
|
|
340
|
+
constructor(private readonly logger: PinoLogger) {}
|
|
341
|
+
|
|
342
|
+
catch(exception: any, host: ArgumentsHost) {
|
|
343
|
+
const ctx = host.switchToHttp();
|
|
344
|
+
const response = ctx.getResponse<Response>();
|
|
345
|
+
const request = ctx.getRequest<Request>();
|
|
346
|
+
|
|
347
|
+
const status = exception instanceof HttpException
|
|
348
|
+
? exception.getStatus()
|
|
349
|
+
: HttpStatus.INTERNAL_SERVER_ERROR;
|
|
350
|
+
|
|
351
|
+
const message = exception instanceof HttpException
|
|
352
|
+
? exception.getResponse()
|
|
353
|
+
: 'Internal server error';
|
|
354
|
+
|
|
355
|
+
this.logger.error({
|
|
356
|
+
err: exception,
|
|
357
|
+
path: request.url,
|
|
358
|
+
status,
|
|
359
|
+
}, 'Unhandled Exception');
|
|
360
|
+
|
|
361
|
+
response.status(status).json({
|
|
362
|
+
statusCode: status,
|
|
363
|
+
timestamp: new Date().toISOString(),
|
|
364
|
+
path: request.url,
|
|
365
|
+
message: typeof message === 'object' ? (message as any).message : message,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
`);
|
|
370
|
+
}
|
|
371
|
+
generateStandardArchitecture(root, options) {
|
|
372
|
+
this.writeFile(root, "src/main.ts", `
|
|
373
|
+
import { NestFactory } from '@nestjs/core';
|
|
374
|
+
import { ValidationPipe } from '@nestjs/common';
|
|
375
|
+
import { Logger } from 'nestjs-pino';
|
|
376
|
+
import { ConfigService } from '@nestjs/config';
|
|
377
|
+
import { AppModule } from './app.module';
|
|
378
|
+
import { HttpExceptionFilter } from './infrastructure/filters/http-exception.filter';
|
|
379
|
+
|
|
380
|
+
async function bootstrap() {
|
|
381
|
+
const app = await NestFactory.create(AppModule, { bufferLogs: true });
|
|
382
|
+
const logger = app.get(Logger);
|
|
383
|
+
const configService = app.get(ConfigService);
|
|
384
|
+
|
|
385
|
+
app.useLogger(logger);
|
|
386
|
+
app.useGlobalFilters(new HttpExceptionFilter(app.get(Logger)));
|
|
387
|
+
app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }));
|
|
388
|
+
|
|
389
|
+
const port = configService.get<string>('PORT') || 3000;
|
|
390
|
+
await app.listen(port);
|
|
391
|
+
|
|
392
|
+
logger.log(\`🚀 NestJS Server running on port \${port}\`);
|
|
393
|
+
}
|
|
394
|
+
bootstrap();
|
|
395
|
+
`);
|
|
396
|
+
this.writeFile(root, "src/app.module.ts", `
|
|
397
|
+
import { Module } from '@nestjs/common';
|
|
398
|
+
import { LoggerModule } from 'nestjs-pino';
|
|
399
|
+
import { ConfigModule } from './infrastructure/config/config.module';
|
|
400
|
+
import { CacheModule } from './infrastructure/cache/cache.module';
|
|
401
|
+
import { HealthModule } from './infrastructure/health/health.module';
|
|
402
|
+
${options.orm === 'prisma' ? "import { PrismaModule } from './infrastructure/prisma/prisma.module';" : ""}
|
|
403
|
+
import { AppController } from './app.controller';
|
|
404
|
+
import { AppService } from './app.service';
|
|
405
|
+
|
|
406
|
+
@Module({
|
|
407
|
+
imports: [
|
|
408
|
+
ConfigModule,
|
|
409
|
+
CacheModule,
|
|
410
|
+
HealthModule,
|
|
411
|
+
LoggerModule.forRoot({
|
|
412
|
+
pinoHttp: {
|
|
413
|
+
transport: process.env.NODE_ENV !== 'production' ? { target: 'pino-pretty' } : undefined,
|
|
414
|
+
autoLogging: true,
|
|
415
|
+
genReqId: (req) => req.headers['x-correlation-id'] || req.id,
|
|
416
|
+
},
|
|
417
|
+
}),
|
|
418
|
+
${options.orm === 'prisma' ? "PrismaModule," : ""}
|
|
419
|
+
],
|
|
420
|
+
controllers: [AppController],
|
|
421
|
+
providers: [AppService],
|
|
422
|
+
})
|
|
423
|
+
export class AppModule {}
|
|
424
|
+
`);
|
|
425
|
+
this.writeFile(root, "src/app.controller.ts", `
|
|
426
|
+
import { Controller, Get } from '@nestjs/common';
|
|
427
|
+
import { AppService } from './app.service';
|
|
428
|
+
|
|
429
|
+
@Controller()
|
|
430
|
+
export class AppController {
|
|
431
|
+
constructor(private readonly appService: AppService) {}
|
|
432
|
+
|
|
433
|
+
@Get()
|
|
434
|
+
getHello(): string {
|
|
435
|
+
return this.appService.getHello();
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
`);
|
|
439
|
+
this.writeFile(root, "src/app.service.ts", `
|
|
440
|
+
import { Injectable } from '@nestjs/common';
|
|
441
|
+
|
|
442
|
+
@Injectable()
|
|
443
|
+
export class AppService {
|
|
444
|
+
getHello(): string {
|
|
445
|
+
return 'Hello World!';
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
`);
|
|
449
|
+
}
|
|
450
|
+
generateCleanArchitecture(root, options) {
|
|
451
|
+
// Domain
|
|
452
|
+
this.writeFile(root, "src/domain/entities/user.entity.ts", `
|
|
453
|
+
export class User {
|
|
454
|
+
constructor(
|
|
455
|
+
public readonly id: string,
|
|
456
|
+
public readonly name: string,
|
|
457
|
+
public readonly email: string
|
|
458
|
+
) {}
|
|
459
|
+
}
|
|
460
|
+
`);
|
|
461
|
+
this.writeFile(root, "src/domain/repositories/user.repository.interface.ts", `
|
|
462
|
+
import { User } from "../entities/user.entity";
|
|
463
|
+
|
|
464
|
+
export interface IUserRepository {
|
|
465
|
+
save(user: User): Promise<User>;
|
|
466
|
+
findByEmail(email: string): Promise<User | null>;
|
|
467
|
+
findById(id: string): Promise<User | null>;
|
|
468
|
+
}
|
|
469
|
+
`);
|
|
470
|
+
// Application
|
|
471
|
+
this.writeFile(root, "src/application/use-cases/create-user.use-case.ts", `
|
|
472
|
+
import { Inject, Injectable, BadRequestException } from '@nestjs/common';
|
|
473
|
+
import { User } from '../../domain/entities/user.entity';
|
|
474
|
+
import { IUserRepository } from '../../domain/repositories/user.repository.interface';
|
|
475
|
+
|
|
476
|
+
@Injectable()
|
|
477
|
+
export class CreateUserUseCase {
|
|
478
|
+
constructor(
|
|
479
|
+
@Inject('IUserRepository')
|
|
480
|
+
private readonly userRepository: IUserRepository
|
|
481
|
+
) {}
|
|
482
|
+
|
|
483
|
+
async execute(name: string, email: string): Promise<User> {
|
|
484
|
+
const existing = await this.userRepository.findByEmail(email);
|
|
485
|
+
if (existing) throw new BadRequestException("User already exists");
|
|
486
|
+
|
|
487
|
+
const user = new User(Date.now().toString(), name, email);
|
|
488
|
+
return this.userRepository.save(user);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
`);
|
|
492
|
+
this.writeFile(root, "src/application/use-cases/get-user.use-case.ts", `
|
|
493
|
+
import { Inject, Injectable, NotFoundException } from '@nestjs/common';
|
|
494
|
+
import { User } from '../../domain/entities/user.entity';
|
|
495
|
+
import { IUserRepository } from '../../domain/repositories/user.repository.interface';
|
|
496
|
+
import { ICache } from '../../infrastructure/cache/cache.interface';
|
|
497
|
+
|
|
498
|
+
@Injectable()
|
|
499
|
+
export class GetUserUseCase {
|
|
500
|
+
constructor(
|
|
501
|
+
@Inject('IUserRepository')
|
|
502
|
+
private readonly userRepository: IUserRepository,
|
|
503
|
+
@Inject('ICache')
|
|
504
|
+
private readonly cache: ICache
|
|
505
|
+
) {}
|
|
506
|
+
|
|
507
|
+
async execute(id: string): Promise<User> {
|
|
508
|
+
const cacheKey = \`user:\${id}\`;
|
|
509
|
+
const cached = await this.cache.get<User>(cacheKey);
|
|
510
|
+
if (cached) return cached;
|
|
511
|
+
|
|
512
|
+
const user = await this.userRepository.findById(id);
|
|
513
|
+
if (!user) throw new NotFoundException("User not found");
|
|
514
|
+
|
|
515
|
+
await this.cache.set(cacheKey, user, 3600);
|
|
516
|
+
return user;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
`);
|
|
520
|
+
// Infrastructure
|
|
521
|
+
this.writeFile(root, "src/infrastructure/repositories/in-memory-user.repository.ts", `
|
|
522
|
+
import { Injectable } from '@nestjs/common';
|
|
523
|
+
import { User } from '../../domain/entities/user.entity';
|
|
524
|
+
import { IUserRepository } from '../../domain/repositories/user.repository.interface';
|
|
525
|
+
|
|
526
|
+
@Injectable()
|
|
527
|
+
export class InMemoryUserRepository implements IUserRepository {
|
|
528
|
+
private users: User[] = [];
|
|
529
|
+
|
|
530
|
+
async save(user: User): Promise<User> {
|
|
531
|
+
this.users.push(user);
|
|
532
|
+
return user;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
536
|
+
return this.users.find(u => u.email === email) || null;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
async findById(id: string): Promise<User | null> {
|
|
540
|
+
return this.users.find(u => u.id === id) || null;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
`);
|
|
544
|
+
// Presentation (Controllers)
|
|
545
|
+
this.writeFile(root, "src/presentation/controllers/user.controller.ts", `
|
|
546
|
+
import { Controller, Post, Get, Body, Param } from '@nestjs/common';
|
|
547
|
+
import { CreateUserUseCase } from '../../application/use-cases/create-user.use-case';
|
|
548
|
+
import { GetUserUseCase } from '../../application/use-cases/get-user.use-case';
|
|
549
|
+
|
|
550
|
+
@Controller('users')
|
|
551
|
+
export class UserController {
|
|
552
|
+
constructor(
|
|
553
|
+
private readonly createUserUseCase: CreateUserUseCase,
|
|
554
|
+
private readonly getUserUseCase: GetUserUseCase
|
|
555
|
+
) {}
|
|
556
|
+
|
|
557
|
+
@Post()
|
|
558
|
+
async create(@Body() body: { name: string; email: string }) {
|
|
559
|
+
return await this.createUserUseCase.execute(body.name, body.email);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
@Get(':id')
|
|
563
|
+
async get(@Param('id') id: string) {
|
|
564
|
+
return await this.getUserUseCase.execute(id);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
`);
|
|
568
|
+
// Modules
|
|
569
|
+
this.writeFile(root, "src/app.module.ts", `
|
|
570
|
+
import { Module } from '@nestjs/common';
|
|
571
|
+
import { LoggerModule } from 'nestjs-pino';
|
|
572
|
+
import { ConfigModule } from './infrastructure/config/config.module';
|
|
573
|
+
import { CacheModule } from './infrastructure/cache/cache.module';
|
|
574
|
+
import { HealthModule } from './infrastructure/health/health.module';
|
|
575
|
+
${options.orm === 'prisma' ? "import { PrismaModule } from './infrastructure/prisma/prisma.module';" : ""}
|
|
576
|
+
import { UserController } from './presentation/controllers/user.controller';
|
|
577
|
+
import { CreateUserUseCase } from './application/use-cases/create-user.use-case';
|
|
578
|
+
import { GetUserUseCase } from './application/use-cases/get-user.use-case';
|
|
579
|
+
import { InMemoryUserRepository } from './infrastructure/repositories/in-memory-user.repository';
|
|
580
|
+
|
|
581
|
+
@Module({
|
|
582
|
+
imports: [
|
|
583
|
+
ConfigModule,
|
|
584
|
+
CacheModule,
|
|
585
|
+
HealthModule,
|
|
586
|
+
LoggerModule.forRoot({
|
|
587
|
+
pinoHttp: {
|
|
588
|
+
transport: process.env.NODE_ENV !== 'production' ? { target: 'pino-pretty' } : undefined,
|
|
589
|
+
autoLogging: true,
|
|
590
|
+
genReqId: (req) => req.headers['x-correlation-id'] || req.id,
|
|
591
|
+
},
|
|
592
|
+
}),
|
|
593
|
+
${options.orm === 'prisma' ? "PrismaModule," : ""}
|
|
594
|
+
],
|
|
595
|
+
controllers: [UserController],
|
|
596
|
+
providers: [
|
|
597
|
+
CreateUserUseCase,
|
|
598
|
+
GetUserUseCase,
|
|
599
|
+
{
|
|
600
|
+
provide: 'IUserRepository',
|
|
601
|
+
useClass: InMemoryUserRepository
|
|
602
|
+
}
|
|
603
|
+
],
|
|
604
|
+
})
|
|
605
|
+
export class AppModule {}
|
|
606
|
+
`);
|
|
607
|
+
this.writeFile(root, "src/main.ts", `
|
|
608
|
+
import { NestFactory } from '@nestjs/core';
|
|
609
|
+
import { ValidationPipe } from '@nestjs/common';
|
|
610
|
+
import { Logger } from 'nestjs-pino';
|
|
611
|
+
import { ConfigService } from '@nestjs/config';
|
|
612
|
+
import { AppModule } from './app.module';
|
|
613
|
+
import { HttpExceptionFilter } from './infrastructure/filters/http-exception.filter';
|
|
614
|
+
|
|
615
|
+
async function bootstrap() {
|
|
616
|
+
const app = await NestFactory.create(AppModule, { bufferLogs: true });
|
|
617
|
+
const logger = app.get(Logger);
|
|
618
|
+
const configService = app.get(ConfigService);
|
|
619
|
+
|
|
620
|
+
app.useLogger(logger);
|
|
621
|
+
app.useGlobalFilters(new HttpExceptionFilter(app.get(Logger)));
|
|
622
|
+
app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }));
|
|
623
|
+
|
|
624
|
+
const port = configService.get<string>('PORT') || 3000;
|
|
625
|
+
await app.listen(port);
|
|
626
|
+
|
|
627
|
+
logger.log(\`🚀 NestJS Clean Architecture Server running on port \${port}\`);
|
|
628
|
+
}
|
|
629
|
+
bootstrap();
|
|
630
|
+
`);
|
|
631
|
+
}
|
|
632
|
+
generateDocker(root, options) {
|
|
633
|
+
const dbService = options.database === "postgresql" ? `
|
|
634
|
+
db:
|
|
635
|
+
image: postgres:15-alpine
|
|
636
|
+
environment:
|
|
637
|
+
- POSTGRES_USER=user
|
|
638
|
+
- POSTGRES_PASSWORD=password
|
|
639
|
+
- POSTGRES_DB=appdb
|
|
640
|
+
ports:
|
|
641
|
+
- "5432:5432"
|
|
642
|
+
` : options.database === "mysql" ? `
|
|
643
|
+
db:
|
|
644
|
+
image: mysql:8
|
|
645
|
+
environment:
|
|
646
|
+
- MYSQL_ROOT_PASSWORD=password
|
|
647
|
+
- MYSQL_DATABASE=appdb
|
|
648
|
+
ports:
|
|
649
|
+
- "3306:3306"
|
|
650
|
+
` : "";
|
|
651
|
+
const dbUrl = options.database === "postgresql" ? "postgresql://user:password@db:5432/appdb?schema=public" :
|
|
652
|
+
options.database === "mysql" ? "mysql://root:password@db:3306/appdb" : "";
|
|
653
|
+
this.writeFile(root, "Dockerfile", `
|
|
654
|
+
FROM node:18-alpine AS builder
|
|
655
|
+
WORKDIR /app
|
|
656
|
+
COPY package*.json ./
|
|
657
|
+
RUN npm ci
|
|
658
|
+
COPY . .
|
|
659
|
+
${options.orm === 'prisma' ? 'RUN npx prisma generate' : ''}
|
|
660
|
+
RUN npm run build
|
|
661
|
+
|
|
662
|
+
FROM node:18-alpine
|
|
663
|
+
WORKDIR /app
|
|
664
|
+
COPY --from=builder /app/dist ./dist
|
|
665
|
+
COPY --from=builder /app/package*.json ./
|
|
666
|
+
${options.orm === 'prisma' ? 'COPY --from=builder /app/prisma ./prisma' : ''}
|
|
667
|
+
RUN npm ci --production
|
|
668
|
+
${options.orm === 'prisma' ? 'RUN npx prisma generate' : ''}
|
|
669
|
+
EXPOSE 3000
|
|
670
|
+
CMD ["npm", "run", "start:prod"]
|
|
671
|
+
`);
|
|
672
|
+
this.writeFile(root, "docker-compose.yml", `
|
|
673
|
+
version: '3.8'
|
|
674
|
+
services:
|
|
675
|
+
app:
|
|
676
|
+
build: .
|
|
677
|
+
ports:
|
|
678
|
+
- "3000:3000"
|
|
679
|
+
environment:
|
|
680
|
+
- PORT=3000
|
|
681
|
+
- NODE_ENV=production
|
|
682
|
+
${dbUrl ? `- DATABASE_URL=\${DATABASE_URL:-${dbUrl}}` : ""}
|
|
683
|
+
- REDIS_URL=redis://cache:6379
|
|
684
|
+
depends_on:
|
|
685
|
+
${dbService ? "- db" : ""}
|
|
686
|
+
- cache
|
|
687
|
+
restart: always
|
|
688
|
+
${dbService}
|
|
689
|
+
cache:
|
|
690
|
+
image: redis:7-alpine
|
|
691
|
+
ports:
|
|
692
|
+
- "6379:6379"
|
|
693
|
+
`);
|
|
694
|
+
}
|
|
695
|
+
generateCI(root, options) {
|
|
696
|
+
this.writeFile(root, ".github/workflows/ci.yml", `
|
|
697
|
+
name: CI
|
|
698
|
+
|
|
699
|
+
on:
|
|
700
|
+
push:
|
|
701
|
+
branches: [ main ]
|
|
702
|
+
pull_request:
|
|
703
|
+
branches: [ main ]
|
|
704
|
+
|
|
705
|
+
jobs:
|
|
706
|
+
build:
|
|
707
|
+
runs-on: ubuntu-latest
|
|
708
|
+
|
|
709
|
+
steps:
|
|
710
|
+
- uses: actions/checkout@v3
|
|
711
|
+
- name: Use Node.js 18.x
|
|
712
|
+
uses: actions/setup-node@v3
|
|
713
|
+
with:
|
|
714
|
+
node-version: 18.x
|
|
715
|
+
cache: 'npm'
|
|
716
|
+
- run: npm ci
|
|
717
|
+
${options.orm === 'prisma' ? '- run: npx prisma generate' : ''}
|
|
718
|
+
- name: Architecture Sync & Validation
|
|
719
|
+
run: npx archforge sync
|
|
720
|
+
- run: npm run lint
|
|
721
|
+
- run: npm test
|
|
722
|
+
- run: npm run build
|
|
723
|
+
`);
|
|
724
|
+
}
|
|
725
|
+
generatePrismaSchema(root, options) {
|
|
726
|
+
const provider = options.database === "mongodb" ? "mongodb" :
|
|
727
|
+
options.database === "mysql" ? "mysql" :
|
|
728
|
+
options.database === "sqlite" ? "sqlite" : "postgresql";
|
|
729
|
+
const url = options.database === "sqlite" ? "file:./dev.db" : "env(\"DATABASE_URL\")";
|
|
730
|
+
this.writeFile(root, "prisma/schema.prisma", `
|
|
731
|
+
generator client {
|
|
732
|
+
provider = "prisma-client-js"
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
datasource db {
|
|
736
|
+
provider = "${provider}"
|
|
737
|
+
url = ${url}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
model User {
|
|
741
|
+
id String @id @default(uuid())
|
|
742
|
+
email String @unique
|
|
743
|
+
name String?
|
|
744
|
+
}
|
|
745
|
+
`);
|
|
746
|
+
this.writeFile(root, "src/infrastructure/prisma/prisma.service.ts", `
|
|
747
|
+
import { Injectable, OnModuleInit } from '@nestjs/common';
|
|
748
|
+
import { PrismaClient } from '@prisma/client';
|
|
749
|
+
|
|
750
|
+
@Injectable()
|
|
751
|
+
export class PrismaService extends PrismaClient implements OnModuleInit {
|
|
752
|
+
async onModuleInit() {
|
|
753
|
+
await this.$connect();
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
`);
|
|
757
|
+
this.writeFile(root, "src/infrastructure/prisma/prisma.module.ts", `
|
|
758
|
+
import { Global, Module } from '@nestjs/common';
|
|
759
|
+
import { PrismaService } from './prisma.service';
|
|
760
|
+
|
|
761
|
+
@Global()
|
|
762
|
+
@Module({
|
|
763
|
+
providers: [PrismaService],
|
|
764
|
+
exports: [PrismaService],
|
|
765
|
+
})
|
|
766
|
+
export class PrismaModule {}
|
|
767
|
+
`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
exports.NestJSGenerator = NestJSGenerator;
|