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
@@ -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) {
package/utils/helpers.js CHANGED
@@ -94,10 +94,16 @@ function formatType(type) {
94
94
  * @returns {string} Le type final formaté
95
95
  */
96
96
  function getFormattedType(field) {
97
+ let typeStr;
97
98
  if (field.name === "role" && field.type.toLowerCase().startsWith("string")) {
98
- return "Role";
99
+ typeStr = "Role";
100
+ } else {
101
+ typeStr = formatType(field.type);
99
102
  }
100
- return formatType(field.type);
103
+ if (field.nullable) {
104
+ typeStr += " | null";
105
+ }
106
+ return typeStr;
101
107
  }
102
108
 
103
109
  module.exports = {
@@ -6,78 +6,15 @@ const inquirer = require("inquirer");
6
6
  const { info, success } = require("../colors");
7
7
  const { logWarning } = require("../loggers/logWarning");
8
8
  const { capitalize } = require("../userInput");
9
+ const { buildEntityInteractive } = require("./entityBuilder");
9
10
  const actualInquirer = inquirer.default || inquirer;
10
11
 
11
12
  async function askEntityInputs(targetName) {
12
- const entity = { name: targetName, fields: [], relation: null };
13
-
14
- console.log(
15
- `\n${info("[ENTITY DESIGN]")} Define fields for "${targetName}" :`,
16
- );
17
-
18
- while (true) {
19
- let fname = readline.question(" Field name (leave empty to finish) : ");
20
- if (!fname) break;
21
-
22
- if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(fname)) {
23
- logWarning("Invalid field name.");
24
- continue;
25
- }
26
-
27
- const baseTypeChoices = [
28
- "string",
29
- "text",
30
- "number",
31
- "boolean",
32
- "Date",
33
- "uuid",
34
- "json",
35
- "enum",
36
- "array",
37
- "object",
38
- ];
39
-
40
- const typeAnswer = await actualInquirer.prompt([
41
- {
42
- type: "list",
43
- name: "ftype",
44
- message: `Type for "${fname}"`,
45
- choices: baseTypeChoices,
46
- },
47
- ]);
48
-
49
- let ftype = typeAnswer.ftype;
50
-
51
- // Logique de raffinement des types (identique à ton script original)
52
- if (ftype === "array") {
53
- const inner = await actualInquirer.prompt([
54
- {
55
- type: "list",
56
- name: "innerType",
57
- message: `Type of elements for "${fname}[]"`,
58
- choices: baseTypeChoices.filter(
59
- (c) => c !== "array" && c !== "object",
60
- ),
61
- },
62
- ]);
63
- ftype = `${inner.innerType}[]`;
64
- } else if (ftype === "enum") {
65
- ftype = capitalize(fname) + "Enum";
66
- } else if (ftype === "object") {
67
- const obj = await actualInquirer.prompt([
68
- {
69
- type: "input",
70
- name: "val",
71
- message: "Complex type name :",
72
- default: "json",
73
- },
74
- ]);
75
- ftype = capitalize(obj.val);
76
- }
77
-
78
- entity.fields.push({ name: fname, type: ftype });
79
- console.log(` Type for "${fname}" : ${ftype} ${success("[✓]")}`);
13
+ const result = await buildEntityInteractive(targetName);
14
+ if (!result) {
15
+ throw new Error("Module generation cancelled by user.");
80
16
  }
17
+ const entity = { name: result.name, fields: result.fields, relation: null };
81
18
 
82
19
  const relationData = await askRelationInputs(entity.name);
83
20
  entity.relation = relationData;
@@ -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 };