nestcraftx 0.2.4 → 0.2.6

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.
Files changed (63) hide show
  1. package/.gitattributes +6 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +33 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.md +19 -0
  4. package/.github/ISSUE_TEMPLATE/pull_request_template.md +24 -0
  5. package/CHANGELOG.fr.md +97 -97
  6. package/CHANGELOG.md +98 -98
  7. package/CLI_USAGE.fr.md +331 -331
  8. package/CLI_USAGE.md +364 -364
  9. package/DEMO.fr.md +292 -292
  10. package/DEMO.md +294 -294
  11. package/LICENSE +21 -21
  12. package/MIGRATION_GUIDE.fr.md +127 -127
  13. package/MIGRATION_GUIDE.md +124 -124
  14. package/QUICK_START.fr.md +152 -152
  15. package/QUICK_START.md +169 -169
  16. package/README.fr.md +653 -659
  17. package/SECURITY.md +10 -0
  18. package/bin/nestcraft.js +84 -64
  19. package/commands/demo.js +333 -330
  20. package/commands/generate.js +93 -0
  21. package/commands/generateConf.js +91 -0
  22. package/commands/help.js +78 -78
  23. package/commands/info.js +48 -48
  24. package/commands/new.js +338 -335
  25. package/commands/start.js +19 -19
  26. package/commands/test.js +7 -7
  27. package/package.json +41 -41
  28. package/readme.md +638 -643
  29. package/utils/cliParser.js +133 -76
  30. package/utils/colors.js +62 -62
  31. package/utils/configs/configureDocker.js +120 -120
  32. package/utils/configs/setupCleanArchitecture.js +563 -557
  33. package/utils/configs/setupLightArchitecture.js +701 -660
  34. package/utils/envGenerator.js +122 -122
  35. package/utils/file-utils/packageJsonUtils.js +49 -55
  36. package/utils/file-utils/saveProjectConfig.js +36 -0
  37. package/utils/fullModeInput.js +607 -607
  38. package/utils/generators/application/dtoUpdater.js +54 -0
  39. package/utils/generators/cleanModuleGenerator.js +475 -0
  40. package/utils/generators/database/setupDatabase.js +31 -0
  41. package/utils/generators/domain/entityUpdater.js +78 -0
  42. package/utils/generators/infrastructure/mapperUpdater.js +65 -0
  43. package/utils/generators/lightModuleGenerator.js +131 -0
  44. package/utils/generators/relation/relation.engine.js +64 -0
  45. package/utils/interactive/askEntityInputs.js +165 -0
  46. package/utils/lightModeInput.js +460 -460
  47. package/utils/loggers/logError.js +7 -7
  48. package/utils/loggers/logInfo.js +7 -7
  49. package/utils/loggers/logSuccess.js +7 -7
  50. package/utils/loggers/logWarning.js +7 -7
  51. package/utils/setups/orms/typeOrmSetup.js +630 -630
  52. package/utils/setups/projectSetup.js +46 -46
  53. package/utils/setups/setupAuth.js +973 -926
  54. package/utils/setups/setupDatabase.js +75 -75
  55. package/utils/setups/setupLogger.js +69 -59
  56. package/utils/setups/setupMongoose.js +377 -432
  57. package/utils/setups/setupPrisma.js +802 -630
  58. package/utils/setups/setupSwagger.js +97 -88
  59. package/utils/shell.js +32 -32
  60. package/utils/spinner.js +57 -57
  61. package/utils/systemCheck.js +124 -124
  62. package/utils/userInput.js +421 -421
  63. package/utils/utils.js +2197 -1762
