nestcraftx 0.2.4 → 0.2.6

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 (63) hide show
  1. package/.gitattributes +6 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +33 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.md +19 -0
  4. package/.github/ISSUE_TEMPLATE/pull_request_template.md +24 -0
  5. package/CHANGELOG.fr.md +97 -97
  6. package/CHANGELOG.md +98 -98
  7. package/CLI_USAGE.fr.md +331 -331
  8. package/CLI_USAGE.md +364 -364
  9. package/DEMO.fr.md +292 -292
  10. package/DEMO.md +294 -294
  11. package/LICENSE +21 -21
  12. package/MIGRATION_GUIDE.fr.md +127 -127
  13. package/MIGRATION_GUIDE.md +124 -124
  14. package/QUICK_START.fr.md +152 -152
  15. package/QUICK_START.md +169 -169
  16. package/README.fr.md +653 -659
  17. package/SECURITY.md +10 -0
  18. package/bin/nestcraft.js +84 -64
  19. package/commands/demo.js +333 -330
  20. package/commands/generate.js +93 -0
  21. package/commands/generateConf.js +91 -0
  22. package/commands/help.js +78 -78
  23. package/commands/info.js +48 -48
  24. package/commands/new.js +338 -335
  25. package/commands/start.js +19 -19
  26. package/commands/test.js +7 -7
  27. package/package.json +41 -41
  28. package/readme.md +638 -643
  29. package/utils/cliParser.js +133 -76
  30. package/utils/colors.js +62 -62
  31. package/utils/configs/configureDocker.js +120 -120
  32. package/utils/configs/setupCleanArchitecture.js +563 -557
  33. package/utils/configs/setupLightArchitecture.js +701 -660
  34. package/utils/envGenerator.js +122 -122
  35. package/utils/file-utils/packageJsonUtils.js +49 -55
  36. package/utils/file-utils/saveProjectConfig.js +36 -0
  37. package/utils/fullModeInput.js +607 -607
  38. package/utils/generators/application/dtoUpdater.js +54 -0
  39. package/utils/generators/cleanModuleGenerator.js +475 -0
  40. package/utils/generators/database/setupDatabase.js +31 -0
  41. package/utils/generators/domain/entityUpdater.js +78 -0
  42. package/utils/generators/infrastructure/mapperUpdater.js +65 -0
  43. package/utils/generators/lightModuleGenerator.js +131 -0
  44. package/utils/generators/relation/relation.engine.js +64 -0
  45. package/utils/interactive/askEntityInputs.js +165 -0
  46. package/utils/lightModeInput.js +460 -460
  47. package/utils/loggers/logError.js +7 -7
  48. package/utils/loggers/logInfo.js +7 -7
  49. package/utils/loggers/logSuccess.js +7 -7
  50. package/utils/loggers/logWarning.js +7 -7
  51. package/utils/setups/orms/typeOrmSetup.js +630 -630
  52. package/utils/setups/projectSetup.js +46 -46
  53. package/utils/setups/setupAuth.js +973 -926
  54. package/utils/setups/setupDatabase.js +75 -75
  55. package/utils/setups/setupLogger.js +69 -59
  56. package/utils/setups/setupMongoose.js +377 -432
  57. package/utils/setups/setupPrisma.js +802 -630
  58. package/utils/setups/setupSwagger.js +97 -88
  59. package/utils/shell.js +32 -32
  60. package/utils/spinner.js +57 -57
  61. package/utils/systemCheck.js +124 -124
  62. package/utils/userInput.js +421 -421
  63. package/utils/utils.js +2197 -1762
