katax-cli 1.0.0 → 1.1.0

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.
Files changed (137) hide show
  1. package/README.md +241 -29
  2. package/dist/commands/add-endpoint.d.ts +1 -0
  3. package/dist/commands/add-endpoint.d.ts.map +1 -0
  4. package/dist/commands/add-endpoint.js +18 -39
  5. package/dist/commands/add-endpoint.js.map +1 -0
  6. package/dist/commands/deploy.d.ts +14 -0
  7. package/dist/commands/deploy.d.ts.map +1 -0
  8. package/dist/commands/deploy.js +416 -0
  9. package/dist/commands/deploy.js.map +1 -0
  10. package/dist/commands/generate-crud.d.ts +1 -0
  11. package/dist/commands/generate-crud.d.ts.map +1 -0
  12. package/dist/commands/generate-crud.js +1 -8
  13. package/dist/commands/generate-crud.js.map +1 -0
  14. package/dist/commands/generate-crud.refactored.d.ts +6 -0
  15. package/dist/commands/generate-crud.refactored.d.ts.map +1 -0
  16. package/dist/commands/generate-crud.refactored.js +139 -0
  17. package/dist/commands/generate-crud.refactored.js.map +1 -0
  18. package/dist/commands/info.d.ts +1 -0
  19. package/dist/commands/info.d.ts.map +1 -0
  20. package/dist/commands/info.js +1 -5
  21. package/dist/commands/info.js.map +1 -0
  22. package/dist/commands/init.d.ts +1 -0
  23. package/dist/commands/init.d.ts.map +1 -0
  24. package/dist/commands/init.js +9 -40
  25. package/dist/commands/init.js.map +1 -0
  26. package/dist/commands/init.refactored.d.ts +6 -0
  27. package/dist/commands/init.refactored.d.ts.map +1 -0
  28. package/dist/commands/init.refactored.js +224 -0
  29. package/dist/commands/init.refactored.js.map +1 -0
  30. package/dist/core/errors.d.ts +77 -0
  31. package/dist/core/errors.d.ts.map +1 -0
  32. package/dist/core/errors.js +130 -0
  33. package/dist/core/errors.js.map +1 -0
  34. package/dist/core/index.d.ts +3 -0
  35. package/dist/core/index.d.ts.map +1 -0
  36. package/dist/core/index.js +3 -0
  37. package/dist/core/index.js.map +1 -0
  38. package/dist/core/result.d.ts +26 -0
  39. package/dist/core/result.d.ts.map +1 -0
  40. package/dist/core/result.js +76 -0
  41. package/dist/core/result.js.map +1 -0
  42. package/dist/generators/controller-generator.d.ts +1 -0
  43. package/dist/generators/controller-generator.d.ts.map +1 -0
  44. package/dist/generators/controller-generator.js +1 -2
  45. package/dist/generators/controller-generator.js.map +1 -0
  46. package/dist/generators/handler-generator.d.ts +1 -0
  47. package/dist/generators/handler-generator.d.ts.map +1 -0
  48. package/dist/generators/handler-generator.js +35 -22
  49. package/dist/generators/handler-generator.js.map +1 -0
  50. package/dist/generators/route-generator.d.ts +1 -0
  51. package/dist/generators/route-generator.d.ts.map +1 -0
  52. package/dist/generators/route-generator.js +1 -2
  53. package/dist/generators/route-generator.js.map +1 -0
  54. package/dist/generators/router-updater.d.ts +1 -0
  55. package/dist/generators/router-updater.d.ts.map +1 -0
  56. package/dist/generators/router-updater.js +1 -6
  57. package/dist/generators/router-updater.js.map +1 -0
  58. package/dist/generators/validator-generator.d.ts +1 -0
  59. package/dist/generators/validator-generator.d.ts.map +1 -0
  60. package/dist/generators/validator-generator.improved.d.ts +3 -0
  61. package/dist/generators/validator-generator.improved.d.ts.map +1 -0
  62. package/dist/generators/validator-generator.improved.js +184 -0
  63. package/dist/generators/validator-generator.improved.js.map +1 -0
  64. package/dist/generators/validator-generator.js +1 -10
  65. package/dist/generators/validator-generator.js.map +1 -0
  66. package/dist/index.d.ts +1 -6
  67. package/dist/index.d.ts.map +1 -0
  68. package/dist/index.js +37 -19
  69. package/dist/index.js.map +1 -0
  70. package/dist/services/ast-router-updater.d.ts +16 -0
  71. package/dist/services/ast-router-updater.d.ts.map +1 -0
  72. package/dist/services/ast-router-updater.js +130 -0
  73. package/dist/services/ast-router-updater.js.map +1 -0
  74. package/dist/services/code-generation.service.d.ts +15 -0
  75. package/dist/services/code-generation.service.d.ts.map +1 -0
  76. package/dist/services/code-generation.service.js +110 -0
  77. package/dist/services/code-generation.service.js.map +1 -0
  78. package/dist/services/index.d.ts +4 -0
  79. package/dist/services/index.d.ts.map +1 -0
  80. package/dist/services/index.js +4 -0
  81. package/dist/services/index.js.map +1 -0
  82. package/dist/services/project-structure-generator.d.ts +30 -0
  83. package/dist/services/project-structure-generator.d.ts.map +1 -0
  84. package/dist/services/project-structure-generator.js +884 -0
  85. package/dist/services/project-structure-generator.js.map +1 -0
  86. package/dist/templates/base/code-builder.d.ts +34 -0
  87. package/dist/templates/base/code-builder.d.ts.map +1 -0
  88. package/dist/templates/base/code-builder.js +154 -0
  89. package/dist/templates/base/code-builder.js.map +1 -0
  90. package/dist/templates/base/template-engine.d.ts +28 -0
  91. package/dist/templates/base/template-engine.d.ts.map +1 -0
  92. package/dist/templates/base/template-engine.js +47 -0
  93. package/dist/templates/base/template-engine.js.map +1 -0
  94. package/dist/templates/generators/controller-template.d.ts +15 -0
  95. package/dist/templates/generators/controller-template.d.ts.map +1 -0
  96. package/dist/templates/generators/controller-template.js +185 -0
  97. package/dist/templates/generators/controller-template.js.map +1 -0
  98. package/dist/templates/generators/di-container.d.ts +6 -0
  99. package/dist/templates/generators/di-container.d.ts.map +1 -0
  100. package/dist/templates/generators/di-container.js +121 -0
  101. package/dist/templates/generators/di-container.js.map +1 -0
  102. package/dist/templates/generators/handler-template.d.ts +15 -0
  103. package/dist/templates/generators/handler-template.d.ts.map +1 -0
  104. package/dist/templates/generators/handler-template.js +208 -0
  105. package/dist/templates/generators/handler-template.js.map +1 -0
  106. package/dist/templates/generators/repository-template.d.ts +23 -0
  107. package/dist/templates/generators/repository-template.d.ts.map +1 -0
  108. package/dist/templates/generators/repository-template.js +237 -0
  109. package/dist/templates/generators/repository-template.js.map +1 -0
  110. package/dist/templates/generators/response-utils-template.d.ts +2 -0
  111. package/dist/templates/generators/response-utils-template.d.ts.map +1 -0
  112. package/dist/templates/generators/response-utils-template.js +112 -0
  113. package/dist/templates/generators/response-utils-template.js.map +1 -0
  114. package/dist/templates/generators/test-generator.d.ts +11 -0
  115. package/dist/templates/generators/test-generator.d.ts.map +1 -0
  116. package/dist/templates/generators/test-generator.js +206 -0
  117. package/dist/templates/generators/test-generator.js.map +1 -0
  118. package/dist/templates/index.d.ts +8 -0
  119. package/dist/templates/index.d.ts.map +1 -0
  120. package/dist/templates/index.js +8 -0
  121. package/dist/templates/index.js.map +1 -0
  122. package/dist/types/index.d.ts +1 -0
  123. package/dist/types/index.d.ts.map +1 -0
  124. package/dist/types/index.js +1 -0
  125. package/dist/types/index.js.map +1 -0
  126. package/dist/utils/file-utils.d.ts +1 -30
  127. package/dist/utils/file-utils.d.ts.map +1 -0
  128. package/dist/utils/file-utils.js +1 -32
  129. package/dist/utils/file-utils.js.map +1 -0
  130. package/dist/utils/logger.d.ts +1 -0
  131. package/dist/utils/logger.d.ts.map +1 -0
  132. package/dist/utils/logger.js +1 -0
  133. package/dist/utils/logger.js.map +1 -0
  134. package/package.json +1 -1
  135. package/dist/database/mongodb.ts +0 -46
  136. package/dist/database/mysql.ts +0 -26
  137. package/dist/database/postgresql.ts +0 -52
