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.
- package/README.md +0 -0
- package/dist/commands/add-endpoint.d.ts +6 -0
- package/dist/commands/add-endpoint.js +229 -0
- package/dist/commands/generate-crud.d.ts +5 -0
- package/dist/commands/generate-crud.js +282 -0
- package/dist/commands/info.d.ts +1 -0
- package/dist/commands/info.js +80 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.js +1315 -0
- package/dist/database/mongodb.ts +46 -0
- package/dist/database/mysql.ts +26 -0
- package/dist/database/postgresql.ts +52 -0
- package/dist/generators/controller-generator.d.ts +2 -0
- package/dist/generators/controller-generator.js +223 -0
- package/dist/generators/handler-generator.d.ts +2 -0
- package/dist/generators/handler-generator.js +84 -0
- package/dist/generators/route-generator.d.ts +2 -0
- package/dist/generators/route-generator.js +50 -0
- package/dist/generators/router-updater.d.ts +2 -0
- package/dist/generators/router-updater.js +48 -0
- package/dist/generators/validator-generator.d.ts +2 -0
- package/dist/generators/validator-generator.js +160 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +101 -0
- package/dist/types/index.d.ts +50 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/file-utils.d.ts +40 -0
- package/dist/utils/file-utils.js +89 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.js +38 -0
- package/package.json +68 -0
|
@@ -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
|
+
}
|