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.
- package/.gitattributes +6 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +33 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +19 -0
- package/.github/ISSUE_TEMPLATE/pull_request_template.md +24 -0
- package/CHANGELOG.fr.md +97 -97
- package/CHANGELOG.md +98 -98
- package/CLI_USAGE.fr.md +331 -331
- package/CLI_USAGE.md +364 -364
- package/DEMO.fr.md +292 -292
- package/DEMO.md +294 -294
- package/LICENSE +21 -21
- package/MIGRATION_GUIDE.fr.md +127 -127
- package/MIGRATION_GUIDE.md +124 -124
- package/QUICK_START.fr.md +152 -152
- package/QUICK_START.md +169 -169
- package/README.fr.md +653 -659
- package/SECURITY.md +10 -0
- package/bin/nestcraft.js +84 -64
- package/commands/demo.js +333 -330
- package/commands/generate.js +93 -0
- package/commands/generateConf.js +91 -0
- package/commands/help.js +78 -78
- package/commands/info.js +48 -48
- package/commands/new.js +338 -335
- package/commands/start.js +19 -19
- package/commands/test.js +7 -7
- package/package.json +41 -41
- package/readme.md +638 -643
- package/utils/cliParser.js +133 -76
- package/utils/colors.js +62 -62
- package/utils/configs/configureDocker.js +120 -120
- package/utils/configs/setupCleanArchitecture.js +563 -557
- package/utils/configs/setupLightArchitecture.js +701 -660
- package/utils/envGenerator.js +122 -122
- package/utils/file-utils/packageJsonUtils.js +49 -55
- package/utils/file-utils/saveProjectConfig.js +36 -0
- package/utils/fullModeInput.js +607 -607
- package/utils/generators/application/dtoUpdater.js +54 -0
- package/utils/generators/cleanModuleGenerator.js +475 -0
- package/utils/generators/database/setupDatabase.js +31 -0
- package/utils/generators/domain/entityUpdater.js +78 -0
- package/utils/generators/infrastructure/mapperUpdater.js +65 -0
- package/utils/generators/lightModuleGenerator.js +131 -0
- package/utils/generators/relation/relation.engine.js +64 -0
- package/utils/interactive/askEntityInputs.js +165 -0
- package/utils/lightModeInput.js +460 -460
- package/utils/loggers/logError.js +7 -7
- package/utils/loggers/logInfo.js +7 -7
- package/utils/loggers/logSuccess.js +7 -7
- package/utils/loggers/logWarning.js +7 -7
- package/utils/setups/orms/typeOrmSetup.js +630 -630
- package/utils/setups/projectSetup.js +46 -46
- package/utils/setups/setupAuth.js +973 -926
- package/utils/setups/setupDatabase.js +75 -75
- package/utils/setups/setupLogger.js +69 -59
- package/utils/setups/setupMongoose.js +377 -432
- package/utils/setups/setupPrisma.js +802 -630
- package/utils/setups/setupSwagger.js +97 -88
- package/utils/shell.js +32 -32
- package/utils/spinner.js +57 -57
- package/utils/systemCheck.js +124 -124
- package/utils/userInput.js +421 -421
- 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
|
|
215
|
-
entity,
|
|
216
|
-
useSwagger,
|
|
217
|
-
isAuthDto = false,
|
|
218
|
-
mode = "full"
|
|
219
|
-
) {
|
|
220
|
-
const entityName = capitalize(entity.name);
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
return
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
: "
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
${
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
${
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
${
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
${
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
});
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
const
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
.
|
|
1229
|
-
.
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
//
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
if (
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
);
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
) {
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
import {
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
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
|
+
}
|