katax-cli 0.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.
@@ -0,0 +1,1315 @@
1
+ import inquirer from 'inquirer';
2
+ import ora from 'ora';
3
+ import path from 'path';
4
+ import crypto from 'crypto';
5
+ import { execa } from 'execa';
6
+ import { success, error, gray, title, info } from '../utils/logger.js';
7
+ import { directoryExists, ensureDir, writeFile } from '../utils/file-utils.js';
8
+ export async function initCommand(projectName, options = {}) {
9
+ title('🚀 Katax CLI - Initialize API Project');
10
+ // Determine project name
11
+ let finalProjectName = projectName || '';
12
+ if (!finalProjectName) {
13
+ const answer = await inquirer.prompt([
14
+ {
15
+ type: 'input',
16
+ name: 'projectName',
17
+ message: 'Project name:',
18
+ default: 'my-api',
19
+ validate: (input) => {
20
+ if (!/^[a-z0-9-_]+$/i.test(input)) {
21
+ return 'Project name can only contain letters, numbers, hyphens, and underscores';
22
+ }
23
+ return true;
24
+ }
25
+ }
26
+ ]);
27
+ finalProjectName = answer.projectName;
28
+ }
29
+ const projectPath = path.join(process.cwd(), finalProjectName);
30
+ // Check if directory exists
31
+ if (directoryExists(projectPath) && !options.force) {
32
+ error(`Directory "${finalProjectName}" already exists!`);
33
+ gray('Use --force to overwrite\n');
34
+ process.exit(1);
35
+ }
36
+ // Interactive configuration
37
+ const answers = await inquirer.prompt([
38
+ {
39
+ type: 'input',
40
+ name: 'description',
41
+ message: 'Project description:',
42
+ default: 'REST API built with Express and TypeScript'
43
+ },
44
+ {
45
+ type: 'list',
46
+ name: 'database',
47
+ message: 'Select database:',
48
+ choices: [
49
+ { name: 'PostgreSQL', value: 'postgresql' },
50
+ { name: 'MySQL', value: 'mysql' },
51
+ { name: 'MongoDB', value: 'mongodb' },
52
+ { name: 'None (no database)', value: 'none' }
53
+ ],
54
+ default: 'postgresql'
55
+ },
56
+ {
57
+ type: 'list',
58
+ name: 'authentication',
59
+ message: 'Add authentication?',
60
+ choices: [
61
+ { name: 'JWT Authentication', value: 'jwt' },
62
+ { name: 'None', value: 'none' }
63
+ ],
64
+ default: 'jwt'
65
+ },
66
+ {
67
+ type: 'confirm',
68
+ name: 'validation',
69
+ message: 'Use katax-core for validation?',
70
+ default: true
71
+ },
72
+ {
73
+ type: 'input',
74
+ name: 'port',
75
+ message: 'Server port:',
76
+ default: '3000',
77
+ validate: (input) => {
78
+ const port = parseInt(input);
79
+ if (isNaN(port) || port < 1 || port > 65535) {
80
+ return 'Port must be a number between 1 and 65535';
81
+ }
82
+ return true;
83
+ }
84
+ }
85
+ ]);
86
+ // Ask for database credentials if database is selected
87
+ let dbConfig = {};
88
+ if (answers.database !== 'none') {
89
+ const dbQuestions = [];
90
+ if (answers.database === 'postgresql' || answers.database === 'mysql') {
91
+ dbQuestions.push({
92
+ type: 'input',
93
+ name: 'host',
94
+ message: `${answers.database === 'postgresql' ? 'PostgreSQL' : 'MySQL'} host:`,
95
+ default: 'localhost'
96
+ }, {
97
+ type: 'input',
98
+ name: 'port',
99
+ message: `${answers.database === 'postgresql' ? 'PostgreSQL' : 'MySQL'} port:`,
100
+ default: answers.database === 'postgresql' ? '5432' : '3306'
101
+ }, {
102
+ type: 'input',
103
+ name: 'user',
104
+ message: 'Database user:',
105
+ default: 'postgres'
106
+ }, {
107
+ type: 'password',
108
+ name: 'password',
109
+ message: 'Database password:',
110
+ default: 'password'
111
+ }, {
112
+ type: 'input',
113
+ name: 'database',
114
+ message: 'Database name:',
115
+ default: finalProjectName.toLowerCase().replace(/-/g, '_')
116
+ });
117
+ }
118
+ else if (answers.database === 'mongodb') {
119
+ dbQuestions.push({
120
+ type: 'input',
121
+ name: 'host',
122
+ message: 'MongoDB host:',
123
+ default: 'localhost'
124
+ }, {
125
+ type: 'input',
126
+ name: 'port',
127
+ message: 'MongoDB port:',
128
+ default: '27017'
129
+ }, {
130
+ type: 'input',
131
+ name: 'database',
132
+ message: 'Database name:',
133
+ default: finalProjectName.toLowerCase().replace(/-/g, '_')
134
+ }, {
135
+ type: 'confirm',
136
+ name: 'useAuth',
137
+ message: 'Use authentication?',
138
+ default: false
139
+ });
140
+ }
141
+ dbConfig = await inquirer.prompt(dbQuestions);
142
+ // Ask for MongoDB credentials if authentication is enabled
143
+ if (answers.database === 'mongodb' && dbConfig.useAuth) {
144
+ const authConfig = await inquirer.prompt([
145
+ {
146
+ type: 'input',
147
+ name: 'user',
148
+ message: 'MongoDB user:',
149
+ default: 'admin'
150
+ },
151
+ {
152
+ type: 'password',
153
+ name: 'password',
154
+ message: 'MongoDB password:',
155
+ default: 'password'
156
+ }
157
+ ]);
158
+ dbConfig.user = authConfig.user;
159
+ dbConfig.password = authConfig.password;
160
+ }
161
+ }
162
+ const config = {
163
+ name: finalProjectName,
164
+ description: answers.description,
165
+ type: 'rest-api',
166
+ typescript: true,
167
+ database: answers.database,
168
+ authentication: answers.authentication,
169
+ validation: answers.validation ? 'katax-core' : 'none',
170
+ orm: 'none',
171
+ port: parseInt(answers.port),
172
+ dbConfig
173
+ };
174
+ // Ask for JWT secret generation if JWT is enabled
175
+ let generateJwtSecrets = false;
176
+ if (config.authentication === 'jwt') {
177
+ const jwtAnswer = await inquirer.prompt([
178
+ {
179
+ type: 'confirm',
180
+ name: 'generate',
181
+ message: 'Generate JWT secrets automatically?',
182
+ default: true
183
+ }
184
+ ]);
185
+ generateJwtSecrets = jwtAnswer.generate;
186
+ }
187
+ // Display configuration
188
+ gray('\n📋 Project Configuration:');
189
+ gray(` Name: ${config.name}`);
190
+ gray(` Database: ${config.database}`);
191
+ gray(` Auth: ${config.authentication}`);
192
+ gray(` Validation: ${config.validation}`);
193
+ gray(` Port: ${config.port}\n`);
194
+ const spinner = ora('Creating project structure...').start();
195
+ try {
196
+ // Create project structure
197
+ await createProjectStructure(projectPath, config, generateJwtSecrets);
198
+ spinner.succeed('Project structure created');
199
+ // Install dependencies
200
+ spinner.start('Installing dependencies...');
201
+ await installDependencies(projectPath);
202
+ spinner.succeed('Dependencies installed');
203
+ success(`\n✨ Project "${finalProjectName}" created successfully!\n`);
204
+ info('Next steps:');
205
+ gray(` cd ${finalProjectName}`);
206
+ gray(` npm run dev\n`);
207
+ info('Available commands:');
208
+ gray(` katax add endpoint <name> - Add a new endpoint`);
209
+ gray(` katax generate crud <name> - Generate CRUD resource`);
210
+ gray(` katax info - Show project structure\n`);
211
+ }
212
+ catch (err) {
213
+ spinner.fail('Failed to create project');
214
+ error(err instanceof Error ? err.message : 'Unknown error');
215
+ process.exit(1);
216
+ }
217
+ }
218
+ async function installDependencies(projectPath) {
219
+ await execa('npm', ['install'], {
220
+ cwd: projectPath,
221
+ stdio: 'ignore'
222
+ });
223
+ }
224
+ async function createDatabaseConnection(projectPath, config) {
225
+ const destPath = path.join(projectPath, 'src/database/connection.ts');
226
+ let content = '';
227
+ if (config.database === 'postgresql') {
228
+ content = [
229
+ "import { Pool } from 'pg';",
230
+ "import dotenv from 'dotenv';",
231
+ "dotenv.config();",
232
+ "",
233
+ "const pool = new Pool({",
234
+ " host: process.env.DB_HOST,",
235
+ " port: Number(process.env.DB_PORT),",
236
+ " database: process.env.DB_NAME,",
237
+ " user: process.env.DB_USER,",
238
+ " password: process.env.DB_PASSWORD,",
239
+ "});",
240
+ "",
241
+ "pool.on('connect', () => {",
242
+ " console.log('✅ Connected to PostgreSQL database');",
243
+ "});",
244
+ "",
245
+ "pool.on('error', (err: Error) => {",
246
+ " console.error('❌ PostgreSQL connection error:', err);",
247
+ " process.exit(-1);",
248
+ "});",
249
+ "",
250
+ "export default pool;",
251
+ "",
252
+ "export async function query(text: string, params?: any[]) {",
253
+ " const start = Date.now();",
254
+ " const res = await pool.query(text, params);",
255
+ " const duration = Date.now() - start;",
256
+ " console.log('Executed query', { text, duration, rows: res.rowCount });",
257
+ " return res;",
258
+ "}",
259
+ "",
260
+ "export async function getClient() {",
261
+ " const client = await pool.connect();",
262
+ " const originalQuery = client.query;",
263
+ " const originalRelease = client.release;",
264
+ " ",
265
+ " const timeout = setTimeout(() => {",
266
+ " console.error('A client has been checked out for more than 5 seconds!');",
267
+ " }, 5000);",
268
+ " ",
269
+ " // Override query method to add logging/monitoring",
270
+ " client.query = (originalQuery as any).bind(client);",
271
+ " ",
272
+ " client.release = () => {",
273
+ " clearTimeout(timeout);",
274
+ " client.query = originalQuery;",
275
+ " client.release = originalRelease;",
276
+ " return originalRelease.apply(client);",
277
+ " };",
278
+ " ",
279
+ " return client;",
280
+ "}"
281
+ ].join('\n');
282
+ }
283
+ else if (config.database === 'mysql') {
284
+ content = [
285
+ "import mysql from 'mysql2/promise';",
286
+ "",
287
+ "const pool = mysql.createPool({",
288
+ " uri: process.env.DATABASE_URL,",
289
+ " waitForConnections: true,",
290
+ " connectionLimit: 10,",
291
+ " queueLimit: 0",
292
+ "});",
293
+ "",
294
+ "export default pool;",
295
+ "",
296
+ "export async function query(sql: string, params?: any[]) {",
297
+ " const start = Date.now();",
298
+ " const [rows] = await pool.execute(sql, params);",
299
+ " const duration = Date.now() - start;",
300
+ " console.log('Executed query', { sql, duration, rows: Array.isArray(rows) ? rows.length : 0 });",
301
+ " return rows;",
302
+ "}",
303
+ "",
304
+ "export async function getConnection() {",
305
+ " return await pool.getConnection();",
306
+ "}"
307
+ ].join('\n');
308
+ }
309
+ else if (config.database === 'mongodb') {
310
+ content = [
311
+ "import { MongoClient, Db } from 'mongodb';",
312
+ "",
313
+ "let client: MongoClient;",
314
+ "let db: Db;",
315
+ "",
316
+ "export async function connect(): Promise<Db> {",
317
+ " if (db) {",
318
+ " return db;",
319
+ " }",
320
+ "",
321
+ " const uri = process.env.DATABASE_URL;",
322
+ " if (!uri) {",
323
+ " throw new Error('DATABASE_URL is not defined in environment variables');",
324
+ " }",
325
+ "",
326
+ " client = new MongoClient(uri);",
327
+ " ",
328
+ " try {",
329
+ " await client.connect();",
330
+ " console.log('✅ Connected to MongoDB database');",
331
+ " ",
332
+ " const dbName = uri.split('/').pop()?.split('?')[0];",
333
+ " db = client.db(dbName);",
334
+ " ",
335
+ " return db;",
336
+ " } catch (error) {",
337
+ " console.error('❌ MongoDB connection error:', error);",
338
+ " throw error;",
339
+ " }",
340
+ "}",
341
+ "",
342
+ "export async function disconnect(): Promise<void> {",
343
+ " if (client) {",
344
+ " await client.close();",
345
+ " console.log('Disconnected from MongoDB');",
346
+ " }",
347
+ "}",
348
+ "",
349
+ "export function getDb(): Db {",
350
+ " if (!db) {",
351
+ " throw new Error('Database not initialized. Call connect() first.');",
352
+ " }",
353
+ " return db;",
354
+ "}",
355
+ "",
356
+ "export default { connect, disconnect, getDb };"
357
+ ].join('\n');
358
+ }
359
+ await writeFile(destPath, content);
360
+ }
361
+ async function createProjectStructure(projectPath, config, generateJwtSecrets) {
362
+ // Create directories
363
+ const dirs = [
364
+ 'src',
365
+ 'src/api',
366
+ 'src/config',
367
+ 'src/middleware',
368
+ 'src/shared',
369
+ 'src/types'
370
+ ];
371
+ if (config.database !== 'none') {
372
+ dirs.push('src/database');
373
+ }
374
+ // Add default hola endpoint
375
+ dirs.push('src/api/hola');
376
+ for (const dir of dirs) {
377
+ await ensureDir(path.join(projectPath, dir));
378
+ }
379
+ // Create package.json
380
+ const packageJson = {
381
+ name: config.name,
382
+ version: '1.0.0',
383
+ description: config.description,
384
+ type: 'module',
385
+ main: 'dist/index.js',
386
+ scripts: {
387
+ dev: 'nodemon --watch src --exec tsx src/index.ts',
388
+ build: 'tsc',
389
+ start: 'node dist/index.js',
390
+ lint: 'eslint . --ext .ts',
391
+ format: 'prettier --write "src/**/*.ts"'
392
+ },
393
+ keywords: ['api', 'express', 'typescript'],
394
+ author: '',
395
+ license: 'MIT',
396
+ dependencies: {
397
+ express: '^4.18.2',
398
+ cors: '^2.8.5',
399
+ dotenv: '^16.3.1',
400
+ pino: '^8.17.2',
401
+ 'pino-pretty': '^10.3.1',
402
+ ...(config.validation === 'katax-core' && { 'katax-core': '^1.1.0' }),
403
+ ...(config.authentication === 'jwt' && {
404
+ jsonwebtoken: '^9.0.2',
405
+ bcrypt: '^5.1.1'
406
+ }),
407
+ ...(config.database === 'postgresql' && { pg: '^8.11.3' }),
408
+ ...(config.database === 'mysql' && { mysql2: '^3.6.5' }),
409
+ ...(config.database === 'mongodb' && { mongodb: '^6.3.0' })
410
+ },
411
+ devDependencies: {
412
+ '@types/express': '^4.17.21',
413
+ '@types/cors': '^2.8.17',
414
+ '@types/node': '^22.10.5',
415
+ ...(config.authentication === 'jwt' && {
416
+ '@types/jsonwebtoken': '^9.0.5',
417
+ '@types/bcrypt': '^5.0.2'
418
+ }),
419
+ ...(config.database === 'postgresql' && { '@types/pg': '^8.10.9' }),
420
+ typescript: '^5.3.3',
421
+ tsx: '^4.7.0',
422
+ nodemon: '^3.0.2',
423
+ eslint: '^8.56.0',
424
+ '@typescript-eslint/eslint-plugin': '^6.19.0',
425
+ '@typescript-eslint/parser': '^6.19.0',
426
+ prettier: '^3.2.4'
427
+ }
428
+ };
429
+ await writeFile(path.join(projectPath, 'package.json'), JSON.stringify(packageJson, null, 2));
430
+ // Create tsconfig.json
431
+ const tsConfig = {
432
+ compilerOptions: {
433
+ target: 'es2016',
434
+ module: 'commonjs',
435
+ sourceMap: true,
436
+ outDir: './dist',
437
+ mapRoot: 'src',
438
+ esModuleInterop: true,
439
+ forceConsistentCasingInFileNames: true,
440
+ strict: true,
441
+ skipLibCheck: true
442
+ },
443
+ include: ['src'],
444
+ exclude: ['node_modules', 'dist']
445
+ };
446
+ await writeFile(path.join(projectPath, 'tsconfig.json'), JSON.stringify(tsConfig, null, 2));
447
+ // Create .env.example and .env
448
+ let databaseUrl = '';
449
+ let dbEnvVars = '';
450
+ if (config.database === 'postgresql' && config.dbConfig) {
451
+ const { host, port, user, password, database } = config.dbConfig;
452
+ databaseUrl = `DATABASE_URL=postgresql://${user}:${password}@${host}:${port}/${database}`;
453
+ dbEnvVars = `DB_HOST=${host}
454
+ DB_PORT=${port}
455
+ DB_NAME=${database}
456
+ DB_USER=${user}
457
+ DB_PASSWORD=${password}`;
458
+ }
459
+ else if (config.database === 'mysql' && config.dbConfig) {
460
+ const { host, port, user, password, database } = config.dbConfig;
461
+ databaseUrl = `DATABASE_URL=mysql://${user}:${password}@${host}:${port}/${database}`;
462
+ dbEnvVars = `DB_HOST=${host}
463
+ DB_PORT=${port}
464
+ DB_NAME=${database}
465
+ DB_USER=${user}
466
+ DB_PASSWORD=${password}`;
467
+ }
468
+ else if (config.database === 'mongodb' && config.dbConfig) {
469
+ const { host, port, database, user, password } = config.dbConfig;
470
+ if (user && password) {
471
+ databaseUrl = `DATABASE_URL=mongodb://${user}:${password}@${host}:${port}/${database}`;
472
+ dbEnvVars = `DB_HOST=${host}
473
+ DB_PORT=${port}
474
+ DB_NAME=${database}
475
+ DB_USER=${user}
476
+ DB_PASSWORD=${password}`;
477
+ }
478
+ else {
479
+ databaseUrl = `DATABASE_URL=mongodb://${host}:${port}/${database}`;
480
+ dbEnvVars = `DB_HOST=${host}
481
+ DB_PORT=${port}
482
+ DB_NAME=${database}`;
483
+ }
484
+ }
485
+ // Generate JWT secrets if needed
486
+ let jwtConfig = '';
487
+ if (config.authentication === 'jwt') {
488
+ if (generateJwtSecrets) {
489
+ const jwtSecret = crypto.randomBytes(64).toString('hex');
490
+ const jwtRefreshSecret = crypto.randomBytes(64).toString('hex');
491
+ jwtConfig = `JWT_SECRET=${jwtSecret}
492
+ JWT_EXPIRES_IN=24h
493
+ JWT_REFRESH_SECRET=${jwtRefreshSecret}
494
+ JWT_REFRESH_EXPIRES_IN=7d`;
495
+ }
496
+ else {
497
+ jwtConfig = `JWT_SECRET=your-secret-key-here
498
+ JWT_EXPIRES_IN=24h
499
+ JWT_REFRESH_SECRET=your-refresh-secret-here
500
+ JWT_REFRESH_EXPIRES_IN=7d`;
501
+ }
502
+ }
503
+ const envContent = `# Server Configuration
504
+ PORT=${config.port}
505
+ NODE_ENV=development
506
+ LOG_LEVEL=info
507
+
508
+ # CORS Configuration
509
+ ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173
510
+
511
+ # Database Configuration
512
+ ${databaseUrl}
513
+ ${dbEnvVars ? '\n# DB connection variables for pool\n' + dbEnvVars : ''}
514
+
515
+ # JWT Configuration
516
+ ${jwtConfig}
517
+ `;
518
+ await writeFile(path.join(projectPath, '.env.example'), envContent);
519
+ await writeFile(path.join(projectPath, '.env'), envContent);
520
+ // Create .gitignore
521
+ const gitignoreContent = `node_modules/
522
+ dist/
523
+ .env
524
+ .DS_Store
525
+ *.log
526
+ coverage/
527
+ .vscode/
528
+ `;
529
+ await writeFile(path.join(projectPath, '.gitignore'), gitignoreContent);
530
+ // Create index.ts
531
+ const indexContent = `import app from './app.js';
532
+ import dotenv from 'dotenv';
533
+ import { logger } from './shared/logger.utils.js';
534
+ import { validateEnvironment } from './config/env.validator.js';
535
+
536
+ dotenv.config();
537
+
538
+ // Validate required environment variables
539
+ validateEnvironment();
540
+
541
+ const PORT = process.env.PORT || ${config.port};
542
+
543
+ app.listen(PORT, () => {
544
+ logger.info(\`Server running on http://localhost:\${PORT}\`);
545
+ logger.info(\`API endpoints available at http://localhost:\${PORT}/api\`);
546
+ logger.info(\`Health check: http://localhost:\${PORT}/api/health\`);
547
+ });
548
+ `;
549
+ await writeFile(path.join(projectPath, 'src/index.ts'), indexContent);
550
+ // Create app.ts
551
+ const appContent = `import express from 'express';
552
+ import cors from 'cors';
553
+ import router from './api/routes.js';
554
+ import { errorMiddleware } from './middleware/error.middleware.js';
555
+ import { requestLogger } from './middleware/logger.middleware.js';
556
+ import { corsOptions } from './config/cors.config.js';
557
+
558
+ const app = express();
559
+
560
+ // Middleware
561
+ app.use(cors(corsOptions));
562
+ app.use(express.json());
563
+ app.use(express.urlencoded({ extended: true }));
564
+ app.use(requestLogger);
565
+
566
+ // Routes
567
+ app.get('/', (req, res) => {
568
+ res.json({
569
+ message: 'Welcome to ${config.name} API',
570
+ version: '1.0.0',
571
+ endpoints: '/api',
572
+ health: '/api/health'
573
+ });
574
+ });
575
+
576
+ app.use('/api', router);
577
+
578
+ // Error handling
579
+ app.use(errorMiddleware);
580
+
581
+ export default app;
582
+ `;
583
+ await writeFile(path.join(projectPath, 'src/app.ts'), appContent);
584
+ // Create routes.ts
585
+ const routesContent = `import { Router } from 'express';
586
+ import holaRouter from './hola/hola.routes.js';
587
+ import { healthCheckHandler } from './health/health.handler.js';
588
+
589
+ const router = Router();
590
+
591
+ // Health check
592
+ router.get('/health', healthCheckHandler);
593
+
594
+ // Example endpoint
595
+ router.use('/hola', holaRouter);
596
+
597
+ export default router;
598
+ `;
599
+ await writeFile(path.join(projectPath, 'src/api/routes.ts'), routesContent);
600
+ // Create error middleware
601
+ const errorMiddlewareContent = `import { Request, Response, NextFunction } from 'express';
602
+ import { logger } from '../shared/logger.utils.js';
603
+
604
+ export interface ApiError extends Error {
605
+ statusCode?: number;
606
+ }
607
+
608
+ export function errorMiddleware(
609
+ err: ApiError,
610
+ req: Request,
611
+ res: Response,
612
+ next: NextFunction
613
+ ): void {
614
+ const statusCode = err.statusCode || 500;
615
+ const message = err.message || 'Internal Server Error';
616
+
617
+ logger.error({
618
+ err,
619
+ req: {
620
+ method: req.method,
621
+ url: req.url,
622
+ headers: req.headers
623
+ },
624
+ statusCode
625
+ }, message);
626
+
627
+ res.status(statusCode).json({
628
+ success: false,
629
+ message,
630
+ ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
631
+ });
632
+ }
633
+ `;
634
+ await writeFile(path.join(projectPath, 'src/middleware/error.middleware.ts'), errorMiddlewareContent);
635
+ // Create database connection if database is selected
636
+ if (config.database !== 'none') {
637
+ await createDatabaseConnection(projectPath, config);
638
+ }
639
+ // Create shared utilities
640
+ if (config.validation === 'katax-core') {
641
+ const apiUtilsContent = `import { Request, Response } from 'express';
642
+ import { logger } from './logger.utils.js';
643
+
644
+ export interface ControllerResult<T = any> {
645
+ success: boolean;
646
+ message: string;
647
+ data?: T;
648
+ error?: string;
649
+ statusCode?: number;
650
+ }
651
+
652
+ export function createSuccessResult<T>(
653
+ message: string,
654
+ data?: T,
655
+ error?: string,
656
+ statusCode = 200
657
+ ): ControllerResult<T> {
658
+ return { success: true, message, data, error, statusCode };
659
+ }
660
+
661
+ export function createErrorResult(
662
+ message: string,
663
+ error?: string,
664
+ statusCode = 400
665
+ ): ControllerResult {
666
+ return { success: false, message, error, statusCode };
667
+ }
668
+
669
+ export interface ValidationResult<T = any> {
670
+ isValid: boolean;
671
+ data?: T;
672
+ errors?: any[];
673
+ }
674
+
675
+ export async function sendResponse<TValidation = any, TResponse = any>(
676
+ req: Request,
677
+ res: Response,
678
+ validator: () => Promise<ValidationResult<TValidation>>,
679
+ controller: (validData: TValidation) => Promise<ControllerResult<TResponse>>
680
+ ): Promise<void> {
681
+ try {
682
+ // 1. Execute validation
683
+ const validationResult = await validator();
684
+
685
+ if (!validationResult.isValid) {
686
+ // Validation error
687
+ logger.warn({
688
+ method: req.method,
689
+ path: req.path,
690
+ errors: validationResult.errors
691
+ }, 'Validation failed');
692
+
693
+ res.status(400).json({
694
+ success: false,
695
+ message: 'Invalid data',
696
+ error: 'Validation failed',
697
+ details: validationResult.errors
698
+ });
699
+ return;
700
+ }
701
+
702
+ // 2. Execute controller if validation passes
703
+ const controllerResult = await controller(validationResult.data as TValidation);
704
+
705
+ // 3. Build HTTP response
706
+ const statusCode = controllerResult.statusCode || (controllerResult.success ? 200 : 400);
707
+
708
+ const response: any = {
709
+ success: controllerResult.success,
710
+ message: controllerResult.message
711
+ };
712
+
713
+ if (controllerResult.data !== undefined) {
714
+ response.data = controllerResult.data;
715
+ }
716
+
717
+ if (controllerResult.error) {
718
+ response.error = controllerResult.error;
719
+ }
720
+
721
+ res.status(statusCode).json(response);
722
+
723
+ } catch (error) {
724
+ // Internal server error
725
+ logger.error({
726
+ err: error,
727
+ method: req.method,
728
+ path: req.path
729
+ }, 'Internal server error');
730
+
731
+ res.status(500).json({
732
+ success: false,
733
+ message: 'Internal server error',
734
+ error: error instanceof Error ? error.message : 'Unknown error'
735
+ });
736
+ }
737
+ }
738
+ `;
739
+ await writeFile(path.join(projectPath, 'src/shared/api.utils.ts'), apiUtilsContent);
740
+ }
741
+ // Create JWT utilities if JWT authentication is selected
742
+ if (config.authentication === 'jwt') {
743
+ const jwtUtilsContent = `import jwt from 'jsonwebtoken';
744
+ import { Request, Response, NextFunction } from 'express';
745
+
746
+ const JWT_SECRET = process.env.JWT_SECRET || 'your-super-secret-jwt-key-change-in-production';
747
+ const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET || 'your-refresh-secret-key';
748
+
749
+ const ACCESS_TOKEN_EXPIRY = '15m'; // 15 minutes
750
+ const REFRESH_TOKEN_EXPIRY = '7d'; // 7 days
751
+
752
+ // ==================== INTERFACES ====================
753
+
754
+ export interface JwtPayload {
755
+ userId: string;
756
+ email: string;
757
+ role: string;
758
+ }
759
+
760
+ // ==================== TOKEN GENERATION ====================
761
+
762
+ /**
763
+ * Generate Access Token
764
+ * @param payload - JWT payload containing user information
765
+ * @returns JWT access token string
766
+ */
767
+ export function generateAccessToken(payload: JwtPayload): string {
768
+ return jwt.sign(payload, JWT_SECRET, {
769
+ expiresIn: ACCESS_TOKEN_EXPIRY
770
+ });
771
+ }
772
+
773
+ /**
774
+ * Generate Refresh Token
775
+ * @param payload - JWT payload containing user information
776
+ * @returns JWT refresh token string
777
+ */
778
+ export function generateRefreshToken(payload: JwtPayload): string {
779
+ return jwt.sign(payload, JWT_REFRESH_SECRET, {
780
+ expiresIn: REFRESH_TOKEN_EXPIRY
781
+ });
782
+ }
783
+
784
+ /**
785
+ * Generate both access and refresh tokens
786
+ * @param payload - JWT payload containing user information
787
+ * @returns Object with both tokens
788
+ */
789
+ export function generateTokens(payload: JwtPayload): { accessToken: string; refreshToken: string } {
790
+ return {
791
+ accessToken: generateAccessToken(payload),
792
+ refreshToken: generateRefreshToken(payload)
793
+ };
794
+ }
795
+
796
+ // ==================== TOKEN VERIFICATION ====================
797
+
798
+ /**
799
+ * Verify Access Token
800
+ * @param token - JWT token to verify
801
+ * @returns Decoded JWT payload or null if invalid
802
+ */
803
+ export function verifyAccessToken(token: string): JwtPayload | null {
804
+ try {
805
+ return jwt.verify(token, JWT_SECRET) as JwtPayload;
806
+ } catch (error) {
807
+ console.error('[JWT] Error verifying access token:', error);
808
+ return null;
809
+ }
810
+ }
811
+
812
+ /**
813
+ * Verify Refresh Token
814
+ * @param token - JWT token to verify
815
+ * @returns Decoded JWT payload or null if invalid
816
+ */
817
+ export function verifyRefreshToken(token: string): JwtPayload | null {
818
+ try {
819
+ return jwt.verify(token, JWT_REFRESH_SECRET) as JwtPayload;
820
+ } catch (error) {
821
+ console.error('[JWT] Error verifying refresh token:', error);
822
+ return null;
823
+ }
824
+ }
825
+
826
+ /**
827
+ * Decode token without verifying (useful for debugging)
828
+ * @param token - JWT token to decode
829
+ * @returns Decoded token data
830
+ */
831
+ export function decodeToken(token: string): any {
832
+ return jwt.decode(token);
833
+ }
834
+
835
+ // ==================== MIDDLEWARE ====================
836
+
837
+ /**
838
+ * Express middleware to authenticate requests using JWT
839
+ * Expects Bearer token in Authorization header
840
+ */
841
+ export function authenticateToken(req: Request, res: Response, next: NextFunction): void {
842
+ const authHeader = req.headers['authorization'];
843
+ const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
844
+
845
+ if (!token) {
846
+ res.status(401).json({
847
+ success: false,
848
+ message: 'Access denied. No token provided.'
849
+ });
850
+ return;
851
+ }
852
+
853
+ const payload = verifyAccessToken(token);
854
+
855
+ if (!payload) {
856
+ res.status(403).json({
857
+ success: false,
858
+ message: 'Invalid or expired token'
859
+ });
860
+ return;
861
+ }
862
+
863
+ // Attach user info to request
864
+ (req as any).user = payload;
865
+ next();
866
+ }
867
+
868
+ /**
869
+ * Middleware to check if user has specific role
870
+ * @param roles - Array of allowed roles
871
+ */
872
+ export function requireRole(...roles: string[]) {
873
+ return (req: Request, res: Response, next: NextFunction): void => {
874
+ const user = (req as any).user;
875
+
876
+ if (!user) {
877
+ res.status(401).json({
878
+ success: false,
879
+ message: 'Authentication required'
880
+ });
881
+ return;
882
+ }
883
+
884
+ if (!roles.includes(user.role)) {
885
+ res.status(403).json({
886
+ success: false,
887
+ message: 'Insufficient permissions'
888
+ });
889
+ return;
890
+ }
891
+
892
+ next();
893
+ };
894
+ }
895
+ `;
896
+ await writeFile(path.join(projectPath, 'src/shared/jwt.utils.ts'), jwtUtilsContent);
897
+ }
898
+ // Create logger utility with pino
899
+ const loggerUtilsContent = `import pino from 'pino';
900
+
901
+ const isDevelopment = process.env.NODE_ENV !== 'production';
902
+
903
+ /**
904
+ * Pino logger configuration
905
+ * - Pretty printing in development
906
+ * - JSON logs in production
907
+ */
908
+ export const logger = pino({
909
+ level: process.env.LOG_LEVEL || 'info',
910
+ transport: isDevelopment
911
+ ? {
912
+ target: 'pino-pretty',
913
+ options: {
914
+ colorize: true,
915
+ translateTime: 'HH:MM:ss',
916
+ ignore: 'pid,hostname'
917
+ }
918
+ }
919
+ : undefined,
920
+ formatters: {
921
+ level: (label) => {
922
+ return { level: label };
923
+ }
924
+ }
925
+ });
926
+
927
+ /**
928
+ * Log HTTP request
929
+ */
930
+ export function logRequest(method: string, url: string, statusCode: number, duration: number): void {
931
+ logger.info({
932
+ method,
933
+ url,
934
+ statusCode,
935
+ duration: \`\${duration}ms\`
936
+ }, \`\${method} \${url} - \${statusCode} (\${duration}ms)\`);
937
+ }
938
+
939
+ /**
940
+ * Log error with context
941
+ */
942
+ export function logError(error: Error, context?: Record<string, any>): void {
943
+ logger.error({
944
+ err: error,
945
+ ...context
946
+ }, error.message);
947
+ }
948
+
949
+ /**
950
+ * Log info message
951
+ */
952
+ export function logInfo(message: string, data?: Record<string, any>): void {
953
+ logger.info(data, message);
954
+ }
955
+
956
+ /**
957
+ * Log warning message
958
+ */
959
+ export function logWarning(message: string, data?: Record<string, any>): void {
960
+ logger.warn(data, message);
961
+ }
962
+
963
+ /**
964
+ * Log debug message (only in development)
965
+ */
966
+ export function logDebug(message: string, data?: Record<string, any>): void {
967
+ logger.debug(data, message);
968
+ }
969
+ `;
970
+ await writeFile(path.join(projectPath, 'src/shared/logger.utils.ts'), loggerUtilsContent);
971
+ // Create logger middleware
972
+ const loggerMiddlewareContent = `import { Request, Response, NextFunction } from 'express';
973
+ import { logRequest } from '../shared/logger.utils.js';
974
+
975
+ /**
976
+ * Express middleware to log all HTTP requests
977
+ */
978
+ export function requestLogger(req: Request, res: Response, next: NextFunction): void {
979
+ const startTime = Date.now();
980
+
981
+ // Log response when it finishes
982
+ res.on('finish', () => {
983
+ const duration = Date.now() - startTime;
984
+ logRequest(req.method, req.url, res.statusCode, duration);
985
+ });
986
+
987
+ next();
988
+ }
989
+ `;
990
+ await writeFile(path.join(projectPath, 'src/middleware/logger.middleware.ts'), loggerMiddlewareContent);
991
+ // Create CORS configuration
992
+ const corsConfigContent = `import { CorsOptions } from 'cors';
993
+
994
+ const allowedOrigins = process.env.ALLOWED_ORIGINS
995
+ ? process.env.ALLOWED_ORIGINS.split(',')
996
+ : ['http://localhost:3000', 'http://localhost:5173'];
997
+
998
+ export const corsOptions: CorsOptions = {
999
+ origin: (origin, callback) => {
1000
+ // Allow requests with no origin (like mobile apps or curl)
1001
+ if (!origin) return callback(null, true);
1002
+
1003
+ if (allowedOrigins.includes(origin)) {
1004
+ callback(null, true);
1005
+ } else {
1006
+ callback(new Error('Not allowed by CORS'));
1007
+ }
1008
+ },
1009
+ credentials: true,
1010
+ optionsSuccessStatus: 200,
1011
+ methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
1012
+ allowedHeaders: ['Content-Type', 'Authorization']
1013
+ };
1014
+ `;
1015
+ await writeFile(path.join(projectPath, 'src/config/cors.config.ts'), corsConfigContent);
1016
+ // Create environment validator
1017
+ const envValidatorContent = `import { logger } from '../shared/logger.utils.js';
1018
+
1019
+ interface RequiredEnvVars {
1020
+ [key: string]: string;
1021
+ }
1022
+
1023
+ /**
1024
+ * Validate that all required environment variables are present
1025
+ */
1026
+ export function validateEnvironment(): void {
1027
+ const required: RequiredEnvVars = {
1028
+ PORT: process.env.PORT || '',
1029
+ NODE_ENV: process.env.NODE_ENV || ''
1030
+ };
1031
+
1032
+ ${config.database !== 'none' ? ` // Database variables\n required.DATABASE_URL = process.env.DATABASE_URL || '';\n` : ''}${config.authentication === 'jwt' ? ` // JWT variables\n required.JWT_SECRET = process.env.JWT_SECRET || '';\n required.JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET || '';\n` : ''}
1033
+ const missing: string[] = [];
1034
+
1035
+ for (const [key, value] of Object.entries(required)) {
1036
+ if (!value || value.trim() === '') {
1037
+ missing.push(key);
1038
+ }
1039
+ }
1040
+
1041
+ if (missing.length > 0) {
1042
+ logger.error(\`Missing required environment variables: \${missing.join(', ')}\`);
1043
+ logger.error('Please check your .env file');
1044
+ process.exit(1);
1045
+ }
1046
+
1047
+ logger.info('Environment variables validated successfully');
1048
+ }
1049
+ `;
1050
+ await writeFile(path.join(projectPath, 'src/config/env.validator.ts'), envValidatorContent);
1051
+ // Create health check handler
1052
+ const healthHandlerContent = `import { Request, Response } from 'express';
1053
+ import os from 'os';
1054
+
1055
+ /**
1056
+ * Health check endpoint handler
1057
+ * Returns system information and service status
1058
+ */
1059
+ export function healthCheckHandler(req: Request, res: Response): void {
1060
+ const uptime = process.uptime();
1061
+ const memoryUsage = process.memoryUsage();
1062
+
1063
+ const healthData = {
1064
+ status: 'ok',
1065
+ timestamp: new Date().toISOString(),
1066
+ uptime: {
1067
+ seconds: Math.floor(uptime),
1068
+ formatted: formatUptime(uptime)
1069
+ },
1070
+ memory: {
1071
+ rss: formatBytes(memoryUsage.rss),
1072
+ heapTotal: formatBytes(memoryUsage.heapTotal),
1073
+ heapUsed: formatBytes(memoryUsage.heapUsed),
1074
+ external: formatBytes(memoryUsage.external)
1075
+ },
1076
+ system: {
1077
+ platform: os.platform(),
1078
+ arch: os.arch(),
1079
+ nodeVersion: process.version,
1080
+ cpus: os.cpus().length,
1081
+ totalMemory: formatBytes(os.totalmem()),
1082
+ freeMemory: formatBytes(os.freemem())
1083
+ },
1084
+ environment: process.env.NODE_ENV || 'development'
1085
+ };
1086
+
1087
+ res.json(healthData);
1088
+ }
1089
+
1090
+ /**
1091
+ * Format uptime in human readable format
1092
+ */
1093
+ function formatUptime(seconds: number): string {
1094
+ const days = Math.floor(seconds / 86400);
1095
+ const hours = Math.floor((seconds % 86400) / 3600);
1096
+ const minutes = Math.floor((seconds % 3600) / 60);
1097
+ const secs = Math.floor(seconds % 60);
1098
+
1099
+ const parts = [];
1100
+ if (days > 0) parts.push(\`\${days}d\`);
1101
+ if (hours > 0) parts.push(\`\${hours}h\`);
1102
+ if (minutes > 0) parts.push(\`\${minutes}m\`);
1103
+ parts.push(\`\${secs}s\`);
1104
+
1105
+ return parts.join(' ');
1106
+ }
1107
+
1108
+ /**
1109
+ * Format bytes to human readable format
1110
+ */
1111
+ function formatBytes(bytes: number): string {
1112
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
1113
+ let size = bytes;
1114
+ let unitIndex = 0;
1115
+
1116
+ while (size >= 1024 && unitIndex < units.length - 1) {
1117
+ size /= 1024;
1118
+ unitIndex++;
1119
+ }
1120
+
1121
+ return \`\${size.toFixed(2)} \${units[unitIndex]}\`;
1122
+ }
1123
+ `;
1124
+ await ensureDir(path.join(projectPath, 'src/api/health'));
1125
+ await writeFile(path.join(projectPath, 'src/api/health/health.handler.ts'), healthHandlerContent);
1126
+ // Create README.md
1127
+ const readmeContent = `# ${config.name}
1128
+
1129
+ ${config.description}
1130
+
1131
+ ## 🚀 Quick Start
1132
+
1133
+ \`\`\`bash
1134
+ # Install dependencies
1135
+ npm install
1136
+
1137
+ # Start development server
1138
+ npm run dev
1139
+
1140
+ # Build for production
1141
+ npm run build
1142
+
1143
+ # Start production server
1144
+ npm start
1145
+ \`\`\`
1146
+
1147
+ ## 📁 Project Structure
1148
+
1149
+ \`\`\`
1150
+ src/
1151
+ ├── api/ # API routes and endpoints
1152
+ ├── config/ # Configuration files
1153
+ ├── middleware/ # Express middleware
1154
+ ├── shared/ # Shared utilities
1155
+ └── index.ts # Entry point
1156
+ \`\`\`
1157
+
1158
+ ## 🛠️ Technologies
1159
+
1160
+ - **Express** - Web framework
1161
+ - **TypeScript** - Type safety
1162
+ ${config.validation === 'katax-core' ? '- **katax-core** - Schema validation\n' : ''}${config.authentication === 'jwt' ? '- **JWT** - Authentication\n' : ''}${config.database !== 'none' ? `- **${config.database}** - Database\n` : ''}
1163
+ ## 📚 API Documentation
1164
+
1165
+ Server runs on \`http://localhost:${config.port}\`
1166
+
1167
+ ### Endpoints
1168
+
1169
+ - \`GET /\` - Welcome message
1170
+ - \`GET /api/health\` - Health check
1171
+
1172
+ ## 🔧 Development
1173
+
1174
+ Add new endpoints using Katax CLI:
1175
+
1176
+ \`\`\`bash
1177
+ # Add a single endpoint
1178
+ katax add endpoint users
1179
+
1180
+ # Generate CRUD resource
1181
+ katax generate crud products
1182
+ \`\`\`
1183
+
1184
+ ## 📝 License
1185
+
1186
+ MIT
1187
+ `;
1188
+ await writeFile(path.join(projectPath, 'README.md'), readmeContent);
1189
+ // Create default hola endpoint
1190
+ await createHolaEndpoint(projectPath, config);
1191
+ }
1192
+ async function createHolaEndpoint(projectPath, config) {
1193
+ const holaPath = path.join(projectPath, 'src/api/hola');
1194
+ // hola.controller.ts
1195
+ const controllerContent = [
1196
+ "import { ControllerResult, createSuccessResult, createErrorResult } from '../../shared/api.utils.js';",
1197
+ "import { HolaQuery } from './hola.validator.js';",
1198
+ "import { logger } from '../../shared/logger.utils.js';",
1199
+ "",
1200
+ "/**",
1201
+ " * Get hola message",
1202
+ " */",
1203
+ "export async function getHola(queryData: HolaQuery): Promise<ControllerResult<{ message: string; timestamp: string }>> {",
1204
+ " try {",
1205
+ " const name = queryData.name || 'World';",
1206
+ " logger.debug({ name }, 'Processing hola request');",
1207
+ " ",
1208
+ " return createSuccessResult(",
1209
+ " 'Hola endpoint working!',",
1210
+ " {",
1211
+ " message: `Hola ${name}! Welcome to your API 🚀`,",
1212
+ " timestamp: new Date().toISOString()",
1213
+ " }",
1214
+ " );",
1215
+ " } catch (error) {",
1216
+ " logger.error({ err: error }, 'Error in getHola controller');",
1217
+ " return createErrorResult(",
1218
+ " 'Failed to get hola message',",
1219
+ " error instanceof Error ? error.message : 'Unknown error',",
1220
+ " 500",
1221
+ " );",
1222
+ " }",
1223
+ "}"
1224
+ ].join('\n');
1225
+ await writeFile(path.join(holaPath, 'hola.controller.ts'), controllerContent);
1226
+ // hola.handler.ts
1227
+ const handlerContent = [
1228
+ "import { Request, Response } from 'express';",
1229
+ "import { getHola } from './hola.controller.js';",
1230
+ "import { validateHolaQuery } from './hola.validator.js';",
1231
+ "import { sendResponse } from '../../shared/api.utils.js';",
1232
+ "",
1233
+ "// ==================== HANDLERS ====================",
1234
+ "",
1235
+ "/**",
1236
+ " * Handler for GET /api/hola",
1237
+ " * Uses sendResponse utility for automatic validation and response handling",
1238
+ " */",
1239
+ "export async function getHolaHandler(req: Request, res: Response): Promise<void> {",
1240
+ " await sendResponse(",
1241
+ " req,",
1242
+ " res,",
1243
+ " // Validator returns Promise<ValidationResult<HolaQuery>>",
1244
+ " () => validateHolaQuery(req.query),",
1245
+ " // validData is automatically: HolaQuery (not any)",
1246
+ " (validData) => getHola(validData)",
1247
+ " );",
1248
+ "}"
1249
+ ].join('\n');
1250
+ await writeFile(path.join(holaPath, 'hola.handler.ts'), handlerContent);
1251
+ // hola.routes.ts
1252
+ const routesContent = [
1253
+ "import { Router } from 'express';",
1254
+ "import { getHolaHandler } from './hola.handler.js';",
1255
+ "",
1256
+ "const router = Router();",
1257
+ "",
1258
+ "// ==================== ROUTES ====================",
1259
+ "",
1260
+ "/**",
1261
+ " * @route GET /api/hola",
1262
+ " * @desc Example endpoint - returns a greeting message",
1263
+ " */",
1264
+ "router.get('/', getHolaHandler);",
1265
+ "",
1266
+ "export default router;"
1267
+ ].join('\n');
1268
+ await writeFile(path.join(holaPath, 'hola.routes.ts'), routesContent);
1269
+ // Only create validator if katax-core is enabled
1270
+ if (config.validation === 'katax-core') {
1271
+ const validatorContent = [
1272
+ "import { k, kataxInfer } from 'katax-core';",
1273
+ "import type { ValidationResult } from '../../shared/api.utils.js';",
1274
+ "",
1275
+ "// ==================== SCHEMAS ====================",
1276
+ "",
1277
+ "/**",
1278
+ " * Schema for hola query params",
1279
+ " */",
1280
+ "export const holaQuerySchema = k.object({",
1281
+ " name: k.string().minLength(2).optional()",
1282
+ "});",
1283
+ "",
1284
+ "/**",
1285
+ " * Inferred TypeScript type from schema",
1286
+ " */",
1287
+ "export type HolaQuery = kataxInfer<typeof holaQuerySchema>;",
1288
+ "",
1289
+ "/**",
1290
+ " * Validate hola query params",
1291
+ " */",
1292
+ "export async function validateHolaQuery(data: unknown): Promise<ValidationResult<HolaQuery>> {",
1293
+ " const result = holaQuerySchema.safeParse(data);",
1294
+ "",
1295
+ " if (!result.success) {",
1296
+ " const errors = result.issues.map(issue => ({",
1297
+ " field: issue.path.join('.'),",
1298
+ " message: issue.message",
1299
+ " }));",
1300
+ "",
1301
+ " return {",
1302
+ " isValid: false,",
1303
+ " errors",
1304
+ " };",
1305
+ " }",
1306
+ "",
1307
+ " return {",
1308
+ " isValid: true,",
1309
+ " data: result.data",
1310
+ " };",
1311
+ "}"
1312
+ ].join('\n');
1313
+ await writeFile(path.join(holaPath, 'hola.validator.ts'), validatorContent);
1314
+ }
1315
+ }