nestcraftx 0.4.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 (54) 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 +59 -57
  5. package/README.fr.md +60 -69
  6. package/bin/nestcraft.js +8 -0
  7. package/commands/demo.js +10 -0
  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 +26 -9
  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/configs/setupCleanArchitecture.js +1 -1
  31. package/utils/configs/setupLightArchitecture.js +98 -26
  32. package/utils/envGenerator.js +3 -1
  33. package/utils/file-utils/saveProjectConfig.js +3 -0
  34. package/utils/fullModeInput.js +4 -0
  35. package/utils/generators/application/dtoGenerator.js +20 -3
  36. package/utils/generators/cleanModuleGenerator.js +40 -11
  37. package/utils/generators/infrastructure/middlewareGenerator.js +112 -315
  38. package/utils/generators/infrastructure/mongooseSchemaGenerator.js +78 -1
  39. package/utils/generators/infrastructure/repositoryGenerator.js +114 -14
  40. package/utils/generators/lightModuleGenerator.js +14 -0
  41. package/utils/generators/presentation/controllerGenerator.js +70 -19
  42. package/utils/interactive/askRelationCommand.js +98 -0
  43. package/utils/interactive/entityBuilder.js +11 -0
  44. package/utils/lightModeInput.js +14 -0
  45. package/utils/setups/orms/typeOrmSetup.js +11 -4
  46. package/utils/setups/projectSetup.js +10 -2
  47. package/utils/setups/setupAuth.js +334 -18
  48. package/utils/setups/setupDatabase.js +4 -1
  49. package/utils/setups/setupHealth.js +74 -0
  50. package/utils/setups/setupMongoose.js +4 -1
  51. package/utils/setups/setupPrisma.js +110 -1
  52. package/utils/setups/setupSwagger.js +4 -1
  53. package/utils/setups/setupThrottler.js +92 -0
  54. package/utils/shell.js +21 -4
