create-flex-stack 1.0.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/LICENSE +21 -0
- package/README.md +57 -0
- package/dist/index.js +330 -0
- package/dist/templates/cleanTemplate.js +697 -0
- package/dist/templates/frontendTemplate.js +855 -0
- package/dist/templates/hexagonalTemplate.js +855 -0
- package/dist/templates/layeredTemplate.js +745 -0
- package/dist/templates/modularTemplate.js +691 -0
- package/dist/templates/sharedTemplate.js +654 -0
- package/dist/templates/vmcTemplate.js +26 -0
- package/dist/types.js +1 -0
- package/dist/utils/generator.js +1120 -0
- package/package.json +46 -0
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
import { getMulterMiddleware, getTypeOrmConfig, getTypeOrmEntity } from './sharedTemplate.js';
|
|
2
|
+
export function getModularFiles(options) {
|
|
3
|
+
const files = {};
|
|
4
|
+
const userIdExpr = options.orm === 'prisma' ? 'user.id' : 'user._id.toString()';
|
|
5
|
+
const validationImport = options.validation === 'zod'
|
|
6
|
+
? `import { z } from 'zod';`
|
|
7
|
+
: `import Joi from 'joi';`;
|
|
8
|
+
// 1. src/config/env.ts
|
|
9
|
+
files['src/config/env.ts'] = options.validation === 'zod'
|
|
10
|
+
? `import dotenv from 'dotenv';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
|
|
13
|
+
dotenv.config();
|
|
14
|
+
|
|
15
|
+
const envSchema = z.object({
|
|
16
|
+
PORT: z.coerce.number().default(3000),
|
|
17
|
+
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
18
|
+
DATABASE_URL: z.string().min(1, 'DATABASE_URL is required'),
|
|
19
|
+
${options.auth ? `JWT_SECRET: z.string().min(8),
|
|
20
|
+
JWT_REFRESH_SECRET: z.string().min(8),
|
|
21
|
+
JWT_EXPIRES_IN: z.string().default('15m'),
|
|
22
|
+
JWT_REFRESH_EXPIRES_IN: z.string().default('7d'),` : ''}
|
|
23
|
+
${options.caching ? `REDIS_URL: z.string().min(1),` : ''}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const parsed = envSchema.safeParse(process.env);
|
|
27
|
+
if (!parsed.success) {
|
|
28
|
+
console.error('❌ Invalid environment variables:', parsed.error.format());
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
export const env = parsed.data;
|
|
32
|
+
`
|
|
33
|
+
: `import dotenv from 'dotenv';
|
|
34
|
+
import Joi from 'joi';
|
|
35
|
+
|
|
36
|
+
dotenv.config();
|
|
37
|
+
|
|
38
|
+
const envSchema = Joi.object({
|
|
39
|
+
PORT: Joi.number().default(3000),
|
|
40
|
+
NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),
|
|
41
|
+
DATABASE_URL: Joi.string().required(),
|
|
42
|
+
${options.auth ? `JWT_SECRET: Joi.string().min(8).required(),
|
|
43
|
+
JWT_REFRESH_SECRET: Joi.string().min(8).required(),
|
|
44
|
+
JWT_EXPIRES_IN: Joi.string().default('15m'),
|
|
45
|
+
JWT_REFRESH_EXPIRES_IN: Joi.string().default('7d'),` : ''}
|
|
46
|
+
${options.caching ? `REDIS_URL: Joi.string().required(),` : ''}
|
|
47
|
+
}).unknown().required();
|
|
48
|
+
|
|
49
|
+
const { error, value } = envSchema.validate(process.env);
|
|
50
|
+
if (error) {
|
|
51
|
+
console.error('❌ Invalid environment variables:', error.message);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
export const env = {
|
|
55
|
+
PORT: Number(value.PORT),
|
|
56
|
+
NODE_ENV: value.NODE_ENV,
|
|
57
|
+
DATABASE_URL: value.DATABASE_URL,
|
|
58
|
+
${options.auth ? `JWT_SECRET: value.JWT_SECRET,
|
|
59
|
+
JWT_REFRESH_SECRET: value.JWT_REFRESH_SECRET,
|
|
60
|
+
JWT_EXPIRES_IN: value.JWT_EXPIRES_IN,
|
|
61
|
+
JWT_REFRESH_EXPIRES_IN: value.JWT_REFRESH_EXPIRES_IN,` : ''}
|
|
62
|
+
${options.caching ? `REDIS_URL: value.REDIS_URL,` : ''}
|
|
63
|
+
};
|
|
64
|
+
`;
|
|
65
|
+
// TypeORM data-source if TypeORM chosen
|
|
66
|
+
if (options.orm === 'typeorm') {
|
|
67
|
+
files['src/config/data-source.ts'] = getTypeOrmConfig(options)
|
|
68
|
+
.replace('../entities/user.entity.js', '../modules/users/users.entity.js')
|
|
69
|
+
.replace('UserEntity', 'UserEntity');
|
|
70
|
+
}
|
|
71
|
+
// 2. src/middlewares/errorHandler.ts
|
|
72
|
+
files['src/middlewares/errorHandler.ts'] = `import { Request, Response, NextFunction } from 'express';
|
|
73
|
+
|
|
74
|
+
export class AppError extends Error {
|
|
75
|
+
constructor(public statusCode: number, message: string, public isOperational = true) {
|
|
76
|
+
super(message);
|
|
77
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
78
|
+
Error.captureStackTrace(this, this.constructor);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const errorHandler = (err: Error | AppError, req: Request, res: Response, next: NextFunction) => {
|
|
83
|
+
const statusCode = err instanceof AppError ? err.statusCode : 500;
|
|
84
|
+
const message = err.message || 'Internal Server Error';
|
|
85
|
+
|
|
86
|
+
res.status(statusCode).json({
|
|
87
|
+
status: 'error',
|
|
88
|
+
statusCode,
|
|
89
|
+
message,
|
|
90
|
+
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
`;
|
|
94
|
+
// 3. Auth Middlewares
|
|
95
|
+
if (options.auth) {
|
|
96
|
+
files['src/middlewares/auth.ts'] = `import { Request, Response, NextFunction } from 'express';
|
|
97
|
+
import jwt from 'jsonwebtoken';
|
|
98
|
+
import { env } from '../config/env.js';
|
|
99
|
+
import { AppError } from './errorHandler.js';
|
|
100
|
+
|
|
101
|
+
export interface AuthenticatedRequest extends Request {
|
|
102
|
+
user?: {
|
|
103
|
+
id: string;
|
|
104
|
+
email: string;
|
|
105
|
+
role: string;
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const requireAuth = (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
|
110
|
+
const authHeader = req.headers.authorization;
|
|
111
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
112
|
+
return next(new AppError(401, 'Authentication token missing or invalid'));
|
|
113
|
+
}
|
|
114
|
+
const token = authHeader.split(' ')[1];
|
|
115
|
+
try {
|
|
116
|
+
const decoded = jwt.verify(token, env.JWT_SECRET) as any;
|
|
117
|
+
req.user = decoded;
|
|
118
|
+
next();
|
|
119
|
+
} catch (error) {
|
|
120
|
+
next(new AppError(401, 'Authentication token expired or corrupt'));
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
`;
|
|
124
|
+
if (options.rbac) {
|
|
125
|
+
files['src/middlewares/rbac.ts'] = `import { Response, NextFunction } from 'express';
|
|
126
|
+
import { AuthenticatedRequest } from './auth.js';
|
|
127
|
+
import { AppError } from './errorHandler.js';
|
|
128
|
+
|
|
129
|
+
export const authorize = (...roles: string[]) => {
|
|
130
|
+
return (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
|
131
|
+
if (!req.user) return next(new AppError(401, 'User not authenticated'));
|
|
132
|
+
if (!roles.includes(req.user.role)) {
|
|
133
|
+
return next(new AppError(403, 'Permission denied: insufficient privileges'));
|
|
134
|
+
}
|
|
135
|
+
next();
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
`;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// 4. Caching & rate limiting
|
|
142
|
+
if (options.rateLimiting) {
|
|
143
|
+
files['src/middlewares/rateLimiter.ts'] = `import rateLimit from 'express-rate-limit';
|
|
144
|
+
|
|
145
|
+
export const apiLimiter = rateLimit({
|
|
146
|
+
windowMs: 15 * 60 * 1000,
|
|
147
|
+
max: 100,
|
|
148
|
+
standardHeaders: true,
|
|
149
|
+
legacyHeaders: false,
|
|
150
|
+
message: {
|
|
151
|
+
status: 'error',
|
|
152
|
+
statusCode: 429,
|
|
153
|
+
message: 'Too many requests, please try again later.',
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
`;
|
|
157
|
+
}
|
|
158
|
+
if (options.fileUpload) {
|
|
159
|
+
files['src/middlewares/upload.ts'] = getMulterMiddleware();
|
|
160
|
+
}
|
|
161
|
+
// Request validation helper
|
|
162
|
+
files['src/middlewares/validate.ts'] = options.validation === 'zod'
|
|
163
|
+
? `import { Request, Response, NextFunction } from 'express';
|
|
164
|
+
import { AnyZodObject, ZodError } from 'zod';
|
|
165
|
+
|
|
166
|
+
export const validate = (schema: AnyZodObject) => {
|
|
167
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
168
|
+
try {
|
|
169
|
+
const parsed = await schema.parseAsync({
|
|
170
|
+
body: req.body,
|
|
171
|
+
query: req.query,
|
|
172
|
+
params: req.params,
|
|
173
|
+
});
|
|
174
|
+
req.body = parsed.body;
|
|
175
|
+
req.query = parsed.query;
|
|
176
|
+
req.params = parsed.params;
|
|
177
|
+
next();
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (error instanceof ZodError) {
|
|
180
|
+
res.status(400).json({
|
|
181
|
+
status: 'error',
|
|
182
|
+
statusCode: 400,
|
|
183
|
+
message: 'Validation failed',
|
|
184
|
+
errors: error.errors.map(err => ({
|
|
185
|
+
field: err.path.join('.'),
|
|
186
|
+
message: err.message,
|
|
187
|
+
})),
|
|
188
|
+
});
|
|
189
|
+
} else {
|
|
190
|
+
next(error);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
`
|
|
196
|
+
: `import { Request, Response, NextFunction } from 'express';
|
|
197
|
+
import { Schema } from 'joi';
|
|
198
|
+
|
|
199
|
+
export const validate = (schema: Schema) => {
|
|
200
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
201
|
+
try {
|
|
202
|
+
const { error, value } = schema.validate({
|
|
203
|
+
body: req.body,
|
|
204
|
+
query: req.query,
|
|
205
|
+
params: req.params,
|
|
206
|
+
}, { abortEarly: false });
|
|
207
|
+
|
|
208
|
+
if (error) {
|
|
209
|
+
return res.status(400).json({
|
|
210
|
+
status: 'error',
|
|
211
|
+
statusCode: 400,
|
|
212
|
+
message: 'Validation failed',
|
|
213
|
+
errors: error.details.map(err => ({
|
|
214
|
+
field: err.path.join('.'),
|
|
215
|
+
message: err.message,
|
|
216
|
+
})),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
req.body = value.body;
|
|
220
|
+
req.query = value.query;
|
|
221
|
+
req.params = value.params;
|
|
222
|
+
next();
|
|
223
|
+
} catch (err) {
|
|
224
|
+
next(err);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
};
|
|
228
|
+
`;
|
|
229
|
+
// 5. USER FEATURE MODULE
|
|
230
|
+
// Users Entity / Model
|
|
231
|
+
if (options.orm === 'typeorm') {
|
|
232
|
+
files['src/modules/users/users.entity.ts'] = getTypeOrmEntity();
|
|
233
|
+
}
|
|
234
|
+
else if (options.orm === 'mongoose') {
|
|
235
|
+
files['src/modules/users/users.model.ts'] = `import mongoose, { Schema } from 'mongoose';
|
|
236
|
+
|
|
237
|
+
const UserSchema = new Schema({
|
|
238
|
+
email: { type: String, required: true, unique: true },
|
|
239
|
+
name: { type: String },
|
|
240
|
+
password: { type: String, required: true },
|
|
241
|
+
role: { type: String, default: 'user' },
|
|
242
|
+
refreshToken: { type: String },
|
|
243
|
+
}, { timestamps: true });
|
|
244
|
+
|
|
245
|
+
export const UserModel = mongoose.models.User || mongoose.model('User', UserSchema);
|
|
246
|
+
`;
|
|
247
|
+
}
|
|
248
|
+
// Users DTO / Schemas
|
|
249
|
+
if (options.validation === 'zod') {
|
|
250
|
+
files['src/modules/users/users.dto.ts'] = `import { z } from 'zod';
|
|
251
|
+
|
|
252
|
+
export const registerSchema = z.object({
|
|
253
|
+
body: z.object({
|
|
254
|
+
email: z.string().email('Invalid email address'),
|
|
255
|
+
name: z.string().min(2).optional(),
|
|
256
|
+
password: z.string().min(6, 'Password must be at least 6 characters'),
|
|
257
|
+
role: z.enum(['user', 'admin']).optional(),
|
|
258
|
+
}),
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
export const loginSchema = z.object({
|
|
262
|
+
body: z.object({
|
|
263
|
+
email: z.string().email('Invalid email address'),
|
|
264
|
+
password: z.string().min(1, 'Password is required'),
|
|
265
|
+
}),
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
export const refreshSchema = z.object({
|
|
269
|
+
body: z.object({
|
|
270
|
+
refreshToken: z.string().min(1, 'Refresh token is required'),
|
|
271
|
+
}),
|
|
272
|
+
});
|
|
273
|
+
`;
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
files['src/modules/users/users.dto.ts'] = `import Joi from 'joi';
|
|
277
|
+
|
|
278
|
+
export const registerSchema = Joi.object({
|
|
279
|
+
body: Joi.object({
|
|
280
|
+
email: Joi.string().email().required(),
|
|
281
|
+
name: Joi.string().min(2).optional(),
|
|
282
|
+
password: Joi.string().min(6).required(),
|
|
283
|
+
role: Joi.string().valid('user', 'admin').default('user'),
|
|
284
|
+
}).required(),
|
|
285
|
+
}).unknown(true);
|
|
286
|
+
|
|
287
|
+
export const loginSchema = Joi.object({
|
|
288
|
+
body: Joi.object({
|
|
289
|
+
email: Joi.string().email().required(),
|
|
290
|
+
password: Joi.string().required(),
|
|
291
|
+
}).required(),
|
|
292
|
+
}).unknown(true);
|
|
293
|
+
|
|
294
|
+
export const refreshSchema = Joi.object({
|
|
295
|
+
body: Joi.object({
|
|
296
|
+
refreshToken: Joi.string().required(),
|
|
297
|
+
}).required(),
|
|
298
|
+
}).unknown(true);
|
|
299
|
+
`;
|
|
300
|
+
}
|
|
301
|
+
// Users Repository
|
|
302
|
+
if (options.orm === 'prisma') {
|
|
303
|
+
files['src/modules/users/users.repository.ts'] = `import { PrismaClient } from '@prisma/client';
|
|
304
|
+
|
|
305
|
+
const prisma = new PrismaClient();
|
|
306
|
+
|
|
307
|
+
export class UsersRepository {
|
|
308
|
+
async findByEmail(email: string) {
|
|
309
|
+
return prisma.user.findUnique({ where: { email } });
|
|
310
|
+
}
|
|
311
|
+
async findById(id: string) {
|
|
312
|
+
return prisma.user.findUnique({ where: { id } });
|
|
313
|
+
}
|
|
314
|
+
async create(data: any) {
|
|
315
|
+
return prisma.user.create({ data });
|
|
316
|
+
}
|
|
317
|
+
async update(id: string, data: any) {
|
|
318
|
+
return prisma.user.update({ where: { id }, data });
|
|
319
|
+
}
|
|
320
|
+
async findAll() {
|
|
321
|
+
return prisma.user.findMany({
|
|
322
|
+
select: { id: true, email: true, name: true, role: true, createdAt: true }
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
`;
|
|
327
|
+
}
|
|
328
|
+
else if (options.orm === 'typeorm') {
|
|
329
|
+
files['src/modules/users/users.repository.ts'] = `import { AppDataSource } from '../../config/data-source.js';
|
|
330
|
+
import { UserEntity } from './users.entity.js';
|
|
331
|
+
|
|
332
|
+
export class UsersRepository {
|
|
333
|
+
private repo = AppDataSource.getRepository(UserEntity);
|
|
334
|
+
|
|
335
|
+
async findByEmail(email: string) {
|
|
336
|
+
return this.repo.findOne({ where: { email } });
|
|
337
|
+
}
|
|
338
|
+
async findById(id: string) {
|
|
339
|
+
return this.repo.findOne({ where: { id } });
|
|
340
|
+
}
|
|
341
|
+
async create(data: any) {
|
|
342
|
+
const user = this.repo.create(data);
|
|
343
|
+
return this.repo.save(user);
|
|
344
|
+
}
|
|
345
|
+
async update(id: string, data: any) {
|
|
346
|
+
await this.repo.update(id, data);
|
|
347
|
+
return this.findById(id);
|
|
348
|
+
}
|
|
349
|
+
async findAll() {
|
|
350
|
+
return this.repo.find({
|
|
351
|
+
select: ["id", "email", "name", "role", "createdAt"]
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
`;
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
// Mongoose
|
|
359
|
+
files['src/modules/users/users.repository.ts'] = `import { UserModel } from './users.model.js';
|
|
360
|
+
|
|
361
|
+
export class UsersRepository {
|
|
362
|
+
async findByEmail(email: string) {
|
|
363
|
+
return UserModel.findOne({ email });
|
|
364
|
+
}
|
|
365
|
+
async findById(id: string) {
|
|
366
|
+
return UserModel.findById(id);
|
|
367
|
+
}
|
|
368
|
+
async create(data: any) {
|
|
369
|
+
return UserModel.create(data);
|
|
370
|
+
}
|
|
371
|
+
async update(id: string, data: any) {
|
|
372
|
+
return UserModel.findByIdAndUpdate(id, data, { new: true });
|
|
373
|
+
}
|
|
374
|
+
async findAll() {
|
|
375
|
+
return UserModel.find({}, 'id email name role createdAt');
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
`;
|
|
379
|
+
}
|
|
380
|
+
// Users Service
|
|
381
|
+
files['src/modules/users/users.service.ts'] = `import bcrypt from 'bcryptjs';
|
|
382
|
+
import jwt from 'jsonwebtoken';
|
|
383
|
+
import { UsersRepository } from './users.repository.js';
|
|
384
|
+
import { env } from '../../config/env.js';
|
|
385
|
+
import { AppError } from '../../middlewares/errorHandler.js';
|
|
386
|
+
|
|
387
|
+
const usersRepository = new UsersRepository();
|
|
388
|
+
|
|
389
|
+
export class UsersService {
|
|
390
|
+
private generateTokens(payload: { id: string; email: string; role: string }) {
|
|
391
|
+
const accessToken = jwt.sign(payload, env.JWT_SECRET, { expiresIn: env.JWT_EXPIRES_IN as any });
|
|
392
|
+
const refreshToken = jwt.sign(payload, env.JWT_REFRESH_SECRET, { expiresIn: env.JWT_REFRESH_EXPIRES_IN as any });
|
|
393
|
+
return { accessToken, refreshToken };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async register(data: any) {
|
|
397
|
+
const existing = await usersRepository.findByEmail(data.email);
|
|
398
|
+
if (existing) throw new AppError(400, 'Email already registered');
|
|
399
|
+
|
|
400
|
+
const hashedPassword = await bcrypt.hash(data.password, 10);
|
|
401
|
+
const user = await usersRepository.create({ ...data, password: hashedPassword });
|
|
402
|
+
const userId = ${userIdExpr};
|
|
403
|
+
|
|
404
|
+
const tokens = this.generateTokens({ id: userId, email: user.email, role: user.role });
|
|
405
|
+
await usersRepository.update(userId, { refreshToken: tokens.refreshToken });
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
user: { id: userId, email: user.email, name: user.name, role: user.role },
|
|
409
|
+
...tokens
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async login(data: any) {
|
|
414
|
+
const user = await usersRepository.findByEmail(data.email);
|
|
415
|
+
if (!user) throw new AppError(401, 'Invalid email or password');
|
|
416
|
+
|
|
417
|
+
const isMatch = await bcrypt.compare(data.password, user.password);
|
|
418
|
+
if (!isMatch) throw new AppError(401, 'Invalid email or password');
|
|
419
|
+
|
|
420
|
+
const userId = ${userIdExpr};
|
|
421
|
+
const tokens = this.generateTokens({ id: userId, email: user.email, role: user.role });
|
|
422
|
+
await usersRepository.update(userId, { refreshToken: tokens.refreshToken });
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
user: { id: userId, email: user.email, name: user.name, role: user.role },
|
|
426
|
+
...tokens
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async refresh(token: string) {
|
|
431
|
+
try {
|
|
432
|
+
const decoded = jwt.verify(token, env.JWT_REFRESH_SECRET) as any;
|
|
433
|
+
const user = await usersRepository.findById(decoded.id);
|
|
434
|
+
if (!user || user.refreshToken !== token) {
|
|
435
|
+
throw new AppError(401, 'Invalid or revoked refresh token');
|
|
436
|
+
}
|
|
437
|
+
const userId = ${userIdExpr};
|
|
438
|
+
const tokens = this.generateTokens({ id: userId, email: user.email, role: user.role });
|
|
439
|
+
await usersRepository.update(userId, { refreshToken: tokens.refreshToken });
|
|
440
|
+
return tokens;
|
|
441
|
+
} catch (err) {
|
|
442
|
+
throw new AppError(401, 'Invalid or expired refresh token');
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async getProfile(id: string) {
|
|
447
|
+
const user = await usersRepository.findById(id);
|
|
448
|
+
if (!user) throw new AppError(404, 'User not found');
|
|
449
|
+
return {
|
|
450
|
+
id: ${userIdExpr},
|
|
451
|
+
email: user.email,
|
|
452
|
+
name: user.name,
|
|
453
|
+
role: user.role,
|
|
454
|
+
createdAt: user.createdAt
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async getAllUsers() {
|
|
459
|
+
return usersRepository.findAll();
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
`;
|
|
463
|
+
// Users Controller
|
|
464
|
+
files['src/modules/users/users.controller.ts'] = `import { Request, Response, NextFunction } from 'express';
|
|
465
|
+
import { UsersService } from './users.service.js';
|
|
466
|
+
import { AuthenticatedRequest } from '../../middlewares/auth.js';
|
|
467
|
+
|
|
468
|
+
const usersService = new UsersService();
|
|
469
|
+
|
|
470
|
+
export class UsersController {
|
|
471
|
+
async register(req: Request, res: Response, next: NextFunction) {
|
|
472
|
+
try {
|
|
473
|
+
const result = await usersService.register(req.body);
|
|
474
|
+
res.status(201).json({ status: 'success', data: result });
|
|
475
|
+
} catch (err) {
|
|
476
|
+
next(err);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
async login(req: Request, res: Response, next: NextFunction) {
|
|
480
|
+
try {
|
|
481
|
+
const result = await usersService.login(req.body);
|
|
482
|
+
res.status(200).json({ status: 'success', data: result });
|
|
483
|
+
} catch (err) {
|
|
484
|
+
next(err);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
async refresh(req: Request, res: Response, next: NextFunction) {
|
|
488
|
+
try {
|
|
489
|
+
const result = await usersService.refresh(req.body.refreshToken);
|
|
490
|
+
res.status(200).json({ status: 'success', data: result });
|
|
491
|
+
} catch (err) {
|
|
492
|
+
next(err);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
async profile(req: AuthenticatedRequest, res: Response, next: NextFunction) {
|
|
496
|
+
try {
|
|
497
|
+
const user = await usersService.getProfile(req.user!.id);
|
|
498
|
+
res.status(200).json({ status: 'success', data: { user } });
|
|
499
|
+
} catch (err) {
|
|
500
|
+
next(err);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
async list(req: Request, res: Response, next: NextFunction) {
|
|
504
|
+
try {
|
|
505
|
+
const users = await usersService.getAllUsers();
|
|
506
|
+
res.status(200).json({ status: 'success', data: { users } });
|
|
507
|
+
} catch (err) {
|
|
508
|
+
next(err);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
`;
|
|
513
|
+
// Users Routes
|
|
514
|
+
files['src/modules/users/users.routes.ts'] = `import { Router } from 'express';
|
|
515
|
+
import { UsersController } from './users.controller.js';
|
|
516
|
+
import { requireAuth } from '../../middlewares/auth.js';
|
|
517
|
+
${options.rbac ? `import { authorize } from '../../middlewares/rbac.js';` : ''}
|
|
518
|
+
import { validate } from '../../middlewares/validate.js';
|
|
519
|
+
import { registerSchema, loginSchema, refreshSchema } from './users.dto.js';
|
|
520
|
+
|
|
521
|
+
const router = Router();
|
|
522
|
+
const controller = new UsersController();
|
|
523
|
+
|
|
524
|
+
router.post('/register', validate(registerSchema), controller.register);
|
|
525
|
+
router.post('/login', validate(loginSchema), controller.login);
|
|
526
|
+
router.post('/refresh', validate(refreshSchema), controller.refresh);
|
|
527
|
+
router.get('/profile', requireAuth, controller.profile);
|
|
528
|
+
router.get('/', requireAuth${options.rbac ? `, authorize('admin')` : ''}, controller.list);
|
|
529
|
+
|
|
530
|
+
export default router;
|
|
531
|
+
`;
|
|
532
|
+
// 6. SECONDARY SAMPLE FEATURE MODULE: PRODUCTS
|
|
533
|
+
files['src/modules/products/products.entity.ts'] = options.orm === 'typeorm'
|
|
534
|
+
? `import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
|
|
535
|
+
|
|
536
|
+
@Entity({ name: "products" })
|
|
537
|
+
export class ProductEntity {
|
|
538
|
+
@PrimaryGeneratedColumn("uuid")
|
|
539
|
+
id!: string;
|
|
540
|
+
|
|
541
|
+
@Column()
|
|
542
|
+
name!: string;
|
|
543
|
+
|
|
544
|
+
@Column("decimal", { precision: 10, scale: 2 })
|
|
545
|
+
price!: number;
|
|
546
|
+
}
|
|
547
|
+
`
|
|
548
|
+
: '';
|
|
549
|
+
files['src/modules/products/products.service.ts'] = `export class ProductsService {
|
|
550
|
+
async getProducts() {
|
|
551
|
+
return [
|
|
552
|
+
{ id: '1', name: 'Product A', price: 99.99 },
|
|
553
|
+
{ id: '2', name: 'Product B', price: 149.50 }
|
|
554
|
+
];
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
`;
|
|
558
|
+
files['src/modules/products/products.controller.ts'] = `import { Request, Response, NextFunction } from 'express';
|
|
559
|
+
import { ProductsService } from './products.service.js';
|
|
560
|
+
|
|
561
|
+
const service = new ProductsService();
|
|
562
|
+
|
|
563
|
+
export class ProductsController {
|
|
564
|
+
async list(req: Request, res: Response, next: NextFunction) {
|
|
565
|
+
try {
|
|
566
|
+
const products = await service.getProducts();
|
|
567
|
+
res.status(200).json({ status: 'success', data: { products } });
|
|
568
|
+
} catch (err) {
|
|
569
|
+
next(err);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
`;
|
|
574
|
+
files['src/modules/products/products.routes.ts'] = `import { Router } from 'express';
|
|
575
|
+
import { ProductsController } from './products.controller.js';
|
|
576
|
+
|
|
577
|
+
const router = Router();
|
|
578
|
+
const controller = new ProductsController();
|
|
579
|
+
|
|
580
|
+
router.get('/', controller.list);
|
|
581
|
+
|
|
582
|
+
export default router;
|
|
583
|
+
`;
|
|
584
|
+
// 7. Core Express Router and Application Setup
|
|
585
|
+
files['src/routes.ts'] = `import { Router } from 'express';
|
|
586
|
+
import userRoutes from './modules/users/users.routes.js';
|
|
587
|
+
import productRoutes from './modules/products/products.routes.js';
|
|
588
|
+
|
|
589
|
+
const router = Router();
|
|
590
|
+
|
|
591
|
+
router.use('/users', userRoutes);
|
|
592
|
+
router.use('/products', productRoutes);
|
|
593
|
+
|
|
594
|
+
export default router;
|
|
595
|
+
`;
|
|
596
|
+
// Express Setup (app.ts)
|
|
597
|
+
let appMiddlewaresImport = '';
|
|
598
|
+
let appMiddlewares = '';
|
|
599
|
+
if (options.rateLimiting) {
|
|
600
|
+
appMiddlewaresImport += `import { apiLimiter } from './middlewares/rateLimiter.js';\n`;
|
|
601
|
+
appMiddlewares += `app.use('/api', apiLimiter);\n`;
|
|
602
|
+
}
|
|
603
|
+
if (options.swagger) {
|
|
604
|
+
appMiddlewaresImport += `import swaggerUi from 'swagger-ui-express';\n`;
|
|
605
|
+
appMiddlewares += `
|
|
606
|
+
const swaggerDocument = {
|
|
607
|
+
openapi: "3.0.0",
|
|
608
|
+
info: { title: "${options.projectName} API (Modular)", version: "1.0.0" },
|
|
609
|
+
servers: [{ url: "http://localhost:3000/api" }],
|
|
610
|
+
paths: {
|
|
611
|
+
"/users/register": {
|
|
612
|
+
post: {
|
|
613
|
+
summary: "Register",
|
|
614
|
+
requestBody: {
|
|
615
|
+
required: true,
|
|
616
|
+
content: { "application/json": { schema: { type: "object", properties: { email: { type: "string" }, password: { type: "string" } } } } }
|
|
617
|
+
},
|
|
618
|
+
responses: { "201": { description: "Created" } }
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
|
|
624
|
+
`;
|
|
625
|
+
}
|
|
626
|
+
files['src/app.ts'] = `import express from 'express';
|
|
627
|
+
import cors from 'cors';
|
|
628
|
+
import helmet from 'helmet';
|
|
629
|
+
import routes from './routes.js';
|
|
630
|
+
import { errorHandler } from './middlewares/errorHandler.js';
|
|
631
|
+
${appMiddlewaresImport}
|
|
632
|
+
const app = express();
|
|
633
|
+
|
|
634
|
+
app.use(helmet());
|
|
635
|
+
app.use(cors());
|
|
636
|
+
app.use(express.json());
|
|
637
|
+
|
|
638
|
+
${appMiddlewares}
|
|
639
|
+
app.use('/api', routes);
|
|
640
|
+
|
|
641
|
+
app.use(errorHandler);
|
|
642
|
+
|
|
643
|
+
export default app;
|
|
644
|
+
`;
|
|
645
|
+
// Server bootstrap (server.ts)
|
|
646
|
+
let dbConnectionCode = '';
|
|
647
|
+
if (options.orm === 'mongoose') {
|
|
648
|
+
dbConnectionCode = `import mongoose from 'mongoose';
|
|
649
|
+
async function connectDB() {
|
|
650
|
+
await mongoose.connect(env.DATABASE_URL);
|
|
651
|
+
console.log('🔌 Connected to MongoDB database successfully.');
|
|
652
|
+
}`;
|
|
653
|
+
}
|
|
654
|
+
else if (options.orm === 'typeorm') {
|
|
655
|
+
dbConnectionCode = `import { AppDataSource } from './config/data-source.js';
|
|
656
|
+
async function connectDB() {
|
|
657
|
+
await AppDataSource.initialize();
|
|
658
|
+
console.log('🔌 Connected to Database via TypeORM datasource.');
|
|
659
|
+
}`;
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
dbConnectionCode = `import { PrismaClient } from '@prisma/client';
|
|
663
|
+
const prisma = new PrismaClient();
|
|
664
|
+
async function connectDB() {
|
|
665
|
+
await prisma.$connect();
|
|
666
|
+
console.log('🔌 Connected to Database via Prisma client.');
|
|
667
|
+
}`;
|
|
668
|
+
}
|
|
669
|
+
files['src/server.ts'] = `import app from './app.js';
|
|
670
|
+
import { env } from './config/env.js';
|
|
671
|
+
${dbConnectionCode}
|
|
672
|
+
|
|
673
|
+
const PORT = env.PORT || 3000;
|
|
674
|
+
|
|
675
|
+
async function start() {
|
|
676
|
+
try {
|
|
677
|
+
await connectDB();
|
|
678
|
+
app.listen(PORT, () => {
|
|
679
|
+
console.log(\`🚀 Server running in \${env.NODE_ENV} mode on port \${PORT} (Modular Feature architecture)\`);
|
|
680
|
+
${options.swagger ? `console.log(\`📄 Swagger Docs: http://localhost:\${PORT}/docs\`);` : ''}
|
|
681
|
+
});
|
|
682
|
+
} catch (err) {
|
|
683
|
+
console.error('❌ Failed to start application:', err);
|
|
684
|
+
process.exit(1);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
start();
|
|
689
|
+
`;
|
|
690
|
+
return files;
|
|
691
|
+
}
|