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