nestcraftx 0.2.6 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,645 @@
1
+ /**
2
+ * utils/generators/infrastructure/middlewareGenerator.js
3
+ *
4
+ * Générateur des middlewares, interceptors, filters et decorators globaux.
5
+ * Extrait de utils.js — Phase 1B.
6
+ *
7
+ * Crée dans src/common/ :
8
+ * - middlewares/logger.middleware.ts
9
+ * - filters/all-exceptions.filter.ts (contenu adapté selon l'ORM)
10
+ * - interceptors/response.interceptor.ts
11
+ * - decorators/public.decorator.ts
12
+ * - decorators/current-user.decorator.ts
13
+ * - decorators/role.decorator.ts
14
+ *
15
+ * Met également à jour :
16
+ * - src/main.ts (imports + injection dans bootstrap)
17
+ * - src/app.module.ts (providers APP_INTERCEPTOR)
18
+ */
19
+
20
+ const { logInfo } = require("../../loggers/logInfo");
21
+ const { logSuccess } = require("../../loggers/logSuccess");
22
+ const { createDirectory, createFile, updateFile } = require("../../file-system");
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Contenu du filtre d'exception selon l'ORM (helper privé)
26
+ // ---------------------------------------------------------------------------
27
+
28
+ /**
29
+ * Génère le contenu TypeScript du filtre global d'exceptions.
30
+ * Adapté selon l'ORM pour capturer les erreurs spécifiques.
31
+ *
32
+ * @param {"prisma"|"mongoose"|"typeorm"|"sequelize"|string} orm
33
+ * @returns {string}
34
+ */
35
+ function getExceptionFilterContent(orm) {
36
+ if (orm === "prisma") {
37
+ return `
38
+ import {
39
+ ExceptionFilter,
40
+ Catch,
41
+ ArgumentsHost,
42
+ HttpException,
43
+ HttpStatus,
44
+ Logger,
45
+ } from '@nestjs/common';
46
+ import { Request, Response } from 'express';
47
+
48
+ @Catch()
49
+ export class AllExceptionsFilter implements ExceptionFilter {
50
+ private readonly logger = new Logger(AllExceptionsFilter.name);
51
+
52
+ catch(exception: unknown, host: ArgumentsHost) {
53
+ const ctx = host.switchToHttp();
54
+ const response = ctx.getResponse<Response>();
55
+ const request = ctx.getRequest<Request>();
56
+
57
+ let status = HttpStatus.INTERNAL_SERVER_ERROR;
58
+ let message: string | string[] = 'Internal server error';
59
+ let errorDetails: any = null;
60
+
61
+ if (exception instanceof HttpException) {
62
+ status = exception.getStatus();
63
+ const res = exception.getResponse();
64
+ if (typeof res === 'string') {
65
+ message = res;
66
+ } else if (typeof res === 'object' && res !== null) {
67
+ const resObj = res as any;
68
+ message = resObj.message || resObj.error || 'HttpException';
69
+ errorDetails = resObj;
70
+ }
71
+ } else if (
72
+ typeof exception === 'object' &&
73
+ exception &&
74
+ exception.constructor &&
75
+ (
76
+ exception.constructor.name === 'PrismaClientKnownRequestError' ||
77
+ exception.constructor.name === 'PrismaClientValidationError'
78
+ )
79
+ ) {
80
+ status = HttpStatus.BAD_REQUEST;
81
+ message = (exception as any).message || 'Prisma error';
82
+ errorDetails = exception;
83
+ } else if (exception instanceof Error) {
84
+ message = exception.message;
85
+ errorDetails = {
86
+ name: exception.name,
87
+ stack: exception.stack,
88
+ };
89
+ } else {
90
+ message = 'Une erreur inattendue est survenue';
91
+ errorDetails = exception;
92
+ }
93
+
94
+ if (process.env.NODE_ENV !== 'production') {
95
+ this.logger.error(
96
+ \`Exception on \${request.method} \${request.url}\`,
97
+ JSON.stringify({ message, status, errorDetails, exception }),
98
+ );
99
+ }
100
+
101
+ response.status(status).json({
102
+ statusCode: status,
103
+ timestamp: new Date().toISOString(),
104
+ path: request.url,
105
+ method: request.method,
106
+ message,
107
+ error: errorDetails,
108
+ });
109
+ }
110
+ }
111
+ `.trim();
112
+ }
113
+
114
+ if (orm === "mongoose") {
115
+ return `
116
+ import {
117
+ ExceptionFilter,
118
+ Catch,
119
+ ArgumentsHost,
120
+ HttpException,
121
+ HttpStatus,
122
+ Logger,
123
+ } from '@nestjs/common';
124
+ import { Request, Response } from 'express';
125
+
126
+ @Catch()
127
+ export class AllExceptionsFilter implements ExceptionFilter {
128
+ private readonly logger = new Logger(AllExceptionsFilter.name);
129
+
130
+ catch(exception: unknown, host: ArgumentsHost) {
131
+ const ctx = host.switchToHttp();
132
+ const response = ctx.getResponse<Response>();
133
+ const request = ctx.getRequest<Request>();
134
+
135
+ let status = HttpStatus.INTERNAL_SERVER_ERROR;
136
+ let message: string | string[] = 'Internal server error';
137
+ let errorDetails: any = null;
138
+
139
+ if (exception instanceof HttpException) {
140
+ status = exception.getStatus();
141
+ const res = exception.getResponse();
142
+ if (typeof res === 'string') {
143
+ message = res;
144
+ } else if (typeof res === 'object' && res !== null) {
145
+ const resObj = res as any;
146
+ message = resObj.message || resObj.error || 'HttpException';
147
+ errorDetails = resObj;
148
+ }
149
+ } else if (
150
+ typeof exception === 'object' &&
151
+ exception &&
152
+ 'name' in exception &&
153
+ (
154
+ (exception as any).name === 'MongoError' ||
155
+ (exception as any).name === 'MongooseError'
156
+ )
157
+ ) {
158
+ status = HttpStatus.BAD_REQUEST;
159
+ message = (exception as any).message || 'MongoDB error';
160
+ errorDetails = exception;
161
+ } else if (exception instanceof Error) {
162
+ message = exception.message;
163
+ errorDetails = {
164
+ name: exception.name,
165
+ stack: exception.stack,
166
+ };
167
+ } else {
168
+ message = 'Une erreur inattendue est survenue';
169
+ errorDetails = exception;
170
+ }
171
+
172
+ if (process.env.NODE_ENV !== 'production') {
173
+ this.logger.error(
174
+ \`Exception on \${request.method} \${request.url}\`,
175
+ JSON.stringify({ message, status, errorDetails, exception }),
176
+ );
177
+ }
178
+
179
+ response.status(status).json({
180
+ statusCode: status,
181
+ timestamp: new Date().toISOString(),
182
+ path: request.url,
183
+ method: request.method,
184
+ message,
185
+ error: errorDetails,
186
+ });
187
+ }
188
+ }
189
+ `.trim();
190
+ }
191
+
192
+ if (orm === "typeorm") {
193
+ return `
194
+ import {
195
+ ExceptionFilter,
196
+ Catch,
197
+ ArgumentsHost,
198
+ HttpException,
199
+ HttpStatus,
200
+ Logger,
201
+ } from '@nestjs/common';
202
+ import { Request, Response } from 'express';
203
+
204
+ @Catch()
205
+ export class AllExceptionsFilter implements ExceptionFilter {
206
+ private readonly logger = new Logger(AllExceptionsFilter.name);
207
+
208
+ catch(exception: unknown, host: ArgumentsHost) {
209
+ const ctx = host.switchToHttp();
210
+ const response = ctx.getResponse<Response>();
211
+ const request = ctx.getRequest<Request>();
212
+
213
+ let status = HttpStatus.INTERNAL_SERVER_ERROR;
214
+ let message: string | string[] = 'Internal server error';
215
+ let errorDetails: any = null;
216
+
217
+ if (exception instanceof HttpException) {
218
+ status = exception.getStatus();
219
+ const res = exception.getResponse();
220
+ if (typeof res === 'string') {
221
+ message = res;
222
+ } else if (typeof res === 'object' && res !== null) {
223
+ const resObj = res as any;
224
+ message = resObj.message || resObj.error || 'HttpException';
225
+ errorDetails = resObj;
226
+ }
227
+ } else if (
228
+ typeof exception === 'object' &&
229
+ exception &&
230
+ 'name' in exception &&
231
+ (
232
+ (exception as any).name === 'QueryFailedError' ||
233
+ (exception as any).name === 'EntityNotFoundError' ||
234
+ (exception as any).name === 'CannotCreateEntityIdMapError'
235
+ )
236
+ ) {
237
+ status = HttpStatus.BAD_REQUEST;
238
+ message = (exception as any).message || 'TypeORM error';
239
+ errorDetails = exception;
240
+ } else if (exception instanceof Error) {
241
+ message = exception.message;
242
+ errorDetails = {
243
+ name: exception.name,
244
+ stack: exception.stack,
245
+ };
246
+ } else {
247
+ message = 'Une erreur inattendue est survenue';
248
+ errorDetails = exception;
249
+ }
250
+
251
+ if (process.env.NODE_ENV !== 'production') {
252
+ this.logger.error(
253
+ \`Exception on \${request.method} \${request.url}\`,
254
+ JSON.stringify({ message, status, errorDetails, exception }),
255
+ );
256
+ }
257
+
258
+ response.status(status).json({
259
+ statusCode: status,
260
+ timestamp: new Date().toISOString(),
261
+ path: request.url,
262
+ method: request.method,
263
+ message,
264
+ error: errorDetails,
265
+ });
266
+ }
267
+ }
268
+ `.trim();
269
+ }
270
+
271
+ if (orm === "sequelize") {
272
+ return `
273
+ import {
274
+ ExceptionFilter,
275
+ Catch,
276
+ ArgumentsHost,
277
+ HttpException,
278
+ HttpStatus,
279
+ Logger,
280
+ } from '@nestjs/common';
281
+ import { Request, Response } from 'express';
282
+
283
+ @Catch()
284
+ export class AllExceptionsFilter implements ExceptionFilter {
285
+ private readonly logger = new Logger(AllExceptionsFilter.name);
286
+
287
+ catch(exception: unknown, host: ArgumentsHost) {
288
+ const ctx = host.switchToHttp();
289
+ const response = ctx.getResponse<Response>();
290
+ const request = ctx.getRequest<Request>();
291
+
292
+ let status = HttpStatus.INTERNAL_SERVER_ERROR;
293
+ let message: string | string[] = 'Internal server error';
294
+ let errorDetails: any = null;
295
+
296
+ if (exception instanceof HttpException) {
297
+ status = exception.getStatus();
298
+ const res = exception.getResponse();
299
+ if (typeof res === 'string') {
300
+ message = res;
301
+ } else if (typeof res === 'object' && res !== null) {
302
+ const resObj = res as any;
303
+ message = resObj.message || resObj.error || 'HttpException';
304
+ errorDetails = resObj;
305
+ }
306
+ } else if (
307
+ typeof exception === 'object' &&
308
+ exception &&
309
+ exception.constructor &&
310
+ (
311
+ exception.constructor.name === 'SequelizeDatabaseError' ||
312
+ exception.constructor.name === 'SequelizeValidationError'
313
+ )
314
+ ) {
315
+ status = HttpStatus.BAD_REQUEST;
316
+ message = (exception as any).message || 'Sequelize error';
317
+ errorDetails = exception;
318
+ } else if (exception instanceof Error) {
319
+ message = exception.message;
320
+ errorDetails = {
321
+ name: exception.name,
322
+ stack: exception.stack,
323
+ };
324
+ } else {
325
+ message = 'Une erreur inattendue est survenue';
326
+ errorDetails = exception;
327
+ }
328
+
329
+ if (process.env.NODE_ENV !== 'production') {
330
+ this.logger.error(
331
+ \`Exception on \${request.method} \${request.url}\`,
332
+ JSON.stringify({ message, status, errorDetails, exception }),
333
+ );
334
+ }
335
+
336
+ response.status(status).json({
337
+ statusCode: status,
338
+ timestamp: new Date().toISOString(),
339
+ path: request.url,
340
+ method: request.method,
341
+ message,
342
+ error: errorDetails,
343
+ });
344
+ }
345
+ }
346
+ `.trim();
347
+ }
348
+
349
+ // Version universelle (multi-ORM / fallback)
350
+ return `
351
+ import {
352
+ ExceptionFilter,
353
+ Catch,
354
+ ArgumentsHost,
355
+ HttpException,
356
+ HttpStatus,
357
+ Logger,
358
+ } from '@nestjs/common';
359
+ import { Request, Response } from 'express';
360
+
361
+ @Catch()
362
+ export class AllExceptionsFilter implements ExceptionFilter {
363
+ private readonly logger = new Logger(AllExceptionsFilter.name);
364
+
365
+ catch(exception: unknown, host: ArgumentsHost) {
366
+ const ctx = host.switchToHttp();
367
+ const response = ctx.getResponse<Response>();
368
+ const request = ctx.getRequest<Request>();
369
+
370
+ let status = HttpStatus.INTERNAL_SERVER_ERROR;
371
+ let message: string | string[] = 'Internal server error';
372
+ let errorDetails: any = null;
373
+
374
+ if (exception instanceof HttpException) {
375
+ status = exception.getStatus();
376
+ const res = exception.getResponse();
377
+ if (typeof res === 'string') {
378
+ message = res;
379
+ } else if (typeof res === 'object' && res !== null) {
380
+ const resObj = res as any;
381
+ message = resObj.message || resObj.error || 'HttpException';
382
+ errorDetails = resObj;
383
+ }
384
+ }
385
+ // Prisma
386
+ else if (
387
+ typeof exception === 'object' &&
388
+ exception &&
389
+ exception.constructor &&
390
+ (
391
+ exception.constructor.name === 'PrismaClientKnownRequestError' ||
392
+ exception.constructor.name === 'PrismaClientValidationError'
393
+ )
394
+ ) {
395
+ status = HttpStatus.BAD_REQUEST;
396
+ message = (exception as any).message || 'Prisma error';
397
+ errorDetails = exception;
398
+ }
399
+ // Mongoose/Mongo
400
+ else if (
401
+ typeof exception === 'object' &&
402
+ exception &&
403
+ 'name' in exception &&
404
+ (
405
+ (exception as any).name === 'MongoError' ||
406
+ (exception as any).name === 'MongooseError'
407
+ )
408
+ ) {
409
+ status = HttpStatus.BAD_REQUEST;
410
+ message = (exception as any).message || 'MongoDB error';
411
+ errorDetails = exception;
412
+ }
413
+ // Sequelize
414
+ else if (
415
+ typeof exception === 'object' &&
416
+ exception &&
417
+ exception.constructor &&
418
+ (
419
+ exception.constructor.name === 'SequelizeDatabaseError' ||
420
+ exception.constructor.name === 'SequelizeValidationError'
421
+ )
422
+ ) {
423
+ status = HttpStatus.BAD_REQUEST;
424
+ message = (exception as any).message || 'Sequelize error';
425
+ errorDetails = exception;
426
+ }
427
+ else if (exception instanceof Error) {
428
+ message = exception.message;
429
+ errorDetails = {
430
+ name: exception.name,
431
+ stack: exception.stack,
432
+ };
433
+ } else {
434
+ message = 'Une erreur inattendue est survenue';
435
+ errorDetails = exception;
436
+ }
437
+
438
+ this.logger.error(
439
+ \`Exception on \${request.method} \${request.url}\`,
440
+ JSON.stringify({ message, status, errorDetails, exception }),
441
+ );
442
+
443
+ response.status(status).json({
444
+ statusCode: status,
445
+ timestamp: new Date().toISOString(),
446
+ path: request.url,
447
+ method: request.method,
448
+ message,
449
+ error: errorDetails,
450
+ });
451
+ }
452
+ }
453
+ `.trim();
454
+ }
455
+
456
+ // ---------------------------------------------------------------------------
457
+ // Fonction principale
458
+ // ---------------------------------------------------------------------------
459
+
460
+ /**
461
+ * Génère et écrit tous les middlewares, interceptors, filters et decorators.
462
+ * Met également à jour src/main.ts et src/app.module.ts.
463
+ *
464
+ * @param {"prisma"|"mongoose"|"typeorm"|"sequelize"|"global"} orm
465
+ * @returns {Promise<void>}
466
+ */
467
+ async function generateMiddlewares(orm = "global") {
468
+ logInfo(
469
+ "\u2728 Génération des middlewares, interceptors, guards et filters personnalisés...",
470
+ );
471
+
472
+ const basePath = "src/common";
473
+ await createDirectory(`${basePath}/middlewares`);
474
+ await createDirectory(`${basePath}/interceptors`);
475
+ await createDirectory(`${basePath}/filters`);
476
+ await createDirectory(`${basePath}/decorators`);
477
+
478
+ // Logger Middleware
479
+ await createFile({
480
+ path: `${basePath}/middlewares/logger.middleware.ts`,
481
+ contente: `import { Request, Response, NextFunction } from 'express';
482
+
483
+ export function LoggerMiddleware(
484
+ req: Request,
485
+ res: Response,
486
+ next: NextFunction,
487
+ ) {
488
+ if (process.env.NODE_ENV === 'production') {
489
+ return next();
490
+ }
491
+
492
+ res.on('finish', () => {
493
+ console.log(
494
+ \`[Request] \${req.method} \${req.originalUrl} - \${res.statusCode}\`,
495
+ );
496
+ });
497
+
498
+ next();
499
+ }
500
+ `,
501
+ });
502
+
503
+ // Error Handling Filter (personnalisé selon l'ORM)
504
+ await createFile({
505
+ path: `${basePath}/filters/all-exceptions.filter.ts`,
506
+ contente: getExceptionFilterContent(orm),
507
+ });
508
+
509
+ // Response Interceptor
510
+ await createFile({
511
+ path: `${basePath}/interceptors/response.interceptor.ts`,
512
+ contente: `import {
513
+ CallHandler,
514
+ ExecutionContext,
515
+ Injectable,
516
+ NestInterceptor,
517
+ } from '@nestjs/common';
518
+ import { Observable } from 'rxjs';
519
+ import { map } from 'rxjs/operators';
520
+
521
+ @Injectable()
522
+ export class ResponseInterceptor<T> implements NestInterceptor<T, any> {
523
+ intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
524
+ const httpContext = context.switchToHttp();
525
+ const response = httpContext.getResponse();
526
+ const request = httpContext.getRequest();
527
+
528
+ return next.handle().pipe(
529
+ map((data) => {
530
+ const message =
531
+ response.locals.message || this.getDefaultMessage(request.method);
532
+
533
+ return {
534
+ statusCode: response.statusCode,
535
+ message,
536
+ path: request.url,
537
+ method: request.method,
538
+ timestamp: new Date().toISOString(),
539
+ data: data ?? null,
540
+ };
541
+ }),
542
+ );
543
+ }
544
+
545
+ private getDefaultMessage(method: string): string {
546
+ switch (method.toUpperCase()) {
547
+ case 'POST':
548
+ return 'Resource created successfully';
549
+ case 'PUT':
550
+ case 'PATCH':
551
+ return 'Resource updated successfully';
552
+ case 'DELETE':
553
+ return 'Resource deleted successfully';
554
+ case 'GET':
555
+ return 'Request processed successfully';
556
+ default:
557
+ return 'Request processed successfully';
558
+ }
559
+ }
560
+ }
561
+ `,
562
+ });
563
+
564
+ // Public Decorator
565
+ await createFile({
566
+ path: `${basePath}/decorators/public.decorator.ts`,
567
+ contente: `import { SetMetadata } from '@nestjs/common';
568
+
569
+ export const IS_PUBLIC_KEY = 'isPublic';
570
+ export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
571
+ `,
572
+ });
573
+
574
+ await createFile({
575
+ path: `${basePath}/decorators/current-user.decorator.ts`,
576
+ contente: `import { createParamDecorator, ExecutionContext } from '@nestjs/common';\nexport const CurrentUser = createParamDecorator((data, ctx: ExecutionContext) => ctx.switchToHttp().getRequest().user);`,
577
+ });
578
+
579
+ // Auth Role Decorator
580
+ await createFile({
581
+ path: `${basePath}/decorators/role.decorator.ts`,
582
+ contente: `import { SetMetadata } from '@nestjs/common';
583
+
584
+ export const ROLES_KEY = 'roles';
585
+ export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
586
+ `,
587
+ });
588
+
589
+ // Mise à jour de main.ts
590
+ const mainTsPath = "src/main.ts";
591
+
592
+ const importPattern = "import { AppModule } from './app.module';";
593
+ const importReplacer = `import { AppModule } from 'src/app.module'
594
+ import { AllExceptionsFilter } from 'src/common/filters/all-exceptions.filter'
595
+ import { ResponseInterceptor } from 'src/common/interceptors/response.interceptor'
596
+ import { LoggerMiddleware } from 'src/common/middlewares/logger.middleware'
597
+ import { ValidationPipe } from '@nestjs/common';`;
598
+
599
+ const contentPattern = `const app = await NestFactory.create(AppModule);`;
600
+ const contentReplacer = `
601
+ const app = await NestFactory.create(AppModule);
602
+
603
+ // 🔒 Global filter pour gérer toutes les exceptions
604
+ app.useGlobalFilters(new AllExceptionsFilter())
605
+
606
+ // 🔁 Global interceptor pour structurer les réponses
607
+ // app.useGlobalInterceptors(new ResponseInterceptor()); //deja appliquer dans le app.module.ts par convention (ne choisir que l'un des deux)
608
+
609
+ // 📋 Middleware pour logger toutes les requêtes entrantes
610
+ app.use(LoggerMiddleware);`;
611
+
612
+ await updateFile({
613
+ path: mainTsPath,
614
+ pattern: importPattern,
615
+ replacement: importReplacer,
616
+ });
617
+
618
+ await updateFile({
619
+ path: mainTsPath,
620
+ pattern: contentPattern,
621
+ replacement: contentReplacer,
622
+ });
623
+
624
+ // Mise à jour de app.module.ts
625
+ const appModulePath = "src/app.module.ts";
626
+
627
+ const addNestModuleInterface = `providers: [`;
628
+ const replaceWithNestModule = `providers: [
629
+ {
630
+ provide: APP_INTERCEPTOR,
631
+ useClass: ResponseInterceptor, // 🔁 Global interceptor pour structurer les réponses
632
+ },`;
633
+
634
+ await updateFile({
635
+ path: appModulePath,
636
+ pattern: addNestModuleInterface,
637
+ replacement: replaceWithNestModule,
638
+ });
639
+
640
+ logSuccess(
641
+ "\u2705 Middlewares, filters, interceptors et guards générés avec succès !",
642
+ );
643
+ }
644
+
645
+ module.exports = { generateMiddlewares };