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
@@ -226,4 +226,81 @@ ${allFields}
226
226
  export const ${entityName}Schema = SchemaFactory.createForClass(${entityName});`.trim();
227
227
  }
228
228
 
229
- module.exports = { generateMongooseSchemaFileContent };
229
+ /**
230
+ * Ajoute une relation entre deux schémas Mongoose EXISTANTS.
231
+ * Patche les deux fichiers .schema.ts pour ajouter les champs de référence.
232
+ *
233
+ * @param {string} source - Nom du module source (ex: "post")
234
+ * @param {string} target - Nom du module cible (ex: "user")
235
+ * @param {string} relationType - "n-1" | "1-n" | "1-1" | "n-n"
236
+ * @param {string} mode - "full" | "light"
237
+ */
238
+ async function addMongooseRelation(source, target, relationType, mode = "full") {
239
+ const fs = require("fs");
240
+ const { updateFile } = require("../../userInput");
241
+
242
+ const sourceLow = source.toLowerCase();
243
+ const targetLow = target.toLowerCase();
244
+ const sourceCap = capitalize(source);
245
+ const targetCap = capitalize(target);
246
+
247
+ const isFull = mode === "full";
248
+
249
+ const sourceSchemaPath = isFull
250
+ ? `src/${sourceLow}/infrastructure/persistence/mongoose/${sourceLow}.schema.ts`
251
+ : `src/${sourceLow}/entities/${sourceLow}.schema.ts`;
252
+
253
+ const targetSchemaPath = isFull
254
+ ? `src/${targetLow}/infrastructure/persistence/mongoose/${targetLow}.schema.ts`
255
+ : `src/${targetLow}/entities/${targetLow}.schema.ts`;
256
+
257
+ // --- Champs à injecter dans SOURCE ---
258
+ let sourceField = "";
259
+ switch (relationType) {
260
+ case "n-1":
261
+ case "1-1":
262
+ // FK : référence vers un document cible
263
+ sourceField = `\n @Prop({ type: 'ObjectId', ref: '${targetCap}' })\n ${targetLow}Id: string;\n`;
264
+ break;
265
+ case "1-n":
266
+ case "n-n":
267
+ // Tableau de références vers la cible
268
+ sourceField = `\n @Prop([{ type: 'ObjectId', ref: '${targetCap}' }])\n ${targetLow}Ids: string[];\n`;
269
+ break;
270
+ }
271
+
272
+ // --- Champs inverses à injecter dans TARGET ---
273
+ let targetField = "";
274
+ switch (relationType) {
275
+ case "n-1":
276
+ case "n-n":
277
+ // Le target a un tableau de sources
278
+ targetField = `\n @Prop([{ type: 'ObjectId', ref: '${sourceCap}' }])\n ${sourceLow}Ids: string[];\n`;
279
+ break;
280
+ case "1-n":
281
+ case "1-1":
282
+ // Le target a une référence unique vers source
283
+ targetField = `\n @Prop({ type: 'ObjectId', ref: '${sourceCap}' })\n ${sourceLow}Id: string;\n`;
284
+ break;
285
+ }
286
+
287
+ // Patch source schema
288
+ if (fs.existsSync(sourceSchemaPath) && sourceField) {
289
+ await updateFile({
290
+ path: sourceSchemaPath,
291
+ pattern: new RegExp(`(export class ${sourceCap} \\{)`),
292
+ replacement: `$1${sourceField}`,
293
+ });
294
+ }
295
+
296
+ // Patch target schema
297
+ if (fs.existsSync(targetSchemaPath) && targetField) {
298
+ await updateFile({
299
+ path: targetSchemaPath,
300
+ pattern: new RegExp(`(export class ${targetCap} \\{)`),
301
+ replacement: `$1${targetField}`,
302
+ });
303
+ }
304
+ }
305
+
306
+ module.exports = { generateMongooseSchemaFileContent, addMongooseRelation };
@@ -26,11 +26,54 @@ const { createFile } = require("../../file-system");
26
26
  * @param {"prisma"|"typeorm"|"mongoose"|"sequelize"} orm - L'ORM cible
27
27
  * @returns {Promise<void>}
28
28
  */
29
- async function generateRepository(entityName, orm) {
29
+ async function generateRepository(entityName, orm, entityData = null) {
30
30
  const entityNameCapitalized = capitalize(entityName);
31
31
  const entityNameLower = entityName.toLowerCase();
32
32
  const entityPath = `src/${entityNameLower}`;
33
33
 
34
+ const path = require("path");
35
+ const fs = require("fs");
36
+
37
+ const configPath = path.join(process.cwd(), ".nestcraftx", ".nestcraftxrc");
38
+ let stringFields = [];
39
+ if (entityData && entityData.fields) {
40
+ stringFields = entityData.fields
41
+ .filter(f => f.type && (f.type.toLowerCase() === "string" || f.type.toLowerCase() === "text"))
42
+ .map(f => f.name);
43
+ } else if (fs.existsSync(configPath)) {
44
+ try {
45
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
46
+ const entity = config.entities?.find(e => e.name.toLowerCase() === entityNameLower);
47
+ if (entity && entity.fields) {
48
+ stringFields = entity.fields
49
+ .filter(f => f.type && (f.type.toLowerCase() === "string" || f.type.toLowerCase() === "text"))
50
+ .map(f => f.name);
51
+ }
52
+ } catch (e) {
53
+ // fallback
54
+ }
55
+ }
56
+
57
+ if (stringFields.length === 0) {
58
+ if (entityNameLower === "user") {
59
+ stringFields = ["email", "username"];
60
+ } else {
61
+ stringFields = ["name", "title"];
62
+ }
63
+ }
64
+
65
+ const prismaWhereClause = stringFields.length > 0
66
+ ? `where.OR = [\n ` + stringFields.map(f => '{ ' + f + ': { contains: search, mode: \'insensitive\' } }').join(',\n ') + `\n ];`
67
+ : `// No searchable string fields defined`;
68
+
69
+ const typeormWhereClause = stringFields.length > 0
70
+ ? `where.push(\n ` + stringFields.map(f => '{ ' + f + ': Like(`%' + '${search}' + '%`) }').join(',\n ') + `\n );`
71
+ : `// No searchable string fields defined`;
72
+
73
+ const mongooseWhereClause = stringFields.length > 0
74
+ ? `query.$or = [\n ` + stringFields.map(f => '{ ' + f + ': { $regex: search, $options: \'i\' } }').join(',\n ') + `\n ];`
75
+ : `// No searchable string fields defined`;
76
+
34
77
  // Méthode supplémentaire findByEmail pour l'entité User
35
78
  const isUser = entityNameLower === "user";
36
79
  const getExtraMethods = (ormType) => {
@@ -81,7 +124,7 @@ async findByEmail(email: string): Promise<${entityNameCapitalized}Entity | null>
81
124
  */
82
125
 
83
126
  import { Injectable, NotFoundException } from '@nestjs/common';
84
- import { Repository } from 'typeorm';
127
+ import { Repository, Like } from 'typeorm';
85
128
  import { InjectRepository } from '@nestjs/typeorm';
86
129
  import { ${entityNameCapitalized}Entity } from '${entityPath}/domain/entities/${entityNameLower}.entity';
87
130
  import { ${entityNameCapitalized} } from 'src/entities/${entityNameCapitalized}.entity';
@@ -125,9 +168,28 @@ export class ${entityNameCapitalized}Repository implements I${entityNameCapitali
125
168
  }
126
169
 
127
170
  // find all
128
- async findAll(): Promise<${entityNameCapitalized}Entity[]> {
129
- const records = await this.repository.find();
130
- return records.map(record => this.mapper.toDomain(record));
171
+ async findAll(page: number = 1, limit: number = 10, search?: string, sortBy?: string, sortOrder?: string): Promise<{ data: ${entityNameCapitalized}Entity[], total: number }> {
172
+ const skip = (page - 1) * limit;
173
+ const where: any = [];
174
+ if (search) {
175
+ ${typeormWhereClause}
176
+ }
177
+ const order: any = {};
178
+ if (sortBy) {
179
+ order[sortBy] = sortOrder?.toUpperCase() === 'ASC' ? 'ASC' : 'DESC';
180
+ } else {
181
+ order.createdAt = 'DESC';
182
+ }
183
+ const [records, total] = await this.repository.findAndCount({
184
+ skip,
185
+ take: limit,
186
+ where: where.length > 0 ? where : undefined,
187
+ order,
188
+ });
189
+ return {
190
+ data: records.map(record => this.mapper.toDomain(record)),
191
+ total,
192
+ };
131
193
  }
132
194
 
133
195
  // delete
@@ -188,9 +250,31 @@ export class ${entityNameCapitalized}Repository implements I${entityNameCapitali
188
250
  }
189
251
 
190
252
  // find all
191
- async findAll(): Promise<${entityNameCapitalized}Entity[]> {
192
- const records = await this.prisma.${entityNameLower}.findMany();
193
- return records.map(record => this.mapper.toDomain(record));
253
+ async findAll(page: number = 1, limit: number = 10, search?: string, sortBy?: string, sortOrder?: string): Promise<{ data: ${entityNameCapitalized}Entity[], total: number }> {
254
+ const skip = (page - 1) * limit;
255
+ const where: any = {};
256
+ if (search) {
257
+ ${prismaWhereClause}
258
+ }
259
+ const orderBy: any = {};
260
+ if (sortBy) {
261
+ orderBy[sortBy] = sortOrder === 'asc' ? 'asc' : 'desc';
262
+ } else {
263
+ orderBy.createdAt = 'desc';
264
+ }
265
+ const [records, total] = await this.prisma.$transaction([
266
+ this.prisma.${entityNameLower}.findMany({
267
+ skip,
268
+ take: limit,
269
+ where,
270
+ orderBy,
271
+ }),
272
+ this.prisma.${entityNameLower}.count({ where }),
273
+ ]);
274
+ return {
275
+ data: records.map(record => this.mapper.toDomain(record)),
276
+ total,
277
+ };
194
278
  }
195
279
 
196
280
  // delete
@@ -250,9 +334,26 @@ export class ${entityNameCapitalized}Repository implements I${entityNameCapitali
250
334
  }
251
335
 
252
336
  // find all
253
- async findAll(): Promise<${entityNameCapitalized}Entity[]> {
254
- const records = await this.model.find().exec();
255
- return records.map(record => this.mapper.toDomain(record));
337
+ async findAll(page: number = 1, limit: number = 10, search?: string, sortBy?: string, sortOrder?: string): Promise<{ data: ${entityNameCapitalized}Entity[], total: number }> {
338
+ const skip = (page - 1) * limit;
339
+ const query: any = {};
340
+ if (search) {
341
+ ${mongooseWhereClause}
342
+ }
343
+ const sort: any = {};
344
+ if (sortBy) {
345
+ sort[sortBy] = sortOrder === 'asc' ? 1 : -1;
346
+ } else {
347
+ sort.createdAt = -1;
348
+ }
349
+ const [records, total] = await Promise.all([
350
+ this.model.find(query).sort(sort).skip(skip).limit(limit).exec(),
351
+ this.model.countDocuments(query).exec(),
352
+ ]);
353
+ return {
354
+ data: records.map(record => this.mapper.toDomain(record)),
355
+ total,
356
+ };
256
357
  }
257
358
 
258
359
  // delete
@@ -311,9 +412,8 @@ export class ${entityNameCapitalized}Repository implements I${entityNameCapitali
311
412
  }
312
413
 
313
414
  // find all
314
- async findAll(): Promise<${entityNameCapitalized}Entity[]> {
315
- const records = await this.model.find();
316
- return records.map(record => this.mapper.toDomain(record));
415
+ async findAll(page: number = 1, limit: number = 10, search?: string, sortBy?: string, sortOrder?: string): Promise<{ data: ${entityNameCapitalized}Entity[], total: number }> {
416
+ throw new Error('Repository not implemented');
317
417
  }
318
418
 
319
419
  // delete
@@ -54,6 +54,19 @@ async function lightModuleGenerator(entity, config) {
54
54
  contente: dtoContent,
55
55
  });
56
56
 
57
+ if (entityNameLow === "user") {
58
+ await createDirectory(`src/common/enums`);
59
+ await createFile({
60
+ path: `src/common/enums/role.enum.ts`,
61
+ contente: `export enum Role {
62
+ USER = 'USER',
63
+ ADMIN = 'ADMIN',
64
+ SUPER_ADMIN = 'SUPER_ADMIN',
65
+ }
66
+ `,
67
+ });
68
+ }
69
+
57
70
  // 4️⃣ Générer le repository
58
71
  const repositoryContent = generateLightRepository(
59
72
  entityNameCap,
@@ -78,6 +91,7 @@ async function lightModuleGenerator(entity, config) {
78
91
  entityNameCap,
79
92
  entityNameLow,
80
93
  config.swagger,
94
+ config.auth || false,
81
95
  );
82
96
  await createFile({
83
97
  path: `${entityPath}/controllers/${entityNameLow}.controller.ts`,
@@ -21,27 +21,59 @@ const { capitalize, decapitalize, pluralize } = require("../../helpers");
21
21
  * @param {string} entityName - Le nom de l'entité (ex: "Post")
22
22
  * @param {string} entityPath - Le chemin de base (ex: "src/post")
23
23
  * @param {boolean} useSwagger - Inclure les décorateurs Swagger
24
+ * @param {boolean} useAuth - Inclure les guards RBAC (JwtAuthGuard + RolesGuard)
24
25
  * @returns {Promise<string>} Le code TypeScript du controller
25
26
  */
26
- async function generateController(entityName, entityPath, useSwagger) {
27
+ async function generateController(entityName, entityPath, useSwagger, useAuth = false) {
27
28
  const entityNameLower = decapitalize(entityName);
28
29
  const entityNameCapitalized = capitalize(entityName);
29
30
  const pluralName = pluralize(entityNameLower);
30
31
 
32
+ // --- Imports NestJS de base ---
33
+ const nestImports = useAuth
34
+ ? `import { Controller, Get, Post, Body, Param, Patch, Delete, HttpCode, HttpStatus, Query, UseGuards } from '@nestjs/common';`
35
+ : `import { Controller, Get, Post, Body, Param, Patch, Delete, HttpCode, HttpStatus, Query } from '@nestjs/common';`;
36
+
37
+ // --- Imports Swagger ---
31
38
  const swaggerImports = useSwagger
32
- ? `import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';`
39
+ ? `import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery${useAuth ? ', ApiBearerAuth, ApiUnauthorizedResponse, ApiForbiddenResponse' : ''} } from '@nestjs/swagger';`
40
+ : "";
41
+
42
+ // --- Imports Guards & Décorateurs (uniquement si auth activé) ---
43
+ const guardImports = useAuth
44
+ ? `import { JwtAuthGuard } from 'src/auth/infrastructure/guards/jwt-auth.guard';
45
+ import { RolesGuard } from 'src/auth/infrastructure/guards/role.guard';
46
+ import { Roles } from 'src/common/decorators/role.decorator';
47
+ import { Public } from 'src/common/decorators/public.decorator';`
33
48
  : "";
34
49
 
50
+ // --- Décorateur de classe Swagger ---
35
51
  const swaggerClassDecorator = useSwagger
36
52
  ? `@ApiTags('${capitalize(pluralName)}')`
37
53
  : "";
38
54
 
55
+ // --- Snippet pour les routes protégées (ADMIN) ---
56
+ const adminGuards = useAuth
57
+ ? ` @UseGuards(JwtAuthGuard, RolesGuard)
58
+ @Roles('ADMIN')`
59
+ : "";
60
+
61
+ // --- Snippet @ApiBearerAuth pour les routes protégées ---
62
+ const apiBearerAuth = useAuth && useSwagger ? ` @ApiBearerAuth()` : "";
63
+ const apiUnauthorized = useAuth && useSwagger
64
+ ? ` @ApiUnauthorizedResponse({ description: 'Missing or invalid JWT token.' })
65
+ @ApiForbiddenResponse({ description: 'Insufficient permissions (ADMIN required).' })`
66
+ : "";
67
+
68
+ // --- Snippet @Public() pour les routes de lecture ---
69
+ const publicDecorator = useAuth ? ` @Public()` : "";
70
+
39
71
  return `
40
- import { Controller, Get, Post, Body, Param, Patch, Delete, HttpCode, HttpStatus, Query } from '@nestjs/common';
72
+ ${nestImports}
41
73
  ${swaggerImports}
42
74
  import { ${entityNameCapitalized}Service } from '${entityPath}/application/services/${entityNameLower}.service';
43
75
  import { Create${entityNameCapitalized}Dto, Update${entityNameCapitalized}Dto } from 'src/${entityNameLower}/application/dtos/${entityNameLower}.dto';
44
-
76
+ ${guardImports}
45
77
 
46
78
  /**
47
79
  * Controller for ${entityNameCapitalized} management.
@@ -55,14 +87,16 @@ export class ${entityNameCapitalized}Controller {
55
87
  ${
56
88
  entityNameLower !== "user"
57
89
  ? `
90
+ ${apiBearerAuth}
91
+ ${adminGuards}
58
92
  @Post()
59
93
  @HttpCode(HttpStatus.CREATED)
60
94
  ${
61
95
  useSwagger
62
- ? `
63
- @ApiOperation({ summary: 'Create a new ${entityNameLower}', description: 'Creates a new record for ${entityNameLower} in the database.' })
96
+ ? `@ApiOperation({ summary: 'Create a new ${entityNameLower}', description: 'Creates a new record for ${entityNameLower} in the database.' })
64
97
  @ApiResponse({ status: 201, description: 'The ${entityNameLower} has been successfully created.' })
65
- @ApiResponse({ status: 400, description: 'Invalid input data.' })`
98
+ @ApiResponse({ status: 400, description: 'Invalid input data.' })
99
+ ${apiUnauthorized}`
66
100
  : ""
67
101
  }
68
102
  async create(@Body() dto: Create${entityNameCapitalized}Dto) {
@@ -76,23 +110,36 @@ export class ${entityNameCapitalized}Controller {
76
110
  : ""
77
111
  }
78
112
 
113
+ ${publicDecorator}
79
114
  @Get()
80
115
  ${
81
116
  useSwagger
82
- ? `
83
- @ApiOperation({ summary: 'Get all ${pluralName}', description: 'Retrieves a list of all ${pluralName} available.' })
117
+ ? `@ApiOperation({ summary: 'Get all ${pluralName}', description: 'Retrieves a list of all ${pluralName} available.' })
118
+ @ApiQuery({ name: 'page', required: false, type: Number, schema: { default: 1 } })
119
+ @ApiQuery({ name: 'limit', required: false, type: Number, schema: { default: 10 } })
120
+ @ApiQuery({ name: 'search', required: false, type: String, description: 'Search term' })
121
+ @ApiQuery({ name: 'sortBy', required: false, type: String, description: 'Field to sort by' })
122
+ @ApiQuery({ name: 'sortOrder', required: false, enum: ['asc', 'desc'], description: 'Sort order' })
84
123
  @ApiResponse({ status: 200, description: 'Return all ${pluralName}.' })`
85
124
  : ""
86
125
  }
87
- async getAll() {
88
- return await this.service.getAll();
126
+ async getAll(
127
+ @Query('page') page?: number,
128
+ @Query('limit') limit?: number,
129
+ @Query('search') search?: string,
130
+ @Query('sortBy') sortBy?: string,
131
+ @Query('sortOrder') sortOrder?: string,
132
+ ) {
133
+ const pageNum = page ? Number(page) : 1;
134
+ const limitNum = limit ? Number(limit) : 10;
135
+ return await this.service.getAll(pageNum, limitNum, search, sortBy, sortOrder);
89
136
  }
90
137
 
138
+ ${publicDecorator}
91
139
  @Get(':id')
92
140
  ${
93
141
  useSwagger
94
- ? `
95
- @ApiOperation({ summary: 'Get ${entityNameLower} by ID' })
142
+ ? `@ApiOperation({ summary: 'Get ${entityNameLower} by ID' })
96
143
  @ApiParam({ name: 'id', description: 'The unique identifier of the ${entityNameLower}' })
97
144
  @ApiResponse({ status: 200, description: 'The ${entityNameLower} has been found.' })
98
145
  @ApiResponse({ status: 404, description: '${entityNameCapitalized} not found.' })`
@@ -102,14 +149,16 @@ export class ${entityNameCapitalized}Controller {
102
149
  return await this.service.getById(id);
103
150
  }
104
151
 
152
+ ${apiBearerAuth}
153
+ ${adminGuards}
105
154
  @Patch(':id')
106
155
  ${
107
156
  useSwagger
108
- ? `
109
- @ApiOperation({ summary: 'Update an existing ${entityNameLower}' })
157
+ ? `@ApiOperation({ summary: 'Update an existing ${entityNameLower}' })
110
158
  @ApiParam({ name: 'id', description: 'The unique identifier of the ${entityNameLower} to update' })
111
159
  @ApiResponse({ status: 200, description: 'The ${entityNameLower} has been successfully updated.' })
112
- @ApiResponse({ status: 404, description: '${entityNameCapitalized} not found.' })`
160
+ @ApiResponse({ status: 404, description: '${entityNameCapitalized} not found.' })
161
+ ${apiUnauthorized}`
113
162
  : ""
114
163
  }
115
164
  async update(
@@ -120,15 +169,17 @@ export class ${entityNameCapitalized}Controller {
120
169
  return { message: '${entityNameCapitalized} updated successfully' };
121
170
  }
122
171
 
172
+ ${apiBearerAuth}
173
+ ${adminGuards}
123
174
  @Delete(':id')
124
175
  @HttpCode(HttpStatus.NO_CONTENT)
125
176
  ${
126
177
  useSwagger
127
- ? `
128
- @ApiOperation({ summary: 'Delete a ${entityNameLower}' })
178
+ ? `@ApiOperation({ summary: 'Delete a ${entityNameLower}' })
129
179
  @ApiParam({ name: 'id', description: 'The unique identifier of the ${entityNameLower} to delete' })
130
180
  @ApiResponse({ status: 204, description: 'The ${entityNameLower} has been successfully deleted.' })
131
- @ApiResponse({ status: 404, description: '${entityNameCapitalized} not found.' })`
181
+ @ApiResponse({ status: 404, description: '${entityNameCapitalized} not found.' })
182
+ ${apiUnauthorized}`
132
183
  : ""
133
184
  }
134
185
  async delete(@Param('id') id: string) {
@@ -0,0 +1,98 @@
1
+ // utils/interactive/askRelationCommand.js
2
+ //
3
+ // Prompt interactif autonome pour `nestcraftx g relation`.
4
+ // Contrairement à askRelationInputs (qui s'intègre à la création d'un module),
5
+ // cette fonction demande AUSSI la source, car les deux modules existent déjà.
6
+
7
+ const inquirer = require("inquirer");
8
+ const { capitalize } = require("../userInput");
9
+ const { logWarning } = require("../loggers/logWarning");
10
+ const { logError } = require("../loggers/logError");
11
+
12
+ const actualInquirer = inquirer.default || inquirer;
13
+
14
+ /**
15
+ * Demande interactivement la source, la cible et le type de relation.
16
+ * Valide que les deux modules existent dans config.entities[].
17
+ * Vérifie l'idempotence (relation déjà enregistrée dans config.relations[]).
18
+ *
19
+ * @param {object} config - La config lue depuis .nestcraftxrc
20
+ * @returns {Promise<{source: string, target: string, type: string} | null>}
21
+ */
22
+ async function askRelationCommand(config) {
23
+ const entities = (config.entities || []).map((e) => capitalize(e.name));
24
+
25
+ if (entities.length < 2) {
26
+ logError(
27
+ "❌ Vous devez avoir au moins 2 modules générés pour créer une relation.\n" +
28
+ " Lancez d'abord : nestcraftx g module <NomEntité>",
29
+ );
30
+ return null;
31
+ }
32
+
33
+ // 1. Demander la source
34
+ const { source } = await actualInquirer.prompt([
35
+ {
36
+ type: "list",
37
+ name: "source",
38
+ message: "Module SOURCE (le module qui porte la relation) :",
39
+ choices: entities,
40
+ },
41
+ ]);
42
+
43
+ // 2. Demander la cible (exclure la source)
44
+ const targetChoices = entities.filter((e) => e !== source);
45
+
46
+ if (targetChoices.length === 0) {
47
+ logError("❌ Aucun autre module disponible comme cible.");
48
+ return null;
49
+ }
50
+
51
+ const { target } = await actualInquirer.prompt([
52
+ {
53
+ type: "list",
54
+ name: "target",
55
+ message: `Module CIBLE (lié à ${source}) :`,
56
+ choices: targetChoices,
57
+ },
58
+ ]);
59
+
60
+ // 3. Demander le type de relation
61
+ const { type } = await actualInquirer.prompt([
62
+ {
63
+ type: "list",
64
+ name: "type",
65
+ message: `Type de relation (${source} → ${target}) :`,
66
+ choices: [
67
+ { name: `n-1 (Many-to-One : chaque ${source} appartient à un ${target})`, value: "n-1" },
68
+ { name: `1-n (One-to-Many : un ${source} possède plusieurs ${target}s)`, value: "1-n" },
69
+ { name: `1-1 (One-to-One : un ${source} est lié à exactement un ${target})`, value: "1-1" },
70
+ { name: `n-n (Many-to-Many : plusieurs ${source}s liés à plusieurs ${target}s)`, value: "n-n" },
71
+ ],
72
+ },
73
+ ]);
74
+
75
+ // 4. Vérification d'idempotence — relation déjà enregistrée ?
76
+ const existing = (config.relations || []).find(
77
+ (r) =>
78
+ r.from.toLowerCase() === source.toLowerCase() &&
79
+ r.to.toLowerCase() === target.toLowerCase() &&
80
+ r.type === type,
81
+ );
82
+
83
+ if (existing) {
84
+ logWarning(
85
+ `⚠️ La relation ${source} →(${type})→ ${target} existe déjà dans .nestcraftxrc.\n` +
86
+ " Aucune modification effectuée.",
87
+ );
88
+ return null;
89
+ }
90
+
91
+ return {
92
+ source: source.toLowerCase(),
93
+ target: target.toLowerCase(),
94
+ type,
95
+ };
96
+ }
97
+
98
+ module.exports = { askRelationCommand };
@@ -50,6 +50,7 @@ async function buildEntityInteractive(initialName) {
50
50
  if (f.unique) constraints.push("unique");
51
51
  if (f.nullable) constraints.push("nullable");
52
52
  if (f.default !== null && f.default !== "") constraints.push(`default: ${f.default}`);
53
+ if (f.description) constraints.push(`desc: "${f.description}"`);
53
54
  const constraintStr = constraints.length > 0 ? ` (${constraints.join(", ")})` : "";
54
55
  console.log(` ${idx + 1}. ${f.name} : ${f.type}${constraintStr}`);
55
56
  });
@@ -256,12 +257,22 @@ async function promptFieldDetails(fieldName, existingField = null) {
256
257
  defaultValue = null;
257
258
  }
258
259
 
260
+ let descriptionPrompt = " Description (press Enter for auto): ";
261
+ if (existingField && existingField.description) {
262
+ descriptionPrompt = ` Description [${existingField.description}]: `;
263
+ }
264
+ let descriptionValue = readline.question(descriptionPrompt);
265
+ if (descriptionValue === "") {
266
+ descriptionValue = existingField ? existingField.description : null;
267
+ }
268
+
259
269
  return {
260
270
  name: fieldName,
261
271
  type: ftype,
262
272
  unique: isUnique,
263
273
  nullable: isNullable,
264
274
  default: defaultValue,
275
+ description: descriptionValue || null,
265
276
  };
266
277
  }
267
278
 
@@ -94,12 +94,16 @@ async function getLightModeInputs(projectName, flags) {
94
94
  // JWT Auth config
95
95
  const useAuth = getAuthChoice(flags);
96
96
 
97
+ // Throttler config
98
+ const useThrottler = getThrottlerChoice(flags);
99
+
97
100
  const packageManager = await getPackageManager(flags);
98
101
  inputs.packageManager = packageManager;
99
102
 
100
103
  inputs.useAuth = useAuth;
101
104
  inputs.useSwagger = useSwagger;
102
105
  inputs.useDocker = useDocker;
106
+ inputs.useThrottler = useThrottler;
103
107
 
104
108
  if (useAuth) {
105
109
  console.log(
@@ -224,4 +228,14 @@ function getDockerChoice(flags) {
224
228
  return readline.keyInYNStrict(`${info("[?]")} Generate Docker files ?`);
225
229
  }
226
230
 
231
+ function getThrottlerChoice(flags) {
232
+ if (flags.throttler !== undefined) {
233
+ console.log(
234
+ `${info("[INFO]")} Throttler: ${flags.throttler ? "Yes" : "No"} (via flag)`
235
+ );
236
+ return flags.throttler === true || flags.throttler === "true";
237
+ }
238
+ return readline.keyInYNStrict(`${info("[?]")} Enable Rate Limiting (Throttler) ?`);
239
+ }
240
+
227
241
  module.exports = { getLightModeInputs };
@@ -21,8 +21,11 @@ async function setupTypeORM(inputs) {
21
21
  logInfo("📦 Installing TypeORM and PostgreSQL dependencies...");
22
22
 
23
23
  const mode = inputs.mode;
24
+ const pkgManager = inputs.packageManager || "npm";
25
+ const installCmd = pkgManager === "yarn" || pkgManager === "pnpm" ? `${pkgManager} add` : "npm install";
26
+
24
27
  await runCommand(
25
- "npm install @nestjs/typeorm typeorm pg reflect-metadata",
28
+ `${installCmd} @nestjs/typeorm@^10.0.2 typeorm@^0.3.20 pg@^8.11.3 reflect-metadata@^0.2.1`,
26
29
  "TypeORM and PostgreSQL dependencies installed successfully"
27
30
  ); // Updating app.module.ts with TypeORM
28
31
 
@@ -446,13 +449,17 @@ async function setupTypeOrmSeeding(inputs) {
446
449
  "typeorm-extension",
447
450
  ];
448
451
 
452
+ const pkgManager = inputs.packageManager || "npm";
453
+ const installDevCmd = pkgManager === "yarn" || pkgManager === "pnpm" ? `${pkgManager} add -D` : "npm install -D";
454
+ const installCmd = pkgManager === "yarn" || pkgManager === "pnpm" ? `${pkgManager} add` : "npm install";
455
+
449
456
  await runCommand(
450
- `${inputs.packageManager} add -D ${typeOrmDevDeps.join(" ")}`,
457
+ `${installDevCmd} ${typeOrmDevDeps.join(" ")}`,
451
458
  "❌ Failed to install TypeORM seeding dependencies"
452
459
  );
453
460
 
454
461
  await runCommand(
455
- `${inputs.packageManager} install bcrypt`,
462
+ `${installCmd} bcrypt`,
456
463
  "❌ Failed to install bcrypt"
457
464
  );
458
465
 
@@ -462,7 +469,7 @@ async function setupTypeOrmSeeding(inputs) {
462
469
  "typeorm-ts-node-commonjs migration:run -d ./src/database/typeorm.config.ts",
463
470
  "typeorm:seed":
464
471
  "typeorm-extension seed:run -d src/database/typeorm.config.ts",
465
- seed: "npm run typeorm:seed",
472
+ seed: `${pkgManager} run typeorm:seed`,
466
473
  };
467
474
 
468
475
  await updatePackageJson(inputs, typeOrmScripts);