@@ -0,0 +1,88 @@
1
+ const { getFullModeInputs } = require("../../utils/fullModeInput");
2
+ const { getLightModeInputs } = require("../../utils/lightModeInput");
3
+ const { runProjectSetupPipeline } = require("../../utils/setups/projectSetup");
4
+ const readline = require("readline-sync");
5
+
6
+ // Mock dependencies at the top before requiring commands/new
7
+ jest.mock("../../utils/fullModeInput", () => ({
8
+ getFullModeInputs: jest.fn()
9
+ }));
10
+ jest.mock("../../utils/lightModeInput", () => ({
11
+ getLightModeInputs: jest.fn()
12
+ }));
13
+ jest.mock("../../utils/setups/projectSetup");
14
+ jest.mock("readline-sync");
15
+ jest.mock("../../utils/loggers/logSuccess");
16
+ jest.mock("../../utils/loggers/logInfo");
17
+ jest.mock("../../utils/loggers/logWarning");
18
+
19
+ const newCommand = require("../../commands/new");
20
+
21
+ describe("newCommand - Interactive vs Non-Interactive Mode", () => {
22
+ beforeEach(() => {
23
+ jest.clearAllMocks();
24
+ });
25
+
26
+ it("should run non-interactively in full mode if --orm and --full are passed", async () => {
27
+ runProjectSetupPipeline.mockResolvedValue();
28
+
29
+ const flags = { orm: "prisma", full: true, docker: false, auth: false, swagger: false };
30
+ await newCommand("test-non-interactive", flags);
31
+
32
+ expect(runProjectSetupPipeline).toHaveBeenCalledWith(
33
+ expect.objectContaining({
34
+ projectName: "test-non-interactive",
35
+ mode: "full",
36
+ useAuth: false,
37
+ useSwagger: false,
38
+ useDocker: false,
39
+ packageManager: "npm",
40
+ dbConfig: expect.objectContaining({
41
+ orm: "prisma"
42
+ })
43
+ })
44
+ );
45
+ });
46
+
47
+ it("should run non-interactively in light mode if --orm and --light are passed", async () => {
48
+ runProjectSetupPipeline.mockResolvedValue();
49
+
50
+ const flags = { orm: "mongoose", light: true, auth: true, swagger: true };
51
+ await newCommand("test-light-non-interactive", flags);
52
+
53
+ expect(runProjectSetupPipeline).toHaveBeenCalledWith(
54
+ expect.objectContaining({
55
+ projectName: "test-light-non-interactive",
56
+ mode: "light",
57
+ useAuth: true,
58
+ useSwagger: true,
59
+ useDocker: true, // defaults to true
60
+ packageManager: "npm",
61
+ dbConfig: expect.objectContaining({
62
+ orm: "mongoose"
63
+ })
64
+ })
65
+ );
66
+ });
67
+
68
+ it("should prompt if mandatory flags are missing", async () => {
69
+ getFullModeInputs.mockResolvedValue({
70
+ projectName: "test-app",
71
+ mode: "full",
72
+ useAuth: false,
73
+ useSwagger: false,
74
+ useDocker: true,
75
+ packageManager: "npm",
76
+ entitiesData: { entities: [], relations: [] },
77
+ selectedDB: "postgresql",
78
+ dbConfig: { orm: "prisma" }
79
+ });
80
+
81
+ runProjectSetupPipeline.mockResolvedValue();
82
+
83
+ // No mode/light/full flags, only orm -> should fall back to interactive
84
+ await newCommand("test-app", { orm: "prisma" });
85
+
86
+ expect(getFullModeInputs).toHaveBeenCalled();
87
+ });
88
+ });
@@ -0,0 +1,202 @@
1
+ // tests/unit/relationCommand.spec.js
2
+ //
3
+ // Tests unitaires pour la commande `nestcraftx g relation` (FEAT-14).
4
+ // Valide :
5
+ // - askRelationCommand : validation idempotence, entités insuffisantes
6
+ // - handleRelationGeneration : orchestration correcte (mocks)
7
+ // - addPrismaRelation : génération des bons champs selon le type de relation
8
+
9
+ const inquirer = require('inquirer');
10
+
11
+ // Mock inquirer globalement avant d'importer askRelationCommand
12
+ jest.mock('inquirer', () => {
13
+ const mockPrompt = jest.fn();
14
+ return {
15
+ prompt: mockPrompt,
16
+ default: {
17
+ prompt: mockPrompt,
18
+ },
19
+ };
20
+ });
21
+
22
+ const { askRelationCommand } = require('../../utils/interactive/askRelationCommand');
23
+ const { resolveForeignKeyOwner } = require('../../utils/generators/relation/relation.engine');
24
+
25
+ // ─── Helpers ────────────────────────────────────────────────────────────────
26
+
27
+ function makeConfig({ entities = [], relations = [] } = {}) {
28
+ return {
29
+ mode: 'full',
30
+ orm: 'prisma',
31
+ swagger: false,
32
+ auth: false,
33
+ entities,
34
+ relations,
35
+ };
36
+ }
37
+
38
+ // ─── Tests : askRelationCommand ──────────────────────────────────────────────
39
+
40
+ describe('askRelationCommand', () => {
41
+ beforeEach(() => {
42
+ jest.clearAllMocks();
43
+ });
44
+
45
+ it('should return null and log error if fewer than 2 entities exist', async () => {
46
+ const config = makeConfig({ entities: [{ name: 'Post' }] });
47
+
48
+ // Intercepter logError sans faire crasher le test
49
+ const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
50
+
51
+ const result = await askRelationCommand(config);
52
+
53
+ expect(result).toBeNull();
54
+ errorSpy.mockRestore();
55
+ });
56
+
57
+ it('should return null and log warning if relation already exists (idempotence)', async () => {
58
+ const config = makeConfig({
59
+ entities: [{ name: 'Post' }, { name: 'User' }],
60
+ relations: [{ from: 'post', to: 'user', type: 'n-1' }],
61
+ });
62
+
63
+ const mockPrompt = inquirer.prompt;
64
+ mockPrompt
65
+ .mockResolvedValueOnce({ source: 'Post' })
66
+ .mockResolvedValueOnce({ target: 'User' })
67
+ .mockResolvedValueOnce({ type: 'n-1' });
68
+
69
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
70
+
71
+ const result = await askRelationCommand(config);
72
+
73
+ // Relation already exists — should be blocked
74
+ expect(result).toBeNull();
75
+ warnSpy.mockRestore();
76
+ });
77
+
78
+ it('should return relation data when valid inputs are provided', async () => {
79
+ const config = makeConfig({
80
+ entities: [{ name: 'Post' }, { name: 'User' }],
81
+ relations: [], // Aucune relation existante
82
+ });
83
+
84
+ const mockPrompt = inquirer.prompt;
85
+ mockPrompt
86
+ .mockResolvedValueOnce({ source: 'Post' })
87
+ .mockResolvedValueOnce({ target: 'User' })
88
+ .mockResolvedValueOnce({ type: 'n-1' });
89
+
90
+ const result = await askRelationCommand(config);
91
+
92
+ expect(result).toEqual({
93
+ source: 'post',
94
+ target: 'user',
95
+ type: 'n-1',
96
+ });
97
+ });
98
+ });
99
+
100
+ // ─── Tests : resolveForeignKeyOwner ─────────────────────────────────────────
101
+
102
+ describe('relation.engine - resolveForeignKeyOwner', () => {
103
+ it('should return null for 1-n (FK on the target side)', () => {
104
+ const result = resolveForeignKeyOwner('Post', 'User', '1-n');
105
+ expect(result).toBeNull();
106
+ });
107
+
108
+ it('should return owner for n-1 (FK on the source side)', () => {
109
+ const result = resolveForeignKeyOwner('Post', 'User', 'n-1');
110
+ expect(result).not.toBeNull();
111
+ expect(result.ownerEntity).toBe('User');
112
+ expect(result.relatedEntity).toBe('Post');
113
+ });
114
+
115
+ it('should return owner for 1-1 (FK on source by convention)', () => {
116
+ const result = resolveForeignKeyOwner('Post', 'User', '1-1');
117
+ expect(result).not.toBeNull();
118
+ expect(result.ownerEntity).toBe('User');
119
+ });
120
+
121
+ it('should return null for n-n (pivot table, no direct FK)', () => {
122
+ const result = resolveForeignKeyOwner('Post', 'Tag', 'n-n');
123
+ expect(result).toBeNull();
124
+ });
125
+
126
+ it('should throw for unknown relation type', () => {
127
+ expect(() => resolveForeignKeyOwner('Post', 'User', 'x-y')).toThrow('Unknown relation type');
128
+ });
129
+ });
130
+
131
+ // ─── Tests : addPrismaRelation (génération de champs) ───────────────────────
132
+
133
+ describe('addPrismaRelation - field generation logic', () => {
134
+ function resolveSourceFields(source, target, type) {
135
+ const targetLow = target.toLowerCase();
136
+ const targetCap = target.charAt(0).toUpperCase() + target.slice(1);
137
+ switch (type) {
138
+ case 'n-1': return `${targetLow} ${targetCap} @relation(fields: [${targetLow}Id], references: [id])\n ${targetLow}Id String`;
139
+ case '1-n': return `${targetLow}s ${targetCap}[]`;
140
+ case '1-1': return `${targetLow} ${targetCap}? @relation(fields: [${targetLow}Id], references: [id])\n ${targetLow}Id String? @unique`;
141
+ case 'n-n': return `${targetLow}s ${targetCap}[]`;
142
+ default: throw new Error(`Unknown relation type: ${type}`);
143
+ }
144
+ }
145
+
146
+ it('n-1: source should get a FK field and @relation decorator', () => {
147
+ const fields = resolveSourceFields('post', 'user', 'n-1');
148
+ expect(fields).toContain('userId String');
149
+ expect(fields).toContain('@relation(fields: [userId], references: [id])');
150
+ });
151
+
152
+ it('1-n: source should get an array field', () => {
153
+ const fields = resolveSourceFields('user', 'post', '1-n');
154
+ expect(fields).toContain('posts Post[]');
155
+ });
156
+
157
+ it('1-1: source should get a unique FK field', () => {
158
+ const fields = resolveSourceFields('profile', 'user', '1-1');
159
+ expect(fields).toContain('userId String? @unique');
160
+ expect(fields).toContain('@relation(fields: [userId], references: [id])');
161
+ });
162
+
163
+ it('n-n: source should get an array field (pivot)', () => {
164
+ const fields = resolveSourceFields('post', 'tag', 'n-n');
165
+ expect(fields).toContain('tags Tag[]');
166
+ });
167
+
168
+ it('should throw for unknown relation type', () => {
169
+ expect(() => resolveSourceFields('post', 'user', 'bad')).toThrow('Unknown relation type');
170
+ });
171
+ });
172
+
173
+ // ─── Tests : .nestcraftxrc persistence logic ─────────────────────────────────
174
+
175
+ describe('handleRelationGeneration - .nestcraftxrc update logic', () => {
176
+ it('should add a new relation to config.relations', () => {
177
+ const config = makeConfig({ relations: [] });
178
+ const newRelation = { from: 'post', to: 'user', type: 'n-1' };
179
+
180
+ config.relations.push(newRelation);
181
+
182
+ expect(config.relations).toHaveLength(1);
183
+ expect(config.relations[0]).toMatchObject({ from: 'post', to: 'user', type: 'n-1' });
184
+ });
185
+
186
+ it('should not duplicate a relation already in config.relations', () => {
187
+ const config = makeConfig({
188
+ relations: [{ from: 'post', to: 'user', type: 'n-1' }],
189
+ });
190
+
191
+ const source = 'post';
192
+ const target = 'user';
193
+ const type = 'n-1';
194
+
195
+ config.relations = config.relations.filter(
196
+ (r) => !(r.from.toLowerCase() === source && r.to.toLowerCase() === target && r.type === type),
197
+ );
198
+ config.relations.push({ from: source, to: target, type });
199
+
200
+ expect(config.relations).toHaveLength(1);
201
+ });
202
+ });
@@ -0,0 +1,117 @@
1
+ // tests/unit/throttler.spec.js
2
+ //
3
+ // Tests unitaires pour la configuration de @nestjs/throttler (FEAT-08).
4
+ // Valide :
5
+ // - L'installation du package avec le bon package manager
6
+ // - La configuration correcte de app.module.ts (imports, modules, providers)
7
+ // - L'idempotence de la configuration
8
+
9
+ const fs = require('fs');
10
+ const { setupThrottler } = require('../../utils/setups/setupThrottler');
11
+ const { runCommand } = require('../../utils/shell');
12
+ const { updateFile } = require('../../utils/file-system');
13
+
14
+ // Mock shell et file-system
15
+ jest.mock('../../utils/shell', () => ({
16
+ runCommand: jest.fn().mockResolvedValue(undefined),
17
+ }));
18
+
19
+ jest.mock('../../utils/file-system', () => ({
20
+ updateFile: jest.fn().mockResolvedValue(undefined),
21
+ createFile: jest.fn().mockResolvedValue(undefined),
22
+ }));
23
+
24
+ describe('setupThrottler', () => {
25
+ const mockAppModulePath = 'src/app.module.ts';
26
+
27
+ beforeEach(() => {
28
+ jest.clearAllMocks();
29
+ jest.spyOn(fs, 'existsSync').mockReturnValue(true);
30
+ });
31
+
32
+ afterEach(() => {
33
+ jest.restoreAllMocks();
34
+ });
35
+
36
+ it('should run installation with selected package manager', async () => {
37
+ const fsReadSpy = jest.spyOn(fs, 'readFileSync').mockReturnValue(`
38
+ import { Module } from '@nestjs/common';
39
+ @Module({
40
+ imports: [],
41
+ })
42
+ export class AppModule {}
43
+ `);
44
+ const fsWriteSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
45
+
46
+ await setupThrottler({ packageManager: 'pnpm' });
47
+
48
+ expect(runCommand).toHaveBeenCalledWith(
49
+ 'pnpm add @nestjs/throttler@^6.0.0',
50
+ expect.any(String),
51
+ expect.any(Object)
52
+ );
53
+
54
+ fsReadSpy.mockRestore();
55
+ fsWriteSpy.mockRestore();
56
+ });
57
+
58
+ it('should update app.module.ts with imports, module and guard provider', async () => {
59
+ const originalContent = `import { Module } from '@nestjs/common';
60
+ import { AppController } from './app.controller';
61
+
62
+ @Module({
63
+ imports: [],
64
+ controllers: [AppController],
65
+ providers: [],
66
+ })
67
+ export class AppModule {}`;
68
+
69
+ let writtenContent = '';
70
+ const fsReadSpy = jest.spyOn(fs, 'readFileSync').mockReturnValue(originalContent);
71
+ const fsWriteSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation((path, content) => {
72
+ writtenContent = content;
73
+ });
74
+
75
+ await setupThrottler({ packageManager: 'npm' });
76
+
77
+ // Doit avoir écrit le fichier
78
+ expect(fsWriteSpy).toHaveBeenCalledWith(mockAppModulePath, expect.any(String), 'utf8');
79
+
80
+ // Doit contenir les nouveaux imports
81
+ expect(writtenContent).toContain("import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';");
82
+ expect(writtenContent).toContain("import { APP_GUARD } from '@nestjs/core';");
83
+
84
+ // Doit contenir ThrottlerModule.forRoot
85
+ expect(writtenContent).toContain('ThrottlerModule.forRoot');
86
+ expect(writtenContent).toContain('ttl: 60000');
87
+ expect(writtenContent).toContain('limit: 10');
88
+
89
+ // Doit contenir le provider APP_GUARD
90
+ expect(writtenContent).toContain('provide: APP_GUARD');
91
+ expect(writtenContent).toContain('useClass: ThrottlerGuard');
92
+
93
+ fsReadSpy.mockRestore();
94
+ fsWriteSpy.mockRestore();
95
+ });
96
+
97
+ it('should not modify app.module.ts if Throttler is already configured (idempotence)', async () => {
98
+ const alreadyConfiguredContent = `import { Module } from '@nestjs/common';
99
+ import { ThrottlerModule } from '@nestjs/throttler';
100
+
101
+ @Module({
102
+ imports: [ThrottlerModule.forRoot()],
103
+ })
104
+ export class AppModule {}`;
105
+
106
+ const fsReadSpy = jest.spyOn(fs, 'readFileSync').mockReturnValue(alreadyConfiguredContent);
107
+ const fsWriteSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
108
+
109
+ await setupThrottler({ packageManager: 'npm' });
110
+
111
+ // writeFileSync ne doit pas être appelé car Throttler est déjà configuré
112
+ expect(fsWriteSpy).not.toHaveBeenCalled();
113
+
114
+ fsReadSpy.mockRestore();
115
+ fsWriteSpy.mockRestore();
116
+ });
117
+ });
@@ -124,7 +124,7 @@ async function setupCleanArchitecture(inputs) {
124
124
  });
125
125
 
126
126
  // 3. Repository Implémentation
127
- await generateRepository(entity.name, dbConfig.orm);
127
+ await generateRepository(entity.name, dbConfig.orm, entity);
128
128
 
129
129
  // 4. Use Cases
130
130
  const useCases = ["Create", "GetById", "GetAll", "Update", "Delete"];
@@ -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 += `# ------------------------------------------------------------------------------
@@ -18,8 +18,11 @@ 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