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.
- package/.github/workflows/ci.yml +35 -0
- package/CHANGELOG.fr.md +74 -0
- package/CHANGELOG.md +74 -0
- package/PROGRESS.md +70 -67
- package/README.fr.md +60 -69
- package/bin/nestcraft.js +8 -0
- package/commands/demo.js +12 -46
- package/commands/generate.js +551 -2
- package/commands/generateConf.js +4 -4
- package/commands/help.js +11 -0
- package/commands/info.js +2 -3
- package/commands/list.js +93 -0
- package/commands/new.js +28 -56
- package/jest.config.js +9 -0
- package/package.json +7 -2
- package/readme.md +59 -68
- package/tests/e2e/generator.spec.js +71 -0
- package/tests/unit/appModuleUpdater.spec.js +111 -0
- package/tests/unit/cliParser.spec.js +74 -0
- package/tests/unit/controllerGenerator.spec.js +145 -0
- package/tests/unit/dtoGenerator.spec.js +87 -0
- package/tests/unit/entityGenerator.spec.js +65 -0
- package/tests/unit/generateCommand.spec.js +100 -0
- package/tests/unit/health.spec.js +69 -0
- package/tests/unit/listCommand.spec.js +91 -0
- package/tests/unit/middlewareGenerator.spec.js +64 -0
- package/tests/unit/newCommand.spec.js +88 -0
- package/tests/unit/relationCommand.spec.js +202 -0
- package/tests/unit/throttler.spec.js +117 -0
- package/utils/app-module.updater.js +6 -0
- package/utils/configs/setupCleanArchitecture.js +1 -1
- package/utils/configs/setupLightArchitecture.js +98 -26
- package/utils/envGenerator.js +9 -1
- package/utils/file-system.js +15 -0
- package/utils/file-utils/packageJsonUtils.js +6 -0
- package/utils/file-utils/saveProjectConfig.js +9 -0
- package/utils/fullModeInput.js +12 -229
- package/utils/generators/application/dtoGenerator.js +26 -8
- package/utils/generators/application/dtoUpdater.js +5 -0
- package/utils/generators/cleanModuleGenerator.js +40 -11
- package/utils/generators/domain/entityUpdater.js +5 -0
- package/utils/generators/infrastructure/mapperUpdater.js +5 -0
- package/utils/generators/infrastructure/middlewareGenerator.js +112 -315
- package/utils/generators/infrastructure/mongooseSchemaGenerator.js +114 -4
- package/utils/generators/infrastructure/repositoryGenerator.js +114 -14
- package/utils/generators/lightModuleGenerator.js +14 -0
- package/utils/generators/presentation/controllerGenerator.js +70 -19
- package/utils/helpers.js +8 -2
- package/utils/interactive/askEntityInputs.js +5 -68
- package/utils/interactive/askRelationCommand.js +98 -0
- package/utils/interactive/entityBuilder.js +411 -0
- package/utils/lightModeInput.js +22 -241
- package/utils/setups/orms/typeOrmSetup.js +42 -8
- package/utils/setups/projectSetup.js +88 -10
- package/utils/setups/setupAuth.js +335 -20
- package/utils/setups/setupDatabase.js +4 -1
- package/utils/setups/setupHealth.js +74 -0
- package/utils/setups/setupMongoose.js +84 -32
- package/utils/setups/setupPrisma.js +151 -4
- package/utils/setups/setupSwagger.js +15 -8
- package/utils/setups/setupThrottler.js +92 -0
- 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
|
|
286
|
-
const
|
|
287
|
-
|
|
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
|
|
350
|
-
const
|
|
351
|
-
|
|
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
|
|
419
|
-
const
|
|
420
|
-
|
|
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
|
|
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
|
|
517
|
-
this.logger.log(
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
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
|
-
|
|
596
|
-
|
|
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
|
}
|
package/utils/envGenerator.js
CHANGED
|
@@ -21,7 +21,8 @@ PORT=3000
|
|
|
21
21
|
`;
|
|
22
22
|
|
|
23
23
|
// --- AUTHENTICATION SECTION ---
|
|
24
|
-
|
|
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");
|
package/utils/file-system.js
CHANGED
|
@@ -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 });
|
package/utils/fullModeInput.js
CHANGED
|
@@ -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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
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,
|