@@ -1,607 +1,607 @@
1
- const readline = require("readline-sync");
2
- const { info, success, warning } = require("./colors");
3
- const inquirer = require("inquirer");
4
- const { capitalize } = require("./userInput");
5
- const { logWarning } = require("./loggers/logWarning");
6
- const { getPackageManager } = require("./utils");
7
- const actualInquirer = inquirer.default || inquirer;
8
-
9
- async function getFullModeInputs(projectName, flags) {
10
- console.log(
11
- `\n${info("[FULL MODE]")} Complete configuration with Clean Architecture\n`
12
- );
13
-
14
- const dataBases = [
15
- {
16
- name: "postgresql",
17
- label: "PostgreSQL",
18
- ormOptions: ["prisma", "typeorm"],
19
- required: [
20
- {
21
- title: "PostgreSQL User",
22
- envVar: "POSTGRES_USER",
23
- defaultValue: "postgres",
24
- hideEchoBack: false,
25
- },
26
- {
27
- title: "PostgreSQL Password",
28
- envVar: "POSTGRES_PASSWORD",
29
- defaultValue: "postgres",
30
- hideEchoBack: true, // Hide password
31
- },
32
- {
33
- title: "PostgreSQL Database Name",
34
- envVar: "POSTGRES_DB",
35
- defaultValue: `${projectName}-db`,
36
- hideEchoBack: false,
37
- },
38
- {
39
- title: "PostgreSQL Host",
40
- envVar: "POSTGRES_HOST",
41
- defaultValue: "localhost",
42
- hideEchoBack: false,
43
- },
44
- {
45
- title: "PostgreSQL Port",
46
- envVar: "POSTGRES_PORT",
47
- defaultValue: "5432",
48
- hideEchoBack: false,
49
- },
50
- ],
51
- },
52
- {
53
- name: "mongodb",
54
- label: "MongoDB",
55
- ormOptions: ["mongoose"],
56
- required: [
57
- {
58
- title: "MongoDB URI",
59
- envVar: "MONGO_URI",
60
- defaultValue: "mongodb://localhost:27017",
61
- hideEchoBack: false,
62
- },
63
- {
64
- title: "MongoDB Database Name",
65
- envVar: "MONGO_DB",
66
- defaultValue: `${projectName}-db`,
67
- hideEchoBack: false,
68
- },
69
- ],
70
- },
71
- ];
72
-
73
- let currentProjectName = projectName;
74
- // La validation du nom de projet reste interactive en cas d'échec
75
- while (true) {
76
- if (/^[A-Za-z][A-Za-z0-9_-]*$/.test(currentProjectName)) break;
77
-
78
- currentProjectName = readline.question(`${info("[?]")} Project name : `);
79
-
80
- logWarning(
81
- "Invalid name. Use letters, numbers, _ or - (start with a letter)."
82
- );
83
- }
84
-
85
- // --- 1. Database Selection ---
86
- const defaultDB = "postgresql";
87
- let usedDB;
88
-
89
- // Prioriser le flag 'db' si présent
90
- if (flags.db && flags.db !== undefined) {
91
- usedDB = getFlagValue(flags, "db", defaultDB);
92
- console.log(
93
- `${info("[?]")} Database (postgresql, mongodb) : ${usedDB} ${success(
94
- "[flag]"
95
- )}`
96
- );
97
- } else {
98
- usedDB = readline.question(
99
- `${info("[?]")} Database (postgresql, mongodb) [${defaultDB}] : `,
100
- { defaultInput: defaultDB }
101
- );
102
- }
103
-
104
- let selectedDB = dataBases.find(
105
- (db) => db.name.toLowerCase() === usedDB.toLowerCase()
106
- );
107
-
108
- // Revenir au mode interactif si le flag fourni est invalide ou si l'utilisateur a saisi une valeur invalide
109
- while (!selectedDB) {
110
- logWarning("Database not recognized.");
111
-
112
- usedDB = readline.question(
113
- `${info("[?]")} Database (postgresql, mongodb) : `
114
- );
115
- selectedDB = dataBases.find(
116
- (db) => db.name.toLowerCase() === usedDB.toLowerCase()
117
- );
118
- }
119
-
120
- // --- 2. DB Configuration (Using flags) ---
121
- const dbConfig = {};
122
- console.log(`\n${info("[INFO]")} ${selectedDB.label} Configuration`);
123
-
124
- selectedDB.required.forEach((field) => {
125
- // Détermine la clé de flag (ex: 'dbUser' pour POSTGRES_USER ou 'mongoUri' pour MONGO_URI)
126
- const flagName = field.envVar
127
- .toLowerCase()
128
- .replace("postgres_", "db")
129
- .replace("mongo_", "mongo");
130
-
131
- const flagValue = flags[flagName];
132
- let answer;
133
-
134
- if (flagValue !== undefined) {
135
- // Flag is present, use its value directly and skip prompt
136
- answer = getFlagValue(flags, flagName, field.defaultValue);
137
-
138
- const displayValue = field.hideEchoBack ? "***" : answer;
139
-
140
- console.log(` ${field.title} : ${displayValue} ${success("[flag]")}`);
141
- } else {
142
- // Flag is absent, ask the question
143
- while (true) {
144
- answer = readline.question(
145
- ` ${field.title} [${field.defaultValue}] : `,
146
- {
147
- hideEchoBack: field.hideEchoBack,
148
- defaultInput: field.defaultValue,
149
- }
150
- );
151
-
152
- // If the user entered something OR if the default value is non-null, continue
153
- if (answer || field.defaultValue !== null) break;
154
-
155
- logWarning("This field is required.");
156
- }
157
- // If the user just pressed Enter, use the default value
158
- answer = answer || field.defaultValue;
159
- }
160
- dbConfig[field.envVar] = answer;
161
- });
162
-
163
- // --- 3. ORM Selection (Full Mode) ---
164
- if (selectedDB.ormOptions && selectedDB.ormOptions.length > 0) {
165
- const defaultOrm = selectedDB.ormOptions[0];
166
- let ormChoice;
167
-
168
- // 1. Vérification du Flag
169
- if (flags.orm !== undefined) {
170
- const flagValue = getFlagValue(flags, "orm", "").toLowerCase();
171
-
172
- if (selectedDB.ormOptions.includes(flagValue)) {
173
- ormChoice = flagValue;
174
- console.log(
175
- `${info("[?]")} ORM for ${selectedDB.label}: ${ormChoice} ${success(
176
- "[flag]"
177
- )}`
178
- );
179
- } else {
180
- logWarning(`ORM flag '${flagValue}' invalid for ${selectedDB.label}.`);
181
- }
182
- }
183
-
184
- // 2. Mode Interactif (si pas de flag ou flag invalide)
185
- if (!ormChoice) {
186
- const answers = await actualInquirer.prompt([
187
- {
188
- type: "list",
189
- name: "orm",
190
- message: `Choose an ORM for ${selectedDB.label}:`,
191
- choices: selectedDB.ormOptions.map((opt) => ({
192
- name: opt.charAt(0).toUpperCase() + opt.slice(1), // Capitalize label
193
- value: opt,
194
- })),
195
- default: defaultOrm,
196
- },
197
- ]);
198
- ormChoice = answers.orm;
199
- }
200
-
201
- dbConfig.orm = ormChoice;
202
- }
203
-
204
- const packageManager = await getPackageManager(flags);
205
-
206
- // --- 4. Boolean Choices (Prioritize flags) ---
207
- const booleanFlags = [
208
- { name: "docker", default: true, prompt: "Generate Docker files?" },
209
-
210
- { name: "auth", default: true, prompt: "Add JWT authentication?" },
211
-
212
- { name: "swagger", default: true, prompt: "Install Swagger?" },
213
- ];
214
-
215
- const booleanResults = {};
216
-
217
- booleanFlags.forEach(({ name, default: defaultValue, prompt }) => {
218
- let result;
219
-
220
- if (flags[name] !== undefined) {
221
- // Flag is present, use its value
222
- result = getFlagValue(flags, name, defaultValue);
223
-
224
- const displayValue = result ? "Yes" : "No";
225
- console.log(
226
- `${info("[?]")} ${prompt} : ${displayValue} ${success("[flag]")}`
227
- );
228
- } else {
229
- // Flag is absent, ask the question
230
- const defaultInput = defaultValue ? "y" : "n";
231
- result = readline.keyInYNStrict(`${info("[?]")} ${prompt}`, {
232
- defaultInput: defaultInput,
233
- });
234
- }
235
- booleanResults[name] = result;
236
- });
237
-
238
- const useDocker = booleanResults.docker;
239
- const useAuth = booleanResults.auth;
240
- const useSwagger = booleanResults.swagger;
241
-
242
- // --- 5. Swagger Configuration (Prioritize flags) ---
243
- let swaggerInputs;
244
- if (useSwagger) {
245
- console.log(`\n${info("[INFO]")} Swagger Configuration`);
246
- const swaggerFields = [
247
- {
248
- name: "title",
249
- flag: "swaggerTitle",
250
- default: `${currentProjectName} API`,
251
- prompt: "API Title",
252
- },
253
- {
254
- name: "description",
255
- flag: "swaggerDesc",
256
- default: "API generated by NestCraftX",
257
- prompt: "Description",
258
- },
259
- {
260
- name: "version",
261
- flag: "swaggerVersion",
262
- default: "1.0.0",
263
- prompt: "Version",
264
- },
265
- {
266
- name: "endpoint",
267
- flag: "swaggerEndpoint",
268
- default: "api/docs",
269
- prompt: "Endpoint",
270
- },
271
- ];
272
-
273
- swaggerInputs = {};
274
-
275
- swaggerFields.forEach((field) => {
276
- const flagValue = flags[field.flag];
277
- const defaultValue = field.default;
278
-
279
- if (flagValue !== undefined) {
280
- // Flag is present, use its value
281
- swaggerInputs[field.name] = flagValue;
282
- console.log(` ${field.prompt} : ${flagValue} ${success("[flag]")}`);
283
- } else {
284
- // Flag is absent, ask the question
285
- swaggerInputs[field.name] = readline.question(
286
- ` ${field.prompt} [${defaultValue}] : `,
287
- { defaultInput: defaultValue }
288
- );
289
- }
290
- });
291
- }
292
-
293
- // --- 6. Entities (Remains fully interactive) ---
294
- const entitiesData = { entities: [], relations: [] };
295
-
296
- if (useAuth) {
297
- console.log(
298
- `\n${info("[INFO]")} Auth active: adding User and Session entities`
299
- );
300
-
301
- // 1. Entité User
302
- entitiesData.entities.push({
303
- name: "user",
304
- fields: [
305
- { name: "email", type: "string", unique: true },
306
- { name: "password", type: "string" },
307
- { name: "role", type: "Role" },
308
- { name: "isActive", type: "boolean", default: true },
309
- ],
310
- });
311
-
312
- // 2. Entité Session
313
- entitiesData.entities.push({
314
- name: "session",
315
- fields: [
316
- { name: "refreshToken", type: "string" },
317
- { name: "userId", type: "string" },
318
- { name: "expiresAt", type: "Date" },
319
- { name: "createdAt", type: "Date", default: "now" },
320
- ],
321
- });
322
-
323
- // 3. relation user & session
324
- entitiesData.relations.push({
325
- from: "user",
326
- to: "session",
327
- type: "1-n",
328
- });
329
- }
330
-
331
- console.log(
332
- `\n${info("[INFO]")} Entity input (FULL Mode - Complete Architecture)`
333
- );
334
-
335
- let addEntity = readline.keyInYNStrict(`${info("[?]")} Add an entity?`);
336
- while (addEntity) {
337
- let name;
338
- while (true) {
339
- name = readline.question(`\n Entity name : `);
340
- if (/^[A-Za-z][A-Za-z0-9_]*$/.test(name)) break;
341
- logWarning("Invalid name. Letters, numbers, _ (start with a letter).");
342
- }
343
-
344
- const fields = [];
345
-
346
- console.log(` Fields for "${name}" :`);
347
- while (true) {
348
- let fname = readline.question(" Field name (leave empty to finish) : ");
349
- if (!fname) break;
350
- if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(fname)) {
351
- logWarning("Invalid field name.");
352
- continue;
353
- }
354
-
355
- const baseTypeChoices = [
356
- "string",
357
- "text",
358
- "number",
359
- "decimal",
360
- "boolean",
361
- "Date",
362
- "uuid",
363
- "json",
364
- "enum",
365
- "array",
366
- "object",
367
- ];
368
-
369
- const typeQuestion = {
370
- type: "list",
371
- name: "ftype",
372
- message: `Type for "${fname}"`,
373
- default: "string",
374
- choices: baseTypeChoices,
375
- transformer: () => "",
376
- };
377
- const typeAnswer = await actualInquirer.prompt([typeQuestion]);
378
- let ftype = typeAnswer.ftype;
379
- process.stdout.write("\x1B[1A");
380
- process.stdout.write("\x1B[K");
381
-
382
- if (ftype === "array") {
383
- const arrayInnerQuestion = {
384
- type: "list",
385
- name: "innerType",
386
- message: `Type of elements for "${fname}[]"`,
387
- default: "string",
388
- choices: baseTypeChoices.filter(
389
- (c) => c !== "array" && c !== "object"
390
- ),
391
- transformer: () => "",
392
- };
393
-
394
- const innerAnswer = await actualInquirer.prompt([arrayInnerQuestion]);
395
- ftype = `${innerAnswer.innerType}[]`;
396
- } else if (ftype === "enum") {
397
- const enumName = capitalize(fname) + "Enum";
398
- console.log(
399
- ` ${info(
400
- "[INFO]"
401
- )} Enum type selected. Consider defining ${enumName} in your code.`
402
- );
403
- ftype = enumName;
404
- } else if (ftype === "object") {
405
- const objectNameQuestion = {
406
- type: "input",
407
- name: "objectName",
408
-
409
- message: `Complex type name (DTO/Class or leave 'json') :`,
410
- default: "json",
411
- transformer: () => "",
412
- };
413
-
414
- const objectAnswer = await actualInquirer.prompt([objectNameQuestion]);
415
- ftype = capitalize(objectAnswer.objectName.trim() || "json");
416
- }
417
-
418
- console.log(` Type for "${fname}" : ${ftype} ${success("[✓]")}`);
419
-
420
- fields.push({ name: fname, type: ftype });
421
- }
422
-
423
- entitiesData.entities.push({ name, fields });
424
- console.log(
425
- `${success("[✓]")} Entity "${name}" added with ${fields.length} field(s)`
426
- );
427
-
428
- addEntity = readline.keyInYNStrict(`${info("[?]")} Add another entity?`);
429
- }
430
-
431
- const wantsRelation = readline.keyInYNStrict(
432
- `${info("[?]")} Add relationships between entities?`
433
- );
434
- if (wantsRelation) {
435
- if (entitiesData.entities.length > 1) {
436
- console.log(`\n${info("[INFO]")} Configuring relationships`);
437
-
438
- let configuring = true;
439
- while (configuring) {
440
- const entityNames = entitiesData.entities.map((e) => e.name);
441
-
442
- // 1. Select entities first
443
- const selection = await actualInquirer.prompt([
444
- {
445
- type: "list",
446
- name: "fromName",
447
- message: "From which entity? (Source)",
448
- choices: entityNames,
449
- },
450
- {
451
- type: "list",
452
- name: "toName",
453
- message: (prev) =>
454
- `To which entity should ${prev.fromName} be linked? (Target)`,
455
- choices: (prev) =>
456
- entityNames.filter((name) => name !== prev.fromName),
457
- },
458
- ]);
459
-
460
- // --- VERIFICATION: Check if link already exists (A->B or B->A) ---
461
- const alreadyExists = entitiesData.relations.find(
462
- (rel) =>
463
- (rel.from === selection.fromName && rel.to === selection.toName) ||
464
- (rel.from === selection.toName && rel.to === selection.fromName)
465
- );
466
-
467
- if (alreadyExists) {
468
- logWarning(
469
- `A relationship already exists between ${selection.fromName} and ${selection.toName} (${alreadyExists.type}).`
470
- );
471
-
472
- const { tryAgain } = await actualInquirer.prompt([
473
- {
474
- type: "confirm",
475
- name: "tryAgain",
476
- message: "Do you want to choose different entities?",
477
- default: true,
478
- },
479
- ]);
480
-
481
- if (!tryAgain) break;
482
- continue; // Restart selection
483
- }
484
-
485
- // 2. Select Relationship type only if verification passed
486
- const typeAnswer = await actualInquirer.prompt([
487
- {
488
- type: "list",
489
- name: "relType",
490
- message: "Relationship type:",
491
- choices: [
492
- {
493
- name: `1-1 (One-to-One) : ${selection.fromName} has one ${selection.toName}`,
494
- value: "1-1",
495
- },
496
- {
497
- name: `1-n (One-to-Many) : ${selection.fromName} has many ${selection.toName}s`,
498
- value: "1-n",
499
- },
500
- {
501
- name: `n-1 (Many-to-One) : Many ${selection.fromName}s belong to one ${selection.toName}`,
502
- value: "n-1",
503
- },
504
- {
505
- name: `n-n (Many-to-Many) : Many ${selection.fromName}s linked to many ${selection.toName}s`,
506
- value: "n-n",
507
- },
508
- ],
509
- },
510
- ]);
511
-
512
- const from = entitiesData.entities.find(
513
- (e) => e.name === selection.fromName
514
- );
515
- const to = entitiesData.entities.find(
516
- (e) => e.name === selection.toName
517
- );
518
- const relType = typeAnswer.relType;
519
-
520
- // Register Relationship
521
- entitiesData.relations.push({
522
- from: from.name,
523
- to: to.name,
524
- type: relType,
525
- });
526
-
527
- const fromLow = from.name.toLowerCase();
528
- const toLow = to.name.toLowerCase();
529
-
530
- // --- Add fields logic ---
531
- if (relType === "1-1") {
532
- from.fields.push(
533
- { name: `${toLow}Id`, type: "string" },
534
- { name: toLow, type: to.name }
535
- );
536
- } else if (relType === "1-n") {
537
- from.fields.push({ name: `${toLow}s`, type: `${to.name}[]` });
538
- to.fields.push(
539
- { name: `${fromLow}Id`, type: "string" },
540
- { name: fromLow, type: from.name }
541
- );
542
- } else if (relType === "n-1") {
543
- from.fields.push(
544
- { name: `${toLow}Id`, type: "string" },
545
- { name: toLow, type: to.name }
546
- );
547
- to.fields.push({ name: `${fromLow}s`, type: `${from.name}[]` });
548
- } else if (relType === "n-n") {
549
- from.fields.push({ name: `${toLow}s`, type: `${to.name}[]` });
550
- to.fields.push({ name: `${fromLow}s`, type: `${from.name}[]` });
551
- }
552
-
553
- console.log(
554
- `\n${success("[✓]")} Relationship added: ${from.name} ${relType} ${
555
- to.name
556
- }`
557
- );
558
-
559
- const { addMore } = await actualInquirer.prompt([
560
- {
561
- type: "confirm",
562
- name: "addMore",
563
- message: "Add another relationship?",
564
- default: false,
565
- },
566
- ]);
567
- configuring = addMore;
568
- }
569
- } else {
570
- logWarning(
571
- "At least two entities are required to configure a relationship."
572
- );
573
- }
574
- }
575
-
576
- return {
577
- projectName: currentProjectName,
578
- useDocker,
579
- useAuth,
580
- useSwagger,
581
- swaggerInputs,
582
- packageManager,
583
- entitiesData,
584
- selectedDB: selectedDB.name,
585
- dbConfig,
586
- mode: "full",
587
- };
588
- }
589
-
590
- /**
591
- * Récupère la valeur d'un flag, ou la valeur par défaut si le flag n'est pas fourni.
592
- * Convertit les flags 'true'/'false' en booléens si nécessaire.
593
- * @param {object} flags - L'objet flags (ex: yargs)
594
- * @param {string} name - Nom du flag (ex: 'auth', 'dbHost')
595
- * @param {*} defaultValue - Valeur par défaut si le flag est absent.
596
- */
597
- function getFlagValue(flags, name, defaultValue) {
598
- const value = flags[name];
599
- if (value !== undefined) {
600
- // Gérer les cas où yargs (ou autre) renvoie une chaîne pour les booléens
601
- if (value === "true") return true;
602
- if (value === "false") return false;
603
- return value;
604
- }
605
- return defaultValue;
606
- }
607
- module.exports = { getFullModeInputs };
1
+ const readline = require("readline-sync");
2
+ const { info, success, warning } = require("./colors");
3
+ const inquirer = require("inquirer");
4
+ const { capitalize } = require("./userInput");
5
+ const { logWarning } = require("./loggers/logWarning");
6
+ const { getPackageManager } = require("./utils");
7
+ const actualInquirer = inquirer.default || inquirer;
8
+
9
+ async function getFullModeInputs(projectName, flags) {
10
+ console.log(
11
+ `\n${info("[FULL MODE]")} Complete configuration with Clean Architecture\n`
12
+ );
13
+
14
+ const dataBases = [
15
+ {
16
+ name: "postgresql",
17
+ label: "PostgreSQL",
18
+ ormOptions: ["prisma", "typeorm"],
19
+ required: [
20
+ {
21
+ title: "PostgreSQL User",
22
+ envVar: "POSTGRES_USER",
23
+ defaultValue: "postgres",
24
+ hideEchoBack: false,
25
+ },
26
+ {
27
+ title: "PostgreSQL Password",
28
+ envVar: "POSTGRES_PASSWORD",
29
+ defaultValue: "postgres",
30
+ hideEchoBack: true, // Hide password
31
+ },
32
+ {
33
+ title: "PostgreSQL Database Name",
34
+ envVar: "POSTGRES_DB",
35
+ defaultValue: `${projectName}-db`,
36
+ hideEchoBack: false,
37
+ },
38
+ {
39
+ title: "PostgreSQL Host",
40
+ envVar: "POSTGRES_HOST",
41
+ defaultValue: "localhost",
42
+ hideEchoBack: false,
43
+ },
44
+ {
45
+ title: "PostgreSQL Port",
46
+ envVar: "POSTGRES_PORT",
47
+ defaultValue: "5432",
48
+ hideEchoBack: false,
49
+ },
50
+ ],
51
+ },
52
+ {
53
+ name: "mongodb",
54
+ label: "MongoDB",
55
+ ormOptions: ["mongoose"],
56
+ required: [
57
+ {
58
+ title: "MongoDB URI",
59
+ envVar: "MONGO_URI",
60
+ defaultValue: "mongodb://localhost:27017",
61
+ hideEchoBack: false,
62
+ },
63
+ {
64
+ title: "MongoDB Database Name",
65
+ envVar: "MONGO_DB",
66
+ defaultValue: `${projectName}-db`,
67
+ hideEchoBack: false,
68
+ },
69
+ ],
70
+ },
71
+ ];
72
+
73
+ let currentProjectName = projectName;
74
+ // La validation du nom de projet reste interactive en cas d'échec
75
+ while (true) {
76
+ if (/^[A-Za-z][A-Za-z0-9_-]*$/.test(currentProjectName)) break;
77
+
78
+ currentProjectName = readline.question(`${info("[?]")} Project name : `);
79
+
80
+ logWarning(
81
+ "Invalid name. Use letters, numbers, _ or - (start with a letter)."
82
+ );
83
+ }
84
+
85
+ // --- 1. Database Selection ---
86
+ const defaultDB = "postgresql";
87
+ let usedDB;
88
+
89
+ // Prioriser le flag 'db' si présent
90
+ if (flags.db && flags.db !== undefined) {
91
+ usedDB = getFlagValue(flags, "db", defaultDB);
92
+ console.log(
93
+ `${info("[?]")} Database (postgresql, mongodb) : ${usedDB} ${success(
94
+ "[flag]"
95
+ )}`
96
+ );
97
+ } else {
98
+ usedDB = readline.question(
99
+ `${info("[?]")} Database (postgresql, mongodb) [${defaultDB}] : `,
100
+ { defaultInput: defaultDB }
101
+ );
102
+ }
103
+
104
+ let selectedDB = dataBases.find(
105
+ (db) => db.name.toLowerCase() === usedDB.toLowerCase()
106
+ );
107
+
108
+ // Revenir au mode interactif si le flag fourni est invalide ou si l'utilisateur a saisi une valeur invalide
109
+ while (!selectedDB) {
110
+ logWarning("Database not recognized.");
111
+
112
+ usedDB = readline.question(
113
+ `${info("[?]")} Database (postgresql, mongodb) : `
114
+ );
115
+ selectedDB = dataBases.find(
116
+ (db) => db.name.toLowerCase() === usedDB.toLowerCase()
117
+ );
118
+ }
119
+
120
+ // --- 2. DB Configuration (Using flags) ---
121
+ const dbConfig = {};
122
+ console.log(`\n${info("[INFO]")} ${selectedDB.label} Configuration`);
123
+
124
+ selectedDB.required.forEach((field) => {
125
+ // Détermine la clé de flag (ex: 'dbUser' pour POSTGRES_USER ou 'mongoUri' pour MONGO_URI)
126
+ const flagName = field.envVar
127
+ .toLowerCase()
128
+ .replace("postgres_", "db")
129
+ .replace("mongo_", "mongo");
130
+
131
+ const flagValue = flags[flagName];
132
+ let answer;
133
+
134
+ if (flagValue !== undefined) {
135
+ // Flag is present, use its value directly and skip prompt
136
+ answer = getFlagValue(flags, flagName, field.defaultValue);
137
+
138
+ const displayValue = field.hideEchoBack ? "***" : answer;
139
+
140
+ console.log(` ${field.title} : ${displayValue} ${success("[flag]")}`);
141
+ } else {
142
+ // Flag is absent, ask the question
143
+ while (true) {
144
+ answer = readline.question(
145
+ ` ${field.title} [${field.defaultValue}] : `,
146
+ {
147
+ hideEchoBack: field.hideEchoBack,
148
+ defaultInput: field.defaultValue,
149
+ }
150
+ );
151
+
152
+ // If the user entered something OR if the default value is non-null, continue
153
+ if (answer || field.defaultValue !== null) break;
154
+
155
+ logWarning("This field is required.");
156
+ }
157
+ // If the user just pressed Enter, use the default value
158
+ answer = answer || field.defaultValue;
159
+ }
160
+ dbConfig[field.envVar] = answer;
161
+ });
162
+
163
+ // --- 3. ORM Selection (Full Mode) ---
164
+ if (selectedDB.ormOptions && selectedDB.ormOptions.length > 0) {
165
+ const defaultOrm = selectedDB.ormOptions[0];
166
+ let ormChoice;
167
+
168
+ // 1. Vérification du Flag
169
+ if (flags.orm !== undefined) {
170
+ const flagValue = getFlagValue(flags, "orm", "").toLowerCase();
171
+
172
+ if (selectedDB.ormOptions.includes(flagValue)) {
173
+ ormChoice = flagValue;
174
+ console.log(
175
+ `${info("[?]")} ORM for ${selectedDB.label}: ${ormChoice} ${success(
176
+ "[flag]"
177
+ )}`
178
+ );
179
+ } else {
180
+ logWarning(`ORM flag '${flagValue}' invalid for ${selectedDB.label}.`);
181
+ }
182
+ }
183
+
184
+ // 2. Mode Interactif (si pas de flag ou flag invalide)
185
+ if (!ormChoice) {
186
+ const answers = await actualInquirer.prompt([
187
+ {
188
+ type: "list",
189
+ name: "orm",
190
+ message: `Choose an ORM for ${selectedDB.label}:`,
191
+ choices: selectedDB.ormOptions.map((opt) => ({
192
+ name: opt.charAt(0).toUpperCase() + opt.slice(1), // Capitalize label
193
+ value: opt,
194
+ })),
195
+ default: defaultOrm,
196
+ },
197
+ ]);
198
+ ormChoice = answers.orm;
199
+ }
200
+
201
+ dbConfig.orm = ormChoice;
202
+ }
203
+
204
+ const packageManager = await getPackageManager(flags);
205
+
206
+ // --- 4. Boolean Choices (Prioritize flags) ---
207
+ const booleanFlags = [
208
+ { name: "docker", default: true, prompt: "Generate Docker files?" },
209
+
210
+ { name: "auth", default: true, prompt: "Add JWT authentication?" },
211
+
212
+ { name: "swagger", default: true, prompt: "Install Swagger?" },
213
+ ];
214
+
215
+ const booleanResults = {};
216
+
217
+ booleanFlags.forEach(({ name, default: defaultValue, prompt }) => {
218
+ let result;
219
+
220
+ if (flags[name] !== undefined) {
221
+ // Flag is present, use its value
222
+ result = getFlagValue(flags, name, defaultValue);
223
+
224
+ const displayValue = result ? "Yes" : "No";
225
+ console.log(
226
+ `${info("[?]")} ${prompt} : ${displayValue} ${success("[flag]")}`
227
+ );
228
+ } else {
229
+ // Flag is absent, ask the question
230
+ const defaultInput = defaultValue ? "y" : "n";
231
+ result = readline.keyInYNStrict(`${info("[?]")} ${prompt}`, {
232
+ defaultInput: defaultInput,
233
+ });
234
+ }
235
+ booleanResults[name] = result;
236
+ });
237
+
238
+ const useDocker = booleanResults.docker;
239
+ const useAuth = booleanResults.auth;
240
+ const useSwagger = booleanResults.swagger;
241
+
242
+ // --- 5. Swagger Configuration (Prioritize flags) ---
243
+ let swaggerInputs;
244
+ if (useSwagger) {
245
+ console.log(`\n${info("[INFO]")} Swagger Configuration`);
246
+ const swaggerFields = [
247
+ {
248
+ name: "title",
249
+ flag: "swaggerTitle",
250
+ default: `${currentProjectName} API`,
251
+ prompt: "API Title",
252
+ },
253
+ {
254
+ name: "description",
255
+ flag: "swaggerDesc",
256
+ default: "API generated by NestCraftX",
257
+ prompt: "Description",
258
+ },
259
+ {
260
+ name: "version",
261
+ flag: "swaggerVersion",
262
+ default: "1.0.0",
263
+ prompt: "Version",
264
+ },
265
+ {
266
+ name: "endpoint",
267
+ flag: "swaggerEndpoint",
268
+ default: "api/docs",
269
+ prompt: "Endpoint",
270
+ },
271
+ ];
272
+
273
+ swaggerInputs = {};
274
+
275
+ swaggerFields.forEach((field) => {
276
+ const flagValue = flags[field.flag];
277
+ const defaultValue = field.default;
278
+
279
+ if (flagValue !== undefined) {
280
+ // Flag is present, use its value
281
+ swaggerInputs[field.name] = flagValue;
282
+ console.log(` ${field.prompt} : ${flagValue} ${success("[flag]")}`);
283
+ } else {
284
+ // Flag is absent, ask the question
285
+ swaggerInputs[field.name] = readline.question(
286
+ ` ${field.prompt} [${defaultValue}] : `,
287
+ { defaultInput: defaultValue }
288
+ );
289
+ }
290
+ });
291
+ }
292
+
293
+ // --- 6. Entities (Remains fully interactive) ---
294
+ const entitiesData = { entities: [], relations: [] };
295
+
296
+ if (useAuth) {
297
+ console.log(
298
+ `\n${info("[INFO]")} Auth active: adding User and Session entities`
299
+ );
300
+
301
+ // 1. Entité User
302
+ entitiesData.entities.push({
303
+ name: "user",
304
+ fields: [
305
+ { name: "email", type: "string", unique: true },
306
+ { name: "password", type: "string" },
307
+ { name: "role", type: "Role" },
308
+ { name: "isActive", type: "boolean", default: true },
309
+ ],
310
+ });
311
+
312
+ // 2. Entité Session
313
+ entitiesData.entities.push({
314
+ name: "session",
315
+ fields: [
316
+ { name: "refreshToken", type: "string" },
317
+ { name: "userId", type: "string" },
318
+ { name: "expiresAt", type: "Date" },
319
+ { name: "createdAt", type: "Date", default: "now" },
320
+ ],
321
+ });
322
+
323
+ // 3. relation user & session
324
+ entitiesData.relations.push({
325
+ from: "user",
326
+ to: "session",
327
+ type: "1-n",
328
+ });
329
+ }
330
+
331
+ console.log(
332
+ `\n${info("[INFO]")} Entity input (FULL Mode - Complete Architecture)`
333
+ );
334
+
335
+ let addEntity = readline.keyInYNStrict(`${info("[?]")} Add an entity?`);
336
+ while (addEntity) {
337
+ let name;
338
+ while (true) {
339
+ name = readline.question(`\n Entity name : `);
340
+ if (/^[A-Za-z][A-Za-z0-9_]*$/.test(name)) break;
341
+ logWarning("Invalid name. Letters, numbers, _ (start with a letter).");
342
+ }
343
+
344
+ const fields = [];
345
+
346
+ console.log(` Fields for "${name}" :`);
347
+ while (true) {
348
+ let fname = readline.question(" Field name (leave empty to finish) : ");
349
+ if (!fname) break;
350
+ if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(fname)) {
351
+ logWarning("Invalid field name.");
352
+ continue;
353
+ }
354
+
355
+ const baseTypeChoices = [
356
+ "string",
357
+ "text",
358
+ "number",
359
+ "decimal",
360
+ "boolean",
361
+ "Date",
362
+ "uuid",
363
+ "json",
364
+ "enum",
365
+ "array",
366
+ "object",
367
+ ];
368
+
369
+ const typeQuestion = {
370
+ type: "list",
371
+ name: "ftype",
372
+ message: `Type for "${fname}"`,
373
+ default: "string",
374
+ choices: baseTypeChoices,
375
+ transformer: () => "",
376
+ };
377
+ const typeAnswer = await actualInquirer.prompt([typeQuestion]);
378
+ let ftype = typeAnswer.ftype;
379
+ process.stdout.write("\x1B[1A");
380
+ process.stdout.write("\x1B[K");
381
+
382
+ if (ftype === "array") {
383
+ const arrayInnerQuestion = {
384
+ type: "list",
385
+ name: "innerType",
386
+ message: `Type of elements for "${fname}[]"`,
387
+ default: "string",
388
+ choices: baseTypeChoices.filter(
389
+ (c) => c !== "array" && c !== "object"
390
+ ),
391
+ transformer: () => "",
392
+ };
393
+
394
+ const innerAnswer = await actualInquirer.prompt([arrayInnerQuestion]);
395
+ ftype = `${innerAnswer.innerType}[]`;
396
+ } else if (ftype === "enum") {
397
+ const enumName = capitalize(fname) + "Enum";
398
+ console.log(
399
+ ` ${info(
400
+ "[INFO]"
401
+ )} Enum type selected. Consider defining ${enumName} in your code.`
402
+ );
403
+ ftype = enumName;
404
+ } else if (ftype === "object") {
405
+ const objectNameQuestion = {
406
+ type: "input",
407
+ name: "objectName",
408
+
409
+ message: `Complex type name (DTO/Class or leave 'json') :`,
410
+ default: "json",
411
+ transformer: () => "",
412
+ };
413
+
414
+ const objectAnswer = await actualInquirer.prompt([objectNameQuestion]);
415
+ ftype = capitalize(objectAnswer.objectName.trim() || "json");
416
+ }
417
+
418
+ console.log(` Type for "${fname}" : ${ftype} ${success("[✓]")}`);
419
+
420
+ fields.push({ name: fname, type: ftype });
421
+ }
422
+
423
+ entitiesData.entities.push({ name, fields });
424
+ console.log(
425
+ `${success("[✓]")} Entity "${name}" added with ${fields.length} field(s)`
426
+ );
427
+
428
+ addEntity = readline.keyInYNStrict(`${info("[?]")} Add another entity?`);
429
+ }
430
+
431
+ const wantsRelation = readline.keyInYNStrict(
432
+ `${info("[?]")} Add relationships between entities?`
433
+ );
434
+ if (wantsRelation) {
435
+ if (entitiesData.entities.length > 1) {
436
+ console.log(`\n${info("[INFO]")} Configuring relationships`);
437
+
438
+ let configuring = true;
439
+ while (configuring) {
440
+ const entityNames = entitiesData.entities.map((e) => e.name);
441
+
442
+ // 1. Select entities first
443
+ const selection = await actualInquirer.prompt([
444
+ {
445
+ type: "list",
446
+ name: "fromName",
447
+ message: "From which entity? (Source)",
448
+ choices: entityNames,
449
+ },
450
+ {
451
+ type: "list",
452
+ name: "toName",
453
+ message: (prev) =>
454
+ `To which entity should ${prev.fromName} be linked? (Target)`,
455
+ choices: (prev) =>
456
+ entityNames.filter((name) => name !== prev.fromName),
457
+ },
458
+ ]);
459
+
460
+ // --- VERIFICATION: Check if link already exists (A->B or B->A) ---
461
+ const alreadyExists = entitiesData.relations.find(
462
+ (rel) =>
463
+ (rel.from === selection.fromName && rel.to === selection.toName) ||
464
+ (rel.from === selection.toName && rel.to === selection.fromName)
465
+ );
466
+
467
+ if (alreadyExists) {
468
+ logWarning(
469
+ `A relationship already exists between ${selection.fromName} and ${selection.toName} (${alreadyExists.type}).`
470
+ );
471
+
472
+ const { tryAgain } = await actualInquirer.prompt([
473
+ {
474
+ type: "confirm",
475
+ name: "tryAgain",
476
+ message: "Do you want to choose different entities?",
477
+ default: true,
478
+ },
479
+ ]);
480
+
481
+ if (!tryAgain) break;
482
+ continue; // Restart selection
483
+ }
484
+
485
+ // 2. Select Relationship type only if verification passed
486
+ const typeAnswer = await actualInquirer.prompt([
487
+ {
488
+ type: "list",
489
+ name: "relType",
490
+ message: "Relationship type:",
491
+ choices: [
492
+ {
493
+ name: `1-1 (One-to-One) : ${selection.fromName} has one ${selection.toName}`,
494
+ value: "1-1",
495
+ },
496
+ {
497
+ name: `1-n (One-to-Many) : ${selection.fromName} has many ${selection.toName}s`,
498
+ value: "1-n",
499
+ },
500
+ {
501
+ name: `n-1 (Many-to-One) : Many ${selection.fromName}s belong to one ${selection.toName}`,
502
+ value: "n-1",
503
+ },
504
+ {
505
+ name: `n-n (Many-to-Many) : Many ${selection.fromName}s linked to many ${selection.toName}s`,
506
+ value: "n-n",
507
+ },
508
+ ],
509
+ },
510
+ ]);
511
+
512
+ const from = entitiesData.entities.find(
513
+ (e) => e.name === selection.fromName
514
+ );
515
+ const to = entitiesData.entities.find(
516
+ (e) => e.name === selection.toName
517
+ );
518
+ const relType = typeAnswer.relType;
519
+
520
+ // Register Relationship
521
+ entitiesData.relations.push({
522
+ from: from.name,
523
+ to: to.name,
524
+ type: relType,
525
+ });
526
+
527
+ const fromLow = from.name.toLowerCase();
528
+ const toLow = to.name.toLowerCase();
529
+
530
+ // --- Add fields logic ---
531
+ if (relType === "1-1") {
532
+ from.fields.push(
533
+ { name: `${toLow}Id`, type: "string" },
534
+ { name: toLow, type: to.name }
535
+ );
536
+ } else if (relType === "1-n") {
537
+ from.fields.push({ name: `${toLow}s`, type: `${to.name}[]` });
538
+ to.fields.push(
539
+ { name: `${fromLow}Id`, type: "string" },
540
+ { name: fromLow, type: from.name }
541
+ );
542
+ } else if (relType === "n-1") {
543
+ from.fields.push(
544
+ { name: `${toLow}Id`, type: "string" },
545
+ { name: toLow, type: to.name }
546
+ );
547
+ to.fields.push({ name: `${fromLow}s`, type: `${from.name}[]` });
548
+ } else if (relType === "n-n") {
549
+ from.fields.push({ name: `${toLow}s`, type: `${to.name}[]` });
550
+ to.fields.push({ name: `${fromLow}s`, type: `${from.name}[]` });
551
+ }
552
+
553
+ console.log(
554
+ `\n${success("[✓]")} Relationship added: ${from.name} ${relType} ${
555
+ to.name
556
+ }`
557
+ );
558
+
559
+ const { addMore } = await actualInquirer.prompt([
560
+ {
561
+ type: "confirm",
562
+ name: "addMore",
563
+ message: "Add another relationship?",
564
+ default: false,
565
+ },
566
+ ]);
567
+ configuring = addMore;
568
+ }
569
+ } else {
570
+ logWarning(
571
+ "At least two entities are required to configure a relationship."
572
+ );
573
+ }
574
+ }
575
+
576
+ return {
577
+ projectName: currentProjectName,
578
+ useDocker,
579
+ useAuth,
580
+ useSwagger,
581
+ swaggerInputs,
582
+ packageManager,
583
+ entitiesData,
584
+ selectedDB: selectedDB.name,
585
+ dbConfig,
586
+ mode: "full",
587
+ };
588
+ }
589
+
590
+ /**
591
+ * Récupère la valeur d'un flag, ou la valeur par défaut si le flag n'est pas fourni.
592
+ * Convertit les flags 'true'/'false' en booléens si nécessaire.
593
+ * @param {object} flags - L'objet flags (ex: yargs)
594
+ * @param {string} name - Nom du flag (ex: 'auth', 'dbHost')
595
+ * @param {*} defaultValue - Valeur par défaut si le flag est absent.
596
+ */
597
+ function getFlagValue(flags, name, defaultValue) {
598
+ const value = flags[name];
599
+ if (value !== undefined) {
600
+ // Gérer les cas où yargs (ou autre) renvoie une chaîne pour les booléens
601
+ if (value === "true") return true;
602
+ if (value === "false") return false;
603
+ return value;
604
+ }
605
+ return defaultValue;
606
+ }
607
+ module.exports = { getFullModeInputs };