nestcraftx 0.3.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/.github/workflows/ci.yml +35 -0
  2. package/CHANGELOG.fr.md +74 -0
  3. package/CHANGELOG.md +74 -0
  4. package/PROGRESS.md +70 -67
  5. package/README.fr.md +60 -69
  6. package/bin/nestcraft.js +8 -0
  7. package/commands/demo.js +12 -46
  8. package/commands/generate.js +551 -2
  9. package/commands/generateConf.js +4 -4
  10. package/commands/help.js +11 -0
  11. package/commands/info.js +2 -3
  12. package/commands/list.js +93 -0
  13. package/commands/new.js +28 -56
  14. package/jest.config.js +9 -0
  15. package/package.json +7 -2
  16. package/readme.md +59 -68
  17. package/tests/e2e/generator.spec.js +71 -0
  18. package/tests/unit/appModuleUpdater.spec.js +111 -0
  19. package/tests/unit/cliParser.spec.js +74 -0
  20. package/tests/unit/controllerGenerator.spec.js +145 -0
  21. package/tests/unit/dtoGenerator.spec.js +87 -0
  22. package/tests/unit/entityGenerator.spec.js +65 -0
  23. package/tests/unit/generateCommand.spec.js +100 -0
  24. package/tests/unit/health.spec.js +69 -0
  25. package/tests/unit/listCommand.spec.js +91 -0
  26. package/tests/unit/middlewareGenerator.spec.js +64 -0
  27. package/tests/unit/newCommand.spec.js +88 -0
  28. package/tests/unit/relationCommand.spec.js +202 -0
  29. package/tests/unit/throttler.spec.js +117 -0
  30. package/utils/app-module.updater.js +6 -0
  31. package/utils/configs/setupCleanArchitecture.js +1 -1
  32. package/utils/configs/setupLightArchitecture.js +98 -26
  33. package/utils/envGenerator.js +9 -1
  34. package/utils/file-system.js +15 -0
  35. package/utils/file-utils/packageJsonUtils.js +6 -0
  36. package/utils/file-utils/saveProjectConfig.js +9 -0
  37. package/utils/fullModeInput.js +12 -229
  38. package/utils/generators/application/dtoGenerator.js +26 -8
  39. package/utils/generators/application/dtoUpdater.js +5 -0
  40. package/utils/generators/cleanModuleGenerator.js +40 -11
  41. package/utils/generators/domain/entityUpdater.js +5 -0
  42. package/utils/generators/infrastructure/mapperUpdater.js +5 -0
  43. package/utils/generators/infrastructure/middlewareGenerator.js +112 -315
  44. package/utils/generators/infrastructure/mongooseSchemaGenerator.js +114 -4
  45. package/utils/generators/infrastructure/repositoryGenerator.js +114 -14
  46. package/utils/generators/lightModuleGenerator.js +14 -0
  47. package/utils/generators/presentation/controllerGenerator.js +70 -19
  48. package/utils/helpers.js +8 -2
  49. package/utils/interactive/askEntityInputs.js +5 -68
  50. package/utils/interactive/askRelationCommand.js +98 -0
  51. package/utils/interactive/entityBuilder.js +411 -0
  52. package/utils/lightModeInput.js +22 -241
  53. package/utils/setups/orms/typeOrmSetup.js +42 -8
  54. package/utils/setups/projectSetup.js +88 -10
  55. package/utils/setups/setupAuth.js +335 -20
  56. package/utils/setups/setupDatabase.js +4 -1
  57. package/utils/setups/setupHealth.js +74 -0
  58. package/utils/setups/setupMongoose.js +84 -32
  59. package/utils/setups/setupPrisma.js +151 -4
  60. package/utils/setups/setupSwagger.js +15 -8
  61. package/utils/setups/setupThrottler.js +92 -0
  62. package/utils/shell.js +61 -9
