create-coreback 1.0.1
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/.github/PUBLISH.md +58 -0
- package/.github/workflows/publish.yml +78 -0
- package/LICENSE +21 -0
- package/README.md +62 -0
- package/dist/createProject.d.ts +2 -0
- package/dist/createProject.d.ts.map +1 -0
- package/dist/createProject.js +70 -0
- package/dist/createProject.js.map +1 -0
- package/dist/generators/cursorRules.d.ts +2 -0
- package/dist/generators/cursorRules.d.ts.map +1 -0
- package/dist/generators/cursorRules.js +35 -0
- package/dist/generators/cursorRules.js.map +1 -0
- package/dist/generators/docker.d.ts +3 -0
- package/dist/generators/docker.d.ts.map +1 -0
- package/dist/generators/docker.js +119 -0
- package/dist/generators/docker.js.map +1 -0
- package/dist/generators/envExample.d.ts +3 -0
- package/dist/generators/envExample.d.ts.map +1 -0
- package/dist/generators/envExample.js +39 -0
- package/dist/generators/envExample.js.map +1 -0
- package/dist/generators/eslint.d.ts +2 -0
- package/dist/generators/eslint.d.ts.map +1 -0
- package/dist/generators/eslint.js +32 -0
- package/dist/generators/eslint.js.map +1 -0
- package/dist/generators/githubActions.d.ts +3 -0
- package/dist/generators/githubActions.d.ts.map +1 -0
- package/dist/generators/githubActions.js +68 -0
- package/dist/generators/githubActions.js.map +1 -0
- package/dist/generators/gitignore.d.ts +2 -0
- package/dist/generators/gitignore.d.ts.map +1 -0
- package/dist/generators/gitignore.js +50 -0
- package/dist/generators/gitignore.js.map +1 -0
- package/dist/generators/index.d.ts +3 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +34 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/generators/jest.d.ts +2 -0
- package/dist/generators/jest.d.ts.map +1 -0
- package/dist/generators/jest.js +23 -0
- package/dist/generators/jest.js.map +1 -0
- package/dist/generators/packageJson.d.ts +3 -0
- package/dist/generators/packageJson.d.ts.map +1 -0
- package/dist/generators/packageJson.js +80 -0
- package/dist/generators/packageJson.js.map +1 -0
- package/dist/generators/prettier.d.ts +2 -0
- package/dist/generators/prettier.d.ts.map +1 -0
- package/dist/generators/prettier.js +14 -0
- package/dist/generators/prettier.js.map +1 -0
- package/dist/generators/prisma.d.ts +3 -0
- package/dist/generators/prisma.d.ts.map +1 -0
- package/dist/generators/prisma.js +55 -0
- package/dist/generators/prisma.js.map +1 -0
- package/dist/generators/sourceFiles.d.ts +3 -0
- package/dist/generators/sourceFiles.d.ts.map +1 -0
- package/dist/generators/sourceFiles.js +767 -0
- package/dist/generators/sourceFiles.js.map +1 -0
- package/dist/generators/tsconfig.d.ts +2 -0
- package/dist/generators/tsconfig.d.ts.map +1 -0
- package/dist/generators/tsconfig.js +32 -0
- package/dist/generators/tsconfig.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts.d.ts +3 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +56 -0
- package/dist/prompts.js.map +1 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/parseArgs.d.ts +2 -0
- package/dist/utils/parseArgs.d.ts.map +1 -0
- package/dist/utils/parseArgs.js +6 -0
- package/dist/utils/parseArgs.js.map +1 -0
- package/package.json +46 -0
- package/src/createProject.ts +86 -0
- package/src/generators/cursorRules.ts +39 -0
- package/src/generators/docker.ts +131 -0
- package/src/generators/envExample.ts +49 -0
- package/src/generators/eslint.ts +38 -0
- package/src/generators/githubActions.ts +79 -0
- package/src/generators/gitignore.ts +52 -0
- package/src/generators/index.ts +45 -0
- package/src/generators/jest.ts +29 -0
- package/src/generators/packageJson.ts +89 -0
- package/src/generators/prettier.ts +22 -0
- package/src/generators/prisma.ts +66 -0
- package/src/generators/sourceFiles.ts +840 -0
- package/src/generators/tsconfig.ts +34 -0
- package/src/index.ts +45 -0
- package/src/prompts.ts +62 -0
- package/src/types.ts +11 -0
- package/src/utils/parseArgs.ts +6 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
import { ProjectConfig } from '../types.js';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
export async function generateSourceFiles(
|
|
6
|
+
projectPath: string,
|
|
7
|
+
config: ProjectConfig
|
|
8
|
+
): Promise<void> {
|
|
9
|
+
const srcDir = path.join(projectPath, 'src');
|
|
10
|
+
const configDir = path.join(srcDir, 'config');
|
|
11
|
+
const controllersDir = path.join(srcDir, 'controllers');
|
|
12
|
+
const servicesDir = path.join(srcDir, 'services');
|
|
13
|
+
const repositoriesDir = path.join(srcDir, 'repositories');
|
|
14
|
+
const middlewaresDir = path.join(srcDir, 'middlewares');
|
|
15
|
+
const routesDir = path.join(srcDir, 'routes');
|
|
16
|
+
const typesDir = path.join(srcDir, 'types');
|
|
17
|
+
const utilsDir = path.join(srcDir, 'utils');
|
|
18
|
+
const validatorsDir = path.join(srcDir, 'validators');
|
|
19
|
+
const testsDir = path.join(projectPath, 'tests');
|
|
20
|
+
const unitTestsDir = path.join(testsDir, 'unit');
|
|
21
|
+
const integrationTestsDir = path.join(testsDir, 'integration');
|
|
22
|
+
|
|
23
|
+
// Create directories
|
|
24
|
+
await Promise.all([
|
|
25
|
+
fs.ensureDir(configDir),
|
|
26
|
+
fs.ensureDir(controllersDir),
|
|
27
|
+
fs.ensureDir(servicesDir),
|
|
28
|
+
fs.ensureDir(repositoriesDir),
|
|
29
|
+
fs.ensureDir(middlewaresDir),
|
|
30
|
+
fs.ensureDir(routesDir),
|
|
31
|
+
fs.ensureDir(typesDir),
|
|
32
|
+
fs.ensureDir(utilsDir),
|
|
33
|
+
fs.ensureDir(validatorsDir),
|
|
34
|
+
fs.ensureDir(unitTestsDir),
|
|
35
|
+
fs.ensureDir(integrationTestsDir),
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
// Generate files
|
|
39
|
+
await generateIndex(srcDir, config);
|
|
40
|
+
await generateConfigFiles(configDir, config);
|
|
41
|
+
await generateMiddlewares(middlewaresDir, config);
|
|
42
|
+
await generateUtils(utilsDir);
|
|
43
|
+
await generateRoutes(routesDir, config);
|
|
44
|
+
await generateTypes(typesDir);
|
|
45
|
+
await generateValidators(validatorsDir, config);
|
|
46
|
+
await generateControllers(controllersDir, config);
|
|
47
|
+
await generateServices(servicesDir, config);
|
|
48
|
+
await generateRepositories(repositoriesDir, config);
|
|
49
|
+
await generateTests(unitTestsDir, integrationTestsDir, config);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function generateIndex(srcDir: string, config: ProjectConfig): Promise<void> {
|
|
53
|
+
const indexContent = `import express from 'express';
|
|
54
|
+
import { config } from './config/env.js';
|
|
55
|
+
import { setupMiddlewares } from './config/middlewares.js';
|
|
56
|
+
import { setupRoutes } from './routes/index.js';
|
|
57
|
+
import { setupSwagger } from './config/swagger.js';
|
|
58
|
+
import { logger } from './utils/logger.js';
|
|
59
|
+
import { errorHandler } from './middlewares/errorHandler.js';
|
|
60
|
+
|
|
61
|
+
const app = express();
|
|
62
|
+
|
|
63
|
+
setupMiddlewares(app);
|
|
64
|
+
setupSwagger(app);
|
|
65
|
+
setupRoutes(app);
|
|
66
|
+
app.use(errorHandler);
|
|
67
|
+
|
|
68
|
+
const server = app.listen(config.PORT, () => {
|
|
69
|
+
logger.info(\`🚀 Server: http://localhost:\${config.PORT}\`);
|
|
70
|
+
logger.info(\`📚 Docs: http://localhost:\${config.PORT}/api-docs\`);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
process.on('SIGTERM', () => {
|
|
74
|
+
server.close();
|
|
75
|
+
process.exit(0);
|
|
76
|
+
});
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
await fs.writeFile(path.join(srcDir, 'index.ts'), indexContent);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function generateConfigFiles(configDir: string, config: ProjectConfig): Promise<void> {
|
|
83
|
+
// env.ts
|
|
84
|
+
const envContent = `import { z } from 'zod';
|
|
85
|
+
import dotenv from 'dotenv';
|
|
86
|
+
|
|
87
|
+
dotenv.config();
|
|
88
|
+
|
|
89
|
+
const envSchema = z.object({
|
|
90
|
+
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
91
|
+
PORT: z.coerce.number().default(3000),
|
|
92
|
+
DATABASE_URL: z.string(),
|
|
93
|
+
${config.includeAuth ? ` JWT_SECRET: z.string(),
|
|
94
|
+
JWT_EXPIRES_IN: z.string().default('7d'),` : ''}
|
|
95
|
+
RATE_LIMIT_WINDOW_MS: z.coerce.number().default(900000),
|
|
96
|
+
RATE_LIMIT_MAX: z.coerce.number().default(100),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
export const config = envSchema.parse(process.env);
|
|
100
|
+
`;
|
|
101
|
+
|
|
102
|
+
await fs.writeFile(path.join(configDir, 'env.ts'), envContent);
|
|
103
|
+
|
|
104
|
+
// database.ts
|
|
105
|
+
const databaseContent = `import { PrismaClient } from '@prisma/client';
|
|
106
|
+
import { logger } from '../utils/logger.js';
|
|
107
|
+
|
|
108
|
+
const prisma = new PrismaClient({
|
|
109
|
+
log: [
|
|
110
|
+
{ level: 'query', emit: 'event' },
|
|
111
|
+
{ level: 'error', emit: 'stdout' },
|
|
112
|
+
{ level: 'warn', emit: 'stdout' },
|
|
113
|
+
],
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
prisma.$on('query', (e) => {
|
|
117
|
+
logger.debug('Query: ' + e.query);
|
|
118
|
+
logger.debug('Params: ' + e.params);
|
|
119
|
+
logger.debug('Duration: ' + e.duration + 'ms');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
export { prisma };
|
|
123
|
+
`;
|
|
124
|
+
|
|
125
|
+
await fs.writeFile(path.join(configDir, 'database.ts'), databaseContent);
|
|
126
|
+
|
|
127
|
+
// middlewares.ts
|
|
128
|
+
const middlewaresContent = `import express, { Express } from 'express';
|
|
129
|
+
import cors from 'cors';
|
|
130
|
+
import helmet from 'helmet';
|
|
131
|
+
import compression from 'compression';
|
|
132
|
+
import rateLimit from 'express-rate-limit';
|
|
133
|
+
import { config } from './env.js';
|
|
134
|
+
|
|
135
|
+
export function setupMiddlewares(app: Express): void {
|
|
136
|
+
app.use(helmet());
|
|
137
|
+
app.use(cors());
|
|
138
|
+
app.use(compression());
|
|
139
|
+
app.use(express.json());
|
|
140
|
+
app.use(express.urlencoded({ extended: true }));
|
|
141
|
+
|
|
142
|
+
const limiter = rateLimit({
|
|
143
|
+
windowMs: config.RATE_LIMIT_WINDOW_MS,
|
|
144
|
+
max: config.RATE_LIMIT_MAX,
|
|
145
|
+
message: 'Too many requests from this IP, please try again later.',
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
app.use('/api/', limiter);
|
|
149
|
+
}
|
|
150
|
+
`;
|
|
151
|
+
|
|
152
|
+
await fs.writeFile(path.join(configDir, 'middlewares.ts'), middlewaresContent);
|
|
153
|
+
|
|
154
|
+
// swagger.ts
|
|
155
|
+
const swaggerContent = `import { Express } from 'express';
|
|
156
|
+
import swaggerJsdoc from 'swagger-jsdoc';
|
|
157
|
+
import swaggerUi from 'swagger-ui-express';
|
|
158
|
+
import { config } from './env.js';
|
|
159
|
+
|
|
160
|
+
const options: swaggerJsdoc.Options = {
|
|
161
|
+
definition: {
|
|
162
|
+
openapi: '3.0.0',
|
|
163
|
+
info: {
|
|
164
|
+
title: 'CoreBack API',
|
|
165
|
+
version: '1.0.0',
|
|
166
|
+
description: 'Production-ready backend API',
|
|
167
|
+
},
|
|
168
|
+
servers: [
|
|
169
|
+
{
|
|
170
|
+
url: \`http://localhost:\${config.PORT}\`,
|
|
171
|
+
description: 'Development server',
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
apis: ['./src/routes/**/*.ts', './src/controllers/**/*.ts'],
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const swaggerSpec = swaggerJsdoc(options);
|
|
179
|
+
|
|
180
|
+
export function setupSwagger(app: Express): void {
|
|
181
|
+
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
|
|
182
|
+
}
|
|
183
|
+
`;
|
|
184
|
+
|
|
185
|
+
await fs.writeFile(path.join(configDir, 'swagger.ts'), swaggerContent);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function generateMiddlewares(middlewaresDir: string, config: ProjectConfig): Promise<void> {
|
|
189
|
+
// errorHandler.ts
|
|
190
|
+
const errorHandlerContent = `import { Request, Response, NextFunction } from 'express';
|
|
191
|
+
import { ZodError } from 'zod';
|
|
192
|
+
import { logger } from '../utils/logger.js';
|
|
193
|
+
|
|
194
|
+
export class AppError extends Error {
|
|
195
|
+
constructor(
|
|
196
|
+
public statusCode: number,
|
|
197
|
+
message: string,
|
|
198
|
+
public isOperational = true
|
|
199
|
+
) {
|
|
200
|
+
super(message);
|
|
201
|
+
Object.setPrototypeOf(this, AppError.prototype);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export const errorHandler = (
|
|
206
|
+
err: Error,
|
|
207
|
+
req: Request,
|
|
208
|
+
res: Response,
|
|
209
|
+
next: NextFunction
|
|
210
|
+
) => {
|
|
211
|
+
logger.error(err);
|
|
212
|
+
|
|
213
|
+
if (err instanceof ZodError) {
|
|
214
|
+
return res.status(400).json({
|
|
215
|
+
status: 'error',
|
|
216
|
+
message: 'Validation error',
|
|
217
|
+
errors: err.errors,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (err instanceof AppError) {
|
|
222
|
+
return res.status(err.statusCode).json({
|
|
223
|
+
status: 'error',
|
|
224
|
+
message: err.message,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
res.status(500).json({
|
|
229
|
+
status: 'error',
|
|
230
|
+
message: 'Internal server error',
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
`;
|
|
234
|
+
|
|
235
|
+
await fs.writeFile(path.join(middlewaresDir, 'errorHandler.ts'), errorHandlerContent);
|
|
236
|
+
|
|
237
|
+
// validator.ts
|
|
238
|
+
const validatorContent = `import { Request, Response, NextFunction } from 'express';
|
|
239
|
+
import { ZodSchema, ZodError } from 'zod';
|
|
240
|
+
|
|
241
|
+
export const validate = (schema: ZodSchema) => {
|
|
242
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
243
|
+
try {
|
|
244
|
+
schema.parse({
|
|
245
|
+
body: req.body,
|
|
246
|
+
query: req.query,
|
|
247
|
+
params: req.params,
|
|
248
|
+
});
|
|
249
|
+
next();
|
|
250
|
+
} catch (error) {
|
|
251
|
+
if (error instanceof ZodError) {
|
|
252
|
+
return res.status(400).json({
|
|
253
|
+
status: 'error',
|
|
254
|
+
message: 'Validation error',
|
|
255
|
+
errors: error.errors,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
next(error);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
};
|
|
262
|
+
`;
|
|
263
|
+
|
|
264
|
+
await fs.writeFile(path.join(middlewaresDir, 'validator.ts'), validatorContent);
|
|
265
|
+
|
|
266
|
+
if (config.includeAuth) {
|
|
267
|
+
// auth.ts
|
|
268
|
+
const authContent = `import { Request, Response, NextFunction } from 'express';
|
|
269
|
+
import jwt from 'jsonwebtoken';
|
|
270
|
+
import { config } from '../config/env.js';
|
|
271
|
+
import { AppError } from './errorHandler.js';
|
|
272
|
+
|
|
273
|
+
export interface AuthRequest extends Request {
|
|
274
|
+
user?: {
|
|
275
|
+
id: string;
|
|
276
|
+
email: string;
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export const authenticate = (
|
|
281
|
+
req: AuthRequest,
|
|
282
|
+
res: Response,
|
|
283
|
+
next: NextFunction
|
|
284
|
+
) => {
|
|
285
|
+
try {
|
|
286
|
+
const authHeader = req.headers.authorization;
|
|
287
|
+
|
|
288
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
289
|
+
throw new AppError(401, 'Authentication required');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const token = authHeader.substring(7);
|
|
293
|
+
const decoded = jwt.verify(token, config.JWT_SECRET) as {
|
|
294
|
+
id: string;
|
|
295
|
+
email: string;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
req.user = decoded;
|
|
299
|
+
next();
|
|
300
|
+
} catch (error) {
|
|
301
|
+
if (error instanceof jwt.JsonWebTokenError) {
|
|
302
|
+
next(new AppError(401, 'Invalid token'));
|
|
303
|
+
} else {
|
|
304
|
+
next(error);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
`;
|
|
309
|
+
|
|
310
|
+
await fs.writeFile(path.join(middlewaresDir, 'auth.ts'), authContent);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function generateUtils(utilsDir: string): Promise<void> {
|
|
315
|
+
const loggerContent = `import winston from 'winston';
|
|
316
|
+
import { config } from '../config/env.js';
|
|
317
|
+
import fs from 'fs-extra';
|
|
318
|
+
import path from 'path';
|
|
319
|
+
|
|
320
|
+
// Ensure logs directory exists
|
|
321
|
+
const logsDir = path.join(process.cwd(), 'logs');
|
|
322
|
+
fs.ensureDirSync(logsDir);
|
|
323
|
+
|
|
324
|
+
const logFormat = winston.format.combine(
|
|
325
|
+
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
|
326
|
+
winston.format.errors({ stack: true }),
|
|
327
|
+
winston.format.splat(),
|
|
328
|
+
winston.format.json()
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
export const logger = winston.createLogger({
|
|
332
|
+
level: config.NODE_ENV === 'production' ? 'info' : 'debug',
|
|
333
|
+
format: logFormat,
|
|
334
|
+
defaultMeta: { service: 'coreback-api' },
|
|
335
|
+
transports: [
|
|
336
|
+
new winston.transports.File({ filename: path.join(logsDir, 'error.log'), level: 'error' }),
|
|
337
|
+
new winston.transports.File({ filename: path.join(logsDir, 'combined.log') }),
|
|
338
|
+
],
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
if (config.NODE_ENV !== 'production') {
|
|
342
|
+
logger.add(
|
|
343
|
+
new winston.transports.Console({
|
|
344
|
+
format: winston.format.combine(
|
|
345
|
+
winston.format.colorize(),
|
|
346
|
+
winston.format.simple()
|
|
347
|
+
),
|
|
348
|
+
})
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
`;
|
|
352
|
+
|
|
353
|
+
await fs.writeFile(path.join(utilsDir, 'logger.ts'), loggerContent);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function generateRoutes(routesDir: string, config: ProjectConfig): Promise<void> {
|
|
357
|
+
// index.ts
|
|
358
|
+
let indexContent = `import { Express } from 'express';
|
|
359
|
+
import { healthRoutes } from './health.routes.js';
|
|
360
|
+
`;
|
|
361
|
+
|
|
362
|
+
if (config.includeAuth) {
|
|
363
|
+
indexContent += `import { authRoutes } from './auth.routes.js';
|
|
364
|
+
`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
indexContent += `
|
|
368
|
+
export function setupRoutes(app: Express): void {
|
|
369
|
+
app.use('/api/health', healthRoutes);
|
|
370
|
+
`;
|
|
371
|
+
|
|
372
|
+
if (config.includeAuth) {
|
|
373
|
+
indexContent += ` app.use('/api/auth', authRoutes);
|
|
374
|
+
`;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
indexContent += `}
|
|
378
|
+
`;
|
|
379
|
+
|
|
380
|
+
await fs.writeFile(path.join(routesDir, 'index.ts'), indexContent);
|
|
381
|
+
|
|
382
|
+
// health.routes.ts
|
|
383
|
+
const healthRoutesContent = `import { Router } from 'express';
|
|
384
|
+
import { healthController } from '../controllers/health.controller.js';
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* @swagger
|
|
388
|
+
* tags:
|
|
389
|
+
* name: Health
|
|
390
|
+
* description: Health check endpoints
|
|
391
|
+
*/
|
|
392
|
+
|
|
393
|
+
const router = Router();
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* @swagger
|
|
397
|
+
* /api/health:
|
|
398
|
+
* get:
|
|
399
|
+
* summary: Health check endpoint
|
|
400
|
+
* tags: [Health]
|
|
401
|
+
* responses:
|
|
402
|
+
* 200:
|
|
403
|
+
* description: Service is healthy
|
|
404
|
+
* content:
|
|
405
|
+
* application/json:
|
|
406
|
+
* schema:
|
|
407
|
+
* type: object
|
|
408
|
+
* properties:
|
|
409
|
+
* status:
|
|
410
|
+
* type: string
|
|
411
|
+
* example: ok
|
|
412
|
+
* timestamp:
|
|
413
|
+
* type: string
|
|
414
|
+
* example: 2024-01-01T00:00:00.000Z
|
|
415
|
+
*/
|
|
416
|
+
router.get('/', healthController.check);
|
|
417
|
+
|
|
418
|
+
export { router as healthRoutes };
|
|
419
|
+
`;
|
|
420
|
+
|
|
421
|
+
await fs.writeFile(path.join(routesDir, 'health.routes.ts'), healthRoutesContent);
|
|
422
|
+
|
|
423
|
+
if (config.includeAuth) {
|
|
424
|
+
// auth.routes.ts
|
|
425
|
+
const authRoutesContent = `import { Router } from 'express';
|
|
426
|
+
import { authController } from '../controllers/auth.controller.js';
|
|
427
|
+
import { validate } from '../middlewares/validator.js';
|
|
428
|
+
import { registerSchema, loginSchema } from '../validators/auth.validator.js';
|
|
429
|
+
import { authenticate } from '../middlewares/auth.js';
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* @swagger
|
|
433
|
+
* tags:
|
|
434
|
+
* name: Auth
|
|
435
|
+
* description: Authentication endpoints
|
|
436
|
+
*/
|
|
437
|
+
|
|
438
|
+
const router = Router();
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* @swagger
|
|
442
|
+
* /api/auth/register:
|
|
443
|
+
* post:
|
|
444
|
+
* summary: Register a new user
|
|
445
|
+
* tags: [Auth]
|
|
446
|
+
* requestBody:
|
|
447
|
+
* required: true
|
|
448
|
+
* content:
|
|
449
|
+
* application/json:
|
|
450
|
+
* schema:
|
|
451
|
+
* type: object
|
|
452
|
+
* required:
|
|
453
|
+
* - email
|
|
454
|
+
* - password
|
|
455
|
+
* properties:
|
|
456
|
+
* email:
|
|
457
|
+
* type: string
|
|
458
|
+
* format: email
|
|
459
|
+
* password:
|
|
460
|
+
* type: string
|
|
461
|
+
* minLength: 8
|
|
462
|
+
* name:
|
|
463
|
+
* type: string
|
|
464
|
+
* responses:
|
|
465
|
+
* 201:
|
|
466
|
+
* description: User created successfully
|
|
467
|
+
* 400:
|
|
468
|
+
* description: Validation error
|
|
469
|
+
*/
|
|
470
|
+
router.post('/register', validate(registerSchema), authController.register);
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* @swagger
|
|
474
|
+
* /api/auth/login:
|
|
475
|
+
* post:
|
|
476
|
+
* summary: Login user
|
|
477
|
+
* tags: [Auth]
|
|
478
|
+
* requestBody:
|
|
479
|
+
* required: true
|
|
480
|
+
* content:
|
|
481
|
+
* application/json:
|
|
482
|
+
* schema:
|
|
483
|
+
* type: object
|
|
484
|
+
* required:
|
|
485
|
+
* - email
|
|
486
|
+
* - password
|
|
487
|
+
* properties:
|
|
488
|
+
* email:
|
|
489
|
+
* type: string
|
|
490
|
+
* format: email
|
|
491
|
+
* password:
|
|
492
|
+
* type: string
|
|
493
|
+
* responses:
|
|
494
|
+
* 200:
|
|
495
|
+
* description: Login successful
|
|
496
|
+
* 401:
|
|
497
|
+
* description: Invalid credentials
|
|
498
|
+
*/
|
|
499
|
+
router.post('/login', validate(loginSchema), authController.login);
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* @swagger
|
|
503
|
+
* /api/auth/me:
|
|
504
|
+
* get:
|
|
505
|
+
* summary: Get current user
|
|
506
|
+
* tags: [Auth]
|
|
507
|
+
* security:
|
|
508
|
+
* - bearerAuth: []
|
|
509
|
+
* responses:
|
|
510
|
+
* 200:
|
|
511
|
+
* description: Current user information
|
|
512
|
+
* 401:
|
|
513
|
+
* description: Unauthorized
|
|
514
|
+
*/
|
|
515
|
+
router.get('/me', authenticate, authController.me);
|
|
516
|
+
|
|
517
|
+
export { router as authRoutes };
|
|
518
|
+
`;
|
|
519
|
+
|
|
520
|
+
await fs.writeFile(path.join(routesDir, 'auth.routes.ts'), authRoutesContent);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async function generateTypes(typesDir: string): Promise<void> {
|
|
525
|
+
const indexContent = `export interface ApiResponse<T = unknown> {
|
|
526
|
+
status: 'success' | 'error';
|
|
527
|
+
data?: T;
|
|
528
|
+
message?: string;
|
|
529
|
+
errors?: unknown[];
|
|
530
|
+
}
|
|
531
|
+
`;
|
|
532
|
+
|
|
533
|
+
await fs.writeFile(path.join(typesDir, 'index.ts'), indexContent);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async function generateValidators(validatorsDir: string, config: ProjectConfig): Promise<void> {
|
|
537
|
+
if (config.includeAuth) {
|
|
538
|
+
const authValidatorContent = `import { z } from 'zod';
|
|
539
|
+
|
|
540
|
+
export const registerSchema = z.object({
|
|
541
|
+
body: z.object({
|
|
542
|
+
email: z.string().email('Invalid email format'),
|
|
543
|
+
password: z.string().min(8, 'Password must be at least 8 characters'),
|
|
544
|
+
name: z.string().optional(),
|
|
545
|
+
}),
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
export const loginSchema = z.object({
|
|
549
|
+
body: z.object({
|
|
550
|
+
email: z.string().email('Invalid email format'),
|
|
551
|
+
password: z.string().min(1, 'Password is required'),
|
|
552
|
+
}),
|
|
553
|
+
});
|
|
554
|
+
`;
|
|
555
|
+
|
|
556
|
+
await fs.writeFile(path.join(validatorsDir, 'auth.validator.ts'), authValidatorContent);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function generateControllers(controllersDir: string, config: ProjectConfig): Promise<void> {
|
|
561
|
+
// health.controller.ts
|
|
562
|
+
const healthControllerContent = `import { Request, Response } from 'express';
|
|
563
|
+
|
|
564
|
+
export const healthController = {
|
|
565
|
+
check: (_req: Request, res: Response) => {
|
|
566
|
+
res.json({
|
|
567
|
+
status: 'ok',
|
|
568
|
+
timestamp: new Date().toISOString(),
|
|
569
|
+
});
|
|
570
|
+
},
|
|
571
|
+
};
|
|
572
|
+
`;
|
|
573
|
+
|
|
574
|
+
await fs.writeFile(
|
|
575
|
+
path.join(controllersDir, 'health.controller.ts'),
|
|
576
|
+
healthControllerContent
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
if (config.includeAuth) {
|
|
580
|
+
// auth.controller.ts
|
|
581
|
+
const authControllerContent = `import { Response } from 'express';
|
|
582
|
+
import { AuthRequest } from '../middlewares/auth.js';
|
|
583
|
+
import { authService } from '../services/auth.service.js';
|
|
584
|
+
import { AppError } from '../middlewares/errorHandler.js';
|
|
585
|
+
|
|
586
|
+
export const authController = {
|
|
587
|
+
register: async (req: AuthRequest, res: Response) => {
|
|
588
|
+
try {
|
|
589
|
+
const { email, password, name } = req.body;
|
|
590
|
+
const user = await authService.register(email, password, name);
|
|
591
|
+
res.status(201).json({
|
|
592
|
+
status: 'success',
|
|
593
|
+
data: user,
|
|
594
|
+
});
|
|
595
|
+
} catch (error) {
|
|
596
|
+
if (error instanceof AppError) {
|
|
597
|
+
throw error;
|
|
598
|
+
}
|
|
599
|
+
throw new AppError(500, 'Failed to register user');
|
|
600
|
+
}
|
|
601
|
+
},
|
|
602
|
+
|
|
603
|
+
login: async (req: AuthRequest, res: Response) => {
|
|
604
|
+
try {
|
|
605
|
+
const { email, password } = req.body;
|
|
606
|
+
const result = await authService.login(email, password);
|
|
607
|
+
res.json({
|
|
608
|
+
status: 'success',
|
|
609
|
+
data: result,
|
|
610
|
+
});
|
|
611
|
+
} catch (error) {
|
|
612
|
+
if (error instanceof AppError) {
|
|
613
|
+
throw error;
|
|
614
|
+
}
|
|
615
|
+
throw new AppError(500, 'Failed to login');
|
|
616
|
+
}
|
|
617
|
+
},
|
|
618
|
+
|
|
619
|
+
me: async (req: AuthRequest, res: Response) => {
|
|
620
|
+
try {
|
|
621
|
+
const user = await authService.getUserById(req.user!.id);
|
|
622
|
+
res.json({
|
|
623
|
+
status: 'success',
|
|
624
|
+
data: user,
|
|
625
|
+
});
|
|
626
|
+
} catch (error) {
|
|
627
|
+
if (error instanceof AppError) {
|
|
628
|
+
throw error;
|
|
629
|
+
}
|
|
630
|
+
throw new AppError(500, 'Failed to get user');
|
|
631
|
+
}
|
|
632
|
+
},
|
|
633
|
+
};
|
|
634
|
+
`;
|
|
635
|
+
|
|
636
|
+
await fs.writeFile(
|
|
637
|
+
path.join(controllersDir, 'auth.controller.ts'),
|
|
638
|
+
authControllerContent
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
async function generateServices(servicesDir: string, config: ProjectConfig): Promise<void> {
|
|
644
|
+
if (config.includeAuth) {
|
|
645
|
+
const authServiceContent = `import bcrypt from 'bcrypt';
|
|
646
|
+
import jwt from 'jsonwebtoken';
|
|
647
|
+
import { config } from '../config/env.js';
|
|
648
|
+
import { userRepository } from '../repositories/user.repository.js';
|
|
649
|
+
import { AppError } from '../middlewares/errorHandler.js';
|
|
650
|
+
|
|
651
|
+
export const authService = {
|
|
652
|
+
async register(email: string, password: string, name?: string) {
|
|
653
|
+
const existingUser = await userRepository.findByEmail(email);
|
|
654
|
+
|
|
655
|
+
if (existingUser) {
|
|
656
|
+
throw new AppError(400, 'User already exists');
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const hashedPassword = await bcrypt.hash(password, 10);
|
|
660
|
+
const user = await userRepository.create({
|
|
661
|
+
email,
|
|
662
|
+
password: hashedPassword,
|
|
663
|
+
name,
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
const token = jwt.sign(
|
|
667
|
+
{ id: user.id, email: user.email },
|
|
668
|
+
config.JWT_SECRET,
|
|
669
|
+
{ expiresIn: config.JWT_EXPIRES_IN }
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
return {
|
|
673
|
+
user: {
|
|
674
|
+
id: user.id,
|
|
675
|
+
email: user.email,
|
|
676
|
+
name: user.name,
|
|
677
|
+
},
|
|
678
|
+
token,
|
|
679
|
+
};
|
|
680
|
+
},
|
|
681
|
+
|
|
682
|
+
async login(email: string, password: string) {
|
|
683
|
+
const user = await userRepository.findByEmail(email);
|
|
684
|
+
|
|
685
|
+
if (!user) {
|
|
686
|
+
throw new AppError(401, 'Invalid credentials');
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const isValidPassword = await bcrypt.compare(password, user.password);
|
|
690
|
+
|
|
691
|
+
if (!isValidPassword) {
|
|
692
|
+
throw new AppError(401, 'Invalid credentials');
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const token = jwt.sign(
|
|
696
|
+
{ id: user.id, email: user.email },
|
|
697
|
+
config.JWT_SECRET,
|
|
698
|
+
{ expiresIn: config.JWT_EXPIRES_IN }
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
return {
|
|
702
|
+
user: {
|
|
703
|
+
id: user.id,
|
|
704
|
+
email: user.email,
|
|
705
|
+
name: user.name,
|
|
706
|
+
},
|
|
707
|
+
token,
|
|
708
|
+
};
|
|
709
|
+
},
|
|
710
|
+
|
|
711
|
+
async getUserById(id: string) {
|
|
712
|
+
const user = await userRepository.findById(id);
|
|
713
|
+
|
|
714
|
+
if (!user) {
|
|
715
|
+
throw new AppError(404, 'User not found');
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
return {
|
|
719
|
+
id: user.id,
|
|
720
|
+
email: user.email,
|
|
721
|
+
name: user.name,
|
|
722
|
+
createdAt: user.createdAt,
|
|
723
|
+
updatedAt: user.updatedAt,
|
|
724
|
+
};
|
|
725
|
+
},
|
|
726
|
+
};
|
|
727
|
+
`;
|
|
728
|
+
|
|
729
|
+
await fs.writeFile(
|
|
730
|
+
path.join(servicesDir, 'auth.service.ts'),
|
|
731
|
+
authServiceContent
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
async function generateRepositories(repositoriesDir: string, config: ProjectConfig): Promise<void> {
|
|
737
|
+
if (config.includeAuth) {
|
|
738
|
+
const userRepositoryContent = `import { prisma } from '../config/database.js';
|
|
739
|
+
|
|
740
|
+
export const userRepository = {
|
|
741
|
+
async findByEmail(email: string) {
|
|
742
|
+
return prisma.user.findUnique({
|
|
743
|
+
where: { email },
|
|
744
|
+
});
|
|
745
|
+
},
|
|
746
|
+
|
|
747
|
+
async findById(id: string) {
|
|
748
|
+
return prisma.user.findUnique({
|
|
749
|
+
where: { id },
|
|
750
|
+
select: {
|
|
751
|
+
id: true,
|
|
752
|
+
email: true,
|
|
753
|
+
name: true,
|
|
754
|
+
createdAt: true,
|
|
755
|
+
updatedAt: true,
|
|
756
|
+
},
|
|
757
|
+
});
|
|
758
|
+
},
|
|
759
|
+
|
|
760
|
+
async create(data: { email: string; password: string; name?: string }) {
|
|
761
|
+
return prisma.user.create({
|
|
762
|
+
data,
|
|
763
|
+
select: {
|
|
764
|
+
id: true,
|
|
765
|
+
email: true,
|
|
766
|
+
name: true,
|
|
767
|
+
createdAt: true,
|
|
768
|
+
updatedAt: true,
|
|
769
|
+
},
|
|
770
|
+
});
|
|
771
|
+
},
|
|
772
|
+
};
|
|
773
|
+
`;
|
|
774
|
+
|
|
775
|
+
await fs.writeFile(
|
|
776
|
+
path.join(repositoriesDir, 'user.repository.ts'),
|
|
777
|
+
userRepositoryContent
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
async function generateTests(
|
|
783
|
+
unitTestsDir: string,
|
|
784
|
+
integrationTestsDir: string,
|
|
785
|
+
config: ProjectConfig
|
|
786
|
+
): Promise<void> {
|
|
787
|
+
// Unit test example
|
|
788
|
+
const unitTestContent = `import { healthController } from '../../src/controllers/health.controller.js';
|
|
789
|
+
|
|
790
|
+
describe('Health Controller', () => {
|
|
791
|
+
it('should return ok status', () => {
|
|
792
|
+
const mockReq = {} as any;
|
|
793
|
+
const mockRes = {
|
|
794
|
+
json: jest.fn(),
|
|
795
|
+
} as any;
|
|
796
|
+
|
|
797
|
+
healthController.check(mockReq, mockRes);
|
|
798
|
+
|
|
799
|
+
expect(mockRes.json).toHaveBeenCalledWith({
|
|
800
|
+
status: 'ok',
|
|
801
|
+
timestamp: expect.any(String),
|
|
802
|
+
});
|
|
803
|
+
});
|
|
804
|
+
});
|
|
805
|
+
`;
|
|
806
|
+
|
|
807
|
+
await fs.writeFile(
|
|
808
|
+
path.join(unitTestsDir, 'health.controller.test.ts'),
|
|
809
|
+
unitTestContent
|
|
810
|
+
);
|
|
811
|
+
|
|
812
|
+
// Integration test example
|
|
813
|
+
const integrationTestContent = `import request from 'supertest';
|
|
814
|
+
import express from 'express';
|
|
815
|
+
import { setupRoutes } from '../../src/routes/index.js';
|
|
816
|
+
import { setupMiddlewares } from '../../src/config/middlewares.js';
|
|
817
|
+
import { errorHandler } from '../../src/middlewares/errorHandler.js';
|
|
818
|
+
|
|
819
|
+
const app = express();
|
|
820
|
+
setupMiddlewares(app);
|
|
821
|
+
setupRoutes(app);
|
|
822
|
+
app.use(errorHandler);
|
|
823
|
+
|
|
824
|
+
describe('Health API', () => {
|
|
825
|
+
it('GET /api/health should return 200', async () => {
|
|
826
|
+
const response = await request(app).get('/api/health');
|
|
827
|
+
|
|
828
|
+
expect(response.status).toBe(200);
|
|
829
|
+
expect(response.body).toHaveProperty('status', 'ok');
|
|
830
|
+
expect(response.body).toHaveProperty('timestamp');
|
|
831
|
+
});
|
|
832
|
+
});
|
|
833
|
+
`;
|
|
834
|
+
|
|
835
|
+
await fs.writeFile(
|
|
836
|
+
path.join(integrationTestsDir, 'health.api.test.ts'),
|
|
837
|
+
integrationTestContent
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
|