package/utils/utils.js CHANGED
@@ -1,1762 +1,2197 @@
1
- import { logInfo } from "./loggers/logInfo.js";
2
- import { logSuccess } from "./loggers/logSuccess.js";
3
- import { createDirectory, createFile, updateFile } from "./userInput.js";
4
- import inquirer from "inquirer";
5
- const actualInquirer = inquirer.default || inquirer;
6
-
7
- export async function generateEntityFileContent(entity, mode = "full") {
8
- // console.log("Entity name:", entity.name); // Log de l'entité
9
-
10
- if (!entity || !entity.name) {
11
- throw new Error("Nom de l'entité manquant !");
12
- }
13
- const entityName = capitalize(entity.name);
14
- const className = `${entityName}Entity`;
15
-
16
- const defaultFields = [
17
- {
18
- name: "id",
19
- type: "string",
20
- comment:
21
- "L'identifiant unique de l'entité.\n * Utilisé pour retrouver de manière unique un enregistrement dans la base de données.\n *\n * Exemple : '123e4567-e89b-12d3-a456-426614174000'",
22
- },
23
- {
24
- name: "createdAt",
25
- type: "Date",
26
- comment:
27
- "La date de création de l'entité.\n * Définie lors de la création et ne change pas.\n *\n * Exemple : new Date('2022-01-01T10:00:00Z')",
28
- },
29
- {
30
- name: "updatedAt",
31
- type: "Date",
32
- comment:
33
- "La date de dernière mise à jour de l'entité.\n * Mise à jour à chaque modification.\n *\n * Exemple : new Date('2022-02-01T15:00:00Z')",
34
- },
35
- ];
36
- // 1. Types de base autorisés dans une Entité de Domaine
37
- const DOMAIN_SCALAR_TYPES = [
38
- "string",
39
- "number",
40
- "boolean",
41
- "date",
42
- "json",
43
- "text",
44
- "uuid",
45
- "decimal",
46
- "float",
47
- "int",
48
- "role",
49
- ];
50
-
51
- // 2. Filtrage : On ne garde que les types scalaires ou les IDs techniques
52
- const filteredFields = entity.fields.filter((f) => {
53
- const typeLower = f.type.toLowerCase().replace("[]", "");
54
- return DOMAIN_SCALAR_TYPES.includes(typeLower) || f.name.endsWith("Id");
55
- });
56
-
57
- const isUserEntityWithRole =
58
- entity.name.toLowerCase() === "user" &&
59
- entity.fields.some((f) => f.name === "role");
60
-
61
- const allFields = [...defaultFields, ...filteredFields];
62
-
63
- const constructorParams = allFields
64
- .map(
65
- (f) => `
66
- private readonly ${f.name}: ${getFormattedType(f)},`
67
- )
68
- .join("");
69
-
70
- /* const constructorAssignments = allFields
71
- .map((f) => ` this.${f.name} = ${f.name};`)
72
- .join("\n"); */
73
-
74
- const getters = allFields
75
- .map(
76
- (f) => `
77
- get${capitalize(f.name)}(): ${getFormattedType(f)}
78
- {
79
- return this.${f.name};
80
- }`
81
- )
82
- .join("\n");
83
-
84
- const jsonFields = allFields
85
- .map((f) => ` ${f.name}: this.${f.name},`)
86
- .join("\n");
87
-
88
- let importStatements = "";
89
- if (isUserEntityWithRole) {
90
- importStatements +=
91
- mode == "full"
92
- ? `import { Role } from 'src/user/domain/enums/role.enum';\n\n`
93
- : `import { Role } from 'src/common/enums/role.enum'; \n\n`;
94
- }
95
-
96
- return `${importStatements}/**
97
- * ${className} représente l'entité principale de ${entityName} dans le domaine.
98
- * Elle contient les propriétés de base nécessaires à la gestion des données liées à ${entityName}.
99
- */
100
- export class ${className} {
101
- constructor(${constructorParams}
102
- ) {}
103
- ${getters}
104
-
105
- /**
106
- * transforme entity data to json
107
- */
108
- toJSON() {
109
- return {
110
- ${jsonFields}
111
- };
112
- }
113
- }
114
- `;
115
- }
116
-
117
- export async function generateMapper(entity) {
118
- const entityName = capitalize(entity.name);
119
-
120
- // 1. Liste des types autorisés (Scalaires / Primitifs)
121
- const SCALAR_TYPES = [
122
- "string",
123
- "text",
124
- "uuid",
125
- "json",
126
- "number",
127
- "int",
128
- "float",
129
- "decimal",
130
- "boolean",
131
- "date",
132
- "role",
133
- ];
134
-
135
- // 2. Fonction de filtrage cohérente
136
- const filterDomainFields = (f) => {
137
- const typeName = f.type.toLowerCase().replace("[]", "");
138
- return (
139
- SCALAR_TYPES.includes(typeName) || f.name.toLowerCase().endsWith("id")
140
- );
141
- };
142
-
143
- // 3. Filtrage des champs pour le constructeur de l'Entity
144
- // On ne passe au constructeur QUE ce qui est filtré
145
- const filteredFields = entity.fields.filter(filterDomainFields);
146
-
147
- const domainArgs = ["data.id"]
148
- .concat(["data.createdAt", "data.updatedAt"])
149
- .concat(filteredFields.map((f) => `data.${f.name}`)) // UTILISE LES CHAMPS FILTRÉS ICI
150
- .join(",\n ");
151
-
152
- // 4. Filtrage pour la persistence (Base de données)
153
- const toPersistenceFields = filteredFields
154
- .map((f) => `${f.name}: dto.${f.name},`)
155
- .join("\n ");
156
-
157
- const toUpdateFields = filteredFields
158
- .map(
159
- (f) => `if (dto.${f.name} !== undefined) data.${f.name} = dto.${f.name};`
160
- )
161
- .join("\n ");
162
-
163
- // ... (Logique isUserWithRole inchangée)
164
-
165
- return `/**
166
- * PostMapper transforms data between
167
- * different layers (Persistence <-> Domain <-> DTO).
168
- *
169
- * Ensures that the internal database structure
170
- * never leaks into the API responses.
171
- */
172
-
173
- import { Injectable } from '@nestjs/common';
174
- import { ${entityName}Entity } from 'src/${decapitalize(
175
- entity.name
176
- )}/domain/entities/${decapitalize(entity.name)}.entity';
177
- import { Create${entityName}Dto, Update${entityName}Dto } from 'src/${decapitalize(
178
- entity.name
179
- )}/application/dtos/${decapitalize(entity.name)}.dto';
180
-
181
- @Injectable()
182
- export class ${entityName}Mapper {
183
-
184
- /**
185
- * Transforme les données (Prisma/TypeORM) en Entité de Domaine
186
- */
187
- toDomain(data: any): ${entityName}Entity {
188
- return new ${entityName}Entity(
189
- ${domainArgs}
190
- );
191
- }
192
-
193
- /**
194
- * Transforme le DTO en objet pour la création en base de données
195
- */
196
- toPersistence(dto: Create${entityName}Dto): any {
197
- return {
198
- ${toPersistenceFields}
199
- };
200
- }
201
-
202
- /**
203
- * Prépare l'objet de mise à jour partielle
204
- */
205
- toUpdatePersistence(dto: Update${entityName}Dto): any {
206
- const data: any = {};
207
- ${toUpdateFields}
208
- return data;
209
- }
210
- }
211
- `;
212
- }
213
-
214
- export async function generateDto(
215
- entity,
216
- useSwagger,
217
- isAuthDto = false,
218
- mode = "full"
219
- ) {
220
- const entityName = capitalize(entity.name);
221
- let enumImport = "";
222
- if (entityName === "User") {
223
- enumImport =
224
- mode === "light"
225
- ? "import { Role } from 'src/common/enums/role.enum';"
226
- : "import { Role } from 'src/user/domain/enums/role.enum';";
227
- }
228
-
229
- /* const getExampleForField = (f) => {
230
- const fieldName = f.name.toLowerCase();
231
-
232
- if (isAuthDto) {
233
- if (fieldName.includes("newpassword")) {
234
- return `"Password@123456"`;
235
- }
236
- if (fieldName.includes("password")) {
237
- return `"SecurePass@2024"`;
238
- }
239
- if (fieldName.includes("otp")) {
240
- return `"654321"`;
241
- }
242
- if (fieldName.includes("token")) {
243
- return `"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U"`;
244
- }
245
- if (fieldName.includes("email")) {
246
- return `"user@example.com"`;
247
- }
248
- }
249
-
250
- if (fieldName.includes("email")) {
251
- return `"alice.johnson@example.com"`;
252
- }
253
- if (fieldName.includes("password")) {
254
- return `"SecurePass@2024"`;
255
- }
256
- if (fieldName.includes("username")) {
257
- return `"john_doe"`;
258
- }
259
- if (fieldName.includes("firstname") || fieldName.includes("first_name")) {
260
- return `"John"`;
261
- }
262
- if (fieldName.includes("lastname") || fieldName.includes("last_name")) {
263
- return `"Doe"`;
264
- }
265
- if (fieldName.includes("phone")) {
266
- return `"+1234567890"`;
267
- }
268
- if (fieldName.includes("title")) {
269
- return `"Product Title"`;
270
- }
271
- if (fieldName.includes("name")) {
272
- return `"${entityName} Name"`;
273
- }
274
- if (fieldName.includes("description")) {
275
- return `"A detailed description of the item"`;
276
- }
277
- if (fieldName.includes("url") || fieldName.includes("image")) {
278
- return `"https://example.com/image.png"`;
279
- }
280
- if (fieldName.includes("price")) {
281
- return 99.99;
282
- }
283
- if (fieldName.includes("amount") || fieldName.includes("total")) {
284
- return 1500.5;
285
- }
286
- if (fieldName.includes("quantity") || fieldName.includes("count")) {
287
- return 5;
288
- }
289
- if (fieldName.includes("discount")) {
290
- return 15;
291
- }
292
- if (fieldName.includes("rate") || fieldName.includes("rating")) {
293
- return 4.5;
294
- }
295
- if (fieldName.includes("role")) {
296
- return `"admin"`;
297
- }
298
- if (fieldName.includes("status")) {
299
- return `"active"`;
300
- }
301
- if (fieldName.includes("type")) {
302
- return `"standard"`;
303
- }
304
- if (fieldName.includes("date") || fieldName.includes("at")) {
305
- return `"2024-12-01T10:30:00Z"`;
306
- }
307
- if (fieldName.includes("time")) {
308
- return `"14:30:00"`;
309
- }
310
- if (fieldName.includes("id") && fieldName !== "id") {
311
- return `"550e8400-e29b-41d4-a716-446655440000"`;
312
- }
313
- if (fieldName.includes("code")) {
314
- return `"CODE123456"`;
315
- }
316
- if (fieldName.includes("reference")) {
317
- return `"REF-2024-001"`;
318
- }
319
-
320
- switch (f.type.toLowerCase().replace("[]", "")) {
321
- case "string":
322
- case "text":
323
- case "uuid":
324
- case "json":
325
- return `"${entityName.toLowerCase()}_${f.name}"`;
326
- case "number":
327
- case "int":
328
- case "decimal":
329
- case "float":
330
- return 42;
331
- case "boolean":
332
- return true;
333
- case "date":
334
- return `"2024-01-15T08:00:00Z"`;
335
- default:
336
- return `"value"`;
337
- }
338
- }; */
339
-
340
- const getExampleForField = (f) => {
341
- const fieldName = f.name.toLowerCase();
342
- const isArray = f.type.endsWith("[]");
343
- const cleanType = f.type.toLowerCase().replace("[]", "");
344
-
345
- // 1. Logique pour obtenir une valeur de base (SANS le tableau)
346
- const getBaseExample = () => {
347
- // --- Priorité aux noms de champs (Auth & Common) ---
348
- if (fieldName.includes("email")) return `"user@example.com"`;
349
- if (fieldName.includes("password")) return `"SecurePass@2024"`;
350
- if (fieldName.includes("token")) return `"eyJhbGciOi..."`;
351
- if (fieldName.includes("id") && fieldName !== "id")
352
- return `"550e8400-e29b-41d4-a716-446655440000"`;
353
-
354
- if (fieldName.includes("title") || fieldName.includes("name"))
355
- return `"${capitalize(entityName)} Example"`;
356
- if (fieldName.includes("description"))
357
- return `"A detailed description example"`;
358
- if (fieldName.includes("url") || fieldName.includes("image"))
359
- return `"https://example.com/image.png"`;
360
-
361
- if (fieldName.includes("price") || fieldName.includes("amount"))
362
- return 99.99;
363
- if (fieldName.includes("quantity") || fieldName.includes("count"))
364
- return 10;
365
- if (fieldName.includes("status") || fieldName.includes("type"))
366
- return `"active"`;
367
- if (fieldName.includes("date") || fieldName.includes("at"))
368
- return `"2024-12-01T10:30:00Z"`;
369
-
370
- // --- Logique par Type ---
371
- switch (cleanType) {
372
- case "string":
373
- case "text":
374
- case "uuid":
375
- return `"${f.name.toLowerCase()}_val"`;
376
- case "number":
377
- case "int":
378
- case "decimal":
379
- case "float":
380
- return 42;
381
- case "boolean":
382
- return true;
383
- case "json":
384
- // IMPORTANT: On retourne un objet littéral (sous forme de string pour le template)
385
- return `{ "key": "value", "active": true }`;
386
- case "date":
387
- return `"2024-01-15T08:00:00Z"`;
388
- default:
389
- return `"value"`;
390
- }
391
- };
392
-
393
- const baseValue = getBaseExample();
394
-
395
- // 2. Si c'est un tableau, on enveloppe la valeur de base
396
- if (isArray) {
397
- // On retourne deux exemples pour montrer que c'est une liste
398
- return `[${baseValue}, ${baseValue}]`;
399
- }
400
-
401
- return baseValue;
402
- };
403
-
404
- // Géneration de ligne de champ DTO
405
- const generateFieldLine = (f, isRequestDto = false) => {
406
- if (entityName === "User" && f.name.toLowerCase() === "role") {
407
- return null;
408
- }
409
-
410
- const field = { ...f };
411
- const rawType = field.type;
412
- const cleanType = rawType.toLowerCase().replace("[]", "");
413
- const isArrayType = rawType.endsWith("[]");
414
-
415
- const SCALAR_TYPES = [
416
- "string",
417
- "text",
418
- "uuid",
419
- "json",
420
- "number",
421
- "decimal",
422
- "int",
423
- "float",
424
- "boolean",
425
- "date",
426
- "role",
427
- "enum",
428
- ];
429
-
430
- const isRelation = !SCALAR_TYPES.includes(cleanType);
431
-
432
- let tsType;
433
- let typeDecorator;
434
- let swaggerDecorator = "";
435
- let swaggerBaseType = "";
436
-
437
- // 1. GESTION DES RELATIONS
438
- if (isRelation) {
439
- const baseTypeName = capitalize(rawType.replace("[]", ""));
440
- const isForeignKey = field.name.toLowerCase().endsWith("id");
441
-
442
- if (isRequestDto) {
443
- if (!isForeignKey) {
444
- return null;
445
- }
446
- tsType = "string";
447
- typeDecorator = "IsUUID()";
448
-
449
- swaggerDecorator = useSwagger
450
- ? field.optional
451
- ? `@ApiPropertyOptional({ example: "550e8400-e29b-41d4-a716-446655440000", type: String })\n`
452
- : `@ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000", type: String })\n`
453
- : "";
454
- } else {
455
- if (isForeignKey) {
456
- return null;
457
- }
458
- tsType = isArrayType ? `${baseTypeName}Dto[]` : `${baseTypeName}Dto`;
459
-
460
- typeDecorator = `ValidateNested({ each: ${isArrayType} })\n @Type(() => ${baseTypeName}Dto)`;
461
- if (field.optional) typeDecorator = `IsOptional()\n @${typeDecorator}`;
462
-
463
- swaggerDecorator = useSwagger
464
- ? field.optional
465
- ? `@ApiPropertyOptional({ type: () => ${baseTypeName}Dto, ${
466
- isArrayType ? "isArray: true" : ""
467
- } })\n`
468
- : `@ApiProperty({ type: () => ${baseTypeName}Dto, ${
469
- isArrayType ? "isArray: true" : ""
470
- } })\n`
471
- : "";
472
- }
473
- }
474
-
475
- // 2. GESTION DES TYPES SCALAIRES
476
- else {
477
- // Utilise formatType pour obtenir le type TS final
478
- tsType = formatType(rawType);
479
-
480
- let validator;
481
- switch (cleanType) {
482
- case "string":
483
- case "text":
484
- validator = "IsString";
485
- swaggerBaseType = "String";
486
- break;
487
- case "uuid":
488
- validator = "IsUUID";
489
- swaggerBaseType = "String";
490
- break;
491
- case "json":
492
- validator = "IsObject";
493
- tsType = isArrayType
494
- ? `Record<string, any>[]`
495
- : `Record<string, any>`;
496
- swaggerBaseType = "Object";
497
- break;
498
- case "number":
499
- case "decimal":
500
- case "float":
501
- validator = "IsNumber";
502
- swaggerBaseType = "Number";
503
- break;
504
- case "int":
505
- validator = "IsInt";
506
- swaggerBaseType = "Number";
507
- break;
508
- case "boolean":
509
- validator = "IsBoolean";
510
- swaggerBaseType = "Boolean";
511
- break;
512
- case "date":
513
- validator = "IsDateString";
514
- swaggerBaseType = "String";
515
- break;
516
- case "role":
517
- validator = "IsEnum(Role)";
518
- swaggerBaseType = "String";
519
- break;
520
- case "enum":
521
- validator = `IsEnum(${rawType.replace("[]", "")})`;
522
- swaggerBaseType = "String";
523
- break;
524
- default:
525
- validator = "IsString";
526
- swaggerBaseType = "String";
527
- }
528
-
529
- // 1. Ajout des parenthèses pour les validateurs scalaires simples (ex: IsObject -> IsObject())
530
- if (!isArrayType && !validator.includes("(")) {
531
- validator = `${validator}()`;
532
- }
533
-
534
- // Gère les tableaux de types scalaires et d'Enums
535
- if (isArrayType) {
536
- if (rawType.toLowerCase().includes("enum")) {
537
- validator = `IsArray()\n@IsEnum(${rawType.replace(
538
- "[]",
539
- ""
540
- )}, { each: true })`;
541
- } else {
542
- // Pour les scalaires simples (string[], number[])
543
- const baseValidatorName = validator.replace("()", ""); // Retire les parenthèses ()
544
-
545
- validator = `IsArray()\n@${baseValidatorName}({ each: true })`;
546
- }
547
- }
548
-
549
- typeDecorator = validator;
550
-
551
- if (field.optional) typeDecorator = `IsOptional()\n@${typeDecorator}`;
552
-
553
- // Swagger pour Scalaires
554
- const swaggerProp = isArrayType
555
- ? `type: ${swaggerBaseType}, isArray: true`
556
- : `type: ${swaggerBaseType}`;
557
-
558
- swaggerDecorator = useSwagger
559
- ? field.optional
560
- ? `@ApiPropertyOptional({ example: ${getExampleForField(
561
- field
562
- )}, ${swaggerProp} })\n`
563
- : `@ApiProperty({ example: ${getExampleForField(
564
- field
565
- )}, ${swaggerProp} })\n`
566
- : "";
567
- }
568
-
569
- if (!field.name) return null;
570
-
571
- return `${swaggerDecorator} @${typeDecorator}\n ${field.name}${
572
- field.optional ? "?" : ""
573
- }: ${tsType};`;
574
- };
575
-
576
- // UTILISATION DE 'true' pour indiquer que ce sont des DTOs de REQUÊTE
577
- const dtoFields = entity.fields
578
- .map((f) => generateFieldLine({ ...f, optional: false }, true))
579
- .filter(Boolean)
580
- .join("\n\n");
581
-
582
- const updateDtoFields = entity.fields
583
- .map((f) => generateFieldLine({ ...f, optional: true }, true))
584
- .filter(Boolean)
585
- .join("\n\n");
586
-
587
- const commonImports = `import { IsOptional, IsString, IsEnum, IsInt, IsBoolean, IsDate, MinLength, IsArray, IsUUID, IsObject, IsNumber, IsDateString, ValidateNested } from 'class-validator';
588
- import { Type } from 'class-transformer';
589
- ${
590
- useSwagger
591
- ? "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';"
592
- : ""
593
- }
594
- ${enumImport}
595
- `;
596
-
597
- if (isAuthDto) {
598
- return `
599
- ${commonImports}
600
-
601
- export class ${entityName}Dto {
602
- ${dtoFields}
603
- }
604
- `;
605
- }
606
-
607
- return `
608
- ${commonImports}
609
-
610
- export class Create${entityName}Dto {
611
- ${dtoFields}
612
- ${
613
- entityName === "User"
614
- ? `\n ${
615
- useSwagger
616
- ? "@ApiPropertyOptional({ example: 'USER', enum: Role, default: Role.USER })\n "
617
- : ""
618
- }@IsEnum(Role)\n @IsOptional()\n role: Role;`
619
- : ""
620
- }
621
- }
622
-
623
- export class Update${entityName}Dto {
624
- ${updateDtoFields}
625
- ${
626
- entityName === "User"
627
- ? `\n ${
628
- useSwagger
629
- ? "@ApiPropertyOptional({ example: 'USER', enum: Role })\n "
630
- : ""
631
- }@IsEnum(Role)\n @IsOptional()\n role?: Role;`
632
- : ""
633
- }
634
- }
635
- `;
636
- }
637
-
638
- export async function generateController(entityName, entityPath, useSwagger) {
639
- const entityNameLower = decapitalize(entityName);
640
- const entityNameCapitalized = capitalize(entityName);
641
-
642
- const swaggerImports = useSwagger
643
- ? `import { ApiTags, ApiOperation } from '@nestjs/swagger';`
644
- : "";
645
-
646
- const swaggerClassDecorator = useSwagger
647
- ? `@ApiTags('${entityNameLower}')`
648
- : "";
649
-
650
- const swaggerMethodDecorator = (summary) =>
651
- useSwagger ? `@ApiOperation({ summary: '${summary}' })\n ` : "";
652
-
653
- return `
654
- /**
655
- * ${entityNameCapitalized}Controller handles HTTP endpoints
656
- * for the ${entityNameCapitalized} entity.
657
- *
658
- * This controller is responsible only for:
659
- * - HTTP transport
660
- * - Request/response handling
661
- * - Returning API messages
662
- *
663
- * Business logic is delegated to the service layer.
664
- */
665
-
666
- import { Controller, Get, Post, Body, Param, Put, Delete } from '@nestjs/common';
667
- ${swaggerImports}
668
- import { ${entityNameCapitalized}Service } from '${entityPath}/application/services/${entityNameLower}.service';
669
- import { Create${entityNameCapitalized}Dto, Update${entityNameCapitalized}Dto } from 'src/${entityNameLower}/application/dtos/${entityNameLower}.dto';
670
-
671
- ${swaggerClassDecorator}
672
- @Controller('${pluralize(entityNameLower)}')
673
- export class ${entityNameCapitalized}Controller {
674
- constructor(private readonly service: ${entityNameCapitalized}Service) {}
675
-
676
- ${
677
- entityNameLower !== "user"
678
- ? `
679
- // Create a new ${entityNameLower}
680
- @Post()
681
- ${swaggerMethodDecorator(`Create a new ${entityNameLower}`)}
682
- async create(@Body() dto: Create${entityNameCapitalized}Dto) {
683
- await this.service.create(dto);
684
- return { message: '${entityNameCapitalized} created successfully' };
685
- }
686
- `
687
- : ""
688
- }
689
-
690
- // Update an existing ${entityNameLower}
691
- @Put(':id')
692
- ${swaggerMethodDecorator(`Update a ${entityNameLower}`)}
693
- async update(
694
- @Param('id') id: string,
695
- @Body() dto: Update${entityNameCapitalized}Dto,
696
- ) {
697
- await this.service.update(id, dto);
698
- return { message: '${entityNameCapitalized} updated successfully' };
699
- }
700
-
701
- // Get a ${entityNameLower} by ID
702
- @Get(':id')
703
- ${swaggerMethodDecorator(`Get a ${entityNameLower} by ID`)}
704
- async getById(@Param('id') id: string) {
705
- return await this.service.getById(id);
706
- }
707
-
708
- // Get all ${pluralize(entityNameLower)}
709
- @Get()
710
- ${swaggerMethodDecorator(`Get all ${pluralize(entityNameLower)}`)}
711
- async getAll() {
712
- return await this.service.getAll();
713
- }
714
-
715
- // Delete a ${entityNameLower}
716
- @Delete(':id')
717
- ${swaggerMethodDecorator(`Delete a ${entityNameLower} by ID`)}
718
- async delete(@Param('id') id: string) {
719
- await this.service.delete(id);
720
- return { message: '${entityNameCapitalized} deleted successfully' };
721
- }
722
- }
723
- `;
724
- }
725
-
726
- export async function generateMiddlewares(orm = "global") {
727
- logInfo(
728
- "\u2728 G\u00e9n\u00e9ration des middlewares, interceptors, guards et filters personnalis\u00e9s..."
729
- );
730
-
731
- const basePath = "src/common";
732
- await createDirectory(`${basePath}/middlewares`);
733
- await createDirectory(`${basePath}/interceptors`);
734
- await createDirectory(`${basePath}/filters`);
735
- await createDirectory(`${basePath}/decorators`);
736
-
737
- // Logger Middleware
738
- await createFile({
739
- path: `${basePath}/middlewares/logger.middleware.ts`,
740
- contente: `import { Request, Response, NextFunction } from 'express';
741
-
742
- export function LoggerMiddleware(
743
- req: Request,
744
- res: Response,
745
- next: NextFunction,
746
- ) {
747
- if (process.env.NODE_ENV === 'production') {
748
- return next();
749
- }
750
-
751
- res.on('finish', () => {
752
- console.log(
753
- \`[Request] \${req.method} \${req.originalUrl} - \${res.statusCode}\`,
754
- );
755
- });
756
-
757
- next();
758
- }
759
- `,
760
- });
761
-
762
- // Error Handling Filter (personnalisé selon l'ORM)
763
- await createFile({
764
- path: `${basePath}/filters/all-exceptions.filter.ts`,
765
- contente: getExceptionFilterContent(orm),
766
- });
767
-
768
- // Response Interceptor
769
- await createFile({
770
- path: `${basePath}/interceptors/response.interceptor.ts`,
771
- contente: `import {
772
- CallHandler,
773
- ExecutionContext,
774
- Injectable,
775
- NestInterceptor,
776
- } from '@nestjs/common';
777
- import { Observable } from 'rxjs';
778
- import { map } from 'rxjs/operators';
779
-
780
- @Injectable()
781
- export class ResponseInterceptor<T> implements NestInterceptor<T, any> {
782
- intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
783
- const httpContext = context.switchToHttp();
784
- const response = httpContext.getResponse();
785
- const request = httpContext.getRequest();
786
-
787
- return next.handle().pipe(
788
- map((data) => {
789
- const message =
790
- response.locals.message || this.getDefaultMessage(request.method);
791
-
792
- return {
793
- statusCode: response.statusCode,
794
- message,
795
- path: request.url,
796
- method: request.method,
797
- timestamp: new Date().toISOString(),
798
- data: data ?? null,
799
- };
800
- }),
801
- );
802
- }
803
-
804
- private getDefaultMessage(method: string): string {
805
- switch (method.toUpperCase()) {
806
- case 'POST':
807
- return 'Resource created successfully';
808
- case 'PUT':
809
- case 'PATCH':
810
- return 'Resource updated successfully';
811
- case 'DELETE':
812
- return 'Resource deleted successfully';
813
- case 'GET':
814
- return 'Request processed successfully';
815
- default:
816
- return 'Request processed successfully';
817
- }
818
- }
819
- }
820
- `,
821
- });
822
-
823
- // public Decorator
824
- await createFile({
825
- path: `${basePath}/decorators/public.decorator.ts`,
826
- contente: `import { SetMetadata } from '@nestjs/common';
827
-
828
- export const IS_PUBLIC_KEY = 'isPublic';
829
- export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
830
- `,
831
- });
832
-
833
- await createFile({
834
- path: `${basePath}/decorators/current-user.decorator.ts`,
835
- contente: `import { createParamDecorator, ExecutionContext } from '@nestjs/common';\nexport const CurrentUser = createParamDecorator((data, ctx: ExecutionContext) => ctx.switchToHttp().getRequest().user);`,
836
- });
837
-
838
- // Auth role Decorator
839
- await createFile({
840
- path: `${basePath}/decorators/role.decorator.ts`,
841
- contente: `import { SetMetadata } from '@nestjs/common';
842
-
843
- export const ROLES_KEY = 'roles';
844
- export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
845
- `,
846
- });
847
-
848
- // Modification de main.ts pour intégrer les middlewares
849
- // Chemin vers main.ts
850
- const mainTsPath = "src/main.ts";
851
-
852
- // 1. Mise à jour des imports
853
- const importPattern = "import { AppModule } from './app.module';";
854
- const importReplacer = `import { AppModule } from 'src/app.module'
855
- import { AllExceptionsFilter } from 'src/common/filters/all-exceptions.filter'
856
- import { ResponseInterceptor } from 'src/common/interceptors/response.interceptor'
857
- import { LoggerMiddleware } from 'src/common/middlewares/logger.middleware'
858
- import { ValidationPipe } from '@nestjs/common';`;
859
-
860
- // 2. Injection dans le contenu de bootstrap()
861
- const contentPattern = `const app = await NestFactory.create(AppModule);`;
862
-
863
- const contentReplacer = `
864
- const app = await NestFactory.create(AppModule);
865
-
866
- // 🔒 Global filter pour gérer toutes les exceptions
867
- app.useGlobalFilters(new AllExceptionsFilter())
868
-
869
- // 🔁 Global interceptor pour structurer les réponses
870
- // app.useGlobalInterceptors(new ResponseInterceptor()); //deja appliquer dans le app.module.ts par convention (ne choisir que l'un des deux)
871
-
872
- // 📋 Middleware pour logger toutes les requêtes entrantes
873
- app.use(LoggerMiddleware);`;
874
-
875
- // Appels
876
- await updateFile({
877
- path: mainTsPath,
878
- pattern: importPattern,
879
- replacement: importReplacer,
880
- });
881
-
882
- await updateFile({
883
- path: mainTsPath,
884
- pattern: contentPattern,
885
- replacement: contentReplacer,
886
- });
887
-
888
- // modification de AppModule
889
- const appModulePath = "src/app.module.ts";
890
-
891
- const addNestModuleInterface = `providers: [`;
892
- const replaceWithNestModule = `providers: [
893
- {
894
- provide: APP_INTERCEPTOR,
895
- useClass: ResponseInterceptor, // 🔁 Global interceptor pour structurer les réponses
896
- },`;
897
-
898
- await updateFile({
899
- path: appModulePath,
900
- pattern: addNestModuleInterface,
901
- replacement: replaceWithNestModule,
902
- });
903
-
904
- logSuccess(
905
- "\u2705 Middlewares, filters, interceptors et guards g\u00e9n\u00e9r\u00e9s avec succ\u00e8s !"
906
- );
907
- }
908
-
909
- export async function generateRepository(entityName, orm) {
910
- const entityNameCapitalized = capitalize(entityName);
911
- const entityNameLower = entityName.toLowerCase();
912
- const entityPath = `src/${entityNameLower}`;
913
-
914
- // Générateur de méthode spécifique (ex: findByEmail pour Auth)
915
- const isUser = entityNameLower === "user";
916
- const getExtraMethods = (ormType) => {
917
- if (!isUser) return "";
918
-
919
- switch (ormType) {
920
- case "typeorm":
921
- return `
922
- async findByEmail(email: string): Promise<${entityNameCapitalized}Entity | null> {
923
- const record = await this.repository.findOne({ where: { email } as any });
924
- return record ? this.mapper.toDomain(record) : null;
925
- }`;
926
- case "prisma":
927
- return `
928
- async findByEmail(email: string): Promise<${entityNameCapitalized}Entity | null> {
929
- const record = await this.prisma.${entityNameLower}.findFirst({ where: { email } });
930
- return record ? this.mapper.toDomain(record) : null;
931
- }`;
932
- case "mongoose":
933
- return `
934
- async findByEmail(email: string): Promise<${entityNameCapitalized}Entity | null> {
935
- const record = await this.model.findOne({ email }).exec();
936
- return record ? this.mapper.toDomain(record) : null;
937
- }`;
938
- case "sequelize":
939
- return `
940
- async findByEmail(email: string): Promise<${entityNameCapitalized}Entity | null> {
941
- const record = await this.model.findOne({ where: { email } });
942
- return record ? this.mapper.toDomain(record) : null;
943
- }`;
944
- default:
945
- return "";
946
- }
947
- };
948
-
949
- const extraMethods = getExtraMethods(orm);
950
-
951
- // Gère le switch en fonction de l'ORM choisi
952
- switch (orm) {
953
- case "typeorm":
954
- // Implémentation du repository pour TypeORM
955
- await createFile({
956
- path: `${entityPath}/infrastructure/repositories/${entityNameLower}.repository.ts`,
957
- contente: `/**
958
- * PostRepository handles data persistence
959
- * for the Post entity.
960
- *
961
- * This layer abstracts the database engine (Prisma/TypeORM)
962
- * and provides a clean interface for data operations.
963
- */
964
-
965
- import { Injectable, NotFoundException } from '@nestjs/common';
966
- import { Repository } from 'typeorm';
967
- import { InjectRepository } from '@nestjs/typeorm';
968
- import { ${entityNameCapitalized}Entity } from 'src/${entityNameLower}/domain/entities/${entityNameLower}.entity';
969
- import { ${entityNameCapitalized} } from 'src/entities/${entityNameCapitalized}.entity';
970
- import { I${entityNameCapitalized}Repository } from 'src/${entityNameLower}/domain/interfaces/${entityNameLower}.repository.interface';
971
- import { Create${entityNameCapitalized}Dto, Update${entityNameCapitalized}Dto } from 'src/${entityNameLower}/application/dtos/${entityNameLower}.dto';
972
- import { ${entityNameCapitalized}Mapper } from 'src/${entityNameLower}/infrastructure/mappers/${entityNameLower}.mapper';
973
-
974
- @Injectable()
975
- export class ${entityNameCapitalized}Repository implements I${entityNameCapitalized}Repository {
976
- constructor(
977
- @InjectRepository(${entityNameCapitalized})
978
- private readonly repository: Repository<${entityNameCapitalized}>,
979
- private readonly mapper: ${entityNameCapitalized}Mapper,
980
- ) {}
981
-
982
- // create
983
- async create(data: Create${entityNameCapitalized}Dto): Promise<${entityNameCapitalized}Entity> {
984
- const toPersist = this.mapper.toPersistence(data);
985
- const created = await this.repository.save(toPersist);
986
- return this.mapper.toDomain(created);
987
- }
988
-
989
- // find by id
990
- async findById(id: string): Promise<${entityNameCapitalized}Entity> {
991
- const record = await this.repository.findOne({
992
- where: { id },
993
- });
994
-
995
- if (!record) {
996
- throw new NotFoundException(\`${entityNameCapitalized}Entity with id \${id} not found\`);
997
- }
998
-
999
- return this.mapper.toDomain(record);
1000
- }
1001
-
1002
- ${extraMethods}
1003
-
1004
- // update
1005
- async update(id: string, data: Update${entityNameCapitalized}Dto): Promise<${entityNameCapitalized}Entity> {
1006
- const toUpdate = this.mapper.toUpdatePersistence(data);
1007
- const updated = await this.repository.save({ ...toUpdate, id });
1008
- return this.mapper.toDomain(updated);
1009
- }
1010
-
1011
- // find all
1012
- async findAll(): Promise<${entityNameCapitalized}Entity[]> {
1013
- const records = await this.repository.find();
1014
- return records.map(record => this.mapper.toDomain(record));
1015
- }
1016
-
1017
- // delete
1018
- async delete(id: string): Promise<void> {
1019
- await this.repository.delete(id);
1020
- }
1021
- }
1022
- `,
1023
- });
1024
- break;
1025
-
1026
- case "prisma":
1027
- // Implémentation pour Prisma
1028
- await createFile({
1029
- path: `${entityPath}/infrastructure/repositories/${entityNameLower}.repository.ts`,
1030
- contente: `import { Injectable, NotFoundException } from '@nestjs/common';
1031
- import { PrismaService } from 'src/prisma/prisma.service';
1032
- import { Create${entityNameCapitalized}Dto, Update${entityNameCapitalized}Dto } from 'src/${entityNameLower}/application/dtos/${entityNameLower}.dto';
1033
- import { I${entityNameCapitalized}Repository } from 'src/${entityNameLower}/domain/interfaces/${entityNameLower}.repository.interface';
1034
- import { ${entityNameCapitalized}Entity } from 'src/${entityNameLower}/domain/entities/${entityNameLower}.entity';
1035
- import { ${entityNameCapitalized}Mapper } from 'src/${entityNameLower}/infrastructure/mappers/${entityNameLower}.mapper';
1036
-
1037
- @Injectable()
1038
- export class ${entityNameCapitalized}Repository implements I${entityNameCapitalized}Repository {
1039
- constructor(
1040
- private readonly prisma: PrismaService,
1041
- private readonly mapper: ${entityNameCapitalized}Mapper,
1042
- ) {}
1043
-
1044
- // create
1045
- async create(data: Create${entityNameCapitalized}Dto): Promise<${entityNameCapitalized}Entity> {
1046
- const toPersist = this.mapper.toPersistence(data);
1047
- const created = await this.prisma.${entityNameLower}.create({ data: toPersist });
1048
- return this.mapper.toDomain(created);
1049
- }
1050
-
1051
- // find by id
1052
- async findById(id: string): Promise<${entityNameCapitalized}Entity> {
1053
- const record = await this.prisma.${entityNameLower}.findUnique({
1054
- where: { id },
1055
- });
1056
-
1057
- if (!record) {
1058
- throw new NotFoundException(\`${entityNameCapitalized}Entity with id \${id} not found\`);
1059
- }
1060
-
1061
- return this.mapper.toDomain(record);
1062
- }
1063
-
1064
- ${extraMethods}
1065
-
1066
- // update
1067
- async update(id: string, data: Update${entityNameCapitalized}Dto): Promise<${entityNameCapitalized}Entity> {
1068
- const toUpdate = this.mapper.toUpdatePersistence(data);
1069
- const updated = await this.prisma.${entityNameLower}.update({
1070
- where: { id },
1071
- data: toUpdate,
1072
- });
1073
-
1074
- return this.mapper.toDomain(updated);
1075
- }
1076
-
1077
- // find all
1078
- async findAll(): Promise<${entityNameCapitalized}Entity[]> {
1079
- const records = await this.prisma.${entityNameLower}.findMany();
1080
- return records.map(record => this.mapper.toDomain(record));
1081
- }
1082
-
1083
- // delete
1084
- async delete(id: string): Promise<void> {
1085
- await this.prisma.${entityNameLower}.delete({
1086
- where: { id },
1087
- });
1088
- }
1089
- }
1090
- `,
1091
- });
1092
- break;
1093
-
1094
- case "mongoose":
1095
- // Implémentation pour MongoDB avec Mongoose
1096
- await createFile({
1097
- path: `${entityPath}/infrastructure/repositories/${entityNameLower}.repository.ts`,
1098
- contente: `import { Injectable, NotFoundException } from '@nestjs/common';
1099
- import { InjectModel } from '@nestjs/mongoose';
1100
- import { Model } from 'mongoose';
1101
- import { ${entityNameCapitalized}Entity } from '${entityPath}/domain/entities/${entityNameLower}.entity';
1102
- import { Create${entityNameCapitalized}Dto, Update${entityNameCapitalized}Dto } from 'src/${entityNameLower}/application/dtos/${entityNameLower}.dto';
1103
- import { I${entityNameCapitalized}Repository } from 'src/${entityNameLower}/domain/interfaces/${entityNameLower}.repository.interface';
1104
- import { ${entityNameCapitalized} } from 'src/${entityNameLower}/domain/entities/${entityNameLower}.schema';
1105
- import { ${entityNameCapitalized}Mapper } from 'src/${entityNameLower}/infrastructure/mappers/${entityNameLower}.mapper';
1106
-
1107
- @Injectable()
1108
- export class ${entityNameCapitalized}Repository implements I${entityNameCapitalized}Repository {
1109
- constructor(
1110
- @InjectModel(${entityNameCapitalized}.name)
1111
- private readonly model: Model<${entityNameCapitalized}>,
1112
- private readonly mapper: ${entityNameCapitalized}Mapper,
1113
- ) {}
1114
-
1115
- // create
1116
- async create(data: Create${entityNameCapitalized}Dto): Promise<${entityNameCapitalized}Entity> {
1117
- const toPersist = this.mapper.toPersistence(data);
1118
- const created = await this.model.create(toPersist);
1119
- return this.mapper.toDomain(created);
1120
- }
1121
-
1122
- // find by id
1123
- async findById(id: string): Promise<${entityNameCapitalized}Entity> {
1124
- const record = await this.model.findById(id);
1125
-
1126
- if (!record) {
1127
- throw new NotFoundException(\`${entityNameCapitalized}Entity with id \${id} not found\`);
1128
- }
1129
-
1130
- return this.mapper.toDomain(record);
1131
- }
1132
-
1133
- ${extraMethods}
1134
-
1135
- // update
1136
- async update(id: string, data: Update${entityNameCapitalized}Dto): Promise<${entityNameCapitalized}Entity> {
1137
- const toUpdate = this.mapper.toUpdatePersistence(data);
1138
- const updated = await this.model.findByIdAndUpdate(id, toUpdate, { new: true });
1139
- return this.mapper.toDomain(updated);
1140
- }
1141
-
1142
- // find all
1143
- async findAll(): Promise<${entityNameCapitalized}Entity[]> {
1144
- const records = await this.model.find();
1145
- return records.map(record => this.mapper.toDomain(record));
1146
- }
1147
-
1148
- // delete
1149
- async delete(id: string): Promise<void> {
1150
- await this.model.findByIdAndDelete(id);
1151
- }
1152
- }
1153
- `,
1154
- });
1155
- break;
1156
-
1157
- case "sequelize":
1158
- // Implémentation pour Sequelize
1159
- await createFile({
1160
- path: `${entityPath}/infrastructure/repositories/${entityNameLower}.repository.ts`,
1161
- contente: `import { Injectable, NotFoundException } from '@nestjs/common';
1162
- import { InjectModel } from '@nestjs/sequelize';
1163
- import { Model } from 'sequelize-typescript';
1164
- import { Create${entityNameCapitalized}Dto, Update${entityNameCapitalized}Dto } from 'src/${entityNameLower}/application/dtos/${entityNameLower}.dto';
1165
- import { I${entityNameCapitalized}Repository } from 'src/${entityNameLower}/domain/interfaces/${entityNameLower}.repository.interface';
1166
- import { ${entityNameCapitalized}Entity } from 'src/${entityNameLower}/domain/entities/${entityNameLower}.entity';
1167
- import { ${entityNameCapitalized}Mapper } from 'src/${entityNameLower}/infrastructure/mappers/${entityNameLower}.mapper';
1168
-
1169
- @Injectable()
1170
- export class ${entityNameCapitalized}Repository implements I${entityNameCapitalized}Repository {
1171
- constructor(
1172
- @InjectModel(${entityNameCapitalized}Entity)
1173
- private readonly model: Model<${entityNameCapitalized}Entity>,
1174
- private readonly mapper: ${entityNameCapitalized}Mapper,
1175
- ) {}
1176
-
1177
- // create
1178
- async create(data: Create${entityNameCapitalized}Dto): Promise<${entityNameCapitalized}Entity> {
1179
- const toPersist = this.mapper.toPersistence(data);
1180
- const created = await this.model.create(toPersist);
1181
- return this.mapper.toDomain(created);
1182
- }
1183
-
1184
- // find by id
1185
- async findById(id: string): Promise<${entityNameCapitalized}Entity> {
1186
- const record = await this.model.findByPk(id);
1187
-
1188
- if (!record) {
1189
- throw new NotFoundException(\`${entityNameCapitalized}Entity with id \${id} not found\`);
1190
- }
1191
-
1192
- return this.mapper.toDomain(record);
1193
- }
1194
-
1195
- ${extraMethods}
1196
-
1197
- // update
1198
- async update(id: string, data: Update${entityNameCapitalized}Dto): Promise<${entityNameCapitalized}Entity> {
1199
- const toUpdate = this.mapper.toUpdatePersistence(data);
1200
- const updated = await this.model.update(toUpdate, { where: { id } });
1201
- return this.mapper.toDomain(updated);
1202
- }
1203
-
1204
- // find all
1205
- async findAll(): Promise<${entityNameCapitalized}Entity[]> {
1206
- const records = await this.model.findAll();
1207
- return records.map(record => this.mapper.toDomain(record));
1208
- }
1209
-
1210
- // delete
1211
- async delete(id: string): Promise<void> {
1212
- await this.model.destroy({ where: { id } });
1213
- }
1214
- }
1215
- `,
1216
- });
1217
- break;
1218
-
1219
- default:
1220
- console.error("Unsupported ORM: " + orm);
1221
- break;
1222
- }
1223
- }
1224
-
1225
- export async function generateMongooseSchemaFileContent(entity) {
1226
- const entityName = capitalize(entity.name);
1227
- const fields = entity.fields
1228
- .map((f) => ` @Prop()\n ${f.name}: ${formatType(f.type)};`)
1229
- .join("\n");
1230
- return `
1231
- import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
1232
- import { Document } from 'mongoose';
1233
-
1234
- @Schema()
1235
- export class ${entityName} extends Document {
1236
- ${fields}
1237
- }
1238
-
1239
- export const ${entityName}Schema = SchemaFactory.createForClass(${entityName});
1240
- `.trim();
1241
- }
1242
-
1243
- function capitalize(str) {
1244
- return str.charAt(0).toUpperCase() + str.slice(1);
1245
- }
1246
-
1247
- function decapitalize(str) {
1248
- return str.charAt(0).toLowerCase() + str.slice(1);
1249
- }
1250
-
1251
- /**
1252
- * Mappe le type de champ interne (sélectionné par l'utilisateur) au type
1253
- * TypeScript ou DTO/Classe/Enum correspondant pour la génération de code.
1254
- *
1255
- * @param {string} type - Le type de base sélectionné (string, number, array, DTO, Enum, etc.)
1256
- * @returns {string} Le type formaté pour la génération de code (ex: 'string', 'number', 'Article[]', 'UserRoleEnum')
1257
- */
1258
- function formatType(type) {
1259
- // 1. Gestion des types simples
1260
- switch (type.toLowerCase()) {
1261
- case "number":
1262
- case "decimal":
1263
- return "number";
1264
-
1265
- case "boolean":
1266
- return "boolean";
1267
-
1268
- case "date":
1269
- return "Date";
1270
-
1271
- case "json":
1272
- return "Record<string, any>";
1273
-
1274
- case "string":
1275
- case "text":
1276
- case "uuid":
1277
- return "string";
1278
- }
1279
-
1280
- //2. Vérifie si c'est un Array
1281
- // Gère les Arrays de scalaires (ex: 'string[]')
1282
- if (type.endsWith("[]")) {
1283
- // Applique le formatage au type interne et ajoute '[]'
1284
- return `${formatType(type.slice(0, -2))}[]`;
1285
- }
1286
-
1287
- if (type.match(/^[A-Za-z][A-Za-z0-9_]*$/)) {
1288
- return type;
1289
- }
1290
-
1291
- return "any";
1292
- }
1293
-
1294
- /**
1295
- * Gère les cas spéciaux (comme le rôle utilisateur) avant l'application du formatage.
1296
- * * @param {object} field - L'objet champ avec { name, type }
1297
- * @returns {string} Le type final formaté.
1298
- */
1299
- function getFormattedType(field) {
1300
- // Cas spécial pour la propriété 'role'
1301
- if (field.name === "role" && field.type.toLowerCase().startsWith("string")) {
1302
- return "Role";
1303
- }
1304
-
1305
- return formatType(field.type);
1306
- }
1307
-
1308
- // Génère le contenu du filtre d'exception selon l'ORM
1309
- function getExceptionFilterContent(orm) {
1310
- if (orm === "prisma") {
1311
- return `
1312
- import {
1313
- ExceptionFilter,
1314
- Catch,
1315
- ArgumentsHost,
1316
- HttpException,
1317
- HttpStatus,
1318
- Logger,
1319
- } from '@nestjs/common';
1320
- import { Request, Response } from 'express';
1321
-
1322
- @Catch()
1323
- export class AllExceptionsFilter implements ExceptionFilter {
1324
- private readonly logger = new Logger(AllExceptionsFilter.name);
1325
-
1326
- catch(exception: unknown, host: ArgumentsHost) {
1327
- const ctx = host.switchToHttp();
1328
- const response = ctx.getResponse<Response>();
1329
- const request = ctx.getRequest<Request>();
1330
-
1331
- let status = HttpStatus.INTERNAL_SERVER_ERROR;
1332
- let message: string | string[] = 'Internal server error';
1333
- let errorDetails: any = null;
1334
-
1335
- if (exception instanceof HttpException) {
1336
- status = exception.getStatus();
1337
- const res = exception.getResponse();
1338
- if (typeof res === 'string') {
1339
- message = res;
1340
- } else if (typeof res === 'object' && res !== null) {
1341
- const resObj = res as any;
1342
- message = resObj.message || resObj.error || 'HttpException';
1343
- errorDetails = resObj;
1344
- }
1345
- } else if (
1346
- typeof exception === 'object' &&
1347
- exception &&
1348
- exception.constructor &&
1349
- (
1350
- exception.constructor.name === 'PrismaClientKnownRequestError' ||
1351
- exception.constructor.name === 'PrismaClientValidationError'
1352
- )
1353
- ) {
1354
- status = HttpStatus.BAD_REQUEST;
1355
- message = (exception as any).message || 'Prisma error';
1356
- errorDetails = exception;
1357
- } else if (exception instanceof Error) {
1358
- message = exception.message;
1359
- errorDetails = {
1360
- name: exception.name,
1361
- stack: exception.stack,
1362
- };
1363
- } else {
1364
- message = 'Une erreur inattendue est survenue';
1365
- errorDetails = exception;
1366
- }
1367
-
1368
- if (process.env.NODE_ENV !== 'production') {
1369
- this.logger.error(
1370
- \`Exception on \${request.method} \${request.url}\`,
1371
- JSON.stringify({ message, status, errorDetails, exception }),
1372
- );
1373
- }
1374
-
1375
- response.status(status).json({
1376
- statusCode: status,
1377
- timestamp: new Date().toISOString(),
1378
- path: request.url,
1379
- method: request.method,
1380
- message,
1381
- error: errorDetails,
1382
- });
1383
- }
1384
- }
1385
- `.trim();
1386
- }
1387
-
1388
- if (orm === "mongoose") {
1389
- return `
1390
- import {
1391
- ExceptionFilter,
1392
- Catch,
1393
- ArgumentsHost,
1394
- HttpException,
1395
- HttpStatus,
1396
- Logger,
1397
- } from '@nestjs/common';
1398
- import { Request, Response } from 'express';
1399
-
1400
- @Catch()
1401
- export class AllExceptionsFilter implements ExceptionFilter {
1402
- private readonly logger = new Logger(AllExceptionsFilter.name);
1403
-
1404
- catch(exception: unknown, host: ArgumentsHost) {
1405
- const ctx = host.switchToHttp();
1406
- const response = ctx.getResponse<Response>();
1407
- const request = ctx.getRequest<Request>();
1408
-
1409
- let status = HttpStatus.INTERNAL_SERVER_ERROR;
1410
- let message: string | string[] = 'Internal server error';
1411
- let errorDetails: any = null;
1412
-
1413
- if (exception instanceof HttpException) {
1414
- status = exception.getStatus();
1415
- const res = exception.getResponse();
1416
- if (typeof res === 'string') {
1417
- message = res;
1418
- } else if (typeof res === 'object' && res !== null) {
1419
- const resObj = res as any;
1420
- message = resObj.message || resObj.error || 'HttpException';
1421
- errorDetails = resObj;
1422
- }
1423
- } else if (
1424
- typeof exception === 'object' &&
1425
- exception &&
1426
- 'name' in exception &&
1427
- (
1428
- (exception as any).name === 'MongoError' ||
1429
- (exception as any).name === 'MongooseError'
1430
- )
1431
- ) {
1432
- status = HttpStatus.BAD_REQUEST;
1433
- message = (exception as any).message || 'MongoDB error';
1434
- errorDetails = exception;
1435
- } else if (exception instanceof Error) {
1436
- message = exception.message;
1437
- errorDetails = {
1438
- name: exception.name,
1439
- stack: exception.stack,
1440
- };
1441
- } else {
1442
- message = 'Une erreur inattendue est survenue';
1443
- errorDetails = exception;
1444
- }
1445
-
1446
- if (process.env.NODE_ENV !== 'production') {
1447
- this.logger.error(
1448
- \`Exception on \${request.method} \${request.url}\`,
1449
- JSON.stringify({ message, status, errorDetails, exception }),
1450
- );
1451
- }
1452
-
1453
- response.status(status).json({
1454
- statusCode: status,
1455
- timestamp: new Date().toISOString(),
1456
- path: request.url,
1457
- method: request.method,
1458
- message,
1459
- error: errorDetails,
1460
- });
1461
- }
1462
- }
1463
- `.trim();
1464
- }
1465
-
1466
- if (orm === "typeorm") {
1467
- return `
1468
- import {
1469
- ExceptionFilter,
1470
- Catch,
1471
- ArgumentsHost,
1472
- HttpException,
1473
- HttpStatus,
1474
- Logger,
1475
- } from '@nestjs/common';
1476
- import { Request, Response } from 'express';
1477
-
1478
- @Catch()
1479
- export class AllExceptionsFilter implements ExceptionFilter {
1480
- private readonly logger = new Logger(AllExceptionsFilter.name);
1481
-
1482
- catch(exception: unknown, host: ArgumentsHost) {
1483
- const ctx = host.switchToHttp();
1484
- const response = ctx.getResponse<Response>();
1485
- const request = ctx.getRequest<Request>();
1486
-
1487
- let status = HttpStatus.INTERNAL_SERVER_ERROR;
1488
- let message: string | string[] = 'Internal server error';
1489
- let errorDetails: any = null;
1490
-
1491
- if (exception instanceof HttpException) {
1492
- status = exception.getStatus();
1493
- const res = exception.getResponse();
1494
- if (typeof res === 'string') {
1495
- message = res;
1496
- } else if (typeof res === 'object' && res !== null) {
1497
- const resObj = res as any;
1498
- message = resObj.message || resObj.error || 'HttpException';
1499
- errorDetails = resObj;
1500
- }
1501
- } else if (
1502
- typeof exception === 'object' &&
1503
- exception &&
1504
- 'name' in exception &&
1505
- (
1506
- (exception as any).name === 'QueryFailedError' ||
1507
- (exception as any).name === 'EntityNotFoundError' ||
1508
- (exception as any).name === 'CannotCreateEntityIdMapError'
1509
- )
1510
- ) {
1511
- status = HttpStatus.BAD_REQUEST;
1512
- message = (exception as any).message || 'TypeORM error';
1513
- errorDetails = exception;
1514
- } else if (exception instanceof Error) {
1515
- message = exception.message;
1516
- errorDetails = {
1517
- name: exception.name,
1518
- stack: exception.stack,
1519
- };
1520
- } else {
1521
- message = 'Une erreur inattendue est survenue';
1522
- errorDetails = exception;
1523
- }
1524
-
1525
- if (process.env.NODE_ENV !== 'production') {
1526
- this.logger.error(
1527
- \`Exception on \${request.method} \${request.url}\`,
1528
- JSON.stringify({ message, status, errorDetails, exception }),
1529
- );
1530
- }
1531
-
1532
- response.status(status).json({
1533
- statusCode: status,
1534
- timestamp: new Date().toISOString(),
1535
- path: request.url,
1536
- method: request.method,
1537
- message,
1538
- error: errorDetails,
1539
- });
1540
- }
1541
- }
1542
- `.trim();
1543
- }
1544
-
1545
- if (orm === "sequelize") {
1546
- return `
1547
- import {
1548
- ExceptionFilter,
1549
- Catch,
1550
- ArgumentsHost,
1551
- HttpException,
1552
- HttpStatus,
1553
- Logger,
1554
- } from '@nestjs/common';
1555
- import { Request, Response } from 'express';
1556
-
1557
- @Catch()
1558
- export class AllExceptionsFilter implements ExceptionFilter {
1559
- private readonly logger = new Logger(AllExceptionsFilter.name);
1560
-
1561
- catch(exception: unknown, host: ArgumentsHost) {
1562
- const ctx = host.switchToHttp();
1563
- const response = ctx.getResponse<Response>();
1564
- const request = ctx.getRequest<Request>();
1565
-
1566
- let status = HttpStatus.INTERNAL_SERVER_ERROR;
1567
- let message: string | string[] = 'Internal server error';
1568
- let errorDetails: any = null;
1569
-
1570
- if (exception instanceof HttpException) {
1571
- status = exception.getStatus();
1572
- const res = exception.getResponse();
1573
- if (typeof res === 'string') {
1574
- message = res;
1575
- } else if (typeof res === 'object' && res !== null) {
1576
- const resObj = res as any;
1577
- message = resObj.message || resObj.error || 'HttpException';
1578
- errorDetails = resObj;
1579
- }
1580
- } else if (
1581
- typeof exception === 'object' &&
1582
- exception &&
1583
- exception.constructor &&
1584
- (
1585
- exception.constructor.name === 'SequelizeDatabaseError' ||
1586
- exception.constructor.name === 'SequelizeValidationError'
1587
- )
1588
- ) {
1589
- status = HttpStatus.BAD_REQUEST;
1590
- message = (exception as any).message || 'Sequelize error';
1591
- errorDetails = exception;
1592
- } else if (exception instanceof Error) {
1593
- message = exception.message;
1594
- errorDetails = {
1595
- name: exception.name,
1596
- stack: exception.stack,
1597
- };
1598
- } else {
1599
- message = 'Une erreur inattendue est survenue';
1600
- errorDetails = exception;
1601
- }
1602
-
1603
- if (process.env.NODE_ENV !== 'production') {
1604
- this.logger.error(
1605
- \`Exception on \${request.method} \${request.url}\`,
1606
- JSON.stringify({ message, status, errorDetails, exception }),
1607
- );
1608
- }
1609
-
1610
- response.status(status).json({
1611
- statusCode: status,
1612
- timestamp: new Date().toISOString(),
1613
- path: request.url,
1614
- method: request.method,
1615
- message,
1616
- error: errorDetails,
1617
- });
1618
- }
1619
- }
1620
- `.trim();
1621
- }
1622
-
1623
- // Version universelle (multi-ORM)
1624
- return `
1625
- import {
1626
- ExceptionFilter,
1627
- Catch,
1628
- ArgumentsHost,
1629
- HttpException,
1630
- HttpStatus,
1631
- Logger,
1632
- } from '@nestjs/common';
1633
- import { Request, Response } from 'express';
1634
-
1635
- @Catch()
1636
- export class AllExceptionsFilter implements ExceptionFilter {
1637
- private readonly logger = new Logger(AllExceptionsFilter.name);
1638
-
1639
- catch(exception: unknown, host: ArgumentsHost) {
1640
- const ctx = host.switchToHttp();
1641
- const response = ctx.getResponse<Response>();
1642
- const request = ctx.getRequest<Request>();
1643
-
1644
- let status = HttpStatus.INTERNAL_SERVER_ERROR;
1645
- let message: string | string[] = 'Internal server error';
1646
- let errorDetails: any = null;
1647
-
1648
- if (exception instanceof HttpException) {
1649
- status = exception.getStatus();
1650
- const res = exception.getResponse();
1651
- if (typeof res === 'string') {
1652
- message = res;
1653
- } else if (typeof res === 'object' && res !== null) {
1654
- const resObj = res as any;
1655
- message = resObj.message || resObj.error || 'HttpException';
1656
- errorDetails = resObj;
1657
- }
1658
- }
1659
- // Prisma
1660
- else if (
1661
- typeof exception === 'object' &&
1662
- exception &&
1663
- exception.constructor &&
1664
- (
1665
- exception.constructor.name === 'PrismaClientKnownRequestError' ||
1666
- exception.constructor.name === 'PrismaClientValidationError'
1667
- )
1668
- ) {
1669
- status = HttpStatus.BAD_REQUEST;
1670
- message = (exception as any).message || 'Prisma error';
1671
- errorDetails = exception;
1672
- }
1673
- // Mongoose/Mongo
1674
- else if (
1675
- typeof exception === 'object' &&
1676
- exception &&
1677
- 'name' in exception &&
1678
- (
1679
- (exception as any).name === 'MongoError' ||
1680
- (exception as any).name === 'MongooseError'
1681
- )
1682
- ) {
1683
- status = HttpStatus.BAD_REQUEST;
1684
- message = (exception as any).message || 'MongoDB error';
1685
- errorDetails = exception;
1686
- }
1687
- // Sequelize
1688
- else if (
1689
- typeof exception === 'object' &&
1690
- exception &&
1691
- exception.constructor &&
1692
- (
1693
- exception.constructor.name === 'SequelizeDatabaseError' ||
1694
- exception.constructor.name === 'SequelizeValidationError'
1695
- )
1696
- ) {
1697
- status = HttpStatus.BAD_REQUEST;
1698
- message = (exception as any).message || 'Sequelize error';
1699
- errorDetails = exception;
1700
- }
1701
- else if (exception instanceof Error) {
1702
- message = exception.message;
1703
- errorDetails = {
1704
- name: exception.name,
1705
- stack: exception.stack,
1706
- };
1707
- } else {
1708
- message = 'Une erreur inattendue est survenue';
1709
- errorDetails = exception;
1710
- }
1711
-
1712
- this.logger.error(
1713
- \`Exception on \${request.method} \${request.url}\`,
1714
- JSON.stringify({ message, status, errorDetails, exception }),
1715
- );
1716
-
1717
- response.status(status).json({
1718
- statusCode: status,
1719
- timestamp: new Date().toISOString(),
1720
- path: request.url,
1721
- method: request.method,
1722
- message,
1723
- error: errorDetails,
1724
- });
1725
- }
1726
- }
1727
- `.trim();
1728
- }
1729
-
1730
- export async function getPackageManager(flags) {
1731
- const managers = ["npm", "yarn", "pnpm"];
1732
-
1733
- // 1. Vérification du flag
1734
- if (
1735
- flags.packageManager &&
1736
- managers.includes(flags.packageManager.toLowerCase())
1737
- ) {
1738
- return flags.packageManager.toLowerCase();
1739
- }
1740
-
1741
- // 2. Mode interactif
1742
- const answers = await actualInquirer.prompt([
1743
- {
1744
- type: "list",
1745
- name: "packageManager",
1746
- message: "Choose your package manager:",
1747
- choices: managers,
1748
- default: "npm",
1749
- },
1750
- ]);
1751
-
1752
- return answers.packageManager;
1753
- }
1754
-
1755
- export function pluralize(name) {
1756
- if (name.endsWith("y")) {
1757
- return name.slice(0, -1) + "ies"; // Category -> Categories
1758
- } else if (name.endsWith("s")) {
1759
- return name; // Déjà un pluriel
1760
- }
1761
- return name + "s"; // User -> Users
1762
- }
1
+ import { logInfo } from "./loggers/logInfo.js";
2
+ import { logSuccess } from "./loggers/logSuccess.js";
3
+ import { createDirectory, createFile, updateFile } from "./userInput.js";
4
+ import inquirer from "inquirer";
5
+ const actualInquirer = inquirer.default || inquirer;
6
+
7
+ export async function generateEntityFileContent(entity, mode = "full") {
8
+ // console.log("Entity name:", entity.name); // Log de l'entité
9
+
10
+ if (!entity || !entity.name) {
11
+ throw new Error("Nom de l'entité manquant !");
12
+ }
13
+ const entityName = capitalize(entity.name);
14
+ const className = `${entityName}Entity`;
15
+
16
+ const defaultFields = [
17
+ {
18
+ name: "id",
19
+ type: "string",
20
+ comment:
21
+ "L'identifiant unique de l'entité.\n * Utilisé pour retrouver de manière unique un enregistrement dans la base de données.\n *\n * Exemple : '123e4567-e89b-12d3-a456-426614174000'",
22
+ },
23
+ {
24
+ name: "createdAt",
25
+ type: "Date",
26
+ comment:
27
+ "La date de création de l'entité.\n * Définie lors de la création et ne change pas.\n *\n * Exemple : new Date('2022-01-01T10:00:00Z')",
28
+ },
29
+ {
30
+ name: "updatedAt",
31
+ type: "Date",
32
+ comment:
33
+ "La date de dernière mise à jour de l'entité.\n * Mise à jour à chaque modification.\n *\n * Exemple : new Date('2022-02-01T15:00:00Z')",
34
+ },
35
+ ];
36
+ // 1. Types de base autorisés dans une Entité de Domaine
37
+ const DOMAIN_SCALAR_TYPES = [
38
+ "string",
39
+ "number",
40
+ "boolean",
41
+ "date",
42
+ "json",
43
+ "text",
44
+ "uuid",
45
+ "decimal",
46
+ "float",
47
+ "int",
48
+ "role",
49
+ ];
50
+
51
+ // 2. Filtrage : On ne garde que les types scalaires ou les IDs techniques
52
+ const filteredFields = entity.fields.filter((f) => {
53
+ const typeLower = f.type.toLowerCase().replace("[]", "");
54
+ return DOMAIN_SCALAR_TYPES.includes(typeLower) || f.name.endsWith("Id");
55
+ });
56
+
57
+ const isUserEntityWithRole =
58
+ entity.name.toLowerCase() === "user" &&
59
+ entity.fields.some((f) => f.name === "role");
60
+
61
+ const allFields = [...defaultFields, ...filteredFields];
62
+
63
+ const constructorParams = allFields
64
+ .map(
65
+ (f) => `
66
+ private readonly ${f.name}: ${getFormattedType(f)},`,
67
+ )
68
+ .join("");
69
+
70
+ /* const constructorAssignments = allFields
71
+ .map((f) => ` this.${f.name} = ${f.name};`)
72
+ .join("\n"); */
73
+
74
+ const getters = allFields
75
+ .map(
76
+ (f) => `
77
+ get${capitalize(f.name)}(): ${getFormattedType(f)}
78
+ {
79
+ return this.${f.name};
80
+ }`,
81
+ )
82
+ .join("\n");
83
+
84
+ const jsonFields = allFields
85
+ .map((f) => ` ${f.name}: this.${f.name},`)
86
+ .join("\n");
87
+
88
+ let importStatements = "";
89
+ if (isUserEntityWithRole) {
90
+ importStatements +=
91
+ mode == "full"
92
+ ? `import { Role } from 'src/user/domain/enums/role.enum';\n\n`
93
+ : `import { Role } from 'src/common/enums/role.enum'; \n\n`;
94
+ }
95
+
96
+ return `${importStatements}/**
97
+ * ${className} représente l'entité principale de ${entityName} dans le domaine.
98
+ * Elle contient les propriétés de base nécessaires à la gestion des données liées à ${entityName}.
99
+ */
100
+ export class ${className} {
101
+ constructor(${constructorParams}
102
+ ) {}
103
+ ${getters}
104
+
105
+ /**
106
+ * transforme entity data to json
107
+ */
108
+ toJSON() {
109
+ return {
110
+ ${jsonFields}
111
+ };
112
+ }
113
+ }
114
+ `;
115
+ }
116
+
117
+ export async function generateMapper(entity) {
118
+ const entityName = capitalize(entity.name);
119
+
120
+ // 1. Liste des types autorisés (Scalaires / Primitifs)
121
+ const SCALAR_TYPES = [
122
+ "string",
123
+ "text",
124
+ "uuid",
125
+ "json",
126
+ "number",
127
+ "int",
128
+ "float",
129
+ "decimal",
130
+ "boolean",
131
+ "date",
132
+ "role",
133
+ ];
134
+
135
+ // 2. Fonction de filtrage cohérente
136
+ const filterDomainFields = (f) => {
137
+ const typeName = f.type.toLowerCase().replace("[]", "");
138
+ return (
139
+ SCALAR_TYPES.includes(typeName) || f.name.toLowerCase().endsWith("id")
140
+ );
141
+ };
142
+
143
+ // 3. Filtrage des champs pour le constructeur de l'Entity
144
+ // On ne passe au constructeur QUE ce qui est filtré
145
+ const filteredFields = entity.fields.filter(filterDomainFields);
146
+
147
+ const domainArgs = ["data.id"]
148
+ .concat(["data.createdAt", "data.updatedAt"])
149
+ .concat(filteredFields.map((f) => `data.${f.name}`)) // UTILISE LES CHAMPS FILTRÉS ICI
150
+ .join(",\n ");
151
+
152
+ // 4. Filtrage pour la persistence (Base de données)
153
+ const toPersistenceFields = filteredFields
154
+ .map((f) => `${f.name}: dto.${f.name},`)
155
+ .join("\n ");
156
+
157
+ const toUpdateFields = filteredFields
158
+ .map(
159
+ (f) => `if (dto.${f.name} !== undefined) data.${f.name} = dto.${f.name};`,
160
+ )
161
+ .join("\n ");
162
+
163
+ // ... (Logique isUserWithRole inchangée)
164
+
165
+ return `/**
166
+ * PostMapper transforms data between
167
+ * different layers (Persistence <-> Domain <-> DTO).
168
+ *
169
+ * Ensures that the internal database structure
170
+ * never leaks into the API responses.
171
+ */
172
+
173
+ import { Injectable } from '@nestjs/common';
174
+ import { ${entityName}Entity } from 'src/${decapitalize(
175
+ entity.name,
176
+ )}/domain/entities/${decapitalize(entity.name)}.entity';
177
+ import { Create${entityName}Dto, Update${entityName}Dto } from 'src/${decapitalize(
178
+ entity.name,
179
+ )}/application/dtos/${decapitalize(entity.name)}.dto';
180
+
181
+ @Injectable()
182
+ export class ${entityName}Mapper {
183
+
184
+ /**
185
+ * Transforme les données (Prisma/TypeORM) en Entité de Domaine
186
+ */
187
+ toDomain(data: any): ${entityName}Entity {
188
+ return new ${entityName}Entity(
189
+ ${domainArgs}
190
+ );
191
+ }
192
+
193
+ /**
194
+ * Transforme le DTO en objet pour la création en base de données
195
+ */
196
+ toPersistence(dto: Create${entityName}Dto): any {
197
+ return {
198
+ ${toPersistenceFields}
199
+ };
200
+ }
201
+
202
+ /**
203
+ * Prépare l'objet de mise à jour partielle
204
+ */
205
+ toUpdatePersistence(dto: Update${entityName}Dto): any {
206
+ const data: any = {};
207
+ ${toUpdateFields}
208
+ return data;
209
+ }
210
+ }
211
+ `;
212
+ }
213
+
214
+ export async function generateDtox(
215
+ entity,
216
+ useSwagger,
217
+ isAuthDto = false,
218
+ mode = "full",
219
+ ) {
220
+ const entityName = capitalize(entity.name);
221
+ const entityNameLower = entity.name.toLowerCase();
222
+
223
+ /* ===============================
224
+ ENUM IMPORT
225
+ =============================== */
226
+ let enumImport = "";
227
+ if (entityName === "User") {
228
+ enumImport =
229
+ mode === "light"
230
+ ? "\nimport { Role } from 'src/common/enums/role.enum';"
231
+ : "\nimport { Role } from 'src/user/domain/enums/role.enum';";
232
+ }
233
+
234
+ /* ===============================
235
+ SWAGGER HELPERS (PRO)
236
+ =============================== */
237
+ const getFieldDescription = (f) => {
238
+ const name = f.name.toLowerCase();
239
+ if (name.includes("email")) return "The official email address of the user";
240
+ if (name.includes("password"))
241
+ return "Must contain at least 8 characters, one letter and one number";
242
+ if (name.includes("token")) return "Authentication token";
243
+ if (name.includes("id") && name !== "id")
244
+ return `Unique identifier of the related ${name.replace("id", "")}`;
245
+ return `The ${f.name} of the ${entityNameLower}`;
246
+ };
247
+
248
+ const getExampleForField = (f) => {
249
+ const fieldName = f.name.toLowerCase();
250
+ const isArray = f.type.endsWith("[]");
251
+ const cleanType = f.type.toLowerCase().replace("[]", "");
252
+
253
+ const getBaseExample = () => {
254
+ if (fieldName.includes("email")) return "user@example.com";
255
+ if (fieldName.includes("password")) return "SecurePass@2024";
256
+ if (fieldName.includes("token")) return "eyJhbGciOi...";
257
+ if (fieldName.includes("id") && fieldName !== "id")
258
+ return "550e8400-e29b-41d4-a716-446655440000";
259
+
260
+ if (fieldName.includes("title") || fieldName.includes("name"))
261
+ return `${capitalize(entityName)} Example`;
262
+ if (fieldName.includes("content") || fieldName.includes("description"))
263
+ return "This is a detailed example content.";
264
+ if (
265
+ fieldName.includes("url") ||
266
+ fieldName.includes("image") ||
267
+ fieldName.includes("avatar")
268
+ )
269
+ return "https://images.unsplash.com/photo-123456789";
270
+
271
+ if (fieldName.includes("price") || fieldName.includes("amount"))
272
+ return 99.99;
273
+ if (fieldName.includes("quantity") || fieldName.includes("count"))
274
+ return 10;
275
+ if (fieldName.includes("status") || fieldName.includes("type"))
276
+ return "active";
277
+ if (fieldName.includes("date") || fieldName.includes("at"))
278
+ return "2024-12-01T10:30:00Z";
279
+
280
+ switch (cleanType) {
281
+ case "string":
282
+ case "text":
283
+ case "uuid":
284
+ return `${f.name.toLowerCase()}_val`;
285
+ case "number":
286
+ case "int":
287
+ case "decimal":
288
+ return 42;
289
+ case "float":
290
+ return 23.5;
291
+ case "boolean":
292
+ return true;
293
+ case "json":
294
+ return { metadata: "value", version: 1 }; // objet réel
295
+ case "date":
296
+ return new Date().toISOString();
297
+ default:
298
+ return `${f.name.toLowerCase()}_val`;
299
+ }
300
+ };
301
+
302
+ const base = getBaseExample();
303
+ return isArray ? [base, base] : base;
304
+ };
305
+
306
+ /* ===============================
307
+ FIELD GENERATOR (PRO)
308
+ =============================== */
309
+ const generateFieldLine = (f, optional = false) => {
310
+ if (entityName === "User" && f.name.toLowerCase() === "role") return null;
311
+
312
+ const name = f.name;
313
+ const type = f.type.toLowerCase();
314
+ const isArray = type.endsWith("[]");
315
+ const cleanType = type.replace("[]", "");
316
+
317
+ const SCALAR_TYPES = [
318
+ "string",
319
+ "text",
320
+ "uuid",
321
+ "json",
322
+ "number",
323
+ "decimal",
324
+ "int",
325
+ "float",
326
+ "boolean",
327
+ "date",
328
+ "role",
329
+ "enum",
330
+ ];
331
+ if (!SCALAR_TYPES.includes(cleanType)) return null;
332
+
333
+ let validators = [];
334
+ if (optional) validators.push("@IsOptional()");
335
+
336
+ if (name.toLowerCase().includes("email")) {
337
+ validators.push("@IsEmail()");
338
+ } else if (name.toLowerCase().includes("password")) {
339
+ validators.push(
340
+ "@MinLength(8, { message: 'Password is too short (min 8 characters)' })",
341
+ );
342
+ }
343
+
344
+ switch (cleanType) {
345
+ case "string":
346
+ case "text":
347
+ validators.push(isArray ? "@IsString({ each: true })" : "@IsString()");
348
+ if (!isArray) validators.push("@MinLength(2)");
349
+ break;
350
+ case "number":
351
+ case "float":
352
+ validators.push(
353
+ isArray ? "@IsNumber({}, { each: true })" : "@IsNumber()",
354
+ );
355
+ break;
356
+ case "int":
357
+ validators.push(isArray ? "@IsInt({ each: true })" : "@IsInt()");
358
+ break;
359
+ case "boolean":
360
+ validators.push(
361
+ isArray ? "@IsBoolean({ each: true })" : "@IsBoolean()",
362
+ );
363
+ break;
364
+ case "uuid":
365
+ validators.push("@IsUUID()");
366
+ break;
367
+ case "date":
368
+ validators.push("@IsDateString()");
369
+ break;
370
+ }
371
+
372
+ if (isArray) validators.push("@IsArray()");
373
+
374
+ let swaggerDecorator = "";
375
+ if (useSwagger) {
376
+ const decorator = optional ? "@ApiPropertyOptional" : "@ApiProperty";
377
+
378
+ const options = JSON.stringify(
379
+ {
380
+ example: getExampleForField(f),
381
+ description: getFieldDescription(f),
382
+ },
383
+ null,
384
+ 2,
385
+ ).replace(/"([^"]+)":/g, "$1:");
386
+
387
+ swaggerDecorator = `${decorator}(${options})\n `;
388
+ }
389
+
390
+ return `${swaggerDecorator}${validators.join("\n ")}\n ${name}${
391
+ optional ? "?" : ""
392
+ }: ${formatType(f.type)};`;
393
+ };
394
+
395
+ /* ======================================================
396
+ AUTH DTO → UN SEUL DTO
397
+ ====================================================== */
398
+ if (isAuthDto) {
399
+ const authFields = entity.fields
400
+ .map((f) => generateFieldLine(f, false))
401
+ .filter(Boolean)
402
+ .join("\n\n ");
403
+
404
+ return `import {
405
+ IsString, IsInt, IsBoolean, IsEmail, IsArray,
406
+ IsUUID, IsDateString, MinLength, IsOptional
407
+ } from 'class-validator';
408
+
409
+ /**
410
+ * Auth DTO
411
+ */
412
+ export class ${entityName}Dto {
413
+ ${authFields}
414
+ }
415
+ `;
416
+ }
417
+
418
+ /* ======================================================
419
+ CRUD DTOs (Create / Update)
420
+ ====================================================== */
421
+ const createFields = entity.fields
422
+ .map((f) => generateFieldLine(f, false))
423
+ .filter(Boolean)
424
+ .join("\n\n ");
425
+
426
+ const updateFields = entity.fields
427
+ .map((f) => generateFieldLine(f, true))
428
+ .filter(Boolean)
429
+ .join("\n\n ");
430
+
431
+ return `import {
432
+ IsOptional, IsString, IsInt, IsBoolean, IsEmail,
433
+ IsArray, IsUUID, IsDateString, MinLength, IsEnum
434
+ } from 'class-validator';
435
+ ${
436
+ useSwagger
437
+ ? "import { ApiProperty, ApiPropertyOptional, PartialType } from '@nestjs/swagger';"
438
+ : ""
439
+ }
440
+ ${enumImport}
441
+ ${useSwagger ? "" : "import { PartialType } from '@nestjs/mapped-types';"}
442
+
443
+ /**
444
+ * DTO for creating a ${entityName}
445
+ */
446
+ export class Create${entityName}Dto {
447
+ ${createFields}
448
+
449
+ ${
450
+ entityName === "User"
451
+ ? `
452
+ ${useSwagger ? "@ApiHideProperty()\n " : ""}
453
+ @IsOptional()
454
+ @IsEnum(Role)
455
+ role: Role = Role.USER;`
456
+ : ""
457
+ }
458
+ }
459
+
460
+ /**
461
+ * DTO for updating a ${entityName}
462
+ */
463
+ ${
464
+ useSwagger
465
+ ? `export class Update${entityName}Dto extends PartialType(Create${entityName}Dto) {}`
466
+ : `export class Update${entityName}Dto {
467
+ ${updateFields}
468
+
469
+ ${
470
+ entityName === "User"
471
+ ? `@IsEnum(Role)
472
+ @IsOptional()
473
+ role?: Role;`
474
+ : ""
475
+ }
476
+ }`
477
+ }
478
+ `;
479
+ }
480
+
481
+ export async function generateDto(
482
+ entity,
483
+ useSwagger,
484
+ isAuthDto = false,
485
+ mode = "full",
486
+ ) {
487
+ const entityName = capitalize(entity.name);
488
+ const entityNameLower = entity.name.toLowerCase();
489
+
490
+ /* ===============================
491
+ ENUM IMPORT
492
+ =============================== */
493
+ let enumImport = "";
494
+ if (entityName === "User") {
495
+ enumImport =
496
+ mode === "light"
497
+ ? "\nimport { Role } from 'src/common/enums/role.enum';"
498
+ : "\nimport { Role } from 'src/user/domain/enums/role.enum';";
499
+ }
500
+
501
+ /* ===============================
502
+ SWAGGER HELPERS
503
+ =============================== */
504
+ const getFieldDescription = (f) => {
505
+ const name = f.name.toLowerCase();
506
+ if (name.includes("email")) return "The official email address of the user";
507
+ if (name.includes("password"))
508
+ return "Must contain at least 8 characters, one letter and one number";
509
+ if (name.includes("token")) return "Authentication token";
510
+ if (name.includes("id") && name !== "id")
511
+ return `Unique identifier of the related ${name.replace("id", "")}`;
512
+ return `The ${f.name} of the ${entityNameLower}`;
513
+ };
514
+
515
+ const getExampleForField = (f) => {
516
+ const fieldName = f.name.toLowerCase();
517
+ const isArray = f.type.endsWith("[]");
518
+ const cleanType = f.type.toLowerCase().replace("[]", "");
519
+
520
+ const getBaseExample = () => {
521
+ if (fieldName.includes("email")) return "user@example.com";
522
+ if (fieldName.includes("password")) return "SecurePass@2024";
523
+ if (fieldName.includes("token")) return "eyJhbGciOi...";
524
+ if (fieldName.includes("id") && fieldName !== "id")
525
+ return "550e8400-e29b-41d4-a716-446655440000";
526
+ if (fieldName.includes("title") || fieldName.includes("name"))
527
+ return `${capitalize(entityName)} Example`;
528
+ if (cleanType === "boolean") return true;
529
+ if (cleanType === "number" || cleanType === "int") return 42;
530
+ return `${f.name.toLowerCase()}_val`;
531
+ };
532
+
533
+ const base = getBaseExample();
534
+ return isArray ? [base, base] : base;
535
+ };
536
+
537
+ /* ===============================
538
+ FIELD GENERATOR
539
+ =============================== */
540
+ const generateFieldLine = (f, optional = false, forceNoSwagger = false) => {
541
+ if (entityName === "User" && f.name.toLowerCase() === "role") return null;
542
+
543
+ const name = f.name;
544
+ const cleanType = f.type.toLowerCase().replace("[]", "");
545
+ const isArray = f.type.endsWith("[]");
546
+
547
+ const SCALAR_TYPES = [
548
+ "string",
549
+ "text",
550
+ "uuid",
551
+ "json",
552
+ "number",
553
+ "decimal",
554
+ "int",
555
+ "float",
556
+ "boolean",
557
+ "date",
558
+ ];
559
+ if (!SCALAR_TYPES.includes(cleanType)) return null;
560
+
561
+ let validators = [];
562
+ if (optional) validators.push("@IsOptional()");
563
+ if (name.toLowerCase().includes("email")) validators.push("@IsEmail()");
564
+ else if (name.toLowerCase().includes("password"))
565
+ validators.push(
566
+ "@MinLength(8, { message: 'Password is too short (min 8 characters)' })",
567
+ );
568
+
569
+ switch (cleanType) {
570
+ case "string":
571
+ case "text":
572
+ validators.push(isArray ? "@IsString({ each: true })" : "@IsString()");
573
+ if (!isArray && !name.toLowerCase().includes("password"))
574
+ validators.push("@MinLength(2)");
575
+ break;
576
+ case "number":
577
+ case "int":
578
+ case "float":
579
+ validators.push(
580
+ isArray ? "@IsNumber({}, { each: true })" : "@IsNumber()",
581
+ );
582
+ break;
583
+ case "boolean":
584
+ validators.push(
585
+ isArray ? "@IsBoolean({ each: true })" : "@IsBoolean()",
586
+ );
587
+ break;
588
+ case "uuid":
589
+ validators.push("@IsUUID()");
590
+ break;
591
+ case "date":
592
+ validators.push("@IsDateString()");
593
+ break;
594
+ }
595
+ if (isArray) validators.push("@IsArray()");
596
+
597
+ let swaggerDecorator = "";
598
+ if (useSwagger && !forceNoSwagger) {
599
+ const decorator = optional ? "@ApiPropertyOptional" : "@ApiProperty";
600
+ const options = JSON.stringify(
601
+ {
602
+ example: getExampleForField(f),
603
+ description: getFieldDescription(f),
604
+ },
605
+ null,
606
+ 2,
607
+ ).replace(/"([^"]+)":/g, "$1:");
608
+ swaggerDecorator = `${decorator}(${options})\n `;
609
+ }
610
+
611
+ return `${swaggerDecorator}${validators.join("\n ")}\n ${name}${
612
+ optional ? "?" : "!"
613
+ }: ${formatType(f.type)};`;
614
+ };
615
+
616
+ /* ======================================================
617
+ AUTH DTO (Strict & Clean)
618
+ ====================================================== */
619
+ if (isAuthDto) {
620
+ const authFields = entity.fields
621
+ .map((f) => generateFieldLine(f, false))
622
+ .filter(Boolean)
623
+ .join("\n\n ");
624
+
625
+ return `import {
626
+ IsString, IsInt, IsBoolean, IsEmail, IsArray,
627
+ IsUUID, IsDateString, MinLength, IsOptional
628
+ } from 'class-validator';
629
+ ${useSwagger ? "import { ApiProperty } from '@nestjs/swagger';" : ""}
630
+
631
+ /**
632
+ * Auth DTO - Strict contract for authentication
633
+ */
634
+ export class ${entityName}Dto {
635
+ ${authFields}
636
+ }
637
+ `;
638
+ }
639
+
640
+ /* ======================================================
641
+ CRUD DTOs (Create / Update)
642
+ ====================================================== */
643
+ const createFields = entity.fields
644
+ .map((f) => generateFieldLine(f, false))
645
+ .filter(Boolean)
646
+ .join("\n\n ");
647
+
648
+ const updateFields = entity.fields
649
+ .map((f) => generateFieldLine(f, true))
650
+ .filter(Boolean)
651
+ .join("\n\n ");
652
+
653
+ // On prépare les imports Swagger dynamiquement
654
+ let swaggerImports = ["ApiProperty", "ApiPropertyOptional", "PartialType"];
655
+ if (entityName === "User") swaggerImports.push("ApiHideProperty");
656
+
657
+ return `import {
658
+ IsOptional, IsString, IsInt, IsBoolean, IsEmail,
659
+ IsArray, IsUUID, IsDateString, MinLength, IsEnum
660
+ } from 'class-validator';
661
+ ${
662
+ useSwagger
663
+ ? `import { ${swaggerImports.join(", ")} } from '@nestjs/swagger';`
664
+ : ""
665
+ }
666
+ ${enumImport}
667
+
668
+ /**
669
+ * DTO for creating a ${entityName}
670
+ */
671
+ export class Create${entityName}Dto {
672
+ ${createFields}
673
+
674
+ ${
675
+ entityName === "User"
676
+ ? `
677
+ ${useSwagger ? "@ApiHideProperty()" : ""}
678
+ @IsOptional()
679
+ @IsEnum(Role)
680
+ role: Role = Role.USER;`
681
+ : ""
682
+ }
683
+ }
684
+
685
+ /**
686
+ * DTO for updating a ${entityName}
687
+ */
688
+ ${
689
+ useSwagger
690
+ ? `export class Update${entityName}Dto extends PartialType(Create${entityName}Dto) {}`
691
+ : `export class Update${entityName}Dto {
692
+ ${updateFields}
693
+
694
+ ${
695
+ entityName === "User"
696
+ ? `@IsEnum(Role)
697
+ @IsOptional()
698
+ role?: Role;`
699
+ : ""
700
+ }
701
+ }`
702
+ }
703
+ `;
704
+ }
705
+
706
+ export async function generateController(entityName, entityPath, useSwagger) {
707
+ const entityNameLower = decapitalize(entityName);
708
+ const entityNameCapitalized = capitalize(entityName);
709
+ const pluralName = pluralize(entityNameLower);
710
+
711
+ const swaggerImports = useSwagger
712
+ ? `import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';`
713
+ : "";
714
+
715
+ const swaggerClassDecorator = useSwagger
716
+ ? `@ApiTags('${capitalize(pluralName)}')` // Tags en Majuscule et au pluriel
717
+ : "";
718
+
719
+ return `
720
+ import { Controller, Get, Post, Body, Param, Patch, Delete, HttpCode, HttpStatus, Query } from '@nestjs/common';
721
+ ${swaggerImports}
722
+ import { ${entityNameCapitalized}Service } from '${entityPath}/application/services/${entityNameLower}.service';
723
+ import { Create${entityNameCapitalized}Dto, Update${entityNameCapitalized}Dto } from 'src/${entityNameLower}/application/dtos/${entityNameLower}.dto';
724
+
725
+
726
+ /**
727
+ * Controller for ${entityNameCapitalized} management.
728
+ * Handles incoming HTTP requests and delegates logic to the service layer.
729
+ */
730
+ ${swaggerClassDecorator}
731
+ @Controller('${pluralName}')
732
+ export class ${entityNameCapitalized}Controller {
733
+ constructor(private readonly service: ${entityNameCapitalized}Service) {}
734
+
735
+ ${
736
+ entityNameLower !== "user"
737
+ ? `
738
+ @Post()
739
+ @HttpCode(HttpStatus.CREATED)
740
+ ${
741
+ useSwagger
742
+ ? `
743
+ @ApiOperation({ summary: 'Create a new ${entityNameLower}', description: 'Creates a new record for ${entityNameLower} in the database.' })
744
+ @ApiResponse({ status: 201, description: 'The ${entityNameLower} has been successfully created.' })
745
+ @ApiResponse({ status: 400, description: 'Invalid input data.' })`
746
+ : ""
747
+ }
748
+ async create(@Body() dto: Create${entityNameCapitalized}Dto) {
749
+ const result = await this.service.create(dto);
750
+ return {
751
+ message: '${entityNameCapitalized} created successfully',
752
+ data: result
753
+ };
754
+ }
755
+ `
756
+ : ""
757
+ }
758
+
759
+ @Get()
760
+ ${
761
+ useSwagger
762
+ ? `
763
+ @ApiOperation({ summary: 'Get all ${pluralName}', description: 'Retrieves a list of all ${pluralName} available.' })
764
+ @ApiResponse({ status: 200, description: 'Return all ${pluralName}.' })`
765
+ : ""
766
+ }
767
+ async getAll() {
768
+ return await this.service.getAll();
769
+ }
770
+
771
+ @Get(':id')
772
+ ${
773
+ useSwagger
774
+ ? `
775
+ @ApiOperation({ summary: 'Get ${entityNameLower} by ID' })
776
+ @ApiParam({ name: 'id', description: 'The unique identifier of the ${entityNameLower}' })
777
+ @ApiResponse({ status: 200, description: 'The ${entityNameLower} has been found.' })
778
+ @ApiResponse({ status: 404, description: '${entityNameCapitalized} not found.' })`
779
+ : ""
780
+ }
781
+ async getById(@Param('id') id: string) {
782
+ return await this.service.getById(id);
783
+ }
784
+
785
+ @Patch(':id')
786
+ ${
787
+ useSwagger
788
+ ? `
789
+ @ApiOperation({ summary: 'Update an existing ${entityNameLower}' })
790
+ @ApiParam({ name: 'id', description: 'The unique identifier of the ${entityNameLower} to update' })
791
+ @ApiResponse({ status: 200, description: 'The ${entityNameLower} has been successfully updated.' })
792
+ @ApiResponse({ status: 404, description: '${entityNameCapitalized} not found.' })`
793
+ : ""
794
+ }
795
+ async update(
796
+ @Param('id') id: string,
797
+ @Body() dto: Update${entityNameCapitalized}Dto,
798
+ ) {
799
+ await this.service.update(id, dto);
800
+ return { message: '${entityNameCapitalized} updated successfully' };
801
+ }
802
+
803
+ @Delete(':id')
804
+ @HttpCode(HttpStatus.NO_CONTENT)
805
+ ${
806
+ useSwagger
807
+ ? `
808
+ @ApiOperation({ summary: 'Delete a ${entityNameLower}' })
809
+ @ApiParam({ name: 'id', description: 'The unique identifier of the ${entityNameLower} to delete' })
810
+ @ApiResponse({ status: 204, description: 'The ${entityNameLower} has been successfully deleted.' })
811
+ @ApiResponse({ status: 404, description: '${entityNameCapitalized} not found.' })`
812
+ : ""
813
+ }
814
+ async delete(@Param('id') id: string) {
815
+ await this.service.delete(id);
816
+ return { message: '${entityNameCapitalized} deleted successfully' };
817
+ }
818
+ }
819
+ `;
820
+ }
821
+
822
+ export async function generateMiddlewares(orm = "global") {
823
+ logInfo(
824
+ "\u2728 G\u00e9n\u00e9ration des middlewares, interceptors, guards et filters personnalis\u00e9s...",
825
+ );
826
+
827
+ const basePath = "src/common";
828
+ await createDirectory(`${basePath}/middlewares`);
829
+ await createDirectory(`${basePath}/interceptors`);
830
+ await createDirectory(`${basePath}/filters`);
831
+ await createDirectory(`${basePath}/decorators`);
832
+
833
+ // Logger Middleware
834
+ await createFile({
835
+ path: `${basePath}/middlewares/logger.middleware.ts`,
836
+ contente: `import { Request, Response, NextFunction } from 'express';
837
+
838
+ export function LoggerMiddleware(
839
+ req: Request,
840
+ res: Response,
841
+ next: NextFunction,
842
+ ) {
843
+ if (process.env.NODE_ENV === 'production') {
844
+ return next();
845
+ }
846
+
847
+ res.on('finish', () => {
848
+ console.log(
849
+ \`[Request] \${req.method} \${req.originalUrl} - \${res.statusCode}\`,
850
+ );
851
+ });
852
+
853
+ next();
854
+ }
855
+ `,
856
+ });
857
+
858
+ // Error Handling Filter (personnalisé selon l'ORM)
859
+ await createFile({
860
+ path: `${basePath}/filters/all-exceptions.filter.ts`,
861
+ contente: getExceptionFilterContent(orm),
862
+ });
863
+
864
+ // Response Interceptor
865
+ await createFile({
866
+ path: `${basePath}/interceptors/response.interceptor.ts`,
867
+ contente: `import {
868
+ CallHandler,
869
+ ExecutionContext,
870
+ Injectable,
871
+ NestInterceptor,
872
+ } from '@nestjs/common';
873
+ import { Observable } from 'rxjs';
874
+ import { map } from 'rxjs/operators';
875
+
876
+ @Injectable()
877
+ export class ResponseInterceptor<T> implements NestInterceptor<T, any> {
878
+ intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
879
+ const httpContext = context.switchToHttp();
880
+ const response = httpContext.getResponse();
881
+ const request = httpContext.getRequest();
882
+
883
+ return next.handle().pipe(
884
+ map((data) => {
885
+ const message =
886
+ response.locals.message || this.getDefaultMessage(request.method);
887
+
888
+ return {
889
+ statusCode: response.statusCode,
890
+ message,
891
+ path: request.url,
892
+ method: request.method,
893
+ timestamp: new Date().toISOString(),
894
+ data: data ?? null,
895
+ };
896
+ }),
897
+ );
898
+ }
899
+
900
+ private getDefaultMessage(method: string): string {
901
+ switch (method.toUpperCase()) {
902
+ case 'POST':
903
+ return 'Resource created successfully';
904
+ case 'PUT':
905
+ case 'PATCH':
906
+ return 'Resource updated successfully';
907
+ case 'DELETE':
908
+ return 'Resource deleted successfully';
909
+ case 'GET':
910
+ return 'Request processed successfully';
911
+ default:
912
+ return 'Request processed successfully';
913
+ }
914
+ }
915
+ }
916
+ `,
917
+ });
918
+
919
+ // public Decorator
920
+ await createFile({
921
+ path: `${basePath}/decorators/public.decorator.ts`,
922
+ contente: `import { SetMetadata } from '@nestjs/common';
923
+
924
+ export const IS_PUBLIC_KEY = 'isPublic';
925
+ export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
926
+ `,
927
+ });
928
+
929
+ await createFile({
930
+ path: `${basePath}/decorators/current-user.decorator.ts`,
931
+ contente: `import { createParamDecorator, ExecutionContext } from '@nestjs/common';\nexport const CurrentUser = createParamDecorator((data, ctx: ExecutionContext) => ctx.switchToHttp().getRequest().user);`,
932
+ });
933
+
934
+ // Auth role Decorator
935
+ await createFile({
936
+ path: `${basePath}/decorators/role.decorator.ts`,
937
+ contente: `import { SetMetadata } from '@nestjs/common';
938
+
939
+ export const ROLES_KEY = 'roles';
940
+ export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
941
+ `,
942
+ });
943
+
944
+ // Modification de main.ts pour intégrer les middlewares
945
+ // Chemin vers main.ts
946
+ const mainTsPath = "src/main.ts";
947
+
948
+ // 1. Mise à jour des imports
949
+ const importPattern = "import { AppModule } from './app.module';";
950
+ const importReplacer = `import { AppModule } from 'src/app.module'
951
+ import { AllExceptionsFilter } from 'src/common/filters/all-exceptions.filter'
952
+ import { ResponseInterceptor } from 'src/common/interceptors/response.interceptor'
953
+ import { LoggerMiddleware } from 'src/common/middlewares/logger.middleware'
954
+ import { ValidationPipe } from '@nestjs/common';`;
955
+
956
+ // 2. Injection dans le contenu de bootstrap()
957
+ const contentPattern = `const app = await NestFactory.create(AppModule);`;
958
+
959
+ const contentReplacer = `
960
+ const app = await NestFactory.create(AppModule);
961
+
962
+ // 🔒 Global filter pour gérer toutes les exceptions
963
+ app.useGlobalFilters(new AllExceptionsFilter())
964
+
965
+ // 🔁 Global interceptor pour structurer les réponses
966
+ // app.useGlobalInterceptors(new ResponseInterceptor()); //deja appliquer dans le app.module.ts par convention (ne choisir que l'un des deux)
967
+
968
+ // 📋 Middleware pour logger toutes les requêtes entrantes
969
+ app.use(LoggerMiddleware);`;
970
+
971
+ // Appels
972
+ await updateFile({
973
+ path: mainTsPath,
974
+ pattern: importPattern,
975
+ replacement: importReplacer,
976
+ });
977
+
978
+ await updateFile({
979
+ path: mainTsPath,
980
+ pattern: contentPattern,
981
+ replacement: contentReplacer,
982
+ });
983
+
984
+ // modification de AppModule
985
+ const appModulePath = "src/app.module.ts";
986
+
987
+ const addNestModuleInterface = `providers: [`;
988
+ const replaceWithNestModule = `providers: [
989
+ {
990
+ provide: APP_INTERCEPTOR,
991
+ useClass: ResponseInterceptor, // 🔁 Global interceptor pour structurer les réponses
992
+ },`;
993
+
994
+ await updateFile({
995
+ path: appModulePath,
996
+ pattern: addNestModuleInterface,
997
+ replacement: replaceWithNestModule,
998
+ });
999
+
1000
+ logSuccess(
1001
+ "\u2705 Middlewares, filters, interceptors et guards g\u00e9n\u00e9r\u00e9s avec succ\u00e8s !",
1002
+ );
1003
+ }
1004
+
1005
+ export async function generateRepository(entityName, orm) {
1006
+ const entityNameCapitalized = capitalize(entityName);
1007
+ const entityNameLower = entityName.toLowerCase();
1008
+ const entityPath = `src/${entityNameLower}`;
1009
+
1010
+ // Générateur de méthode spécifique (ex: findByEmail pour Auth)
1011
+ const isUser = entityNameLower === "user";
1012
+ const getExtraMethods = (ormType) => {
1013
+ if (!isUser) return "";
1014
+
1015
+ switch (ormType) {
1016
+ case "typeorm":
1017
+ return `
1018
+ async findByEmail(email: string): Promise<${entityNameCapitalized}Entity | null> {
1019
+ const record = await this.repository.findOne({ where: { email } as any });
1020
+ return record ? this.mapper.toDomain(record) : null;
1021
+ }`;
1022
+ case "prisma":
1023
+ return `
1024
+ async findByEmail(email: string): Promise<${entityNameCapitalized}Entity | null> {
1025
+ const record = await this.prisma.${entityNameLower}.findFirst({ where: { email } });
1026
+ return record ? this.mapper.toDomain(record) : null;
1027
+ }`;
1028
+ case "mongoose":
1029
+ return `
1030
+ async findByEmail(email: string): Promise<${entityNameCapitalized}Entity | null> {
1031
+ const record = await this.model.findOne({ email }).exec();
1032
+ return record ? this.mapper.toDomain(record) : null;
1033
+ }`;
1034
+ case "sequelize":
1035
+ return `
1036
+ async findByEmail(email: string): Promise<${entityNameCapitalized}Entity | null> {
1037
+ const record = await this.model.findOne({ where: { email } });
1038
+ return record ? this.mapper.toDomain(record) : null;
1039
+ }`;
1040
+ default:
1041
+ return "";
1042
+ }
1043
+ };
1044
+
1045
+ const extraMethods = getExtraMethods(orm);
1046
+
1047
+ // Gère le switch en fonction de l'ORM choisi
1048
+ switch (orm) {
1049
+ case "typeorm":
1050
+ // Implémentation du repository pour TypeORM
1051
+ await createFile({
1052
+ path: `${entityPath}/infrastructure/repositories/${entityNameLower}.repository.ts`,
1053
+ contente: `/**
1054
+ * PostRepository handles data persistence
1055
+ * for the Post entity.
1056
+ *
1057
+ * This layer abstracts the database engine (Prisma/TypeORM)
1058
+ * and provides a clean interface for data operations.
1059
+ */
1060
+
1061
+ import { Injectable, NotFoundException } from '@nestjs/common';
1062
+ import { Repository } from 'typeorm';
1063
+ import { InjectRepository } from '@nestjs/typeorm';
1064
+ import { ${entityNameCapitalized}Entity } from '${entityPath}/domain/entities/${entityNameLower}.entity';
1065
+ import { ${entityNameCapitalized} } from 'src/entities/${entityNameCapitalized}.entity';
1066
+ import type { I${entityNameCapitalized}Repository } from '${entityPath}/domain/interfaces/${entityNameLower}.repository.interface';
1067
+ import { Create${entityNameCapitalized}Dto, Update${entityNameCapitalized}Dto } from '${entityPath}/application/dtos/${entityNameLower}.dto';
1068
+ import { ${entityNameCapitalized}Mapper } from '${entityPath}/infrastructure/mappers/${entityNameLower}.mapper';
1069
+
1070
+ @Injectable()
1071
+ export class ${entityNameCapitalized}Repository implements I${entityNameCapitalized}Repository {
1072
+ constructor(
1073
+ @InjectRepository(${entityNameCapitalized})
1074
+ private readonly repository: Repository<${entityNameCapitalized}>,
1075
+ private readonly mapper: ${entityNameCapitalized}Mapper,
1076
+ ) {}
1077
+
1078
+ // create
1079
+ async create(data: Create${entityNameCapitalized}Dto): Promise<${entityNameCapitalized}Entity> {
1080
+ const toPersist = this.mapper.toPersistence(data);
1081
+ const created = await this.repository.save(toPersist);
1082
+ return this.mapper.toDomain(created);
1083
+ }
1084
+
1085
+ // find by id
1086
+ async findById(id: string): Promise<${entityNameCapitalized}Entity | null> {
1087
+ const record = await this.repository.findOne({
1088
+ where: { id },
1089
+ });
1090
+
1091
+ if (!record) return null;
1092
+
1093
+ return this.mapper.toDomain(record);
1094
+ }
1095
+
1096
+ ${extraMethods}
1097
+
1098
+ // update
1099
+ async update(id: string, data: Update${entityNameCapitalized}Dto): Promise<${entityNameCapitalized}Entity> {
1100
+ const toUpdate = this.mapper.toUpdatePersistence(data);
1101
+ const updated = await this.repository.save({ ...toUpdate, id });
1102
+ return this.mapper.toDomain(updated);
1103
+ }
1104
+
1105
+ // find all
1106
+ async findAll(): Promise<${entityNameCapitalized}Entity[]> {
1107
+ const records = await this.repository.find();
1108
+ return records.map(record => this.mapper.toDomain(record));
1109
+ }
1110
+
1111
+ // delete
1112
+ async delete(id: string): Promise<void> {
1113
+ await this.repository.delete(id);
1114
+ }
1115
+ }
1116
+ `,
1117
+ });
1118
+ break;
1119
+
1120
+ case "prisma":
1121
+ // Implémentation pour Prisma
1122
+ await createFile({
1123
+ path: `${entityPath}/infrastructure/repositories/${entityNameLower}.repository.ts`,
1124
+ contente: `import { Injectable, NotFoundException } from '@nestjs/common';
1125
+ import { PrismaService } from 'src/prisma/prisma.service';
1126
+ import { Create${entityNameCapitalized}Dto, Update${entityNameCapitalized}Dto } from '${entityPath}/application/dtos/${entityNameLower}.dto';
1127
+ import { I${entityNameCapitalized}Repository } from '${entityPath}/domain/interfaces/${entityNameLower}.repository.interface';
1128
+ import { ${entityNameCapitalized}Entity } from '${entityPath}/domain/entities/${entityNameLower}.entity';
1129
+ import { ${entityNameCapitalized}Mapper } from '${entityPath}/infrastructure/mappers/${entityNameLower}.mapper';
1130
+
1131
+ @Injectable()
1132
+ export class ${entityNameCapitalized}Repository implements I${entityNameCapitalized}Repository {
1133
+ constructor(
1134
+ private readonly prisma: PrismaService,
1135
+ private readonly mapper: ${entityNameCapitalized}Mapper,
1136
+ ) {}
1137
+
1138
+ // create
1139
+ async create(data: Create${entityNameCapitalized}Dto): Promise<${entityNameCapitalized}Entity> {
1140
+ const toPersist = this.mapper.toPersistence(data);
1141
+ const created = await this.prisma.${entityNameLower}.create({ data: toPersist });
1142
+ return this.mapper.toDomain(created);
1143
+ }
1144
+
1145
+ // find by id
1146
+ async findById(id: string): Promise<${entityNameCapitalized}Entity | null> {
1147
+ const record = await this.prisma.${entityNameLower}.findUnique({
1148
+ where: { id },
1149
+ });
1150
+
1151
+ if (!record) return null;
1152
+
1153
+ return this.mapper.toDomain(record);
1154
+ }
1155
+
1156
+ ${extraMethods}
1157
+
1158
+ // update
1159
+ async update(id: string, data: Update${entityNameCapitalized}Dto): Promise<${entityNameCapitalized}Entity> {
1160
+ const toUpdate = this.mapper.toUpdatePersistence(data);
1161
+ const updated = await this.prisma.${entityNameLower}.update({
1162
+ where: { id },
1163
+ data: toUpdate,
1164
+ });
1165
+
1166
+ return this.mapper.toDomain(updated);
1167
+ }
1168
+
1169
+ // find all
1170
+ async findAll(): Promise<${entityNameCapitalized}Entity[]> {
1171
+ const records = await this.prisma.${entityNameLower}.findMany();
1172
+ return records.map(record => this.mapper.toDomain(record));
1173
+ }
1174
+
1175
+ // delete
1176
+ async delete(id: string): Promise<void> {
1177
+ await this.prisma.${entityNameLower}.delete({
1178
+ where: { id },
1179
+ });
1180
+ }
1181
+ }
1182
+ `,
1183
+ });
1184
+ break;
1185
+
1186
+ case "mongoose":
1187
+ // Implémentation pour MongoDB avec Mongoose
1188
+ await createFile({
1189
+ path: `${entityPath}/infrastructure/repositories/${entityNameLower}.repository.ts`,
1190
+ contente: `import { Injectable, NotFoundException } from '@nestjs/common';
1191
+ import { InjectModel } from '@nestjs/mongoose';
1192
+ import { Model } from 'mongoose';
1193
+ import { ${entityNameCapitalized}Entity } from '${entityPath}/domain/entities/${entityNameLower}.entity';
1194
+ import { Create${entityNameCapitalized}Dto, Update${entityNameCapitalized}Dto } from '${entityPath}/application/dtos/${entityNameLower}.dto';
1195
+ import { I${entityNameCapitalized}Repository } from '${entityPath}/domain/interfaces/${entityNameLower}.repository.interface';
1196
+ import { ${entityNameCapitalized} } from '${entityPath}/infrastructure/persistence/mongoose/${entityNameLower}.schema';
1197
+ import { ${entityNameCapitalized}Mapper } from '${entityPath}/infrastructure/mappers/${entityNameLower}.mapper';
1198
+
1199
+ @Injectable()
1200
+ export class ${entityNameCapitalized}Repository implements I${entityNameCapitalized}Repository {
1201
+ constructor(
1202
+ @InjectModel(${entityNameCapitalized}.name)
1203
+ private readonly model: Model<${entityNameCapitalized}>,
1204
+ private readonly mapper: ${entityNameCapitalized}Mapper,
1205
+ ) {}
1206
+
1207
+ // create
1208
+ async create(data: Create${entityNameCapitalized}Dto): Promise<${entityNameCapitalized}Entity> {
1209
+ const toPersist = this.mapper.toPersistence(data);
1210
+ const created = await this.model.create(toPersist);
1211
+ return this.mapper.toDomain(created);
1212
+ }
1213
+
1214
+ // find by id
1215
+ async findById(id: string): Promise<${entityNameCapitalized}Entity | null> {
1216
+ const record = await this.model.findById(id).exec();
1217
+
1218
+ if (!record) return null;
1219
+
1220
+ return this.mapper.toDomain(record);
1221
+ }
1222
+
1223
+ ${extraMethods}
1224
+
1225
+ // update
1226
+ async update(id: string, data: Update${entityNameCapitalized}Dto): Promise<${entityNameCapitalized}Entity> {
1227
+ const toUpdate = this.mapper.toUpdatePersistence(data);
1228
+ const updated = await this.model.findByIdAndUpdate(id, toUpdate, { new: true }).exec();
1229
+ return this.mapper.toDomain(updated);
1230
+ }
1231
+
1232
+ // find all
1233
+ async findAll(): Promise<${entityNameCapitalized}Entity[]> {
1234
+ const records = await this.model.find().exec();
1235
+ return records.map(record => this.mapper.toDomain(record));
1236
+ }
1237
+
1238
+ // delete
1239
+ async delete(id: string): Promise<void> {
1240
+ await this.model.findByIdAndDelete(id).exec();
1241
+ }
1242
+ }
1243
+ `,
1244
+ });
1245
+ break;
1246
+
1247
+ case "sequelize":
1248
+ // Implémentation pour Sequelize
1249
+ await createFile({
1250
+ path: `${entityPath}/infrastructure/repositories/${entityNameLower}.repository.ts`,
1251
+ contente: `import { Injectable, NotFoundException } from '@nestjs/common';
1252
+ import { InjectModel } from '@nestjs/sequelize';
1253
+ import { Model } from 'sequelize-typescript';
1254
+ import { Create${entityNameCapitalized}Dto, Update${entityNameCapitalized}Dto } from '${entityPath}/application/dtos/${entityNameLower}.dto';
1255
+ import { I${entityNameCapitalized}Repository } from '${entityPath}/domain/interfaces/${entityNameLower}.repository.interface';
1256
+ import { ${entityNameCapitalized}Entity } from '${entityPath}/domain/entities/${entityNameLower}.entity';
1257
+ import { ${entityNameCapitalized}Mapper } from '${entityPath}/infrastructure/mappers/${entityNameLower}.mapper';
1258
+
1259
+ @Injectable()
1260
+ export class ${entityNameCapitalized}Repository implements I${entityNameCapitalized}Repository {
1261
+ constructor(
1262
+ @InjectModel(${entityNameCapitalized}Entity)
1263
+ private readonly model: Model<${entityNameCapitalized}Entity>,
1264
+ private readonly mapper: ${entityNameCapitalized}Mapper,
1265
+ ) {}
1266
+
1267
+ // create
1268
+ async create(data: Create${entityNameCapitalized}Dto): Promise<${entityNameCapitalized}Entity> {
1269
+ const toPersist = this.mapper.toPersistence(data);
1270
+ const created = await this.model.create(toPersist);
1271
+ return this.mapper.toDomain(created);
1272
+ }
1273
+
1274
+ // find by id
1275
+ async findById(id: string): Promise<${entityNameCapitalized}Entity> {
1276
+ const record = await this.model.findByPk(id);
1277
+
1278
+ if (!record) {
1279
+ throw new NotFoundException(\`${entityNameCapitalized}Entity with id \${id} not found\`);
1280
+ }
1281
+
1282
+ return this.mapper.toDomain(record);
1283
+ }
1284
+
1285
+ ${extraMethods}
1286
+
1287
+ // update
1288
+ async update(id: string, data: Update${entityNameCapitalized}Dto): Promise<${entityNameCapitalized}Entity> {
1289
+ const toUpdate = this.mapper.toUpdatePersistence(data);
1290
+ const updated = await this.model.update(toUpdate, { where: { id } });
1291
+ return this.mapper.toDomain(updated);
1292
+ }
1293
+
1294
+ // find all
1295
+ async findAll(): Promise<${entityNameCapitalized}Entity[]> {
1296
+ const records = await this.model.find();
1297
+ return records.map(record => this.mapper.toDomain(record));
1298
+ }
1299
+
1300
+ // delete
1301
+ async delete(id: string): Promise<void> {
1302
+ await this.model.destroy({ where: { id } });
1303
+ }
1304
+ }
1305
+ `,
1306
+ });
1307
+ break;
1308
+
1309
+ default:
1310
+ console.error("Unsupported ORM: " + orm);
1311
+ break;
1312
+ }
1313
+ }
1314
+
1315
+ export async function generateMongooseSchemaFileContentx(
1316
+ entity,
1317
+ entitiesData,
1318
+ mode = "full",
1319
+ ) {
1320
+ const entityName = capitalize(entity.name);
1321
+ const entityNameLower = entity.name.toLowerCase();
1322
+ const isFull = mode === "full";
1323
+
1324
+ let extraImports = "";
1325
+
1326
+ // 1. GESTION DES IMPORTS DES RELATIONS (Inclus Session dans Auth)
1327
+ const relatedEntities = entitiesData.relations
1328
+ .filter((rel) => rel.from === entityNameLower || rel.to === entityNameLower)
1329
+ .map((rel) => (rel.from === entityNameLower ? rel.to : rel.from));
1330
+
1331
+ // On retire les doublons et l'entité elle-même
1332
+ const uniqueRelated = [...new Set(relatedEntities)].filter(
1333
+ (e) => e !== entityNameLower,
1334
+ );
1335
+
1336
+ uniqueRelated.forEach((target) => {
1337
+ const targetCap = capitalize(target);
1338
+ // EXCEPTION : Si c'est session, le dossier est auth
1339
+ const moduleName = target === "session" ? "auth" : target;
1340
+
1341
+ const importPath =
1342
+ mode === "full"
1343
+ ? `../../../../${moduleName}/infrastructure/persistence/mongoose/${target}.schema`
1344
+ : `../../${moduleName}/entities/${target}.schema`;
1345
+
1346
+ extraImports += `import { ${targetCap} } from '${importPath}';\n`;
1347
+ });
1348
+
1349
+ // GESTION DU ROLE (USER)
1350
+ if (entityNameLower === "user") {
1351
+ const rolePath = isFull
1352
+ ? "../../../domain/enums/role.enum"
1353
+ : "../../common/enums/role.enum";
1354
+ extraImports += `import { Role } from '${rolePath}';\n`;
1355
+ }
1356
+
1357
+ // --- RESTE DE TON CODE (directFields & dynamicRelations) ---
1358
+
1359
+ /* const directFields = entity.fields.map((f) => {
1360
+
1361
+ const fieldName = f.name;
1362
+ const fieldNameLow = fieldName.toLowerCase();
1363
+
1364
+ if (entityNameLower === "user" && fieldName === "role") {
1365
+ return ` @Prop({ type: String, enum: Role, default: Role.USER })\n role: Role;`;
1366
+ }
1367
+
1368
+ if (fieldNameLow.endsWith("id")) {
1369
+ const targetEntity = fieldNameLow.replace("id", "");
1370
+ const hasRelation = entitiesData.relations.some(
1371
+ (r) => r.from === targetEntity || r.to === targetEntity
1372
+ );
1373
+
1374
+ if (hasRelation) {
1375
+ const refModel = capitalize(targetEntity);
1376
+ return ` @Prop({ type: mongoose.Schema.Types.ObjectId, ref: ${refModel}.name, required: true })\n ${fieldName}: mongoose.Types.ObjectId;`;
1377
+ }
1378
+ }
1379
+
1380
+ return ` @Prop({ required: true })\n ${fieldName}: ${
1381
+ f.type.toLowerCase() === "date" ? "Date" : f.type.toLowerCase()
1382
+ };`;
1383
+ }); */
1384
+
1385
+ const directFields = entity.fields
1386
+ .filter((f) => {
1387
+ // ❌ ignore les champs relationnels objets (post, user, etc.)
1388
+ if (isRelationObjectField(f, entitiesData)) return false;
1389
+
1390
+ // ❌ ignore aussi les champs non scalaires
1391
+ const scalarTypes = [
1392
+ "string",
1393
+ "text",
1394
+ "number",
1395
+ "int",
1396
+ "float",
1397
+ "boolean",
1398
+ "date",
1399
+ "uuid",
1400
+ "json",
1401
+ ];
1402
+
1403
+ return scalarTypes.includes(f.type.toLowerCase());
1404
+ })
1405
+ .map((f) => {
1406
+ const fieldName = f.name;
1407
+ const fieldNameLow = fieldName.toLowerCase();
1408
+
1409
+ if (entityNameLower === "user" && fieldName === "role") {
1410
+ return ` @Prop({ type: String, enum: Role, default: Role.USER })\n role: Role;`;
1411
+ }
1412
+
1413
+ if (fieldNameLow.endsWith("id")) {
1414
+ const targetEntity = fieldNameLow.replace("id", "");
1415
+ const hasRelation = entitiesData.relations.some(
1416
+ (r) => r.from === targetEntity || r.to === targetEntity,
1417
+ );
1418
+
1419
+ if (hasRelation) {
1420
+ const refModel = capitalize(targetEntity);
1421
+ return ` @Prop({ type: mongoose.Schema.Types.ObjectId, ref: ${refModel}.name, required: true })\n ${fieldName}: mongoose.Types.ObjectId;`;
1422
+ }
1423
+ }
1424
+
1425
+ return ` @Prop({ required: true })\n ${fieldName}: ${
1426
+ f.type.toLowerCase() === "date" ? "Date" : f.type.toLowerCase()
1427
+ };`;
1428
+ });
1429
+
1430
+ const dynamicRelations = entitiesData.relations
1431
+ .map((rel) => {
1432
+ const isFrom = rel.from === entityNameLower;
1433
+ const isTo = rel.to === entityNameLower;
1434
+ if (!isFrom && !isTo) return null;
1435
+
1436
+ const otherEntity = isFrom ? rel.to : rel.from;
1437
+ const otherCap = capitalize(otherEntity);
1438
+
1439
+ switch (rel.type) {
1440
+ // ==========================================
1441
+ // CASE 1-n
1442
+ // ==========================================
1443
+ case "1-n":
1444
+ if (isTo) {
1445
+ return ` @Prop({ type: mongoose.Schema.Types.ObjectId, ref: ${otherCap}.name, required: true })\n ${otherEntity}Id: mongoose.Types.ObjectId;`;
1446
+ }
1447
+ return null;
1448
+
1449
+ // ==========================================
1450
+ // CASE n-1
1451
+ // ==========================================
1452
+ case "n-1":
1453
+ if (isFrom) {
1454
+ return ` @Prop({ type: mongoose.Schema.Types.ObjectId, ref: ${otherCap}.name, required: true })\n ${otherEntity}Id: mongoose.Types.ObjectId;`;
1455
+ }
1456
+ return null;
1457
+
1458
+ // ==========================================
1459
+ // CASE 1-1
1460
+ // ==========================================
1461
+ case "1-1":
1462
+ if (isFrom) {
1463
+ return ` @Prop({ type: mongoose.Schema.Types.ObjectId, ref: ${otherCap}.name, unique: true })\n ${otherEntity}Id: mongoose.Types.ObjectId;`;
1464
+ }
1465
+ return null;
1466
+
1467
+ // ==========================================
1468
+ // CASE n-n
1469
+ // ==========================================
1470
+ case "n-n":
1471
+ return ` @Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: ${otherCap}.name }] })\n ${otherEntity}Ids: mongoose.Types.ObjectId[];`;
1472
+
1473
+ default:
1474
+ return null;
1475
+ }
1476
+ })
1477
+ .filter(Boolean);
1478
+
1479
+ const allFields = [...new Set([...directFields, ...dynamicRelations])].join(
1480
+ "\n\n",
1481
+ );
1482
+
1483
+ return `
1484
+ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
1485
+ import * as mongoose from 'mongoose';
1486
+ import { Document } from 'mongoose';
1487
+ ${extraImports}
1488
+
1489
+ export type ${entityName}Document = ${entityName} & Document;
1490
+
1491
+ @Schema({ timestamps: true })
1492
+ export class ${entityName} {
1493
+ ${allFields}
1494
+ }
1495
+
1496
+ export const ${entityName}Schema = SchemaFactory.createForClass(${entityName});
1497
+ `.trim();
1498
+ }
1499
+
1500
+ export async function generateMongooseSchemaFileContent(
1501
+ entity,
1502
+ entitiesData,
1503
+ mode = "full",
1504
+ ) {
1505
+ const entityName = capitalize(entity.name);
1506
+ const entityNameLower = entity.name.toLowerCase();
1507
+ const isFull = mode === "full";
1508
+
1509
+ // --- 1. IMPORTS DYNAMIQUES ---
1510
+ const relatedEntities = entitiesData.relations
1511
+ .filter((rel) => rel.from === entityNameLower || rel.to === entityNameLower)
1512
+ .map((rel) => (rel.from === entityNameLower ? rel.to : rel.from));
1513
+
1514
+ const uniqueRelated = [...new Set(relatedEntities)].filter(
1515
+ (e) => e !== entityNameLower,
1516
+ );
1517
+
1518
+ let extraImports = "";
1519
+ /* uniqueRelated.forEach((target) => {
1520
+ const targetCap = capitalize(target);
1521
+ const moduleName = target === "session" ? "auth" : target;
1522
+ const importPath = isFull
1523
+ ? `../../../../${moduleName}/infrastructure/persistence/mongoose/${target}.schema`
1524
+ : `../../${moduleName}/entities/${target}.schema`;
1525
+
1526
+ extraImports += `import { ${targetCap} } from '${importPath}';\n`;
1527
+ }); */
1528
+ uniqueRelated.forEach((target) => {
1529
+ const targetCap = capitalize(target);
1530
+ const moduleName = target === "session" ? "auth" : target;
1531
+
1532
+ let importPath = "";
1533
+
1534
+ if (isFull) {
1535
+ // Mode Clean Architecture (Full)
1536
+ importPath = `../../../../${moduleName}/infrastructure/persistence/mongoose/${target}.schema`;
1537
+ } else {
1538
+ // Mode Light
1539
+ // Si pour Auth tu as fait une exception et mis le schéma dans un sous-dossier :
1540
+ if (target === "session") {
1541
+ importPath = `../../auth/persistence/${target}.schema`;
1542
+ } else {
1543
+ // Pour les autres entités en mode Light (ex: src/post/entities/post.schema.ts)
1544
+ importPath = `../../${moduleName}/entities/${target}.schema`;
1545
+ }
1546
+ }
1547
+
1548
+ extraImports += `import { ${targetCap} } from '${importPath}';\n`;
1549
+ });
1550
+
1551
+ if (entityNameLower === "user") {
1552
+ const rolePath = isFull
1553
+ ? "../../../domain/enums/role.enum"
1554
+ : "../../common/enums/role.enum";
1555
+ extraImports += `import { Role } from '${rolePath}';\n`;
1556
+ }
1557
+
1558
+ // --- 2. LOGIQUE DES RELATIONS DYNAMIQUES (Prioritaire) ---
1559
+ const dynamicRelations = entitiesData.relations
1560
+ .map((rel) => {
1561
+ const isFrom = rel.from === entityNameLower;
1562
+ const isTo = rel.to === entityNameLower;
1563
+ if (!isFrom && !isTo) return null;
1564
+
1565
+ const otherEntity = isFrom ? rel.to : rel.from;
1566
+ const otherCap = capitalize(otherEntity);
1567
+
1568
+ switch (rel.type) {
1569
+ case "1-n":
1570
+ return isTo
1571
+ ? ` @Prop({ type: mongoose.Schema.Types.ObjectId, ref: ${otherCap}.name, required: true })\n ${otherEntity}Id: mongoose.Types.ObjectId;`
1572
+ : null;
1573
+ case "n-1":
1574
+ return isFrom
1575
+ ? ` @Prop({ type: mongoose.Schema.Types.ObjectId, ref: ${otherCap}.name, required: true })\n ${otherEntity}Id: mongoose.Types.ObjectId;`
1576
+ : null;
1577
+ case "1-1":
1578
+ return isFrom
1579
+ ? ` @Prop({ type: mongoose.Schema.Types.ObjectId, ref: ${otherCap}.name, unique: true })\n ${otherEntity}Id: mongoose.Types.ObjectId;`
1580
+ : null;
1581
+ case "n-n":
1582
+ return ` @Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: ${otherCap}.name }] })\n ${otherEntity}Ids: mongoose.Types.ObjectId[];`;
1583
+ default:
1584
+ return null;
1585
+ }
1586
+ })
1587
+ .filter(Boolean);
1588
+
1589
+ // --- 3. FILTRAGE DES CHAMPS DIRECTS (Scalaires uniquement) ---
1590
+ const scalarTypes = [
1591
+ "string",
1592
+ "text",
1593
+ "number",
1594
+ "int",
1595
+ "float",
1596
+ "boolean",
1597
+ "date",
1598
+ "uuid",
1599
+ "json",
1600
+ ];
1601
+
1602
+ const directFields = entity.fields
1603
+ .filter((f) => {
1604
+ const nameLow = f.name.toLowerCase();
1605
+ // On dégage tout ce qui ressemble à une relation pour éviter les doublons avec dynamicRelations
1606
+ const isRelId = uniqueRelated.some(
1607
+ (rel) => nameLow === rel + "id" || nameLow === rel,
1608
+ );
1609
+ return scalarTypes.includes(f.type.toLowerCase()) && !isRelId;
1610
+ })
1611
+ .map((f) => {
1612
+ const fieldName = f.name;
1613
+ const rawType = f.type.toLowerCase();
1614
+
1615
+ // 1. Gestion spécifique du Role
1616
+ if (entityNameLower === "user" && fieldName === "role") {
1617
+ return ` @Prop({ type: String, enum: Role, default: Role.USER })\n role: Role;`;
1618
+ }
1619
+
1620
+ // 2. Mapping des types CLI -> TypeScript/Mongoose
1621
+ let tsType = "string"; // Valeur par défaut
1622
+ let propOptions = "required: true";
1623
+
1624
+ switch (rawType) {
1625
+ case "text":
1626
+ case "uuid":
1627
+ case "string":
1628
+ tsType = "string";
1629
+ break;
1630
+ case "int":
1631
+ case "number":
1632
+ case "float":
1633
+ case "decimal":
1634
+ tsType = "number";
1635
+ break;
1636
+ case "boolean":
1637
+ tsType = "boolean";
1638
+ break;
1639
+ case "date":
1640
+ tsType = "Date";
1641
+ break;
1642
+ case "json":
1643
+ tsType = "Record<string, any>"; // Ou 'any'
1644
+ propOptions = "type: Object, required: true";
1645
+ break;
1646
+ default:
1647
+ tsType = "any";
1648
+ }
1649
+
1650
+ return ` @Prop({ ${propOptions} })\n ${fieldName}: ${tsType};`;
1651
+ });
1652
+
1653
+ const allFields = [...new Set([...directFields, ...dynamicRelations])].join(
1654
+ "\n\n",
1655
+ );
1656
+
1657
+ return `import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
1658
+ import * as mongoose from 'mongoose';
1659
+ import { Document } from 'mongoose';
1660
+ ${extraImports}
1661
+
1662
+ export type ${entityName}Document = ${entityName} & Document;
1663
+
1664
+ @Schema({ timestamps: true })
1665
+ export class ${entityName} {
1666
+ ${allFields}
1667
+ }
1668
+
1669
+ export const ${entityName}Schema = SchemaFactory.createForClass(${entityName});`.trim();
1670
+ }
1671
+
1672
+ function isRelationObjectField(field, entitiesData) {
1673
+ const typeLower = field.type.toLowerCase();
1674
+
1675
+ return entitiesData.entities?.some((e) => e.name.toLowerCase() === typeLower);
1676
+ }
1677
+
1678
+ function capitalize(str) {
1679
+ return str.charAt(0).toUpperCase() + str.slice(1);
1680
+ }
1681
+
1682
+ function decapitalize(str) {
1683
+ return str.charAt(0).toLowerCase() + str.slice(1);
1684
+ }
1685
+
1686
+ /**
1687
+ * Mappe le type de champ interne (sélectionné par l'utilisateur) au type
1688
+ * TypeScript ou DTO/Classe/Enum correspondant pour la génération de code.
1689
+ *
1690
+ * @param {string} type - Le type de base sélectionné (string, number, array, DTO, Enum, etc.)
1691
+ * @returns {string} Le type formaté pour la génération de code (ex: 'string', 'number', 'Article[]', 'UserRoleEnum')
1692
+ */
1693
+ function formatType(type) {
1694
+ // 1. Gestion des types simples
1695
+ switch (type.toLowerCase()) {
1696
+ case "number":
1697
+ case "decimal":
1698
+ return "number";
1699
+
1700
+ case "boolean":
1701
+ return "boolean";
1702
+
1703
+ case "date":
1704
+ return "Date";
1705
+
1706
+ case "json":
1707
+ return "Record<string, any>";
1708
+
1709
+ case "string":
1710
+ case "text":
1711
+ case "uuid":
1712
+ return "string";
1713
+ }
1714
+
1715
+ //2. Vérifie si c'est un Array
1716
+ // Gère les Arrays de scalaires (ex: 'string[]')
1717
+ if (type.endsWith("[]")) {
1718
+ // Applique le formatage au type interne et ajoute '[]'
1719
+ return `${formatType(type.slice(0, -2))}[]`;
1720
+ }
1721
+
1722
+ if (type.match(/^[A-Za-z][A-Za-z0-9_]*$/)) {
1723
+ return type;
1724
+ }
1725
+
1726
+ return "any";
1727
+ }
1728
+
1729
+ /**
1730
+ * Gère les cas spéciaux (comme le rôle utilisateur) avant l'application du formatage.
1731
+ * * @param {object} field - L'objet champ avec { name, type }
1732
+ * @returns {string} Le type final formaté.
1733
+ */
1734
+ function getFormattedType(field) {
1735
+ // Cas spécial pour la propriété 'role'
1736
+ if (field.name === "role" && field.type.toLowerCase().startsWith("string")) {
1737
+ return "Role";
1738
+ }
1739
+
1740
+ return formatType(field.type);
1741
+ }
1742
+
1743
+ // Génère le contenu du filtre d'exception selon l'ORM
1744
+ function getExceptionFilterContent(orm) {
1745
+ if (orm === "prisma") {
1746
+ return `
1747
+ import {
1748
+ ExceptionFilter,
1749
+ Catch,
1750
+ ArgumentsHost,
1751
+ HttpException,
1752
+ HttpStatus,
1753
+ Logger,
1754
+ } from '@nestjs/common';
1755
+ import { Request, Response } from 'express';
1756
+
1757
+ @Catch()
1758
+ export class AllExceptionsFilter implements ExceptionFilter {
1759
+ private readonly logger = new Logger(AllExceptionsFilter.name);
1760
+
1761
+ catch(exception: unknown, host: ArgumentsHost) {
1762
+ const ctx = host.switchToHttp();
1763
+ const response = ctx.getResponse<Response>();
1764
+ const request = ctx.getRequest<Request>();
1765
+
1766
+ let status = HttpStatus.INTERNAL_SERVER_ERROR;
1767
+ let message: string | string[] = 'Internal server error';
1768
+ let errorDetails: any = null;
1769
+
1770
+ if (exception instanceof HttpException) {
1771
+ status = exception.getStatus();
1772
+ const res = exception.getResponse();
1773
+ if (typeof res === 'string') {
1774
+ message = res;
1775
+ } else if (typeof res === 'object' && res !== null) {
1776
+ const resObj = res as any;
1777
+ message = resObj.message || resObj.error || 'HttpException';
1778
+ errorDetails = resObj;
1779
+ }
1780
+ } else if (
1781
+ typeof exception === 'object' &&
1782
+ exception &&
1783
+ exception.constructor &&
1784
+ (
1785
+ exception.constructor.name === 'PrismaClientKnownRequestError' ||
1786
+ exception.constructor.name === 'PrismaClientValidationError'
1787
+ )
1788
+ ) {
1789
+ status = HttpStatus.BAD_REQUEST;
1790
+ message = (exception as any).message || 'Prisma error';
1791
+ errorDetails = exception;
1792
+ } else if (exception instanceof Error) {
1793
+ message = exception.message;
1794
+ errorDetails = {
1795
+ name: exception.name,
1796
+ stack: exception.stack,
1797
+ };
1798
+ } else {
1799
+ message = 'Une erreur inattendue est survenue';
1800
+ errorDetails = exception;
1801
+ }
1802
+
1803
+ if (process.env.NODE_ENV !== 'production') {
1804
+ this.logger.error(
1805
+ \`Exception on \${request.method} \${request.url}\`,
1806
+ JSON.stringify({ message, status, errorDetails, exception }),
1807
+ );
1808
+ }
1809
+
1810
+ response.status(status).json({
1811
+ statusCode: status,
1812
+ timestamp: new Date().toISOString(),
1813
+ path: request.url,
1814
+ method: request.method,
1815
+ message,
1816
+ error: errorDetails,
1817
+ });
1818
+ }
1819
+ }
1820
+ `.trim();
1821
+ }
1822
+
1823
+ if (orm === "mongoose") {
1824
+ return `
1825
+ import {
1826
+ ExceptionFilter,
1827
+ Catch,
1828
+ ArgumentsHost,
1829
+ HttpException,
1830
+ HttpStatus,
1831
+ Logger,
1832
+ } from '@nestjs/common';
1833
+ import { Request, Response } from 'express';
1834
+
1835
+ @Catch()
1836
+ export class AllExceptionsFilter implements ExceptionFilter {
1837
+ private readonly logger = new Logger(AllExceptionsFilter.name);
1838
+
1839
+ catch(exception: unknown, host: ArgumentsHost) {
1840
+ const ctx = host.switchToHttp();
1841
+ const response = ctx.getResponse<Response>();
1842
+ const request = ctx.getRequest<Request>();
1843
+
1844
+ let status = HttpStatus.INTERNAL_SERVER_ERROR;
1845
+ let message: string | string[] = 'Internal server error';
1846
+ let errorDetails: any = null;
1847
+
1848
+ if (exception instanceof HttpException) {
1849
+ status = exception.getStatus();
1850
+ const res = exception.getResponse();
1851
+ if (typeof res === 'string') {
1852
+ message = res;
1853
+ } else if (typeof res === 'object' && res !== null) {
1854
+ const resObj = res as any;
1855
+ message = resObj.message || resObj.error || 'HttpException';
1856
+ errorDetails = resObj;
1857
+ }
1858
+ } else if (
1859
+ typeof exception === 'object' &&
1860
+ exception &&
1861
+ 'name' in exception &&
1862
+ (
1863
+ (exception as any).name === 'MongoError' ||
1864
+ (exception as any).name === 'MongooseError'
1865
+ )
1866
+ ) {
1867
+ status = HttpStatus.BAD_REQUEST;
1868
+ message = (exception as any).message || 'MongoDB error';
1869
+ errorDetails = exception;
1870
+ } else if (exception instanceof Error) {
1871
+ message = exception.message;
1872
+ errorDetails = {
1873
+ name: exception.name,
1874
+ stack: exception.stack,
1875
+ };
1876
+ } else {
1877
+ message = 'Une erreur inattendue est survenue';
1878
+ errorDetails = exception;
1879
+ }
1880
+
1881
+ if (process.env.NODE_ENV !== 'production') {
1882
+ this.logger.error(
1883
+ \`Exception on \${request.method} \${request.url}\`,
1884
+ JSON.stringify({ message, status, errorDetails, exception }),
1885
+ );
1886
+ }
1887
+
1888
+ response.status(status).json({
1889
+ statusCode: status,
1890
+ timestamp: new Date().toISOString(),
1891
+ path: request.url,
1892
+ method: request.method,
1893
+ message,
1894
+ error: errorDetails,
1895
+ });
1896
+ }
1897
+ }
1898
+ `.trim();
1899
+ }
1900
+
1901
+ if (orm === "typeorm") {
1902
+ return `
1903
+ import {
1904
+ ExceptionFilter,
1905
+ Catch,
1906
+ ArgumentsHost,
1907
+ HttpException,
1908
+ HttpStatus,
1909
+ Logger,
1910
+ } from '@nestjs/common';
1911
+ import { Request, Response } from 'express';
1912
+
1913
+ @Catch()
1914
+ export class AllExceptionsFilter implements ExceptionFilter {
1915
+ private readonly logger = new Logger(AllExceptionsFilter.name);
1916
+
1917
+ catch(exception: unknown, host: ArgumentsHost) {
1918
+ const ctx = host.switchToHttp();
1919
+ const response = ctx.getResponse<Response>();
1920
+ const request = ctx.getRequest<Request>();
1921
+
1922
+ let status = HttpStatus.INTERNAL_SERVER_ERROR;
1923
+ let message: string | string[] = 'Internal server error';
1924
+ let errorDetails: any = null;
1925
+
1926
+ if (exception instanceof HttpException) {
1927
+ status = exception.getStatus();
1928
+ const res = exception.getResponse();
1929
+ if (typeof res === 'string') {
1930
+ message = res;
1931
+ } else if (typeof res === 'object' && res !== null) {
1932
+ const resObj = res as any;
1933
+ message = resObj.message || resObj.error || 'HttpException';
1934
+ errorDetails = resObj;
1935
+ }
1936
+ } else if (
1937
+ typeof exception === 'object' &&
1938
+ exception &&
1939
+ 'name' in exception &&
1940
+ (
1941
+ (exception as any).name === 'QueryFailedError' ||
1942
+ (exception as any).name === 'EntityNotFoundError' ||
1943
+ (exception as any).name === 'CannotCreateEntityIdMapError'
1944
+ )
1945
+ ) {
1946
+ status = HttpStatus.BAD_REQUEST;
1947
+ message = (exception as any).message || 'TypeORM error';
1948
+ errorDetails = exception;
1949
+ } else if (exception instanceof Error) {
1950
+ message = exception.message;
1951
+ errorDetails = {
1952
+ name: exception.name,
1953
+ stack: exception.stack,
1954
+ };
1955
+ } else {
1956
+ message = 'Une erreur inattendue est survenue';
1957
+ errorDetails = exception;
1958
+ }
1959
+
1960
+ if (process.env.NODE_ENV !== 'production') {
1961
+ this.logger.error(
1962
+ \`Exception on \${request.method} \${request.url}\`,
1963
+ JSON.stringify({ message, status, errorDetails, exception }),
1964
+ );
1965
+ }
1966
+
1967
+ response.status(status).json({
1968
+ statusCode: status,
1969
+ timestamp: new Date().toISOString(),
1970
+ path: request.url,
1971
+ method: request.method,
1972
+ message,
1973
+ error: errorDetails,
1974
+ });
1975
+ }
1976
+ }
1977
+ `.trim();
1978
+ }
1979
+
1980
+ if (orm === "sequelize") {
1981
+ return `
1982
+ import {
1983
+ ExceptionFilter,
1984
+ Catch,
1985
+ ArgumentsHost,
1986
+ HttpException,
1987
+ HttpStatus,
1988
+ Logger,
1989
+ } from '@nestjs/common';
1990
+ import { Request, Response } from 'express';
1991
+
1992
+ @Catch()
1993
+ export class AllExceptionsFilter implements ExceptionFilter {
1994
+ private readonly logger = new Logger(AllExceptionsFilter.name);
1995
+
1996
+ catch(exception: unknown, host: ArgumentsHost) {
1997
+ const ctx = host.switchToHttp();
1998
+ const response = ctx.getResponse<Response>();
1999
+ const request = ctx.getRequest<Request>();
2000
+
2001
+ let status = HttpStatus.INTERNAL_SERVER_ERROR;
2002
+ let message: string | string[] = 'Internal server error';
2003
+ let errorDetails: any = null;
2004
+
2005
+ if (exception instanceof HttpException) {
2006
+ status = exception.getStatus();
2007
+ const res = exception.getResponse();
2008
+ if (typeof res === 'string') {
2009
+ message = res;
2010
+ } else if (typeof res === 'object' && res !== null) {
2011
+ const resObj = res as any;
2012
+ message = resObj.message || resObj.error || 'HttpException';
2013
+ errorDetails = resObj;
2014
+ }
2015
+ } else if (
2016
+ typeof exception === 'object' &&
2017
+ exception &&
2018
+ exception.constructor &&
2019
+ (
2020
+ exception.constructor.name === 'SequelizeDatabaseError' ||
2021
+ exception.constructor.name === 'SequelizeValidationError'
2022
+ )
2023
+ ) {
2024
+ status = HttpStatus.BAD_REQUEST;
2025
+ message = (exception as any).message || 'Sequelize error';
2026
+ errorDetails = exception;
2027
+ } else if (exception instanceof Error) {
2028
+ message = exception.message;
2029
+ errorDetails = {
2030
+ name: exception.name,
2031
+ stack: exception.stack,
2032
+ };
2033
+ } else {
2034
+ message = 'Une erreur inattendue est survenue';
2035
+ errorDetails = exception;
2036
+ }
2037
+
2038
+ if (process.env.NODE_ENV !== 'production') {
2039
+ this.logger.error(
2040
+ \`Exception on \${request.method} \${request.url}\`,
2041
+ JSON.stringify({ message, status, errorDetails, exception }),
2042
+ );
2043
+ }
2044
+
2045
+ response.status(status).json({
2046
+ statusCode: status,
2047
+ timestamp: new Date().toISOString(),
2048
+ path: request.url,
2049
+ method: request.method,
2050
+ message,
2051
+ error: errorDetails,
2052
+ });
2053
+ }
2054
+ }
2055
+ `.trim();
2056
+ }
2057
+
2058
+ // Version universelle (multi-ORM)
2059
+ return `
2060
+ import {
2061
+ ExceptionFilter,
2062
+ Catch,
2063
+ ArgumentsHost,
2064
+ HttpException,
2065
+ HttpStatus,
2066
+ Logger,
2067
+ } from '@nestjs/common';
2068
+ import { Request, Response } from 'express';
2069
+
2070
+ @Catch()
2071
+ export class AllExceptionsFilter implements ExceptionFilter {
2072
+ private readonly logger = new Logger(AllExceptionsFilter.name);
2073
+
2074
+ catch(exception: unknown, host: ArgumentsHost) {
2075
+ const ctx = host.switchToHttp();
2076
+ const response = ctx.getResponse<Response>();
2077
+ const request = ctx.getRequest<Request>();
2078
+
2079
+ let status = HttpStatus.INTERNAL_SERVER_ERROR;
2080
+ let message: string | string[] = 'Internal server error';
2081
+ let errorDetails: any = null;
2082
+
2083
+ if (exception instanceof HttpException) {
2084
+ status = exception.getStatus();
2085
+ const res = exception.getResponse();
2086
+ if (typeof res === 'string') {
2087
+ message = res;
2088
+ } else if (typeof res === 'object' && res !== null) {
2089
+ const resObj = res as any;
2090
+ message = resObj.message || resObj.error || 'HttpException';
2091
+ errorDetails = resObj;
2092
+ }
2093
+ }
2094
+ // Prisma
2095
+ else if (
2096
+ typeof exception === 'object' &&
2097
+ exception &&
2098
+ exception.constructor &&
2099
+ (
2100
+ exception.constructor.name === 'PrismaClientKnownRequestError' ||
2101
+ exception.constructor.name === 'PrismaClientValidationError'
2102
+ )
2103
+ ) {
2104
+ status = HttpStatus.BAD_REQUEST;
2105
+ message = (exception as any).message || 'Prisma error';
2106
+ errorDetails = exception;
2107
+ }
2108
+ // Mongoose/Mongo
2109
+ else if (
2110
+ typeof exception === 'object' &&
2111
+ exception &&
2112
+ 'name' in exception &&
2113
+ (
2114
+ (exception as any).name === 'MongoError' ||
2115
+ (exception as any).name === 'MongooseError'
2116
+ )
2117
+ ) {
2118
+ status = HttpStatus.BAD_REQUEST;
2119
+ message = (exception as any).message || 'MongoDB error';
2120
+ errorDetails = exception;
2121
+ }
2122
+ // Sequelize
2123
+ else if (
2124
+ typeof exception === 'object' &&
2125
+ exception &&
2126
+ exception.constructor &&
2127
+ (
2128
+ exception.constructor.name === 'SequelizeDatabaseError' ||
2129
+ exception.constructor.name === 'SequelizeValidationError'
2130
+ )
2131
+ ) {
2132
+ status = HttpStatus.BAD_REQUEST;
2133
+ message = (exception as any).message || 'Sequelize error';
2134
+ errorDetails = exception;
2135
+ }
2136
+ else if (exception instanceof Error) {
2137
+ message = exception.message;
2138
+ errorDetails = {
2139
+ name: exception.name,
2140
+ stack: exception.stack,
2141
+ };
2142
+ } else {
2143
+ message = 'Une erreur inattendue est survenue';
2144
+ errorDetails = exception;
2145
+ }
2146
+
2147
+ this.logger.error(
2148
+ \`Exception on \${request.method} \${request.url}\`,
2149
+ JSON.stringify({ message, status, errorDetails, exception }),
2150
+ );
2151
+
2152
+ response.status(status).json({
2153
+ statusCode: status,
2154
+ timestamp: new Date().toISOString(),
2155
+ path: request.url,
2156
+ method: request.method,
2157
+ message,
2158
+ error: errorDetails,
2159
+ });
2160
+ }
2161
+ }
2162
+ `.trim();
2163
+ }
2164
+
2165
+ export async function getPackageManager(flags) {
2166
+ const managers = ["npm", "yarn", "pnpm"];
2167
+
2168
+ // 1. Vérification du flag
2169
+ if (
2170
+ flags.packageManager &&
2171
+ managers.includes(flags.packageManager.toLowerCase())
2172
+ ) {
2173
+ return flags.packageManager.toLowerCase();
2174
+ }
2175
+
2176
+ // 2. Mode interactif
2177
+ const answers = await actualInquirer.prompt([
2178
+ {
2179
+ type: "list",
2180
+ name: "packageManager",
2181
+ message: "Choose your package manager:",
2182
+ choices: managers,
2183
+ default: "npm",
2184
+ },
2185
+ ]);
2186
+
2187
+ return answers.packageManager;
2188
+ }
2189
+
2190
+ export function pluralize(name) {
2191
+ if (name.endsWith("y")) {
2192
+ return name.slice(0, -1) + "ies"; // Category -> Categories
2193
+ } else if (name.endsWith("s")) {
2194
+ return name; // Déjà un pluriel
2195
+ }
2196
+ return name + "s"; // User -> Users
2197
+ }