@@ -0,0 +1,884 @@
1
+ import path from 'path';
2
+ import { writeFile, ensureDir } from '../utils/file-utils.js';
3
+ import { CodeBuilder } from '../templates/base/code-builder.js';
4
+ export class ProjectStructureGenerator {
5
+ projectPath;
6
+ config;
7
+ constructor(projectPath, config) {
8
+ this.projectPath = projectPath;
9
+ this.config = config;
10
+ }
11
+ async generate() {
12
+ await this.createDirectories();
13
+ await this.generatePackageJson();
14
+ await this.generateTsConfig();
15
+ await this.generateEnvFile();
16
+ await this.generateGitIgnore();
17
+ await this.generateGitAttributes();
18
+ await this.generateIndexFile();
19
+ await this.generateAppFile();
20
+ await this.generateRoutesFile();
21
+ await this.generateCorsConfig();
22
+ await this.generateEnvValidator();
23
+ await this.generateErrorMiddleware();
24
+ await this.generateLoggerMiddleware();
25
+ await this.generateResultTypes();
26
+ await this.generateErrorTypes();
27
+ await this.generateLoggerUtils();
28
+ await this.generateResponseUtils();
29
+ if (this.config.authentication === 'jwt') {
30
+ await this.generateJwtUtils();
31
+ }
32
+ await this.generateDIContainer();
33
+ if (this.config.database !== 'none') {
34
+ await this.generateDatabaseConnection();
35
+ }
36
+ await this.generateHealthCheck();
37
+ await this.generateHelloExample();
38
+ }
39
+ async createDirectories() {
40
+ const dirs = [
41
+ 'src',
42
+ 'src/api',
43
+ 'src/api/hello',
44
+ 'src/api/health',
45
+ 'src/config',
46
+ 'src/middleware',
47
+ 'src/shared',
48
+ 'src/core'
49
+ ];
50
+ if (this.config.database !== 'none') {
51
+ dirs.push('src/database');
52
+ }
53
+ for (const dir of dirs) {
54
+ await ensureDir(path.join(this.projectPath, dir));
55
+ }
56
+ }
57
+ async generateIndexFile() {
58
+ const builder = new CodeBuilder();
59
+ builder
60
+ .importDefault('app', './app.js')
61
+ .importDefault('dotenv', 'dotenv')
62
+ .import(['logger'], './shared/logger.utils.js')
63
+ .import(['validateEnvironment'], './config/env.validator.js')
64
+ .line()
65
+ .line('dotenv.config();')
66
+ .line()
67
+ .comment('Validate required environment variables')
68
+ .line('validateEnvironment();')
69
+ .line()
70
+ .line(`const PORT = process.env.PORT || ${this.config.port};`)
71
+ .line()
72
+ .line('app.listen(PORT, () => {')
73
+ .line(` logger.info(\`Server running on http://localhost:\${PORT}\`);`)
74
+ .line(` logger.info(\`API endpoints available at http://localhost:\${PORT}/api\`);`)
75
+ .line(` logger.info(\`Health check: http://localhost:\${PORT}/api/health\`);`)
76
+ .line('});');
77
+ await writeFile(path.join(this.projectPath, 'src/index.ts'), builder.build());
78
+ }
79
+ async generateAppFile() {
80
+ const builder = new CodeBuilder();
81
+ builder
82
+ .importDefault('express', 'express')
83
+ .importDefault('cors', 'cors')
84
+ .importDefault('router', './api/routes.js')
85
+ .import(['errorMiddleware'], './middleware/error.middleware.js')
86
+ .import(['requestLogger'], './middleware/logger.middleware.js')
87
+ .import(['corsOptions'], './config/cors.config.js')
88
+ .line()
89
+ .line('const app = express();')
90
+ .line()
91
+ .comment('Middleware')
92
+ .line('app.use(cors(corsOptions));')
93
+ .line('app.use(express.json());')
94
+ .line('app.use(express.urlencoded({ extended: true }));')
95
+ .line('app.use(requestLogger);')
96
+ .line()
97
+ .comment('Root route')
98
+ .line("app.get('/', (req, res) => {")
99
+ .line(' res.json({')
100
+ .line(` message: 'Welcome to ${this.config.name} API',`)
101
+ .line(` version: '1.0.0',`)
102
+ .line(` endpoints: '/api',`)
103
+ .line(` health: '/api/health'`)
104
+ .line(' });')
105
+ .line('});')
106
+ .line()
107
+ .comment('API Routes')
108
+ .line("app.use('/api', router);")
109
+ .line()
110
+ .comment('Error handling')
111
+ .line('app.use(errorMiddleware);')
112
+ .line()
113
+ .line('export default app;');
114
+ await writeFile(path.join(this.projectPath, 'src/app.ts'), builder.build());
115
+ }
116
+ async generateRoutesFile() {
117
+ const builder = new CodeBuilder();
118
+ builder
119
+ .import(['Router'], 'express')
120
+ .importDefault('helloRouter', './hello/hello.routes.js')
121
+ .import(['healthCheckHandler'], './health/health.handler.js')
122
+ .line()
123
+ .line('const router = Router();')
124
+ .line()
125
+ .comment('Health check')
126
+ .line("router.get('/health', healthCheckHandler);")
127
+ .line()
128
+ .comment('Example endpoint')
129
+ .line("router.use('/hello', helloRouter);")
130
+ .line()
131
+ .line('export default router;');
132
+ await writeFile(path.join(this.projectPath, 'src/api/routes.ts'), builder.build());
133
+ }
134
+ async generateResultTypes() {
135
+ const fs = await import('fs/promises');
136
+ const sourceFile = path.join(process.cwd(), 'src/core/result.ts');
137
+ const destFile = path.join(this.projectPath, 'src/core/result.ts');
138
+ try {
139
+ const content = await fs.readFile(sourceFile, 'utf-8');
140
+ await writeFile(destFile, content);
141
+ }
142
+ catch (error) {
143
+ const builder = new CodeBuilder();
144
+ builder
145
+ .comment('Type-safe Result pattern')
146
+ .line('export type Ok<T> = { readonly ok: true; readonly value: T };')
147
+ .line('export type Err<E> = { readonly ok: false; readonly error: E };')
148
+ .line('export type Result<T, E = Error> = Ok<T> | Err<E>;')
149
+ .line()
150
+ .line('export function ok<T>(value: T): Ok<T> {')
151
+ .line(' return { ok: true, value };')
152
+ .line('}')
153
+ .line()
154
+ .line('export function err<E>(error: E): Err<E> {')
155
+ .line(' return { ok: false, error };')
156
+ .line('}')
157
+ .line()
158
+ .line('export function isOk<T, E>(result: Result<T, E>): result is Ok<T> {')
159
+ .line(' return result.ok === true;')
160
+ .line('}');
161
+ await writeFile(destFile, builder.build());
162
+ }
163
+ }
164
+ async generateErrorTypes() {
165
+ const fs = await import('fs/promises');
166
+ const sourceFile = path.join(process.cwd(), 'src/core/errors.ts');
167
+ const destFile = path.join(this.projectPath, 'src/core/errors.ts');
168
+ try {
169
+ const content = await fs.readFile(sourceFile, 'utf-8');
170
+ await writeFile(destFile, content);
171
+ }
172
+ catch (error) {
173
+ const builder = new CodeBuilder();
174
+ builder
175
+ .comment('Application error hierarchy')
176
+ .line('export abstract class AppError extends Error {')
177
+ .line(' abstract readonly statusCode: number;')
178
+ .line(' abstract readonly code: string;')
179
+ .line(' readonly timestamp: Date = new Date();')
180
+ .line()
181
+ .line(' constructor(message: string, public readonly details?: Record<string, any>) {')
182
+ .line(' super(message);')
183
+ .line(' this.name = this.constructor.name;')
184
+ .line(' Error.captureStackTrace(this, this.constructor);')
185
+ .line(' }')
186
+ .line('}')
187
+ .line()
188
+ .line('export class ValidationError extends AppError {')
189
+ .line(' readonly statusCode = 400;')
190
+ .line(' readonly code = "VALIDATION_ERROR";')
191
+ .line('}')
192
+ .line()
193
+ .line('export class NotFoundError extends AppError {')
194
+ .line(' readonly statusCode = 404;')
195
+ .line(' readonly code = "NOT_FOUND";')
196
+ .line('}');
197
+ await writeFile(destFile, builder.build());
198
+ }
199
+ }
200
+ async generateLoggerUtils() {
201
+ const builder = new CodeBuilder();
202
+ builder
203
+ .importDefault('pino', 'pino')
204
+ .line()
205
+ .line("const isDev = process.env.NODE_ENV !== 'production';")
206
+ .line()
207
+ .line('export const logger = pino({')
208
+ .line(' level: process.env.LOG_LEVEL || (isDev ? "debug" : "info"),')
209
+ .line(' transport: isDev')
210
+ .line(' ? {')
211
+ .line(' target: "pino-pretty",')
212
+ .line(' options: {')
213
+ .line(' colorize: true,')
214
+ .line(' translateTime: "SYS:standard",')
215
+ .line(' ignore: "pid,hostname"')
216
+ .line(' }')
217
+ .line(' }')
218
+ .line(' : undefined')
219
+ .line('});')
220
+ .line()
221
+ .line('export type Logger = typeof logger;');
222
+ await writeFile(path.join(this.projectPath, 'src/shared/logger.utils.ts'), builder.build());
223
+ }
224
+ async generateResponseUtils() {
225
+ const { generateResponseUtils } = await import('../templates/generators/response-utils-template.js');
226
+ const content = generateResponseUtils();
227
+ await writeFile(path.join(this.projectPath, 'src/shared/response.utils.ts'), content);
228
+ }
229
+ async generatePackageJson() {
230
+ const packageJson = {
231
+ name: this.config.name,
232
+ version: '1.0.0',
233
+ description: this.config.description,
234
+ type: 'module',
235
+ main: 'dist/index.js',
236
+ scripts: {
237
+ dev: 'nodemon --watch src --exec tsx src/index.ts',
238
+ build: 'tsc',
239
+ start: 'node dist/index.js',
240
+ 'type-check': 'tsc --noEmit'
241
+ },
242
+ keywords: ['api', 'express', 'typescript'],
243
+ author: '',
244
+ license: 'MIT',
245
+ dependencies: {
246
+ express: '^4.18.2',
247
+ cors: '^2.8.5',
248
+ dotenv: '^16.3.1',
249
+ pino: '^8.17.2',
250
+ 'pino-pretty': '^10.3.1',
251
+ ...(this.config.validation === 'katax-core' && { 'katax-core': '^1.1.0' }),
252
+ ...(this.config.authentication === 'jwt' && {
253
+ jsonwebtoken: '^9.0.2',
254
+ bcrypt: '^5.1.1'
255
+ }),
256
+ ...(this.config.database === 'postgresql' && { pg: '^8.11.3' }),
257
+ ...(this.config.database === 'mysql' && { mysql2: '^3.6.5' }),
258
+ ...(this.config.database === 'mongodb' && { mongodb: '^6.3.0' })
259
+ },
260
+ devDependencies: {
261
+ '@types/express': '^4.17.21',
262
+ '@types/cors': '^2.8.17',
263
+ '@types/node': '^20.10.6',
264
+ ...(this.config.authentication === 'jwt' && {
265
+ '@types/jsonwebtoken': '^9.0.5',
266
+ '@types/bcrypt': '^5.0.2'
267
+ }),
268
+ ...(this.config.database === 'postgresql' && { '@types/pg': '^8.10.9' }),
269
+ typescript: '^5.3.3',
270
+ tsx: '^4.19.2',
271
+ nodemon: '^3.1.7'
272
+ }
273
+ };
274
+ await writeFile(path.join(this.projectPath, 'package.json'), JSON.stringify(packageJson, null, 2));
275
+ }
276
+ async generateTsConfig() {
277
+ const tsconfig = {
278
+ compilerOptions: {
279
+ target: 'ES2022',
280
+ module: 'ESNext',
281
+ lib: ['ES2022'],
282
+ moduleResolution: 'node',
283
+ esModuleInterop: true,
284
+ allowSyntheticDefaultImports: true,
285
+ strict: true,
286
+ skipLibCheck: true,
287
+ forceConsistentCasingInFileNames: true,
288
+ resolveJsonModule: true,
289
+ declaration: true,
290
+ declarationMap: true,
291
+ sourceMap: true,
292
+ removeComments: true,
293
+ rootDir: './src',
294
+ outDir: './dist'
295
+ },
296
+ include: ['src/**/*'],
297
+ exclude: ['node_modules', 'dist']
298
+ };
299
+ await writeFile(path.join(this.projectPath, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2));
300
+ }
301
+ async generateEnvFile() {
302
+ const lines = [
303
+ '# Server Configuration',
304
+ `PORT=${this.config.port}`,
305
+ `NODE_ENV=development`,
306
+ ''
307
+ ];
308
+ if (this.config.database !== 'none' && this.config.dbConfig) {
309
+ lines.push('# Database Configuration');
310
+ lines.push(`DB_HOST=${this.config.dbConfig.host || 'localhost'}`);
311
+ lines.push(`DB_PORT=${this.config.dbConfig.port || ''}`);
312
+ if (this.config.dbConfig.user)
313
+ lines.push(`DB_USER=${this.config.dbConfig.user}`);
314
+ if (this.config.dbConfig.password)
315
+ lines.push(`DB_PASSWORD=${this.config.dbConfig.password}`);
316
+ if (this.config.dbConfig.database)
317
+ lines.push(`DB_NAME=${this.config.dbConfig.database}`);
318
+ lines.push('');
319
+ lines.push('# Connection Pool Settings (optional - defaults are set)');
320
+ lines.push('DB_POOL_MAX=20 # Maximum connections in pool');
321
+ lines.push('DB_POOL_MIN=2 # Minimum connections in pool');
322
+ lines.push('');
323
+ }
324
+ if (this.config.authentication === 'jwt') {
325
+ lines.push('# JWT Configuration');
326
+ lines.push('JWT_SECRET=your-secret-key-here');
327
+ lines.push('JWT_REFRESH_SECRET=your-refresh-secret-here');
328
+ lines.push('JWT_EXPIRES_IN=15m');
329
+ lines.push('JWT_REFRESH_EXPIRES_IN=7d');
330
+ lines.push('');
331
+ }
332
+ await writeFile(path.join(this.projectPath, '.env'), lines.join('\n'));
333
+ }
334
+ async generateGitIgnore() {
335
+ const content = `node_modules/
336
+ dist/
337
+ .env
338
+ .env.local
339
+ .env.*.local
340
+ *.log
341
+ .DS_Store
342
+ coverage/
343
+ .vscode/
344
+ .idea/
345
+ `;
346
+ await writeFile(path.join(this.projectPath, '.gitignore'), content);
347
+ }
348
+ async generateGitAttributes() {
349
+ const content = `# Auto normalize line endings to LF
350
+ * text=auto eol=lf
351
+
352
+ # Explicit file types
353
+ *.ts text eol=lf
354
+ *.js text eol=lf
355
+ *.json text eol=lf
356
+ *.md text eol=lf
357
+ `;
358
+ await writeFile(path.join(this.projectPath, '.gitattributes'), content);
359
+ }
360
+ async generateCorsConfig() {
361
+ const builder = new CodeBuilder();
362
+ builder
363
+ .import(['CorsOptions'], 'cors')
364
+ .line()
365
+ .line('const allowedOrigins = process.env.ALLOWED_ORIGINS')
366
+ .line(' ? process.env.ALLOWED_ORIGINS.split(",")')
367
+ .line(' : ["http://localhost:3000"];')
368
+ .line()
369
+ .line('export const corsOptions: CorsOptions = {')
370
+ .line(' origin: (origin, callback) => {')
371
+ .line(' if (!origin || allowedOrigins.includes(origin)) {')
372
+ .line(' callback(null, true);')
373
+ .line(' } else {')
374
+ .line(' callback(new Error("Not allowed by CORS"));')
375
+ .line(' }')
376
+ .line(' },')
377
+ .line(' credentials: true')
378
+ .line('};');
379
+ await writeFile(path.join(this.projectPath, 'src/config/cors.config.ts'), builder.build());
380
+ }
381
+ async generateEnvValidator() {
382
+ const builder = new CodeBuilder();
383
+ if (this.config.validation === 'katax-core') {
384
+ builder
385
+ .import(['k'], 'katax-core')
386
+ .line()
387
+ .line('const envSchema = k.object({')
388
+ .line(' PORT: k.string().optional(),')
389
+ .line(` NODE_ENV: k.enum(['development', 'production', 'test']).optional(),`);
390
+ if (this.config.database !== 'none') {
391
+ builder
392
+ .line(' DB_HOST: k.string(),')
393
+ .line(' DB_PORT: k.string(),')
394
+ .line(' DB_USER: k.string(),')
395
+ .line(' DB_PASSWORD: k.string(),')
396
+ .line(' DB_NAME: k.string(),');
397
+ }
398
+ if (this.config.authentication === 'jwt') {
399
+ builder
400
+ .line(' JWT_SECRET: k.string(),')
401
+ .line(' JWT_REFRESH_SECRET: k.string(),');
402
+ }
403
+ builder
404
+ .line('});')
405
+ .line()
406
+ .line('export function validateEnvironment(): void {')
407
+ .line(' const result = envSchema.safeParse(process.env);')
408
+ .line(' if (!result.success) {')
409
+ .line(' console.error("❌ Invalid environment variables:");')
410
+ .line(' console.error(result.errors);')
411
+ .line(' process.exit(1);')
412
+ .line(' }')
413
+ .line('}');
414
+ }
415
+ else {
416
+ builder
417
+ .line('export function validateEnvironment(): void {')
418
+ .line(' // Add environment validation here')
419
+ .line('}');
420
+ }
421
+ await writeFile(path.join(this.projectPath, 'src/config/env.validator.ts'), builder.build());
422
+ }
423
+ async generateErrorMiddleware() {
424
+ const builder = new CodeBuilder();
425
+ builder
426
+ .import(['Request', 'Response', 'NextFunction'], 'express')
427
+ .import(['isAppError', 'InternalServerError'], '../core/errors.js')
428
+ .import(['logger'], '../shared/logger.utils.js')
429
+ .line()
430
+ .comment('Global error handling middleware')
431
+ .line('export function errorMiddleware(')
432
+ .line(' err: Error,')
433
+ .line(' req: Request,')
434
+ .line(' res: Response,')
435
+ .line(' next: NextFunction')
436
+ .line('): void {')
437
+ .line(' logger.error("Error occurred:", err);')
438
+ .line()
439
+ .line(' const error = isAppError(err) ? err : InternalServerError.fromError(err);')
440
+ .line()
441
+ .line(' res.status(error.statusCode).json({')
442
+ .line(' success: false,')
443
+ .line(' error: error.toJSON()')
444
+ .line(' });')
445
+ .line('}');
446
+ await writeFile(path.join(this.projectPath, 'src/middleware/error.middleware.ts'), builder.build());
447
+ }
448
+ async generateLoggerMiddleware() {
449
+ const builder = new CodeBuilder();
450
+ builder
451
+ .import(['Request', 'Response', 'NextFunction'], 'express')
452
+ .import(['logger'], '../shared/logger.utils.js')
453
+ .line()
454
+ .comment('Request logging middleware')
455
+ .line('export function requestLogger(')
456
+ .line(' req: Request,')
457
+ .line(' res: Response,')
458
+ .line(' next: NextFunction')
459
+ .line('): void {')
460
+ .line(' const start = Date.now();')
461
+ .line()
462
+ .line(' res.on("finish", () => {')
463
+ .line(' const duration = Date.now() - start;')
464
+ .line(' logger.info({')
465
+ .line(' method: req.method,')
466
+ .line(' url: req.url,')
467
+ .line(' status: res.statusCode,')
468
+ .line(' duration: `${duration}ms`')
469
+ .line(' });')
470
+ .line(' });')
471
+ .line()
472
+ .line(' next();')
473
+ .line('}');
474
+ await writeFile(path.join(this.projectPath, 'src/middleware/logger.middleware.ts'), builder.build());
475
+ }
476
+ async generateJwtUtils() {
477
+ const content = `import jwt from 'jsonwebtoken';
478
+ import { Request, Response, NextFunction } from 'express';
479
+
480
+ export function generateTokens(payload: any) {
481
+ const accessToken = jwt.sign(payload, process.env.JWT_SECRET!, {
482
+ expiresIn: process.env.JWT_EXPIRES_IN || '15m'
483
+ });
484
+
485
+ const refreshToken = jwt.sign(payload, process.env.JWT_REFRESH_SECRET!, {
486
+ expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d'
487
+ });
488
+
489
+ return { accessToken, refreshToken };
490
+ }
491
+
492
+ export function authenticateToken(req: Request, res: Response, next: NextFunction): void {
493
+ const authHeader = req.headers['authorization'];
494
+ const token = authHeader && authHeader.split(' ')[1];
495
+
496
+ if (!token) {
497
+ res.status(401).json({ success: false, error: 'No token provided' });
498
+ return;
499
+ }
500
+
501
+ jwt.verify(token, process.env.JWT_SECRET!, (err, user) => {
502
+ if (err) {
503
+ res.status(403).json({ success: false, error: 'Invalid token' });
504
+ return;
505
+ }
506
+ (req as any).user = user;
507
+ next();
508
+ });
509
+ }
510
+ `;
511
+ await writeFile(path.join(this.projectPath, 'src/shared/jwt.utils.ts'), content);
512
+ }
513
+ async generateDatabaseConnection() {
514
+ const builder = new CodeBuilder();
515
+ if (this.config.database === 'postgresql') {
516
+ builder
517
+ .import(['Pool', 'PoolClient'], 'pg')
518
+ .importDefault('dotenv', 'dotenv')
519
+ .line()
520
+ .line('dotenv.config();')
521
+ .line()
522
+ .comment('PostgreSQL connection pool with production settings')
523
+ .line('const pool = new Pool({')
524
+ .line(' host: process.env.DB_HOST || "localhost",')
525
+ .line(' port: Number(process.env.DB_PORT) || 5432,')
526
+ .line(' database: process.env.DB_NAME,')
527
+ .line(' user: process.env.DB_USER,')
528
+ .line(' password: process.env.DB_PASSWORD,')
529
+ .line(' // Connection pool settings')
530
+ .line(' max: Number(process.env.DB_POOL_MAX) || 20, // Maximum pool size')
531
+ .line(' min: Number(process.env.DB_POOL_MIN) || 2, // Minimum pool size')
532
+ .line(' idleTimeoutMillis: 30000, // Close idle clients after 30s')
533
+ .line(' connectionTimeoutMillis: 5000, // Connection timeout')
534
+ .line(' // Retry and error handling')
535
+ .line(' allowExitOnIdle: false, // Keep pool alive')
536
+ .line(' statement_timeout: 30000, // 30s query timeout')
537
+ .line('});')
538
+ .line()
539
+ .comment('Connection event handlers')
540
+ .line('pool.on("connect", (client: PoolClient) => {')
541
+ .line(' console.log("✅ New PostgreSQL client connected");')
542
+ .line(' // Set session-level settings if needed')
543
+ .line(' // client.query("SET timezone = \'UTC\'");')
544
+ .line('});')
545
+ .line()
546
+ .line('pool.on("acquire", () => {')
547
+ .line(' console.log("📤 PostgreSQL client checked out from pool");')
548
+ .line('});')
549
+ .line()
550
+ .line('pool.on("remove", () => {')
551
+ .line(' console.log("🗑️ PostgreSQL client removed from pool");')
552
+ .line('});')
553
+ .line()
554
+ .line('pool.on("error", (err: Error) => {')
555
+ .line(' console.error("❌ PostgreSQL pool error:", err);')
556
+ .line(' // Don\'t exit - let the pool handle reconnection')
557
+ .line('});')
558
+ .line()
559
+ .comment('Test connection on startup')
560
+ .line('pool.query("SELECT NOW()")')
561
+ .line(' .then(() => console.log("✅ PostgreSQL connection successful"))')
562
+ .line(' .catch((err) => {')
563
+ .line(' console.error("❌ PostgreSQL connection failed:", err);')
564
+ .line(' process.exit(1);')
565
+ .line(' });')
566
+ .line()
567
+ .comment('Graceful shutdown')
568
+ .line('process.on("SIGTERM", async () => {')
569
+ .line(' console.log("🛑 SIGTERM received, closing PostgreSQL pool...");')
570
+ .line(' await pool.end();')
571
+ .line(' console.log("✅ PostgreSQL pool closed");')
572
+ .line(' process.exit(0);')
573
+ .line('});')
574
+ .line()
575
+ .comment('Health check function')
576
+ .line('export async function checkDatabaseHealth(): Promise<boolean> {')
577
+ .line(' try {')
578
+ .line(' const result = await pool.query("SELECT 1 as health");')
579
+ .line(' return result.rows[0].health === 1;')
580
+ .line(' } catch (error) {')
581
+ .line(' console.error("Database health check failed:", error);')
582
+ .line(' return false;')
583
+ .line(' }')
584
+ .line('}')
585
+ .line()
586
+ .line('export default pool;');
587
+ }
588
+ else if (this.config.database === 'mysql') {
589
+ builder
590
+ .importDefault('mysql', 'mysql2/promise')
591
+ .importDefault('dotenv', 'dotenv')
592
+ .line()
593
+ .line('dotenv.config();')
594
+ .line()
595
+ .comment('MySQL connection pool with production settings')
596
+ .line('const pool = mysql.createPool({')
597
+ .line(' host: process.env.DB_HOST || "localhost",')
598
+ .line(' port: Number(process.env.DB_PORT) || 3306,')
599
+ .line(' database: process.env.DB_NAME,')
600
+ .line(' user: process.env.DB_USER,')
601
+ .line(' password: process.env.DB_PASSWORD,')
602
+ .line(' // Connection pool settings')
603
+ .line(' waitForConnections: true, // Wait for available connection')
604
+ .line(' connectionLimit: Number(process.env.DB_POOL_MAX) || 10, // Max connections')
605
+ .line(' maxIdle: 10, // Max idle connections')
606
+ .line(' idleTimeout: 60000, // Idle timeout (60s)')
607
+ .line(' queueLimit: 0, // No limit on queue')
608
+ .line(' enableKeepAlive: true, // Keep connections alive')
609
+ .line(' keepAliveInitialDelay: 0, // Start keep-alive immediately')
610
+ .line(' // Timeouts')
611
+ .line(' connectTimeout: 10000, // 10s connection timeout')
612
+ .line(' acquireTimeout: 10000, // 10s acquire timeout')
613
+ .line(' timeout: 30000, // 30s query timeout')
614
+ .line(' // Charset and timezone')
615
+ .line(' charset: "utf8mb4", // UTF-8 support')
616
+ .line(' timezone: "Z", // UTC timezone')
617
+ .line('});')
618
+ .line()
619
+ .comment('Test connection on startup')
620
+ .line('pool.getConnection()')
621
+ .line(' .then((connection) => {')
622
+ .line(' console.log("✅ MySQL connection successful");')
623
+ .line(' connection.release();')
624
+ .line(' })')
625
+ .line(' .catch((err) => {')
626
+ .line(' console.error("❌ MySQL connection failed:", err);')
627
+ .line(' process.exit(1);')
628
+ .line(' });')
629
+ .line()
630
+ .comment('Graceful shutdown')
631
+ .line('process.on("SIGTERM", async () => {')
632
+ .line(' console.log("🛑 SIGTERM received, closing MySQL pool...");')
633
+ .line(' await pool.end();')
634
+ .line(' console.log("✅ MySQL pool closed");')
635
+ .line(' process.exit(0);')
636
+ .line('});')
637
+ .line()
638
+ .comment('Health check function')
639
+ .line('export async function checkDatabaseHealth(): Promise<boolean> {')
640
+ .line(' try {')
641
+ .line(' const [rows] = await pool.query("SELECT 1 as health");')
642
+ .line(' return Array.isArray(rows) && rows.length > 0;')
643
+ .line(' } catch (error) {')
644
+ .line(' console.error("Database health check failed:", error);')
645
+ .line(' return false;')
646
+ .line(' }')
647
+ .line('}')
648
+ .line()
649
+ .line('export default pool;');
650
+ }
651
+ else if (this.config.database === 'mongodb') {
652
+ builder
653
+ .import(['MongoClient', 'Db', 'MongoClientOptions'], 'mongodb')
654
+ .importDefault('dotenv', 'dotenv')
655
+ .line()
656
+ .line('dotenv.config();')
657
+ .line()
658
+ .comment('MongoDB connection configuration')
659
+ .line('const user = process.env.DB_USER;')
660
+ .line('const password = process.env.DB_PASSWORD;')
661
+ .line('const host = process.env.DB_HOST || "localhost:27017";')
662
+ .line('const dbName = process.env.DB_NAME;')
663
+ .line()
664
+ .comment('Build connection URI')
665
+ .line('const isAtlas = host.includes(".mongodb.net");')
666
+ .line('const protocol = isAtlas ? "mongodb+srv" : "mongodb";')
667
+ .line('const authString = user && password ? `${user}:${password}@` : "";')
668
+ .line('const uri = `${protocol}://${authString}${host}/${dbName}?retryWrites=true&w=majority`;')
669
+ .line()
670
+ .comment('MongoDB client options - Production settings')
671
+ .line('const options: MongoClientOptions = {')
672
+ .line(' maxPoolSize: Number(process.env.DB_POOL_MAX) || 10, // Max connections')
673
+ .line(' minPoolSize: Number(process.env.DB_POOL_MIN) || 2, // Min connections')
674
+ .line(' maxIdleTimeMS: 60000, // Close idle after 60s')
675
+ .line(' serverSelectionTimeoutMS: 5000, // 5s timeout')
676
+ .line(' socketTimeoutMS: 45000, // 45s socket timeout')
677
+ .line(' connectTimeoutMS: 10000, // 10s connection timeout')
678
+ .line(' retryWrites: true, // Retry writes')
679
+ .line(' retryReads: true, // Retry reads')
680
+ .line('};')
681
+ .line()
682
+ .line('const client = new MongoClient(uri, options);')
683
+ .line('let db: Db | null = null;')
684
+ .line()
685
+ .comment('Connect to MongoDB')
686
+ .line('client.connect()')
687
+ .line(' .then(() => {')
688
+ .line(' console.log("✅ MongoDB connected successfully");')
689
+ .line(' db = client.db(dbName);')
690
+ .line(' // Ping to verify connection')
691
+ .line(' return db.admin().ping();')
692
+ .line(' })')
693
+ .line(' .then(() => console.log("✅ MongoDB ping successful"))')
694
+ .line(' .catch((err) => {')
695
+ .line(' console.error("❌ MongoDB connection failed:", err);')
696
+ .line(' process.exit(1);')
697
+ .line(' });')
698
+ .line()
699
+ .comment('Graceful shutdown')
700
+ .line('process.on("SIGTERM", async () => {')
701
+ .line(' console.log("🛑 SIGTERM received, closing MongoDB connection...");')
702
+ .line(' await client.close();')
703
+ .line(' console.log("✅ MongoDB connection closed");')
704
+ .line(' process.exit(0);')
705
+ .line('});')
706
+ .line()
707
+ .comment('Get database instance')
708
+ .line('export function getDatabase(): Db {')
709
+ .line(' if (!db) {')
710
+ .line(' throw new Error("Database not initialized. Call connect() first.");')
711
+ .line(' }')
712
+ .line(' return db;')
713
+ .line('}')
714
+ .line()
715
+ .comment('Health check function')
716
+ .line('export async function checkDatabaseHealth(): Promise<boolean> {')
717
+ .line(' try {')
718
+ .line(' const database = getDatabase();')
719
+ .line(' await database.admin().ping();')
720
+ .line(' return true;')
721
+ .line(' } catch (error) {')
722
+ .line(' console.error("Database health check failed:", error);')
723
+ .line(' return false;')
724
+ .line(' }')
725
+ .line('}')
726
+ .line()
727
+ .line('export default client;');
728
+ }
729
+ await writeFile(path.join(this.projectPath, 'src/database/connection.ts'), builder.build());
730
+ }
731
+ async generateDIContainer() {
732
+ const { generateDIContainer } = await import('../templates/generators/di-container.js');
733
+ const content = generateDIContainer({
734
+ hasDatabase: !!this.config.database && this.config.database !== 'none',
735
+ database: this.config.database !== 'none' ? this.config.database : undefined
736
+ });
737
+ await writeFile(path.join(this.projectPath, 'src/container.ts'), content);
738
+ }
739
+ async generateHelloExample() {
740
+ await ensureDir(path.join(this.projectPath, 'src/api/hello'));
741
+ const validatorContent = `import { k, kataxInfer } from 'katax-core';
742
+
743
+ // ==================== SCHEMAS ====================
744
+
745
+ export const helloQuerySchema = k.object({
746
+ name: k.string().min(1).max(50).optional()
747
+ });
748
+
749
+ export type HelloQueryData = kataxInfer<typeof helloQuerySchema>;
750
+
751
+ // ==================== VALIDATORS ====================
752
+
753
+ export async function validateHelloQuery(data: unknown) {
754
+ return await helloQuerySchema.safeParse(data);
755
+ }
756
+ `;
757
+ await writeFile(path.join(this.projectPath, 'src/api/hello/hello.validator.ts'), validatorContent);
758
+ const controllerContent = `import { Result, ok } from '../../core/result.js';
759
+ import { AppError } from '../../core/errors.js';
760
+ import { HelloQueryData } from './hello.validator.js';
761
+ import { logger } from '../../shared/logger.utils.js';
762
+
763
+ /**
764
+ * Hello controller - Example endpoint
765
+ */
766
+ export class HelloController {
767
+ async greet(query: HelloQueryData): Promise<Result<{ message: string }, AppError>> {
768
+ const name = query.name || 'World';
769
+ logger.info('Greeting', { name });
770
+
771
+ return ok({ message: \`Hello, \${name}! Welcome to ${this.config.name} API.\` });
772
+ }
773
+ }
774
+ `;
775
+ await writeFile(path.join(this.projectPath, 'src/api/hello/hello.controller.ts'), controllerContent);
776
+ const handlerContent = `import { Request, Response } from 'express';
777
+ import { isOk } from '../../core/result.js';
778
+ import { isAppError, InternalServerError } from '../../core/errors.js';
779
+ import { HelloController } from './hello.controller.js';
780
+ import { validateHelloQuery } from './hello.validator.js';
781
+
782
+ const helloController = new HelloController();
783
+
784
+ /**
785
+ * Get hello handler
786
+ */
787
+ export async function getHelloHandler(req: Request, res: Response): Promise<void> {
788
+ // Validate query params
789
+ const validationResult = await validateHelloQuery(req.query);
790
+ if (!validationResult.success) {
791
+ res.status(400).json({
792
+ success: false,
793
+ error: {
794
+ code: 'VALIDATION_ERROR',
795
+ message: 'Invalid query parameters',
796
+ errors: validationResult.errors
797
+ }
798
+ });
799
+ return;
800
+ }
801
+
802
+ const result = await helloController.greet(validationResult.data);
803
+
804
+ if (isOk(result)) {
805
+ res.status(200).json({
806
+ success: true,
807
+ data: result.value
808
+ });
809
+ return;
810
+ }
811
+
812
+ const error = isAppError(result.error) ? result.error : InternalServerError.fromError(result.error as Error);
813
+ res.status(error.statusCode).json({
814
+ success: false,
815
+ error: error.toJSON()
816
+ });
817
+ }
818
+ `;
819
+ await writeFile(path.join(this.projectPath, 'src/api/hello/hello.handler.ts'), handlerContent);
820
+ const routesContent = `import { Router } from 'express';
821
+ import { getHelloHandler } from './hello.handler.js';
822
+
823
+ const router = Router();
824
+
825
+ router.get('/', getHelloHandler);
826
+
827
+ export default router;
828
+ `;
829
+ await writeFile(path.join(this.projectPath, 'src/api/hello/hello.routes.ts'), routesContent);
830
+ }
831
+ async generateHealthCheck() {
832
+ const builder = new CodeBuilder();
833
+ builder
834
+ .import(['Request', 'Response'], 'express')
835
+ .line();
836
+ if (this.config.database !== 'none') {
837
+ builder.import(['checkDatabaseHealth'], '../database/connection.js').line();
838
+ }
839
+ builder
840
+ .comment('Health check handler with database status')
841
+ .line('export async function healthCheckHandler(req: Request, res: Response): Promise<void> {')
842
+ .line(' const health = {')
843
+ .line(' status: \"ok\",')
844
+ .line(' timestamp: new Date().toISOString(),')
845
+ .line(' uptime: process.uptime(),')
846
+ .line(' memory: {')
847
+ .line(' used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),')
848
+ .line(' total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024),')
849
+ .line(' unit: \"MB\"')
850
+ .line(' },')
851
+ .line(' environment: process.env.NODE_ENV || \"development\"');
852
+ if (this.config.database !== 'none') {
853
+ builder
854
+ .line(' };')
855
+ .line()
856
+ .comment('Check database connection')
857
+ .line(' try {')
858
+ .line(' const dbHealthy = await checkDatabaseHealth();')
859
+ .line(' Object.assign(health, {')
860
+ .line(' database: {')
861
+ .line(' status: dbHealthy ? \"connected\" : \"disconnected\",')
862
+ .line(` type: \"${this.config.database}\"`)
863
+ .line(' }')
864
+ .line(' });')
865
+ .line(' } catch (error) {')
866
+ .line(' Object.assign(health, {')
867
+ .line(' database: { status: \"error\", type: \"${this.config.database}\" }')
868
+ .line(' });')
869
+ .line(' }')
870
+ .line()
871
+ .line(' res.json(health);')
872
+ .line('}');
873
+ }
874
+ else {
875
+ builder
876
+ .line(' };')
877
+ .line()
878
+ .line(' res.json(health);')
879
+ .line('}');
880
+ }
881
+ await writeFile(path.join(this.projectPath, 'src/api/health/health.handler.ts'), builder.build());
882
+ }
883
+ }
884
+ //# sourceMappingURL=project-structure-generator.js.map