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
@@ -0,0 +1,411 @@
1
+ const readline = require("readline-sync");
2
+ const inquirer = require("inquirer");
3
+ const { info, success, warning } = require("../colors");
4
+ const { logWarning } = require("../loggers/logWarning");
5
+ const { capitalize } = require("../helpers");
6
+ const actualInquirer = inquirer.default || inquirer;
7
+
8
+ /**
9
+ * Interactive entity builder with full revision capabilities.
10
+ * @param {string} [initialName] - Optional starting name for the entity.
11
+ * @returns {Promise<{name: string, fields: Array<{name: string, type: string, unique: boolean, nullable: boolean, default: string|null>}|null>}
12
+ */
13
+ async function buildEntityInteractive(initialName) {
14
+ let entityName = initialName;
15
+ while (!entityName || !/^[A-Za-z][A-Za-z0-9_]*$/.test(entityName)) {
16
+ entityName = readline.question(`\n${info("[?]")} Entity name : `);
17
+ if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(entityName)) {
18
+ logWarning("Invalid name. Letters, numbers, _ (start with a letter).");
19
+ }
20
+ }
21
+
22
+ const fields = [];
23
+
24
+ console.log(`\n Fields for "${entityName}" :`);
25
+
26
+ // Initial field prompting loop
27
+ while (true) {
28
+ let fname = readline.question(" Field name (leave empty to finish) : ");
29
+ if (!fname) break;
30
+ if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(fname)) {
31
+ logWarning("Invalid field name.");
32
+ continue;
33
+ }
34
+
35
+ const field = await promptFieldDetails(fname);
36
+ fields.push(field);
37
+ const defaultText = field.default !== null && field.default !== "" ? `, default=${field.default}` : " (no default)";
38
+ console.log(` Field "${fname}" added: type=${field.type}, unique=${field.unique}, nullable=${field.nullable}${defaultText} ${success("[✓]")}`);
39
+ }
40
+
41
+ // Revision Loop
42
+ while (true) {
43
+ console.log(`\n--------------------------------------------`);
44
+ console.log(`📋 Current Entity: "${entityName}"`);
45
+ if (fields.length === 0) {
46
+ console.log(" (No fields added yet)");
47
+ } else {
48
+ fields.forEach((f, idx) => {
49
+ const constraints = [];
50
+ if (f.unique) constraints.push("unique");
51
+ if (f.nullable) constraints.push("nullable");
52
+ if (f.default !== null && f.default !== "") constraints.push(`default: ${f.default}`);
53
+ if (f.description) constraints.push(`desc: "${f.description}"`);
54
+ const constraintStr = constraints.length > 0 ? ` (${constraints.join(", ")})` : "";
55
+ console.log(` ${idx + 1}. ${f.name} : ${f.type}${constraintStr}`);
56
+ });
57
+ }
58
+ console.log(`--------------------------------------------`);
59
+
60
+ const answers = await actualInquirer.prompt([
61
+ {
62
+ type: "list",
63
+ name: "action",
64
+ message: "Select an option:",
65
+ choices: [
66
+ { name: "✅ Confirm and continue", value: "confirm" },
67
+ { name: "➕ Add another field", value: "add" },
68
+ { name: "✏️ Edit an existing field", value: "edit" },
69
+ { name: "🗑️ Delete a field", value: "delete" },
70
+ { name: "✏️ Rename the entity", value: "rename" },
71
+ { name: "❌ Cancel this entity", value: "cancel" },
72
+ ],
73
+ },
74
+ ]);
75
+
76
+ switch (answers.action) {
77
+ case "confirm":
78
+ return { name: entityName, fields };
79
+
80
+ case "add": {
81
+ let fname = readline.question("\n Field name (leave empty to cancel) : ");
82
+ if (!fname) break;
83
+ if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(fname)) {
84
+ logWarning("Invalid field name.");
85
+ break;
86
+ }
87
+ const field = await promptFieldDetails(fname);
88
+ fields.push(field);
89
+ const defaultText = field.default !== null && field.default !== "" ? `, default=${field.default}` : " (no default)";
90
+ console.log(` Field "${fname}" added: type=${field.type}, unique=${field.unique}, nullable=${field.nullable}${defaultText} ${success("[✓]")}`);
91
+ break;
92
+ }
93
+
94
+ case "delete": {
95
+ if (fields.length === 0) {
96
+ logWarning("No fields to delete.");
97
+ break;
98
+ }
99
+ const deleteAns = await actualInquirer.prompt([
100
+ {
101
+ type: "list",
102
+ name: "targetIndex",
103
+ message: "Select the field to delete:",
104
+ choices: fields.map((f, idx) => ({
105
+ name: `${f.name} (${f.type})`,
106
+ value: idx,
107
+ })),
108
+ },
109
+ ]);
110
+ const deletedField = fields.splice(deleteAns.targetIndex, 1)[0];
111
+ console.log(`${success("[✓]")} Field "${deletedField.name}" deleted.`);
112
+ break;
113
+ }
114
+
115
+ case "rename": {
116
+ let newName = readline.question(`\n${info("[?]")} New Entity name [${entityName}]: `);
117
+ if (!newName) break;
118
+ if (/^[A-Za-z][A-Za-z0-9_]*$/.test(newName)) {
119
+ entityName = newName;
120
+ console.log(`${success("[✓]")} Entity renamed to "${entityName}"`);
121
+ } else {
122
+ logWarning("Invalid name.");
123
+ }
124
+ break;
125
+ }
126
+
127
+ case "edit": {
128
+ if (fields.length === 0) {
129
+ logWarning("No fields to edit.");
130
+ break;
131
+ }
132
+ const selectAns = await actualInquirer.prompt([
133
+ {
134
+ type: "list",
135
+ name: "targetIndex",
136
+ message: "Select the field to edit:",
137
+ choices: fields.map((f, idx) => ({
138
+ name: `${f.name} (${f.type})`,
139
+ value: idx,
140
+ })),
141
+ },
142
+ ]);
143
+ const fIdx = selectAns.targetIndex;
144
+ const currentField = fields[fIdx];
145
+
146
+ console.log(`\n✏️ Editing field "${currentField.name}":`);
147
+ let newName = readline.question(` New field name [${currentField.name}]: `) || currentField.name;
148
+ while (!/^[A-Za-z][A-Za-z0-9_]*$/.test(newName)) {
149
+ logWarning("Invalid field name.");
150
+ newName = readline.question(` New field name [${currentField.name}]: `) || currentField.name;
151
+ }
152
+
153
+ const editedField = await promptFieldDetails(newName, currentField);
154
+ fields[fIdx] = editedField;
155
+ console.log(`${success("[✓]")} Field successfully updated.`);
156
+ break;
157
+ }
158
+
159
+ case "cancel": {
160
+ const confirmCancel = readline.keyInYNStrict(`${warning("[warning]")} Are you sure you want to cancel and discard this entity?`);
161
+ if (confirmCancel) {
162
+ return null;
163
+ }
164
+ break;
165
+ }
166
+ }
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Prompts the user for all details and constraints of a field.
172
+ * @private
173
+ */
174
+ async function promptFieldDetails(fieldName, existingField = null) {
175
+ const baseTypeChoices = [
176
+ "string",
177
+ "text",
178
+ "number",
179
+ "boolean",
180
+ "Date",
181
+ "uuid",
182
+ "json",
183
+ "enum",
184
+ "array",
185
+ "object",
186
+ ];
187
+
188
+ let defaultType = "string";
189
+ if (existingField) {
190
+ if (existingField.type.endsWith("[]")) defaultType = "array";
191
+ else if (existingField.type.endsWith("Enum")) defaultType = "enum";
192
+ else if (baseTypeChoices.includes(existingField.type)) defaultType = existingField.type;
193
+ else defaultType = "object";
194
+ }
195
+
196
+ const typeQuestion = {
197
+ type: "list",
198
+ name: "ftype",
199
+ message: `Type for "${fieldName}"`,
200
+ default: defaultType,
201
+ choices: baseTypeChoices,
202
+ };
203
+
204
+ const typeAnswer = await actualInquirer.prompt([typeQuestion]);
205
+ let ftype = typeAnswer.ftype;
206
+
207
+ if (ftype === "array") {
208
+ let defaultInner = "string";
209
+ if (existingField && existingField.type.endsWith("[]")) {
210
+ defaultInner = existingField.type.slice(0, -2);
211
+ }
212
+ const arrayInnerQuestion = {
213
+ type: "list",
214
+ name: "innerType",
215
+ message: `Type of elements for "${fieldName}[]"`,
216
+ default: defaultInner,
217
+ choices: baseTypeChoices.filter((c) => c !== "array" && c !== "object"),
218
+ };
219
+ const innerAnswer = await actualInquirer.prompt([arrayInnerQuestion]);
220
+ ftype = `${innerAnswer.innerType}[]`;
221
+ } else if (ftype === "enum") {
222
+ const enumName = capitalize(fieldName) + "Enum";
223
+ console.log(` ${info("[INFO]")} Enum type selected: ${enumName}`);
224
+ ftype = enumName;
225
+ } else if (ftype === "object") {
226
+ let defaultObj = "json";
227
+ if (existingField && !baseTypeChoices.includes(existingField.type) && !existingField.type.endsWith("[]") && !existingField.type.endsWith("Enum")) {
228
+ defaultObj = existingField.type;
229
+ }
230
+ const objectNameQuestion = {
231
+ type: "input",
232
+ name: "objectName",
233
+ message: `Complex type name (DTO/Class or leave 'json') :`,
234
+ default: defaultObj,
235
+ };
236
+ const objectAnswer = await actualInquirer.prompt([objectNameQuestion]);
237
+ ftype = capitalize(objectAnswer.objectName.trim() || "json");
238
+ }
239
+
240
+ // Ask constraints: unique, nullable, default
241
+ const isUnique = readline.keyInYNStrict(` Is "${fieldName}" unique?`, {
242
+ defaultInput: existingField && existingField.unique ? "y" : "n",
243
+ });
244
+
245
+ const isNullable = readline.keyInYNStrict(` Is "${fieldName}" nullable/optional?`, {
246
+ defaultInput: existingField && existingField.nullable ? "y" : "n",
247
+ });
248
+
249
+ let defaultValuePrompt = " Default value (press Enter for none): ";
250
+ if (existingField && existingField.default !== null) {
251
+ defaultValuePrompt = ` Default value [${existingField.default}]: `;
252
+ }
253
+ let defaultValue = readline.question(defaultValuePrompt);
254
+ if (defaultValue === "") {
255
+ defaultValue = existingField ? existingField.default : null;
256
+ } else if (defaultValue.toLowerCase() === "none" || defaultValue.toLowerCase() === "null") {
257
+ defaultValue = null;
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
+
269
+ return {
270
+ name: fieldName,
271
+ type: ftype,
272
+ unique: isUnique,
273
+ nullable: isNullable,
274
+ default: defaultValue,
275
+ description: descriptionValue || null,
276
+ };
277
+ }
278
+
279
+ /**
280
+ * Manages adding relationships between entities via an interactive interface.
281
+ * @param {Object} entitiesData - The object containing entities and relations.
282
+ */
283
+ async function buildRelationsInteractive(entitiesData) {
284
+ if (entitiesData.entities.length < 2) {
285
+ logWarning("At least two entities are required to configure a relationship.");
286
+ return;
287
+ }
288
+
289
+ let configuring = true;
290
+ while (configuring) {
291
+ const entityNames = entitiesData.entities.map((e) => e.name);
292
+
293
+ // 1. Select entities
294
+ const selection = await actualInquirer.prompt([
295
+ {
296
+ type: "list",
297
+ name: "fromName",
298
+ message: "Select the source entity (From) :",
299
+ choices: entityNames,
300
+ },
301
+ {
302
+ type: "list",
303
+ name: "toName",
304
+ message: (prev) => `To which entity should "${prev.fromName}" be linked?`,
305
+ choices: (prev) => entityNames.filter((name) => name !== prev.fromName),
306
+ },
307
+ ]);
308
+
309
+ // Check if link already exists
310
+ const alreadyExists = entitiesData.relations.find(
311
+ (rel) =>
312
+ (rel.from === selection.fromName && rel.to === selection.toName) ||
313
+ (rel.from === selection.toName && rel.to === selection.fromName)
314
+ );
315
+
316
+ if (alreadyExists) {
317
+ logWarning(`A relationship already exists between ${selection.fromName} and ${selection.toName} (${alreadyExists.type}).`);
318
+ const { tryAgain } = await actualInquirer.prompt([
319
+ {
320
+ type: "confirm",
321
+ name: "tryAgain",
322
+ message: "Do you want to choose different entities?",
323
+ default: true,
324
+ },
325
+ ]);
326
+ if (!tryAgain) break;
327
+ continue;
328
+ }
329
+
330
+ // 2. Select Relationship type
331
+ const typeAnswer = await actualInquirer.prompt([
332
+ {
333
+ type: "list",
334
+ name: "relType",
335
+ message: "Relationship type:",
336
+ choices: [
337
+ {
338
+ name: `1-1 (One-to-One) : ${selection.fromName} has one ${selection.toName}`,
339
+ value: "1-1",
340
+ },
341
+ {
342
+ name: `1-n (One-to-Many) : ${selection.fromName} has many ${selection.toName}s`,
343
+ value: "1-n",
344
+ },
345
+ {
346
+ name: `n-1 (Many-to-One) : Many ${selection.fromName}s belong to one ${selection.toName}`,
347
+ value: "n-1",
348
+ },
349
+ {
350
+ name: `n-n (Many-to-Many) : Many ${selection.fromName}s linked to many ${selection.toName}s`,
351
+ value: "n-n",
352
+ },
353
+ ],
354
+ },
355
+ ]);
356
+
357
+ const from = entitiesData.entities.find((e) => e.name === selection.fromName);
358
+ const to = entitiesData.entities.find((e) => e.name === selection.toName);
359
+ const relType = typeAnswer.relType;
360
+
361
+ // Register Relationship
362
+ entitiesData.relations.push({
363
+ from: from.name,
364
+ to: to.name,
365
+ type: relType,
366
+ });
367
+
368
+ const fromLow = from.name.toLowerCase();
369
+ const toLow = to.name.toLowerCase();
370
+
371
+ // Add fields logic
372
+ if (relType === "1-1") {
373
+ from.fields.push(
374
+ { name: `${toLow}Id`, type: "string" },
375
+ { name: toLow, type: to.name }
376
+ );
377
+ } else if (relType === "1-n") {
378
+ from.fields.push({ name: `${toLow}s`, type: `${to.name}[]` });
379
+ to.fields.push(
380
+ { name: `${fromLow}Id`, type: "string" },
381
+ { name: fromLow, type: from.name }
382
+ );
383
+ } else if (relType === "n-1") {
384
+ from.fields.push(
385
+ { name: `${toLow}Id`, type: "string" },
386
+ { name: toLow, type: to.name }
387
+ );
388
+ to.fields.push({ name: `${fromLow}s`, type: `${from.name}[]` });
389
+ } else if (relType === "n-n") {
390
+ from.fields.push({ name: `${toLow}s`, type: `${to.name}[]` });
391
+ to.fields.push({ name: `${fromLow}s`, type: `${from.name}[]` });
392
+ }
393
+
394
+ console.log(`\n${success("[✓]")} Relationship added: ${from.name} ${relType} ${to.name}`);
395
+
396
+ const { addMore } = await actualInquirer.prompt([
397
+ {
398
+ type: "confirm",
399
+ name: "addMore",
400
+ message: "Add another relationship?",
401
+ default: false,
402
+ },
403
+ ]);
404
+ configuring = addMore;
405
+ }
406
+ }
407
+
408
+ module.exports = {
409
+ buildEntityInteractive,
410
+ buildRelationsInteractive,
411
+ };