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,920 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ExpressGenerator = void 0;
|
|
4
|
+
const base_1 = require("../base");
|
|
5
|
+
class ExpressGenerator extends base_1.BaseGenerator {
|
|
6
|
+
getGeneratorName() {
|
|
7
|
+
return "Express.js";
|
|
8
|
+
}
|
|
9
|
+
async generateProjectStructure(root, arch, options) {
|
|
10
|
+
const archStyle = options.architecture || "clean";
|
|
11
|
+
// 1. Common Configuration Files
|
|
12
|
+
this.generateCommonFiles(root, options);
|
|
13
|
+
// 2. Source Code Structure based on Architecture
|
|
14
|
+
if (archStyle === "clean") {
|
|
15
|
+
this.generateCleanArchitecture(root, options);
|
|
16
|
+
}
|
|
17
|
+
else if (archStyle === "mvc") {
|
|
18
|
+
this.generateMVCArchitecture(root, options);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
this.generateLayeredArchitecture(root, options);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
generateCommonFiles(root, options) {
|
|
25
|
+
const projectName = options.projectName || "express-app";
|
|
26
|
+
// package.json
|
|
27
|
+
this.writeFile(root, "package.json", JSON.stringify({
|
|
28
|
+
name: projectName,
|
|
29
|
+
version: "1.0.0",
|
|
30
|
+
description: "Generated by ArchForge X",
|
|
31
|
+
main: "dist/main.js",
|
|
32
|
+
scripts: {
|
|
33
|
+
"start": "node dist/main.js",
|
|
34
|
+
"start:dev": "ts-node-dev --respawn --transpile-only src/main.ts",
|
|
35
|
+
"build": "tsc",
|
|
36
|
+
"lint": "eslint . --ext .ts",
|
|
37
|
+
"test": "jest"
|
|
38
|
+
},
|
|
39
|
+
dependencies: {
|
|
40
|
+
"express": "^4.18.2",
|
|
41
|
+
"express-async-errors": "^3.1.1",
|
|
42
|
+
"cors": "^2.8.5",
|
|
43
|
+
"dotenv": "^16.3.1",
|
|
44
|
+
"helmet": "^7.0.0",
|
|
45
|
+
"morgan": "^1.10.0",
|
|
46
|
+
"winston": "^3.10.0",
|
|
47
|
+
"uuid": "^9.0.0",
|
|
48
|
+
"zod": "^3.22.2",
|
|
49
|
+
"ioredis": "^5.3.2",
|
|
50
|
+
"http-status-codes": "^2.2.0",
|
|
51
|
+
...(options.orm === "prisma" ? { "@prisma/client": "^5.0.0" } : {}),
|
|
52
|
+
...(options.orm === "typeorm" ? { "typeorm": "^0.3.17", "reflect-metadata": "^0.1.13" } : {})
|
|
53
|
+
},
|
|
54
|
+
devDependencies: {
|
|
55
|
+
"@types/express": "^4.17.17",
|
|
56
|
+
"@types/cors": "^2.8.13",
|
|
57
|
+
"@types/node": "^20.4.5",
|
|
58
|
+
"@types/morgan": "^1.10.0",
|
|
59
|
+
"@types/uuid": "^9.0.2",
|
|
60
|
+
"typescript": "^5.1.6",
|
|
61
|
+
"ts-node-dev": "^2.0.0",
|
|
62
|
+
"eslint": "^8.45.0",
|
|
63
|
+
"eslint-plugin-import": "^2.27.5",
|
|
64
|
+
"jest": "^29.6.1",
|
|
65
|
+
"ts-jest": "^29.1.1",
|
|
66
|
+
"@types/jest": "^29.5.3",
|
|
67
|
+
"pino-pretty": "^10.2.0",
|
|
68
|
+
...(options.orm === "prisma" ? { "prisma": "^5.0.0" } : {})
|
|
69
|
+
}
|
|
70
|
+
}, null, 2));
|
|
71
|
+
// Prisma Schema
|
|
72
|
+
if (options.orm === "prisma") {
|
|
73
|
+
this.generatePrismaSchema(root, options);
|
|
74
|
+
}
|
|
75
|
+
// ... (tsconfig.json and .eslintrc.json remain same)
|
|
76
|
+
// Logger Configuration
|
|
77
|
+
this.writeFile(root, "src/infrastructure/logger.ts", `
|
|
78
|
+
import winston from "winston";
|
|
79
|
+
|
|
80
|
+
const logger = winston.createLogger({
|
|
81
|
+
level: process.env.LOG_LEVEL || "info",
|
|
82
|
+
format: winston.format.combine(
|
|
83
|
+
winston.format.timestamp(),
|
|
84
|
+
winston.format.json()
|
|
85
|
+
),
|
|
86
|
+
transports: [
|
|
87
|
+
new winston.transports.Console({
|
|
88
|
+
format: winston.format.combine(
|
|
89
|
+
winston.format.colorize(),
|
|
90
|
+
winston.format.simple()
|
|
91
|
+
)
|
|
92
|
+
})
|
|
93
|
+
]
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
export default logger;
|
|
97
|
+
`);
|
|
98
|
+
// tsconfig.json
|
|
99
|
+
this.writeFile(root, "tsconfig.json", JSON.stringify({
|
|
100
|
+
compilerOptions: {
|
|
101
|
+
target: "ES2020",
|
|
102
|
+
module: "commonjs",
|
|
103
|
+
outDir: "./dist",
|
|
104
|
+
rootDir: "./src",
|
|
105
|
+
strict: true,
|
|
106
|
+
esModuleInterop: true,
|
|
107
|
+
skipLibCheck: true,
|
|
108
|
+
forceConsistentCasingInFileNames: true
|
|
109
|
+
},
|
|
110
|
+
exclude: ["node_modules", "dist", "**/*.test.ts"]
|
|
111
|
+
}, null, 2));
|
|
112
|
+
// .eslintrc.json (Architecture Guardrails)
|
|
113
|
+
const archStyle = options.architecture || "clean";
|
|
114
|
+
let restrictedPaths = [];
|
|
115
|
+
if (archStyle === "clean") {
|
|
116
|
+
restrictedPaths = [
|
|
117
|
+
{
|
|
118
|
+
target: "./src/domain",
|
|
119
|
+
from: "./src/application",
|
|
120
|
+
message: "Domain layer cannot import from Application layer"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
target: "./src/domain",
|
|
124
|
+
from: "./src/infrastructure",
|
|
125
|
+
message: "Domain layer cannot import from Infrastructure layer"
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
target: "./src/domain",
|
|
129
|
+
from: "./src/interface-adapters",
|
|
130
|
+
message: "Domain layer cannot import from Interface Adapters layer"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
target: "./src/application",
|
|
134
|
+
from: "./src/infrastructure",
|
|
135
|
+
message: "Application layer cannot import from Infrastructure layer"
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
target: "./src/application",
|
|
139
|
+
from: "./src/interface-adapters",
|
|
140
|
+
message: "Application layer cannot import from Interface Adapters layer"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
target: "./src/infrastructure",
|
|
144
|
+
from: "./src/interface-adapters",
|
|
145
|
+
message: "Infrastructure layer cannot import from Interface Adapters layer"
|
|
146
|
+
}
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
else if (archStyle === "mvc") {
|
|
150
|
+
restrictedPaths = [
|
|
151
|
+
{
|
|
152
|
+
target: "./src/models",
|
|
153
|
+
from: "./src/controllers",
|
|
154
|
+
message: "Models cannot import from Controllers"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
target: "./src/models",
|
|
158
|
+
from: "./src/routes",
|
|
159
|
+
message: "Models cannot import from Routes"
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
target: "./src/services",
|
|
163
|
+
from: "./src/controllers",
|
|
164
|
+
message: "Services cannot import from Controllers"
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
target: "./src/services",
|
|
168
|
+
from: "./src/routes",
|
|
169
|
+
message: "Services cannot import from Routes"
|
|
170
|
+
}
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
restrictedPaths = [
|
|
175
|
+
{
|
|
176
|
+
target: "./src/core",
|
|
177
|
+
from: "./src/services",
|
|
178
|
+
message: "Core cannot import from Services"
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
target: "./src/core",
|
|
182
|
+
from: "./src/api",
|
|
183
|
+
message: "Core cannot import from API"
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
target: "./src/services",
|
|
187
|
+
from: "./src/api",
|
|
188
|
+
message: "Services cannot import from API"
|
|
189
|
+
}
|
|
190
|
+
];
|
|
191
|
+
}
|
|
192
|
+
this.writeFile(root, ".eslintrc.json", JSON.stringify({
|
|
193
|
+
"env": {
|
|
194
|
+
"es2021": true,
|
|
195
|
+
"node": true
|
|
196
|
+
},
|
|
197
|
+
"extends": [
|
|
198
|
+
"eslint:recommended",
|
|
199
|
+
"plugin:@typescript-eslint/recommended"
|
|
200
|
+
],
|
|
201
|
+
"parser": "@typescript-eslint/parser",
|
|
202
|
+
"parserOptions": {
|
|
203
|
+
"ecmaVersion": "latest",
|
|
204
|
+
"sourceType": "module"
|
|
205
|
+
},
|
|
206
|
+
"plugins": [
|
|
207
|
+
"@typescript-eslint",
|
|
208
|
+
"import"
|
|
209
|
+
],
|
|
210
|
+
"rules": {
|
|
211
|
+
"import/no-restricted-paths": ["error", {
|
|
212
|
+
"zones": restrictedPaths
|
|
213
|
+
}]
|
|
214
|
+
}
|
|
215
|
+
}, null, 2));
|
|
216
|
+
// .env
|
|
217
|
+
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`);
|
|
218
|
+
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`);
|
|
219
|
+
this.writeFile(root, ".gitignore", `node_modules/\ndist/\n.env\ncoverage/\n`);
|
|
220
|
+
this.generateConfig(root, options);
|
|
221
|
+
this.generateCacheLayer(root, options);
|
|
222
|
+
this.generateErrorHandling(root, options);
|
|
223
|
+
this.generateHealthChecks(root, options);
|
|
224
|
+
}
|
|
225
|
+
generateConfig(root, options) {
|
|
226
|
+
this.writeFile(root, "src/config/env.schema.ts", `
|
|
227
|
+
import { z } from "zod";
|
|
228
|
+
|
|
229
|
+
export const envSchema = z.object({
|
|
230
|
+
PORT: z.string().default("3000"),
|
|
231
|
+
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
|
|
232
|
+
DATABASE_URL: z.string(),
|
|
233
|
+
REDIS_URL: z.string().default("redis://localhost:6379"),
|
|
234
|
+
LOG_LEVEL: z.string().default("info"),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
export type Env = z.infer<typeof envSchema>;
|
|
238
|
+
`);
|
|
239
|
+
this.writeFile(root, "src/config/index.ts", `
|
|
240
|
+
import dotenv from "dotenv";
|
|
241
|
+
import { envSchema } from "./env.schema";
|
|
242
|
+
|
|
243
|
+
dotenv.config();
|
|
244
|
+
|
|
245
|
+
const parsed = envSchema.safeParse(process.env);
|
|
246
|
+
|
|
247
|
+
if (!parsed.success) {
|
|
248
|
+
console.error("❌ Invalid environment variables:", parsed.error.format());
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export const config = {
|
|
253
|
+
app: {
|
|
254
|
+
port: parseInt(parsed.data.PORT, 10),
|
|
255
|
+
env: parsed.data.NODE_ENV,
|
|
256
|
+
logLevel: parsed.data.LOG_LEVEL,
|
|
257
|
+
},
|
|
258
|
+
database: {
|
|
259
|
+
url: parsed.data.DATABASE_URL,
|
|
260
|
+
},
|
|
261
|
+
cache: {
|
|
262
|
+
url: parsed.data.REDIS_URL,
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
`);
|
|
266
|
+
}
|
|
267
|
+
generateCacheLayer(root, options) {
|
|
268
|
+
this.writeFile(root, "src/infrastructure/cache/cache.interface.ts", `
|
|
269
|
+
export interface ICache {
|
|
270
|
+
get<T>(key: string): Promise<T | null>;
|
|
271
|
+
set(key: string, value: any, ttlSeconds?: number): Promise<void>;
|
|
272
|
+
delete(key: string): Promise<void>;
|
|
273
|
+
}
|
|
274
|
+
`);
|
|
275
|
+
this.writeFile(root, "src/infrastructure/cache/redis.cache.ts", `
|
|
276
|
+
import Redis from "ioredis";
|
|
277
|
+
import { ICache } from "./cache.interface";
|
|
278
|
+
import { config } from "../../config";
|
|
279
|
+
import logger from "../logger";
|
|
280
|
+
|
|
281
|
+
export class RedisCache implements ICache {
|
|
282
|
+
private redis: Redis;
|
|
283
|
+
|
|
284
|
+
constructor() {
|
|
285
|
+
this.redis = new Redis(config.cache.url);
|
|
286
|
+
this.redis.on("error", (err) => logger.error("Redis Error", err));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async get<T>(key: string): Promise<T | null> {
|
|
290
|
+
const data = await this.redis.get(key);
|
|
291
|
+
return data ? JSON.parse(data) : null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async set(key: string, value: any, ttlSeconds?: number): Promise<void> {
|
|
295
|
+
const data = JSON.stringify(value);
|
|
296
|
+
if (ttlSeconds) {
|
|
297
|
+
await this.redis.setex(key, ttlSeconds, data);
|
|
298
|
+
} else {
|
|
299
|
+
await this.redis.set(key, data);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async delete(key: string): Promise<void> {
|
|
304
|
+
await this.redis.del(key);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
`);
|
|
308
|
+
this.writeFile(root, "src/infrastructure/cache/memory.cache.ts", `
|
|
309
|
+
import { ICache } from "./cache.interface";
|
|
310
|
+
|
|
311
|
+
export class MemoryCache implements ICache {
|
|
312
|
+
private cache = new Map<string, { value: any; expiresAt?: number }>();
|
|
313
|
+
|
|
314
|
+
async get<T>(key: string): Promise<T | null> {
|
|
315
|
+
const item = this.cache.get(key);
|
|
316
|
+
if (!item) return null;
|
|
317
|
+
if (item.expiresAt && item.expiresAt < Date.now()) {
|
|
318
|
+
this.cache.delete(key);
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
return item.value;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async set(key: string, value: any, ttlSeconds?: number): Promise<void> {
|
|
325
|
+
const expiresAt = ttlSeconds ? Date.now() + ttlSeconds * 1000 : undefined;
|
|
326
|
+
this.cache.set(key, { value, expiresAt });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async delete(key: string): Promise<void> {
|
|
330
|
+
this.cache.delete(key);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
`);
|
|
334
|
+
}
|
|
335
|
+
generateErrorHandling(root, options) {
|
|
336
|
+
this.writeFile(root, "src/infrastructure/errors/app.error.ts", `
|
|
337
|
+
import { StatusCodes } from "http-status-codes";
|
|
338
|
+
|
|
339
|
+
export class AppError extends Error {
|
|
340
|
+
constructor(
|
|
341
|
+
public message: string,
|
|
342
|
+
public statusCode: number = StatusCodes.INTERNAL_SERVER_ERROR,
|
|
343
|
+
public isOperational: boolean = true
|
|
344
|
+
) {
|
|
345
|
+
super(message);
|
|
346
|
+
Object.setPrototypeOf(this, AppError.prototype);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export class NotFoundError extends AppError {
|
|
351
|
+
constructor(message: string = "Resource not found") {
|
|
352
|
+
super(message, StatusCodes.NOT_FOUND);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export class BadRequestError extends AppError {
|
|
357
|
+
constructor(message: string = "Bad request") {
|
|
358
|
+
super(message, StatusCodes.BAD_REQUEST);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
`);
|
|
362
|
+
this.writeFile(root, "src/infrastructure/middleware/error.middleware.ts", `
|
|
363
|
+
import { Request, Response, NextFunction } from "express";
|
|
364
|
+
import { AppError } from "../errors/app.error";
|
|
365
|
+
import logger from "../logger";
|
|
366
|
+
import { StatusCodes } from "http-status-codes";
|
|
367
|
+
|
|
368
|
+
export const errorMiddleware = (
|
|
369
|
+
err: Error,
|
|
370
|
+
req: Request,
|
|
371
|
+
res: Response,
|
|
372
|
+
next: NextFunction
|
|
373
|
+
) => {
|
|
374
|
+
if (err instanceof AppError) {
|
|
375
|
+
return res.status(err.statusCode).json({
|
|
376
|
+
status: "error",
|
|
377
|
+
message: err.message,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
logger.error("Unhandled Error", err);
|
|
382
|
+
|
|
383
|
+
return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
|
|
384
|
+
status: "error",
|
|
385
|
+
message: "Something went wrong",
|
|
386
|
+
});
|
|
387
|
+
};
|
|
388
|
+
`);
|
|
389
|
+
}
|
|
390
|
+
generateHealthChecks(root, options) {
|
|
391
|
+
this.writeFile(root, "src/interface-adapters/controllers/health.controller.ts", `
|
|
392
|
+
import { Request, Response } from "express";
|
|
393
|
+
import { StatusCodes } from "http-status-codes";
|
|
394
|
+
import prisma from "../../infrastructure/database/prisma";
|
|
395
|
+
|
|
396
|
+
export class HealthController {
|
|
397
|
+
async health(req: Request, res: Response) {
|
|
398
|
+
res.status(StatusCodes.OK).json({ status: "ok", timestamp: new Date().toISOString() });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async ready(req: Request, res: Response) {
|
|
402
|
+
const checks: any = {
|
|
403
|
+
uptime: process.uptime(),
|
|
404
|
+
timestamp: new Date().toISOString(),
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
${options.orm === 'prisma' ? 'await prisma.$queryRaw`SELECT 1`' : ''}
|
|
409
|
+
checks.database = "connected";
|
|
410
|
+
} catch (e) {
|
|
411
|
+
checks.database = "disconnected";
|
|
412
|
+
return res.status(StatusCodes.SERVICE_UNAVAILABLE).json({ status: "error", checks });
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
res.status(StatusCodes.OK).json({ status: "ok", checks });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
`);
|
|
419
|
+
}
|
|
420
|
+
generateCleanArchitecture(root, options) {
|
|
421
|
+
// Domain Layer
|
|
422
|
+
this.writeFile(root, "src/domain/entities/user.entity.ts", `
|
|
423
|
+
export class User {
|
|
424
|
+
constructor(
|
|
425
|
+
public readonly id: string,
|
|
426
|
+
public readonly name: string,
|
|
427
|
+
public readonly email: string
|
|
428
|
+
) {}
|
|
429
|
+
}
|
|
430
|
+
`);
|
|
431
|
+
this.writeFile(root, "src/domain/repositories/user.repository.ts", `
|
|
432
|
+
import { User } from "../entities/user.entity";
|
|
433
|
+
|
|
434
|
+
export interface UserRepository {
|
|
435
|
+
save(user: User): Promise<User>;
|
|
436
|
+
findByEmail(email: string): Promise<User | null>;
|
|
437
|
+
findById(id: string): Promise<User | null>;
|
|
438
|
+
}
|
|
439
|
+
`);
|
|
440
|
+
// Application Layer
|
|
441
|
+
this.writeFile(root, "src/application/use-cases/create-user.use-case.ts", `
|
|
442
|
+
import { User } from "../../domain/entities/user.entity";
|
|
443
|
+
import { UserRepository } from "../../domain/repositories/user.repository";
|
|
444
|
+
import { BadRequestError } from "../../infrastructure/errors/app.error";
|
|
445
|
+
|
|
446
|
+
export class CreateUserUseCase {
|
|
447
|
+
constructor(private userRepository: UserRepository) {}
|
|
448
|
+
|
|
449
|
+
async execute(name: string, email: string): Promise<User> {
|
|
450
|
+
const existing = await this.userRepository.findByEmail(email);
|
|
451
|
+
if (existing) throw new BadRequestError("User already exists");
|
|
452
|
+
|
|
453
|
+
const user = new User(Date.now().toString(), name, email);
|
|
454
|
+
return this.userRepository.save(user);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
`);
|
|
458
|
+
this.writeFile(root, "src/application/use-cases/get-user.use-case.ts", `
|
|
459
|
+
import { User } from "../../domain/entities/user.entity";
|
|
460
|
+
import { UserRepository } from "../../domain/repositories/user.repository";
|
|
461
|
+
import { NotFoundError } from "../../infrastructure/errors/app.error";
|
|
462
|
+
import { ICache } from "../../infrastructure/cache/cache.interface";
|
|
463
|
+
|
|
464
|
+
export class GetUserUseCase {
|
|
465
|
+
constructor(
|
|
466
|
+
private userRepository: UserRepository,
|
|
467
|
+
private cache: ICache
|
|
468
|
+
) {}
|
|
469
|
+
|
|
470
|
+
async execute(id: string): Promise<User> {
|
|
471
|
+
const cacheKey = \`user:\${id}\`;
|
|
472
|
+
const cached = await this.cache.get<User>(cacheKey);
|
|
473
|
+
if (cached) return cached;
|
|
474
|
+
|
|
475
|
+
const user = await this.userRepository.findById(id);
|
|
476
|
+
if (!user) throw new NotFoundError("User not found");
|
|
477
|
+
|
|
478
|
+
await this.cache.set(cacheKey, user, 3600);
|
|
479
|
+
return user;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
`);
|
|
483
|
+
// Infrastructure Layer
|
|
484
|
+
this.writeFile(root, "src/infrastructure/persistence/in-memory-user.repository.ts", `
|
|
485
|
+
import { User } from "../../domain/entities/user.entity";
|
|
486
|
+
import { UserRepository } from "../../domain/repositories/user.repository";
|
|
487
|
+
|
|
488
|
+
export class InMemoryUserRepository implements UserRepository {
|
|
489
|
+
private users: User[] = [];
|
|
490
|
+
|
|
491
|
+
async save(user: User): Promise<User> {
|
|
492
|
+
this.users.push(user);
|
|
493
|
+
return user;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
497
|
+
return this.users.find(u => u.email === email) || null;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async findById(id: string): Promise<User | null> {
|
|
501
|
+
return this.users.find(u => u.id === id) || null;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
`);
|
|
505
|
+
// Interface Adapters (Controllers)
|
|
506
|
+
this.writeFile(root, "src/interface-adapters/controllers/user.controller.ts", `
|
|
507
|
+
import { Request, Response } from "express";
|
|
508
|
+
import { StatusCodes } from "http-status-codes";
|
|
509
|
+
import { CreateUserUseCase } from "../../application/use-cases/create-user.use-case";
|
|
510
|
+
import { GetUserUseCase } from "../../application/use-cases/get-user.use-case";
|
|
511
|
+
|
|
512
|
+
export class UserController {
|
|
513
|
+
constructor(
|
|
514
|
+
private createUserUseCase: CreateUserUseCase,
|
|
515
|
+
private getUserUseCase: GetUserUseCase
|
|
516
|
+
) {}
|
|
517
|
+
|
|
518
|
+
async create(req: Request, res: Response) {
|
|
519
|
+
const { name, email } = req.body;
|
|
520
|
+
const user = await this.createUserUseCase.execute(name, email);
|
|
521
|
+
res.status(StatusCodes.CREATED).json(user);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async get(req: Request, res: Response) {
|
|
525
|
+
const { id } = req.params;
|
|
526
|
+
const user = await this.getUserUseCase.execute(id);
|
|
527
|
+
res.status(StatusCodes.OK).json(user);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
`);
|
|
531
|
+
// Main Entry Point
|
|
532
|
+
this.writeFile(root, "src/main.ts", `
|
|
533
|
+
import "express-async-errors";
|
|
534
|
+
import express from "express";
|
|
535
|
+
import cors from "cors";
|
|
536
|
+
import helmet from "helmet";
|
|
537
|
+
import morgan from "morgan";
|
|
538
|
+
import { v4 as uuidv4 } from "uuid";
|
|
539
|
+
import { config } from "./config";
|
|
540
|
+
import logger from "./infrastructure/logger";
|
|
541
|
+
import { errorMiddleware } from "./infrastructure/middleware/error.middleware";
|
|
542
|
+
import { InMemoryUserRepository } from "./infrastructure/persistence/in-memory-user.repository";
|
|
543
|
+
import { MemoryCache } from "./infrastructure/cache/memory.cache";
|
|
544
|
+
import { CreateUserUseCase } from "./application/use-cases/create-user.use-case";
|
|
545
|
+
import { GetUserUseCase } from "./application/use-cases/get-user.use-case";
|
|
546
|
+
import { UserController } from "./interface-adapters/controllers/user.controller";
|
|
547
|
+
import { HealthController } from "./interface-adapters/controllers/health.controller";
|
|
548
|
+
|
|
549
|
+
const app = express();
|
|
550
|
+
|
|
551
|
+
// Security & Middleware
|
|
552
|
+
app.use(helmet());
|
|
553
|
+
app.use(express.json());
|
|
554
|
+
app.use(cors());
|
|
555
|
+
app.use(morgan("combined"));
|
|
556
|
+
|
|
557
|
+
// Correlation ID & Logging
|
|
558
|
+
app.use((req, res, next) => {
|
|
559
|
+
const correlationId = (req.headers["x-correlation-id"] as string) || uuidv4();
|
|
560
|
+
req.headers["x-correlation-id"] = correlationId;
|
|
561
|
+
res.setHeader("X-Correlation-ID", correlationId);
|
|
562
|
+
next();
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// Dependency Injection
|
|
566
|
+
const userRepository = new InMemoryUserRepository();
|
|
567
|
+
const cache = new MemoryCache(); // Use RedisCache in production
|
|
568
|
+
const createUserUseCase = new CreateUserUseCase(userRepository);
|
|
569
|
+
const getUserUseCase = new GetUserUseCase(userRepository, cache);
|
|
570
|
+
const userController = new UserController(createUserUseCase, getUserUseCase);
|
|
571
|
+
const healthController = new HealthController();
|
|
572
|
+
|
|
573
|
+
// Routes
|
|
574
|
+
app.get("/health", (req, res) => healthController.health(req, res));
|
|
575
|
+
app.get("/ready", (req, res) => healthController.ready(req, res));
|
|
576
|
+
|
|
577
|
+
app.post("/users", (req, res) => userController.create(req, res));
|
|
578
|
+
app.get("/users/:id", (req, res) => userController.get(req, res));
|
|
579
|
+
|
|
580
|
+
// Error Handling
|
|
581
|
+
app.use(errorMiddleware);
|
|
582
|
+
|
|
583
|
+
const PORT = config.app.port;
|
|
584
|
+
app.listen(PORT, () => {
|
|
585
|
+
logger.info(\`🚀 Server running in \${config.app.env} mode on port \${PORT}\`);
|
|
586
|
+
});
|
|
587
|
+
`);
|
|
588
|
+
}
|
|
589
|
+
generateMVCArchitecture(root, options) {
|
|
590
|
+
// Model
|
|
591
|
+
this.writeFile(root, "src/models/user.model.ts", `
|
|
592
|
+
export interface User {
|
|
593
|
+
id: string;
|
|
594
|
+
name: string;
|
|
595
|
+
email: string;
|
|
596
|
+
}
|
|
597
|
+
`);
|
|
598
|
+
// Service (Business Logic)
|
|
599
|
+
this.writeFile(root, "src/services/user.service.ts", `
|
|
600
|
+
import { User } from "../models/user.model";
|
|
601
|
+
import { ICache } from "../infrastructure/cache/cache.interface";
|
|
602
|
+
import { BadRequestError, NotFoundError } from "../infrastructure/errors/app.error";
|
|
603
|
+
|
|
604
|
+
const users: User[] = [];
|
|
605
|
+
|
|
606
|
+
export class UserService {
|
|
607
|
+
constructor(private cache: ICache) {}
|
|
608
|
+
|
|
609
|
+
async create(name: string, email: string): Promise<User> {
|
|
610
|
+
if (users.find(u => u.email === email)) {
|
|
611
|
+
throw new BadRequestError("User already exists");
|
|
612
|
+
}
|
|
613
|
+
const user = { id: Date.now().toString(), name, email };
|
|
614
|
+
users.push(user);
|
|
615
|
+
return user;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
async getById(id: string): Promise<User> {
|
|
619
|
+
const cacheKey = \`user:\${id}\`;
|
|
620
|
+
const cached = await this.cache.get<User>(cacheKey);
|
|
621
|
+
if (cached) return cached;
|
|
622
|
+
|
|
623
|
+
const user = users.find(u => u.id === id);
|
|
624
|
+
if (!user) throw new NotFoundError("User not found");
|
|
625
|
+
|
|
626
|
+
await this.cache.set(cacheKey, user, 3600);
|
|
627
|
+
return user;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
`);
|
|
631
|
+
// Controller
|
|
632
|
+
this.writeFile(root, "src/controllers/user.controller.ts", `
|
|
633
|
+
import { Request, Response } from "express";
|
|
634
|
+
import { StatusCodes } from "http-status-codes";
|
|
635
|
+
import { UserService } from "../services/user.service";
|
|
636
|
+
|
|
637
|
+
export class UserController {
|
|
638
|
+
constructor(private userService: UserService) {}
|
|
639
|
+
|
|
640
|
+
async create(req: Request, res: Response) {
|
|
641
|
+
const { name, email } = req.body;
|
|
642
|
+
const user = await this.userService.create(name, email);
|
|
643
|
+
res.status(StatusCodes.CREATED).json(user);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
async get(req: Request, res: Response) {
|
|
647
|
+
const { id } = req.params;
|
|
648
|
+
const user = await this.userService.getById(id);
|
|
649
|
+
res.status(StatusCodes.OK).json(user);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
`);
|
|
653
|
+
// Routes
|
|
654
|
+
this.writeFile(root, "src/routes/user.routes.ts", `
|
|
655
|
+
import { Router } from "express";
|
|
656
|
+
import { UserController } from "../controllers/user.controller";
|
|
657
|
+
import { UserService } from "../services/user.service";
|
|
658
|
+
import { MemoryCache } from "../infrastructure/cache/memory.cache";
|
|
659
|
+
|
|
660
|
+
const router = Router();
|
|
661
|
+
const cache = new MemoryCache();
|
|
662
|
+
const userService = new UserService(cache);
|
|
663
|
+
const userController = new UserController(userService);
|
|
664
|
+
|
|
665
|
+
router.post("/", (req, res) => userController.create(req, res));
|
|
666
|
+
router.get("/:id", (req, res) => userController.get(req, res));
|
|
667
|
+
|
|
668
|
+
export default router;
|
|
669
|
+
`);
|
|
670
|
+
// Main
|
|
671
|
+
this.writeFile(root, "src/main.ts", `
|
|
672
|
+
import "express-async-errors";
|
|
673
|
+
import express from "express";
|
|
674
|
+
import cors from "cors";
|
|
675
|
+
import helmet from "helmet";
|
|
676
|
+
import morgan from "morgan";
|
|
677
|
+
import { config } from "./config";
|
|
678
|
+
import logger from "./infrastructure/logger";
|
|
679
|
+
import { errorMiddleware } from "./infrastructure/middleware/error.middleware";
|
|
680
|
+
import userRoutes from "./routes/user.routes";
|
|
681
|
+
import { HealthController } from "./interface-adapters/controllers/health.controller";
|
|
682
|
+
|
|
683
|
+
const app = express();
|
|
684
|
+
|
|
685
|
+
app.use(helmet());
|
|
686
|
+
app.use(express.json());
|
|
687
|
+
app.use(cors());
|
|
688
|
+
app.use(morgan("combined"));
|
|
689
|
+
|
|
690
|
+
const healthController = new HealthController();
|
|
691
|
+
app.get("/health", (req, res) => healthController.health(req, res));
|
|
692
|
+
app.get("/ready", (req, res) => healthController.ready(req, res));
|
|
693
|
+
|
|
694
|
+
app.use("/users", userRoutes);
|
|
695
|
+
|
|
696
|
+
app.use(errorMiddleware);
|
|
697
|
+
|
|
698
|
+
const PORT = config.app.port;
|
|
699
|
+
app.listen(PORT, () => {
|
|
700
|
+
logger.info(\`🚀 Server running in \${config.app.env} mode on port \${PORT}\`);
|
|
701
|
+
});
|
|
702
|
+
`);
|
|
703
|
+
}
|
|
704
|
+
generateLayeredArchitecture(root, options) {
|
|
705
|
+
// Core Layer
|
|
706
|
+
this.writeFile(root, "src/core/types/user.ts", `
|
|
707
|
+
export interface User {
|
|
708
|
+
id: string;
|
|
709
|
+
name: string;
|
|
710
|
+
email: string;
|
|
711
|
+
}
|
|
712
|
+
`);
|
|
713
|
+
// Services Layer
|
|
714
|
+
this.writeFile(root, "src/services/user.service.ts", `
|
|
715
|
+
import { User } from "../core/types/user";
|
|
716
|
+
import { ICache } from "../infrastructure/cache/cache.interface";
|
|
717
|
+
import { BadRequestError, NotFoundError } from "../infrastructure/errors/app.error";
|
|
718
|
+
|
|
719
|
+
const users: User[] = [];
|
|
720
|
+
|
|
721
|
+
export class UserService {
|
|
722
|
+
constructor(private cache: ICache) {}
|
|
723
|
+
|
|
724
|
+
async create(name: string, email: string): Promise<User> {
|
|
725
|
+
const user = { id: Date.now().toString(), name, email };
|
|
726
|
+
users.push(user);
|
|
727
|
+
return user;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
async getById(id: string): Promise<User> {
|
|
731
|
+
const cacheKey = \`user:\${id}\`;
|
|
732
|
+
const cached = await this.cache.get<User>(cacheKey);
|
|
733
|
+
if (cached) return cached;
|
|
734
|
+
|
|
735
|
+
const user = users.find(u => u.id === id);
|
|
736
|
+
if (!user) throw new NotFoundError("User not found");
|
|
737
|
+
|
|
738
|
+
await this.cache.set(cacheKey, user, 3600);
|
|
739
|
+
return user;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
`);
|
|
743
|
+
// API Layer
|
|
744
|
+
this.writeFile(root, "src/api/routes/user.routes.ts", `
|
|
745
|
+
import { Router } from "express";
|
|
746
|
+
import { UserService } from "../../services/user.service";
|
|
747
|
+
import { MemoryCache } from "../../infrastructure/cache/memory.cache";
|
|
748
|
+
import { StatusCodes } from "http-status-codes";
|
|
749
|
+
|
|
750
|
+
const router = Router();
|
|
751
|
+
const cache = new MemoryCache();
|
|
752
|
+
const userService = new UserService(cache);
|
|
753
|
+
|
|
754
|
+
router.post("/", async (req, res) => {
|
|
755
|
+
const user = await userService.create(req.body.name, req.body.email);
|
|
756
|
+
res.status(StatusCodes.CREATED).json(user);
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
router.get("/:id", async (req, res) => {
|
|
760
|
+
const user = await userService.getById(req.params.id);
|
|
761
|
+
res.status(StatusCodes.OK).json(user);
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
export default router;
|
|
765
|
+
`);
|
|
766
|
+
// Main
|
|
767
|
+
this.writeFile(root, "src/main.ts", `
|
|
768
|
+
import "express-async-errors";
|
|
769
|
+
import express from "express";
|
|
770
|
+
import helmet from "helmet";
|
|
771
|
+
import cors from "cors";
|
|
772
|
+
import { config } from "./config";
|
|
773
|
+
import logger from "./infrastructure/logger";
|
|
774
|
+
import { errorMiddleware } from "./infrastructure/middleware/error.middleware";
|
|
775
|
+
import userRoutes from "./api/routes/user.routes";
|
|
776
|
+
import { HealthController } from "./interface-adapters/controllers/health.controller";
|
|
777
|
+
|
|
778
|
+
const app = express();
|
|
779
|
+
app.use(helmet());
|
|
780
|
+
app.use(express.json());
|
|
781
|
+
app.use(cors());
|
|
782
|
+
|
|
783
|
+
const healthController = new HealthController();
|
|
784
|
+
app.get("/health", (req, res) => healthController.health(req, res));
|
|
785
|
+
app.get("/ready", (req, res) => healthController.ready(req, res));
|
|
786
|
+
|
|
787
|
+
app.use("/api/users", userRoutes);
|
|
788
|
+
|
|
789
|
+
app.use(errorMiddleware);
|
|
790
|
+
|
|
791
|
+
const PORT = config.app.port;
|
|
792
|
+
app.listen(PORT, () => {
|
|
793
|
+
logger.info(\`🚀 Server running in \${config.app.env} mode on port \${PORT}\`);
|
|
794
|
+
});
|
|
795
|
+
`);
|
|
796
|
+
}
|
|
797
|
+
generateDocker(root, options) {
|
|
798
|
+
const dbService = options.database === "postgresql" ? `
|
|
799
|
+
db:
|
|
800
|
+
image: postgres:15-alpine
|
|
801
|
+
environment:
|
|
802
|
+
- POSTGRES_USER=user
|
|
803
|
+
- POSTGRES_PASSWORD=password
|
|
804
|
+
- POSTGRES_DB=appdb
|
|
805
|
+
ports:
|
|
806
|
+
- "5432:5432"
|
|
807
|
+
` : options.database === "mysql" ? `
|
|
808
|
+
db:
|
|
809
|
+
image: mysql:8
|
|
810
|
+
environment:
|
|
811
|
+
- MYSQL_ROOT_PASSWORD=password
|
|
812
|
+
- MYSQL_DATABASE=appdb
|
|
813
|
+
ports:
|
|
814
|
+
- "3306:3306"
|
|
815
|
+
` : "";
|
|
816
|
+
const dbUrl = options.database === "postgresql" ? "postgresql://user:password@db:5432/appdb?schema=public" :
|
|
817
|
+
options.database === "mysql" ? "mysql://root:password@db:3306/appdb" : "";
|
|
818
|
+
this.writeFile(root, "Dockerfile", `
|
|
819
|
+
FROM node:18-alpine AS builder
|
|
820
|
+
WORKDIR /app
|
|
821
|
+
COPY package*.json ./
|
|
822
|
+
RUN npm ci
|
|
823
|
+
COPY . .
|
|
824
|
+
${options.orm === 'prisma' ? 'RUN npx prisma generate' : ''}
|
|
825
|
+
RUN npm run build
|
|
826
|
+
|
|
827
|
+
FROM node:18-alpine
|
|
828
|
+
WORKDIR /app
|
|
829
|
+
COPY --from=builder /app/dist ./dist
|
|
830
|
+
COPY --from=builder /app/package*.json ./
|
|
831
|
+
${options.orm === 'prisma' ? 'COPY --from=builder /app/prisma ./prisma' : ''}
|
|
832
|
+
RUN npm ci --production
|
|
833
|
+
${options.orm === 'prisma' ? 'RUN npx prisma generate' : ''}
|
|
834
|
+
EXPOSE 3000
|
|
835
|
+
CMD ["npm", "start"]
|
|
836
|
+
`);
|
|
837
|
+
this.writeFile(root, "docker-compose.yml", `
|
|
838
|
+
version: '3.8'
|
|
839
|
+
services:
|
|
840
|
+
app:
|
|
841
|
+
build: .
|
|
842
|
+
ports:
|
|
843
|
+
- "3000:3000"
|
|
844
|
+
environment:
|
|
845
|
+
- PORT=3000
|
|
846
|
+
- NODE_ENV=production
|
|
847
|
+
${dbUrl ? `- DATABASE_URL=\${DATABASE_URL:-${dbUrl}}` : ""}
|
|
848
|
+
- REDIS_URL=redis://cache:6379
|
|
849
|
+
depends_on:
|
|
850
|
+
${dbService ? "- db" : ""}
|
|
851
|
+
- cache
|
|
852
|
+
restart: always
|
|
853
|
+
${dbService}
|
|
854
|
+
cache:
|
|
855
|
+
image: redis:7-alpine
|
|
856
|
+
ports:
|
|
857
|
+
- "6379:6379"
|
|
858
|
+
`);
|
|
859
|
+
}
|
|
860
|
+
generateCI(root, options) {
|
|
861
|
+
this.writeFile(root, ".github/workflows/ci.yml", `
|
|
862
|
+
name: CI
|
|
863
|
+
|
|
864
|
+
on:
|
|
865
|
+
push:
|
|
866
|
+
branches: [ main ]
|
|
867
|
+
pull_request:
|
|
868
|
+
branches: [ main ]
|
|
869
|
+
|
|
870
|
+
jobs:
|
|
871
|
+
build:
|
|
872
|
+
runs-on: ubuntu-latest
|
|
873
|
+
|
|
874
|
+
steps:
|
|
875
|
+
- uses: actions/checkout@v3
|
|
876
|
+
- name: Use Node.js 18.x
|
|
877
|
+
uses: actions/setup-node@v3
|
|
878
|
+
with:
|
|
879
|
+
node-version: 18.x
|
|
880
|
+
cache: 'npm'
|
|
881
|
+
- run: npm ci
|
|
882
|
+
${options.orm === 'prisma' ? '- run: npx prisma generate' : ''}
|
|
883
|
+
- name: Architecture Sync & Validation
|
|
884
|
+
run: npx archforge sync
|
|
885
|
+
- run: npm run lint
|
|
886
|
+
- run: npm test
|
|
887
|
+
- run: npm run build
|
|
888
|
+
`);
|
|
889
|
+
}
|
|
890
|
+
generatePrismaSchema(root, options) {
|
|
891
|
+
const provider = options.database === "mongodb" ? "mongodb" :
|
|
892
|
+
options.database === "mysql" ? "mysql" :
|
|
893
|
+
options.database === "sqlite" ? "sqlite" : "postgresql";
|
|
894
|
+
const url = options.database === "sqlite" ? "file:./dev.db" : "env(\"DATABASE_URL\")";
|
|
895
|
+
this.writeFile(root, "prisma/schema.prisma", `
|
|
896
|
+
generator client {
|
|
897
|
+
provider = "prisma-client-js"
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
datasource db {
|
|
901
|
+
provider = "${provider}"
|
|
902
|
+
url = ${url}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
model User {
|
|
906
|
+
id String @id @default(uuid())
|
|
907
|
+
email String @unique
|
|
908
|
+
name String?
|
|
909
|
+
}
|
|
910
|
+
`);
|
|
911
|
+
this.writeFile(root, "src/infrastructure/database/prisma.ts", `
|
|
912
|
+
import { PrismaClient } from "@prisma/client";
|
|
913
|
+
|
|
914
|
+
const prisma = new PrismaClient();
|
|
915
|
+
|
|
916
|
+
export default prisma;
|
|
917
|
+
`);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
exports.ExpressGenerator = ExpressGenerator;
|