@@ -124,6 +124,7 @@ async function setupLightArchitecture(inputs) {
124
124
  entityNameCapitalized,
125
125
  entityNameLower,
126
126
  useSwagger,
127
+ useAuth || false,
127
128
  );
128
129
  await createFile({
129
130
  path: `${entityPath}/controllers/${entityNameLower}.controller.ts`,
@@ -282,9 +283,19 @@ export class ${entityName}Repository {
282
283
 
283
284
  ${extraMethods}
284
285
 
285
- async findAll(): Promise<${entityName}Entity[]> {
286
- const results = await this.prisma.${entityLower}.findMany();
287
- return results.map(r => this.toEntity(r));
286
+ async findAll(page: number = 1, limit: number = 10): Promise<{ data: ${entityName}Entity[], total: number }> {
287
+ const skip = (page - 1) * limit;
288
+ const [results, total] = await this.prisma.$transaction([
289
+ this.prisma.${entityLower}.findMany({
290
+ skip,
291
+ take: limit,
292
+ }),
293
+ this.prisma.${entityLower}.count(),
294
+ ]);
295
+ return {
296
+ data: results.map(r => this.toEntity(r)),
297
+ total,
298
+ };
288
299
  }
289
300
 
290
301
  async update(id: string, data: Update${entityName}Dto): Promise<${entityName}Entity | null> {
@@ -346,9 +357,16 @@ export class ${entityName}Repository {
346
357
 
347
358
  ${extraMethods}
348
359
 
349
- async findAll(): Promise<${entityName}Entity[]> {
350
- const results = await this.repository.find();
351
- return results.map(r => this.toEntity(r));
360
+ async findAll(page: number = 1, limit: number = 10): Promise<{ data: ${entityName}Entity[], total: number }> {
361
+ const skip = (page - 1) * limit;
362
+ const [results, total] = await this.repository.findAndCount({
363
+ skip,
364
+ take: limit,
365
+ });
366
+ return {
367
+ data: results.map(r => this.toEntity(r)),
368
+ total,
369
+ };
352
370
  }
353
371
 
354
372
  async update(id: string, data: Update${entityName}Dto): Promise<${entityName}Entity | null> {
@@ -415,9 +433,16 @@ export class ${entityName}Repository {
415
433
 
416
434
  ${extraMethods}
417
435
 
418
- async findAll(): Promise<${entityName}Entity[]> {
419
- const results = await this.model.find().exec();
420
- return results.map(r => this.toEntity(r));
436
+ async findAll(page: number = 1, limit: number = 10): Promise<{ data: ${entityName}Entity[], total: number }> {
437
+ const skip = (page - 1) * limit;
438
+ const [results, total] = await Promise.all([
439
+ this.model.find().skip(skip).limit(limit).exec(),
440
+ this.model.countDocuments().exec(),
441
+ ]);
442
+ return {
443
+ data: results.map(r => this.toEntity(r)),
444
+ total,
445
+ };
421
446
  }
422
447
 
423
448
  async update(id: string, data: Update${entityName}Dto): Promise<${entityName}Entity | null> {
@@ -457,7 +482,7 @@ export class ${entityName}Repository {
457
482
 
458
483
  ${extraMethods}
459
484
 
460
- async findAll(): Promise<${entityName}Entity[]> {
485
+ async findAll(page: number = 1, limit: number = 10): Promise<{ data: ${entityName}Entity[], total: number }> {
461
486
  throw new Error('Repository not implemented');
462
487
  }
463
488
 
@@ -513,11 +538,20 @@ export class ${entityName}Service {
513
538
  return entity;
514
539
  }
515
540
 
516
- async findAll(): Promise<${entityName}Entity[]> {
517
- this.logger.log('Fetching all ${entityName} records');
518
- const results = await this.repository.findAll();
519
- this.logger.log(\`Successfully retrieved \${results.length} ${entityName}(s)\`);
520
- return results;
541
+ async findAll(page: number = 1, limit: number = 10): Promise<{ data: ${entityName}Entity[], meta: { total: number, page: number, limit: number, totalPages: number } }> {
542
+ this.logger.log(\`Fetching page \${page} of ${entityName} records (limit: \${limit})\`);
543
+ const { data, total } = await this.repository.findAll(page, limit);
544
+ const totalPages = Math.ceil(total / limit);
545
+ this.logger.log(\`Successfully retrieved \${data.length} of \${total} ${entityName}(s)\`);
546
+ return {
547
+ data,
548
+ meta: {
549
+ total,
550
+ page,
551
+ limit,
552
+ totalPages,
553
+ },
554
+ };
521
555
  }
522
556
 
523
557
  async update(id: string, dto: Update${entityName}Dto): Promise<${entityName}Entity> {
@@ -549,21 +583,46 @@ export class ${entityName}Service {
549
583
  }`;
550
584
  }
551
585
 
552
- function generateLightController(entityName, entityLower, useSwagger) {
586
+ function generateLightController(entityName, entityLower, useSwagger, useAuth = false) {
553
587
  const entityCap = capitalize(entityName);
554
588
  const pluralName = pluralize(entityLower);
555
589
 
590
+ // --- Imports NestJS de base ---
591
+ const nestCoreImports = useAuth
592
+ ? `import { Controller, Get, Post, Patch, Delete, Body, Param, Query, Logger, HttpCode, HttpStatus, UseGuards } from '@nestjs/common';`
593
+ : `import { Controller, Get, Post, Patch, Delete, Body, Param, Query, Logger, HttpCode, HttpStatus } from '@nestjs/common';`;
594
+
595
+ // --- Imports Swagger ---
556
596
  const swaggerImports = useSwagger
557
- ? `import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';\n`
597
+ ? `import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery${useAuth ? ', ApiBearerAuth, ApiUnauthorizedResponse, ApiForbiddenResponse' : ''} } from '@nestjs/swagger';\n`
598
+ : "";
599
+
600
+ // --- Imports Guards & Décorateurs (uniquement si auth activé) ---
601
+ const guardImports = useAuth
602
+ ? `import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
603
+ import { RolesGuard } from 'src/auth/guards/role.guard';
604
+ import { Roles } from 'src/common/decorators/role.decorator';
605
+ import { Public } from 'src/common/decorators/public.decorator';`
558
606
  : "";
559
607
 
560
608
  const swaggerDecorators = useSwagger
561
609
  ? `@ApiTags('${capitalize(pluralName)}')\n`
562
610
  : "";
563
611
 
564
- return `import { Controller, Get, Post, Patch, Delete, Body, Param, Logger, HttpCode, HttpStatus } from '@nestjs/common';
612
+ // --- Snippets décorateurs ---
613
+ const adminGuards = useAuth
614
+ ? ` @UseGuards(JwtAuthGuard, RolesGuard)\n @Roles('ADMIN')`
615
+ : "";
616
+ const apiBearerAuth = useAuth && useSwagger ? ` @ApiBearerAuth()` : "";
617
+ const apiUnauthorized = useAuth && useSwagger
618
+ ? ` @ApiUnauthorizedResponse({ description: 'Missing or invalid JWT token.' })\n @ApiForbiddenResponse({ description: 'Insufficient permissions (ADMIN required).' })`
619
+ : "";
620
+ const publicDecorator = useAuth ? ` @Public()` : "";
621
+
622
+ return `${nestCoreImports}
565
623
  ${swaggerImports}import { ${entityCap}Service } from '../services/${entityLower}.service';
566
624
  import { Create${entityCap}Dto, Update${entityCap}Dto } from '../dtos/${entityLower}.dto';
625
+ ${guardImports}
567
626
 
568
627
  ${swaggerDecorators}@Controller('${pluralName}')
569
628
  export class ${entityCap}Controller {
@@ -571,10 +630,12 @@ export class ${entityCap}Controller {
571
630
 
572
631
  constructor(private readonly service: ${entityCap}Service) {}
573
632
 
633
+ ${apiBearerAuth}
634
+ ${adminGuards}
574
635
  @Post()
575
636
  ${
576
637
  useSwagger
577
- ? `@ApiOperation({ summary: 'Create a new ${entityLower}' })\n @ApiResponse({ status: 201, description: 'The record has been successfully created.' })`
638
+ ? `@ApiOperation({ summary: 'Create a new ${entityLower}' })\n @ApiResponse({ status: 201, description: 'The record has been successfully created.' })\n ${apiUnauthorized}`
578
639
  : ""
579
640
  }
580
641
  async create(@Body() dto: Create${entityCap}Dto) {
@@ -585,17 +646,24 @@ export class ${entityCap}Controller {
585
646
  };
586
647
  }
587
648
 
649
+ ${publicDecorator}
588
650
  @Get()
589
651
  ${
590
652
  useSwagger
591
- ? `@ApiOperation({ summary: 'Get all ${pluralName}' })\n @ApiResponse({ status: 200, description: 'List of records retrieved.' })`
653
+ ? `@ApiOperation({ summary: 'Get all ${pluralName}' })\n @ApiQuery({ name: 'page', required: false, type: Number, schema: { default: 1 } })\n @ApiQuery({ name: 'limit', required: false, type: Number, schema: { default: 10 } })\n @ApiResponse({ status: 200, description: 'List of records retrieved.' })`
592
654
  : ""
593
655
  }
594
- async findAll() {
595
- this.logger.log(\`Fetching all ${pluralName}\`);
596
- return await this.service.findAll();
656
+ async findAll(
657
+ @Query('page') page?: number,
658
+ @Query('limit') limit?: number,
659
+ ) {
660
+ this.logger.log(\`Fetching all ${pluralName} (page: \${page}, limit: \${limit})\`);
661
+ const pageNum = page ? Number(page) : 1;
662
+ const limitNum = limit ? Number(limit) : 10;
663
+ return await this.service.findAll(pageNum, limitNum);
597
664
  }
598
665
 
666
+ ${publicDecorator}
599
667
  @Get(':id')
600
668
  ${
601
669
  useSwagger
@@ -607,10 +675,12 @@ export class ${entityCap}Controller {
607
675
  return await this.service.findById(id);
608
676
  }
609
677
 
678
+ ${apiBearerAuth}
679
+ ${adminGuards}
610
680
  @Patch(':id')
611
681
  ${
612
682
  useSwagger
613
- ? `@ApiOperation({ summary: 'Update ${entityLower}' })\n @ApiParam({ name: 'id', type: String })\n @ApiResponse({ status: 200, description: 'Record updated.' })`
683
+ ? `@ApiOperation({ summary: 'Update ${entityLower}' })\n @ApiParam({ name: 'id', type: String })\n @ApiResponse({ status: 200, description: 'Record updated.' })\n ${apiUnauthorized}`
614
684
  : ""
615
685
  }
616
686
  async update(@Param('id') id: string, @Body() dto: Update${entityCap}Dto) {
@@ -619,11 +689,13 @@ export class ${entityCap}Controller {
619
689
  return { message: '${entityCap} updated successfully' };
620
690
  }
621
691
 
692
+ ${apiBearerAuth}
693
+ ${adminGuards}
622
694
  @Delete(':id')
623
695
  @HttpCode(HttpStatus.NO_CONTENT)
624
696
  ${
625
697
  useSwagger
626
- ? `@ApiOperation({ summary: 'Delete ${entityLower}' })\n @ApiParam({ name: 'id', type: String })\n @ApiResponse({ status: 204, description: 'Record deleted.' })`
698
+ ? `@ApiOperation({ summary: 'Delete ${entityLower}' })\n @ApiParam({ name: 'id', type: String })\n @ApiResponse({ status: 204, description: 'Record deleted.' })\n ${apiUnauthorized}`
627
699
  : ""
628
700
  }
629
701
  async delete(@Param('id') id: string) {
@@ -631,7 +703,7 @@ export class ${entityCap}Controller {
631
703
  await this.service.delete(id);
632
704
  return {
633
705
  message: '${entityCap} deleted successfully',
634
- };;
706
+ };
635
707
  }
636
708
  }`;
637
709
  }
@@ -21,7 +21,8 @@ PORT=3000
21
21
  `;
22
22
 
23
23
  // --- AUTHENTICATION SECTION ---
24
- content += `# ------------------------------------------------------------------------------
24
+ if (inputs.useAuth) {
25
+ content += `# ------------------------------------------------------------------------------
25
26
  # AUTHENTICATION (JWT)
26
27
  # ------------------------------------------------------------------------------
27
28
  # Auto-generated secrets to secure tokens.
@@ -34,6 +35,7 @@ JWT_EXPIRES_IN=15m
34
35
  JWT_REFRESH_EXPIRES_IN=7d
35
36
 
36
37
  `;
38
+ }
37
39
 
38
40
  // --- DATABASE SECTION ---
39
41
  content += `# ------------------------------------------------------------------------------
@@ -85,6 +87,12 @@ function buildDatabaseUrl(user, password, host, port, database, orm) {
85
87
  }
86
88
 
87
89
  function writeEnvFile(envContent, projectPath = ".") {
90
+ const isDryRun = process.argv.includes("--dry-run");
91
+ if (isDryRun) {
92
+ console.log(`[DRY-RUN] Simulated: write env file to ${path.join(projectPath, ".env")}`);
93
+ console.log(`[DRY-RUN] Simulated: write env example file to ${path.join(projectPath, ".env.example")}`);
94
+ return;
95
+ }
88
96
  // Écriture du .env (avec secrets)
89
97
  const envPath = path.join(projectPath, ".env");
90
98
  fs.writeFileSync(envPath, envContent, "utf8");
@@ -15,6 +15,11 @@ const fs = require("fs");
15
15
  * @param {string} directoryPath - Le chemin du répertoire à créer.
16
16
  */
17
17
  async function createDirectory(directoryPath) {
18
+ const isDryRun = process.argv.includes("--dry-run");
19
+ if (isDryRun) {
20
+ console.log(`[DRY-RUN] Simulated: create directory ${directoryPath}`);
21
+ return;
22
+ }
18
23
  try {
19
24
  if (!fs.existsSync(directoryPath)) {
20
25
  fs.mkdirSync(directoryPath, { recursive: true });
@@ -36,6 +41,11 @@ async function createDirectory(directoryPath) {
36
41
  * @param {{ path: string, contente: string, overwrite?: boolean }} fileData
37
42
  */
38
43
  async function createFile(fileData) {
44
+ const isDryRun = process.argv.includes("--dry-run");
45
+ if (isDryRun) {
46
+ console.log(`[DRY-RUN] Simulated: create/write file ${fileData.path}`);
47
+ return;
48
+ }
39
49
  try {
40
50
  if (!fs.existsSync(fileData.path)) {
41
51
  fs.writeFileSync(`${fileData.path}`, `${fileData.contente}`);
@@ -59,6 +69,11 @@ async function createFile(fileData) {
59
69
  * @param {{ path: string, pattern: string|RegExp, replacement: string }} options
60
70
  */
61
71
  async function updateFile({ path, pattern, replacement }) {
72
+ const isDryRun = process.argv.includes("--dry-run");
73
+ if (isDryRun) {
74
+ console.log(`[DRY-RUN] Simulated: update file ${path}`);
75
+ return;
76
+ }
62
77
  try {
63
78
  let mainTs = fs.readFileSync(path, "utf8");
64
79
  const updatedContent = mainTs.replace(pattern, replacement);
@@ -13,6 +13,12 @@ async function updatePackageJson(inputs, scripts, devDependencies = {}) {
13
13
  const projectPath = process.cwd();
14
14
  const packageJsonPath = path.join(projectPath, "package.json");
15
15
 
16
+ const isDryRun = process.argv.includes("--dry-run");
17
+ if (isDryRun) {
18
+ console.log(`[DRY-RUN] Simulated: update package.json scripts and devDependencies for ${inputs.projectName}`);
19
+ return;
20
+ }
21
+
16
22
  try {
17
23
  // 1. Lire le contenu actuel
18
24
  const fileContent = fs.readFileSync(packageJsonPath, "utf8");
@@ -18,11 +18,20 @@ async function saveProjectConfig(inputs) {
18
18
  database: inputs.selectedDB,
19
19
  auth: inputs.useAuth,
20
20
  swagger: inputs.useSwagger,
21
+ throttler: inputs.useThrottler,
21
22
  packageManager: inputs.packageManager,
22
23
  docker: inputs.useDocker,
24
+ entities: inputs.entitiesData ? inputs.entitiesData.entities : [],
25
+ relations: inputs.entitiesData ? inputs.entitiesData.relations : [],
23
26
  generatedAt: new Date().toISOString(),
24
27
  };
25
28
 
29
+ const isDryRun = process.argv.includes("--dry-run");
30
+ if (isDryRun) {
31
+ console.log(`[DRY-RUN] Simulated: save project config to ${configFile}`);
32
+ return;
33
+ }
34
+
26
35
  try {
27
36
  if (!fs.existsSync(configDir)) {
28
37
  fs.mkdirSync(configDir, { recursive: true });
@@ -4,6 +4,7 @@ const inquirer = require("inquirer");
4
4
  const { capitalize } = require("./userInput");
5
5
  const { logWarning } = require("./loggers/logWarning");
6
6
  const { getPackageManager } = require("./utils");
7
+ const { buildEntityInteractive, buildRelationsInteractive } = require("./interactive/entityBuilder");
7
8
  const actualInquirer = inquirer.default || inquirer;
8
9
 
9
10
  async function getFullModeInputs(projectName, flags) {
@@ -210,6 +211,8 @@ async function getFullModeInputs(projectName, flags) {
210
211
  { name: "auth", default: true, prompt: "Add JWT authentication?" },
211
212
 
212
213
  { name: "swagger", default: true, prompt: "Install Swagger?" },
214
+
215
+ { name: "throttler", default: true, prompt: "Enable Rate Limiting (Throttler)?" },
213
216
  ];
214
217
 
215
218
  const booleanResults = {};
@@ -238,6 +241,7 @@ async function getFullModeInputs(projectName, flags) {
238
241
  const useDocker = booleanResults.docker;
239
242
  const useAuth = booleanResults.auth;
240
243
  const useSwagger = booleanResults.swagger;
244
+ const useThrottler = booleanResults.throttler;
241
245
 
242
246
  // --- 5. Swagger Configuration (Prioritize flags) ---
243
247
  let swaggerInputs;
@@ -334,97 +338,13 @@ async function getFullModeInputs(projectName, flags) {
334
338
 
335
339
  let addEntity = readline.keyInYNStrict(`${info("[?]")} Add an entity?`);
336
340
  while (addEntity) {
337
- let name;
338
- while (true) {
339
- name = readline.question(`\n Entity name : `);
340
- if (/^[A-Za-z][A-Za-z0-9_]*$/.test(name)) break;
341
- logWarning("Invalid name. Letters, numbers, _ (start with a letter).");
342
- }
343
-
344
- const fields = [];
345
-
346
- console.log(` Fields for "${name}" :`);
347
- while (true) {
348
- let fname = readline.question(" Field name (leave empty to finish) : ");
349
- if (!fname) break;
350
- if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(fname)) {
351
- logWarning("Invalid field name.");
352
- continue;
353
- }
354
-
355
- const baseTypeChoices = [
356
- "string",
357
- "text",
358
- "number",
359
- "decimal",
360
- "boolean",
361
- "Date",
362
- "uuid",
363
- "json",
364
- "enum",
365
- "array",
366
- "object",
367
- ];
368
-
369
- const typeQuestion = {
370
- type: "list",
371
- name: "ftype",
372
- message: `Type for "${fname}"`,
373
- default: "string",
374
- choices: baseTypeChoices,
375
- transformer: () => "",
376
- };
377
- const typeAnswer = await actualInquirer.prompt([typeQuestion]);
378
- let ftype = typeAnswer.ftype;
379
- process.stdout.write("\x1B[1A");
380
- process.stdout.write("\x1B[K");
381
-
382
- if (ftype === "array") {
383
- const arrayInnerQuestion = {
384
- type: "list",
385
- name: "innerType",
386
- message: `Type of elements for "${fname}[]"`,
387
- default: "string",
388
- choices: baseTypeChoices.filter(
389
- (c) => c !== "array" && c !== "object"
390
- ),
391
- transformer: () => "",
392
- };
393
-
394
- const innerAnswer = await actualInquirer.prompt([arrayInnerQuestion]);
395
- ftype = `${innerAnswer.innerType}[]`;
396
- } else if (ftype === "enum") {
397
- const enumName = capitalize(fname) + "Enum";
398
- console.log(
399
- ` ${info(
400
- "[INFO]"
401
- )} Enum type selected. Consider defining ${enumName} in your code.`
402
- );
403
- ftype = enumName;
404
- } else if (ftype === "object") {
405
- const objectNameQuestion = {
406
- type: "input",
407
- name: "objectName",
408
-
409
- message: `Complex type name (DTO/Class or leave 'json') :`,
410
- default: "json",
411
- transformer: () => "",
412
- };
413
-
414
- const objectAnswer = await actualInquirer.prompt([objectNameQuestion]);
415
- ftype = capitalize(objectAnswer.objectName.trim() || "json");
416
- }
417
-
418
- console.log(` Type for "${fname}" : ${ftype} ${success("[✓]")}`);
419
-
420
- fields.push({ name: fname, type: ftype });
341
+ const entity = await buildEntityInteractive();
342
+ if (entity) {
343
+ entitiesData.entities.push(entity);
344
+ console.log(
345
+ `${success("[✓]")} Entity "${entity.name}" added with ${entity.fields.length} field(s)`
346
+ );
421
347
  }
422
-
423
- entitiesData.entities.push({ name, fields });
424
- console.log(
425
- `${success("[✓]")} Entity "${name}" added with ${fields.length} field(s)`
426
- );
427
-
428
348
  addEntity = readline.keyInYNStrict(`${info("[?]")} Add another entity?`);
429
349
  }
430
350
 
@@ -432,145 +352,7 @@ async function getFullModeInputs(projectName, flags) {
432
352
  `${info("[?]")} Add relationships between entities?`
433
353
  );
434
354
  if (wantsRelation) {
435
- if (entitiesData.entities.length > 1) {
436
- console.log(`\n${info("[INFO]")} Configuring relationships`);
437
-
438
- let configuring = true;
439
- while (configuring) {
440
- const entityNames = entitiesData.entities.map((e) => e.name);
441
-
442
- // 1. Select entities first
443
- const selection = await actualInquirer.prompt([
444
- {
445
- type: "list",
446
- name: "fromName",
447
- message: "From which entity? (Source)",
448
- choices: entityNames,
449
- },
450
- {
451
- type: "list",
452
- name: "toName",
453
- message: (prev) =>
454
- `To which entity should ${prev.fromName} be linked? (Target)`,
455
- choices: (prev) =>
456
- entityNames.filter((name) => name !== prev.fromName),
457
- },
458
- ]);
459
-
460
- // --- VERIFICATION: Check if link already exists (A->B or B->A) ---
461
- const alreadyExists = entitiesData.relations.find(
462
- (rel) =>
463
- (rel.from === selection.fromName && rel.to === selection.toName) ||
464
- (rel.from === selection.toName && rel.to === selection.fromName)
465
- );
466
-
467
- if (alreadyExists) {
468
- logWarning(
469
- `A relationship already exists between ${selection.fromName} and ${selection.toName} (${alreadyExists.type}).`
470
- );
471
-
472
- const { tryAgain } = await actualInquirer.prompt([
473
- {
474
- type: "confirm",
475
- name: "tryAgain",
476
- message: "Do you want to choose different entities?",
477
- default: true,
478
- },
479
- ]);
480
-
481
- if (!tryAgain) break;
482
- continue; // Restart selection
483
- }
484
-
485
- // 2. Select Relationship type only if verification passed
486
- const typeAnswer = await actualInquirer.prompt([
487
- {
488
- type: "list",
489
- name: "relType",
490
- message: "Relationship type:",
491
- choices: [
492
- {
493
- name: `1-1 (One-to-One) : ${selection.fromName} has one ${selection.toName}`,
494
- value: "1-1",
495
- },
496
- {
497
- name: `1-n (One-to-Many) : ${selection.fromName} has many ${selection.toName}s`,
498
- value: "1-n",
499
- },
500
- {
501
- name: `n-1 (Many-to-One) : Many ${selection.fromName}s belong to one ${selection.toName}`,
502
- value: "n-1",
503
- },
504
- {
505
- name: `n-n (Many-to-Many) : Many ${selection.fromName}s linked to many ${selection.toName}s`,
506
- value: "n-n",
507
- },
508
- ],
509
- },
510
- ]);
511
-
512
- const from = entitiesData.entities.find(
513
- (e) => e.name === selection.fromName
514
- );
515
- const to = entitiesData.entities.find(
516
- (e) => e.name === selection.toName
517
- );
518
- const relType = typeAnswer.relType;
519
-
520
- // Register Relationship
521
- entitiesData.relations.push({
522
- from: from.name,
523
- to: to.name,
524
- type: relType,
525
- });
526
-
527
- const fromLow = from.name.toLowerCase();
528
- const toLow = to.name.toLowerCase();
529
-
530
- // --- Add fields logic ---
531
- if (relType === "1-1") {
532
- from.fields.push(
533
- { name: `${toLow}Id`, type: "string" },
534
- { name: toLow, type: to.name }
535
- );
536
- } else if (relType === "1-n") {
537
- from.fields.push({ name: `${toLow}s`, type: `${to.name}[]` });
538
- to.fields.push(
539
- { name: `${fromLow}Id`, type: "string" },
540
- { name: fromLow, type: from.name }
541
- );
542
- } else if (relType === "n-1") {
543
- from.fields.push(
544
- { name: `${toLow}Id`, type: "string" },
545
- { name: toLow, type: to.name }
546
- );
547
- to.fields.push({ name: `${fromLow}s`, type: `${from.name}[]` });
548
- } else if (relType === "n-n") {
549
- from.fields.push({ name: `${toLow}s`, type: `${to.name}[]` });
550
- to.fields.push({ name: `${fromLow}s`, type: `${from.name}[]` });
551
- }
552
-
553
- console.log(
554
- `\n${success("[✓]")} Relationship added: ${from.name} ${relType} ${
555
- to.name
556
- }`
557
- );
558
-
559
- const { addMore } = await actualInquirer.prompt([
560
- {
561
- type: "confirm",
562
- name: "addMore",
563
- message: "Add another relationship?",
564
- default: false,
565
- },
566
- ]);
567
- configuring = addMore;
568
- }
569
- } else {
570
- logWarning(
571
- "At least two entities are required to configure a relationship."
572
- );
573
- }
355
+ await buildRelationsInteractive(entitiesData);
574
356
  }
575
357
 
576
358
  return {
@@ -578,6 +360,7 @@ async function getFullModeInputs(projectName, flags) {
578
360
  useDocker,
579
361
  useAuth,
580
362
  useSwagger,
363
+ useThrottler,
581
364
  swaggerInputs,
582
365
  packageManager,
583
366
  entitiesData,