nestcraftx 0.2.5 → 0.3.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 (56) hide show
  1. package/AUDIT_ROADMAP.md +798 -0
  2. package/CHANGELOG.fr.md +22 -0
  3. package/CHANGELOG.md +22 -0
  4. package/CLI_USAGE.fr.md +331 -331
  5. package/CLI_USAGE.md +364 -364
  6. package/LICENSE +21 -21
  7. package/PROGRESS.md +258 -0
  8. package/bin/nestcraft.js +84 -64
  9. package/commands/demo.js +337 -330
  10. package/commands/generate.js +94 -0
  11. package/commands/generateConf.js +91 -0
  12. package/commands/help.js +29 -2
  13. package/commands/info.js +48 -48
  14. package/commands/new.js +340 -335
  15. package/commands/start.js +19 -19
  16. package/commands/test.js +7 -7
  17. package/package.json +1 -1
  18. package/utils/app-module.updater.js +96 -0
  19. package/utils/cliParser.js +88 -76
  20. package/utils/colors.js +62 -62
  21. package/utils/configs/configureDocker.js +120 -120
  22. package/utils/configs/setupCleanArchitecture.js +17 -15
  23. package/utils/configs/setupLightArchitecture.js +15 -9
  24. package/utils/file-system.js +75 -0
  25. package/utils/file-utils/saveProjectConfig.js +36 -0
  26. package/utils/fullModeInput.js +607 -607
  27. package/utils/generators/application/dtoGenerator.js +251 -0
  28. package/utils/generators/application/dtoUpdater.js +54 -0
  29. package/utils/generators/cleanModuleGenerator.js +475 -0
  30. package/utils/generators/database/setupDatabase.js +49 -0
  31. package/utils/generators/domain/entityGenerator.js +126 -0
  32. package/utils/generators/domain/entityUpdater.js +78 -0
  33. package/utils/generators/infrastructure/mapperGenerator.js +114 -0
  34. package/utils/generators/infrastructure/mapperUpdater.js +65 -0
  35. package/utils/generators/infrastructure/middlewareGenerator.js +645 -0
  36. package/utils/generators/infrastructure/mongooseSchemaGenerator.js +196 -0
  37. package/utils/generators/infrastructure/repositoryGenerator.js +334 -0
  38. package/utils/generators/lightModuleGenerator.js +131 -0
  39. package/utils/generators/presentation/controllerGenerator.js +142 -0
  40. package/utils/generators/relation/relation.engine.js +64 -0
  41. package/utils/helpers.js +109 -0
  42. package/utils/interactive/askEntityInputs.js +165 -0
  43. package/utils/loggers/logError.js +7 -7
  44. package/utils/loggers/logInfo.js +7 -7
  45. package/utils/loggers/logSuccess.js +7 -7
  46. package/utils/loggers/logWarning.js +7 -7
  47. package/utils/setups/orms/typeOrmSetup.js +630 -630
  48. package/utils/setups/setupAuth.js +33 -20
  49. package/utils/setups/setupDatabase.js +76 -75
  50. package/utils/setups/setupMongoose.js +22 -5
  51. package/utils/setups/setupPrisma.js +748 -630
  52. package/utils/shell.js +112 -32
  53. package/utils/spinner.js +57 -57
  54. package/utils/systemCheck.js +124 -124
  55. package/utils/userInput.js +357 -421
  56. package/utils/utils.js +29 -2164
@@ -0,0 +1,142 @@
1
+ /**
2
+ * utils/generators/presentation/controllerGenerator.js
3
+ *
4
+ * Générateur du Controller (présentation) pour le mode Full/Clean Architecture.
5
+ * Extrait de utils.js — Phase 1B.
6
+ *
7
+ * ATTENTION : Ce générateur est UNIQUEMENT pour le mode Full.
8
+ * Le mode Light a son propre générateur dans setupLightArchitecture.js
9
+ * avec des différences comportementales :
10
+ * - Full : chemins "presentation/controllers/", méthodes getAll()/getById()
11
+ * - Light : chemins "controllers/", méthodes findAll()/findById(), Logger intégré
12
+ *
13
+ * Ne jamais fusionner les deux.
14
+ */
15
+
16
+ const { capitalize, decapitalize, pluralize } = require("../../helpers");
17
+
18
+ /**
19
+ * Génère le contenu TypeScript du Controller NestJS (Full Architecture).
20
+ *
21
+ * @param {string} entityName - Le nom de l'entité (ex: "Post")
22
+ * @param {string} entityPath - Le chemin de base (ex: "src/post")
23
+ * @param {boolean} useSwagger - Inclure les décorateurs Swagger
24
+ * @returns {Promise<string>} Le code TypeScript du controller
25
+ */
26
+ async function generateController(entityName, entityPath, useSwagger) {
27
+ const entityNameLower = decapitalize(entityName);
28
+ const entityNameCapitalized = capitalize(entityName);
29
+ const pluralName = pluralize(entityNameLower);
30
+
31
+ const swaggerImports = useSwagger
32
+ ? `import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';`
33
+ : "";
34
+
35
+ const swaggerClassDecorator = useSwagger
36
+ ? `@ApiTags('${capitalize(pluralName)}')`
37
+ : "";
38
+
39
+ return `
40
+ import { Controller, Get, Post, Body, Param, Patch, Delete, HttpCode, HttpStatus, Query } from '@nestjs/common';
41
+ ${swaggerImports}
42
+ import { ${entityNameCapitalized}Service } from '${entityPath}/application/services/${entityNameLower}.service';
43
+ import { Create${entityNameCapitalized}Dto, Update${entityNameCapitalized}Dto } from 'src/${entityNameLower}/application/dtos/${entityNameLower}.dto';
44
+
45
+
46
+ /**
47
+ * Controller for ${entityNameCapitalized} management.
48
+ * Handles incoming HTTP requests and delegates logic to the service layer.
49
+ */
50
+ ${swaggerClassDecorator}
51
+ @Controller('${pluralName}')
52
+ export class ${entityNameCapitalized}Controller {
53
+ constructor(private readonly service: ${entityNameCapitalized}Service) {}
54
+
55
+ ${
56
+ entityNameLower !== "user"
57
+ ? `
58
+ @Post()
59
+ @HttpCode(HttpStatus.CREATED)
60
+ ${
61
+ useSwagger
62
+ ? `
63
+ @ApiOperation({ summary: 'Create a new ${entityNameLower}', description: 'Creates a new record for ${entityNameLower} in the database.' })
64
+ @ApiResponse({ status: 201, description: 'The ${entityNameLower} has been successfully created.' })
65
+ @ApiResponse({ status: 400, description: 'Invalid input data.' })`
66
+ : ""
67
+ }
68
+ async create(@Body() dto: Create${entityNameCapitalized}Dto) {
69
+ const result = await this.service.create(dto);
70
+ return {
71
+ message: '${entityNameCapitalized} created successfully',
72
+ data: result
73
+ };
74
+ }
75
+ `
76
+ : ""
77
+ }
78
+
79
+ @Get()
80
+ ${
81
+ useSwagger
82
+ ? `
83
+ @ApiOperation({ summary: 'Get all ${pluralName}', description: 'Retrieves a list of all ${pluralName} available.' })
84
+ @ApiResponse({ status: 200, description: 'Return all ${pluralName}.' })`
85
+ : ""
86
+ }
87
+ async getAll() {
88
+ return await this.service.getAll();
89
+ }
90
+
91
+ @Get(':id')
92
+ ${
93
+ useSwagger
94
+ ? `
95
+ @ApiOperation({ summary: 'Get ${entityNameLower} by ID' })
96
+ @ApiParam({ name: 'id', description: 'The unique identifier of the ${entityNameLower}' })
97
+ @ApiResponse({ status: 200, description: 'The ${entityNameLower} has been found.' })
98
+ @ApiResponse({ status: 404, description: '${entityNameCapitalized} not found.' })`
99
+ : ""
100
+ }
101
+ async getById(@Param('id') id: string) {
102
+ return await this.service.getById(id);
103
+ }
104
+
105
+ @Patch(':id')
106
+ ${
107
+ useSwagger
108
+ ? `
109
+ @ApiOperation({ summary: 'Update an existing ${entityNameLower}' })
110
+ @ApiParam({ name: 'id', description: 'The unique identifier of the ${entityNameLower} to update' })
111
+ @ApiResponse({ status: 200, description: 'The ${entityNameLower} has been successfully updated.' })
112
+ @ApiResponse({ status: 404, description: '${entityNameCapitalized} not found.' })`
113
+ : ""
114
+ }
115
+ async update(
116
+ @Param('id') id: string,
117
+ @Body() dto: Update${entityNameCapitalized}Dto,
118
+ ) {
119
+ await this.service.update(id, dto);
120
+ return { message: '${entityNameCapitalized} updated successfully' };
121
+ }
122
+
123
+ @Delete(':id')
124
+ @HttpCode(HttpStatus.NO_CONTENT)
125
+ ${
126
+ useSwagger
127
+ ? `
128
+ @ApiOperation({ summary: 'Delete a ${entityNameLower}' })
129
+ @ApiParam({ name: 'id', description: 'The unique identifier of the ${entityNameLower} to delete' })
130
+ @ApiResponse({ status: 204, description: 'The ${entityNameLower} has been successfully deleted.' })
131
+ @ApiResponse({ status: 404, description: '${entityNameCapitalized} not found.' })`
132
+ : ""
133
+ }
134
+ async delete(@Param('id') id: string) {
135
+ await this.service.delete(id);
136
+ return { message: '${entityNameCapitalized} deleted successfully' };
137
+ }
138
+ }
139
+ `;
140
+ }
141
+
142
+ module.exports = { generateController };
@@ -0,0 +1,64 @@
1
+ const { patchDtoWithRelation } = require("../application/dtoUpdater");
2
+ const { patchMapperWithRelation } = require("../infrastructure/mapperUpdater");
3
+
4
+ function resolveForeignKeyOwner(newEntityName, target, relationType) {
5
+ switch (relationType) {
6
+ case "1-n":
7
+ // FK on the N side → target
8
+ return null /* {
9
+ ownerEntity: target,
10
+ relatedEntity: newEntityName,
11
+ } */;
12
+
13
+ case "n-1":
14
+ // FK on the N side → source
15
+ return {
16
+ ownerEntity: target,
17
+ relatedEntity: newEntityName,
18
+ };
19
+
20
+ case "1-1":
21
+ // Convention: FK on source side
22
+ return {
23
+ ownerEntity: target,
24
+ relatedEntity: newEntityName,
25
+ };
26
+
27
+ case "n-n":
28
+ // Pivot table → no FK field
29
+ return null;
30
+
31
+ default:
32
+ throw new Error(`❌ Unknown relation type: ${relationType}`);
33
+ }
34
+ }
35
+
36
+ async function applyRelationPatches(
37
+ newEntityName,
38
+ target,
39
+ relationType,
40
+ useSwagger,
41
+ mode = "full",
42
+ ) {
43
+ const owner = resolveForeignKeyOwner(newEntityName, target, relationType);
44
+
45
+ if (!owner) {
46
+ // console.log("ℹ️ n-n relation detected → pivot table, skipping DTO patch");
47
+ return;
48
+ }
49
+
50
+ // Patch DTO
51
+ await patchDtoWithRelation(
52
+ owner.ownerEntity,
53
+ owner.relatedEntity,
54
+ useSwagger,
55
+ mode,
56
+ );
57
+
58
+ await patchMapperWithRelation(owner.ownerEntity, owner.relatedEntity, mode);
59
+ }
60
+
61
+ module.exports = {
62
+ resolveForeignKeyOwner,
63
+ applyRelationPatches,
64
+ };
@@ -0,0 +1,109 @@
1
+ /**
2
+ * utils/helpers.js
3
+ *
4
+ * Fonctions utilitaires de manipulation de chaînes et de types.
5
+ * Partagées entre les générateurs full-mode et light-mode.
6
+ *
7
+ * Ces fonctions sont purement stateless : aucune dépendance externe,
8
+ * aucun effet de bord, aucune logique métier.
9
+ */
10
+
11
+ /**
12
+ * Met en majuscule la première lettre d'une chaîne.
13
+ * @param {string} str
14
+ * @returns {string}
15
+ */
16
+ function capitalize(str) {
17
+ if (!str) return str;
18
+ return str.charAt(0).toUpperCase() + str.slice(1);
19
+ }
20
+
21
+ /**
22
+ * Met en minuscule la première lettre d'une chaîne.
23
+ * @param {string} str
24
+ * @returns {string}
25
+ */
26
+ function decapitalize(str) {
27
+ if (!str) return str;
28
+ return str.charAt(0).toLowerCase() + str.slice(1);
29
+ }
30
+
31
+ /**
32
+ * Retourne le pluriel simple d'un nom.
33
+ * - si finit par 'y' → remplace par 'ies' (category → categories)
34
+ * - si finit déjà par 's' → retourne tel quel
35
+ * - sinon → ajoute 's'
36
+ * @param {string} name
37
+ * @returns {string}
38
+ */
39
+ function pluralize(name) {
40
+ if (name.endsWith("y")) {
41
+ return name.slice(0, -1) + "ies";
42
+ } else if (name.endsWith("s")) {
43
+ return name;
44
+ }
45
+ return name + "s";
46
+ }
47
+
48
+ /**
49
+ * Mappe le type de champ interne (sélectionné par l'utilisateur) au type
50
+ * TypeScript correspondant pour la génération de code.
51
+ *
52
+ * @param {string} type - Le type de base (string, number, boolean, Date, json, etc.)
53
+ * @returns {string} Le type formaté TypeScript
54
+ */
55
+ function formatType(type) {
56
+ switch (type.toLowerCase()) {
57
+ case "number":
58
+ case "decimal":
59
+ return "number";
60
+
61
+ case "boolean":
62
+ return "boolean";
63
+
64
+ case "date":
65
+ return "Date";
66
+
67
+ case "json":
68
+ return "Record<string, any>";
69
+
70
+ case "string":
71
+ case "text":
72
+ case "uuid":
73
+ return "string";
74
+ }
75
+
76
+ // Gère les tableaux (ex: 'string[]', 'number[]')
77
+ if (type.endsWith("[]")) {
78
+ return `${formatType(type.slice(0, -2))}[]`;
79
+ }
80
+
81
+ // Laisse passer les types complexes (DTO, Enum, classe custom)
82
+ if (type.match(/^[A-Za-z][A-Za-z0-9_]*$/)) {
83
+ return type;
84
+ }
85
+
86
+ return "any";
87
+ }
88
+
89
+ /**
90
+ * Gère les cas spéciaux avant l'application de formatType.
91
+ * Ex: le champ 'role' de type string est traité comme le type 'Role' (enum).
92
+ *
93
+ * @param {object} field - L'objet champ { name, type }
94
+ * @returns {string} Le type final formaté
95
+ */
96
+ function getFormattedType(field) {
97
+ if (field.name === "role" && field.type.toLowerCase().startsWith("string")) {
98
+ return "Role";
99
+ }
100
+ return formatType(field.type);
101
+ }
102
+
103
+ module.exports = {
104
+ capitalize,
105
+ decapitalize,
106
+ pluralize,
107
+ formatType,
108
+ getFormattedType,
109
+ };
@@ -0,0 +1,165 @@
1
+ // src/utils/interactive/askEntityInputs.js
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+ const readline = require("readline-sync");
5
+ const inquirer = require("inquirer");
6
+ const { info, success } = require("../colors");
7
+ const { logWarning } = require("../loggers/logWarning");
8
+ const { capitalize } = require("../userInput");
9
+ const actualInquirer = inquirer.default || inquirer;
10
+
11
+ 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("[✓]")}`);
80
+ }
81
+
82
+ const relationData = await askRelationInputs(entity.name);
83
+ entity.relation = relationData;
84
+
85
+ return entity;
86
+ }
87
+
88
+ async function askRelationInputs(newEntityName) {
89
+ const srcPath = path.join(process.cwd(), "src");
90
+
91
+ // Sécurité : Vérifie si le dossier src existe
92
+ if (!fs.existsSync(srcPath)) return null;
93
+
94
+ const blacklist = [
95
+ "common",
96
+ "prisma",
97
+ "auth",
98
+ "mail",
99
+ "infrastructure",
100
+ "shared",
101
+ ];
102
+
103
+ const modules = fs.readdirSync(srcPath).filter((folder) => {
104
+ const fullPath = path.join(srcPath, folder);
105
+
106
+ // 1. D'abord vérifier si c'est un dossier (important pour éviter ENOTDIR)
107
+ const isDirectory = fs.lstatSync(fullPath).isDirectory();
108
+ if (!isDirectory) return false;
109
+
110
+ // 2. Ensuite les autres filtres
111
+ const isNotBlacklisted = !blacklist.includes(folder.toLowerCase());
112
+ const isNotCurrent = folder.toLowerCase() !== newEntityName.toLowerCase();
113
+
114
+ // 3. Enfin vérifier s'il y a un fichier .module.ts à l'intérieur
115
+ const hasModuleFile = fs
116
+ .readdirSync(fullPath)
117
+ .some((file) => file.endsWith(".module.ts"));
118
+
119
+ return isNotBlacklisted && isNotCurrent && hasModuleFile;
120
+ });
121
+
122
+ if (modules.length === 0) {
123
+ console.log(info("\n[INFO] No existing modules found for relations."));
124
+ return null;
125
+ }
126
+
127
+ const { wantRelation } = await actualInquirer.prompt([
128
+ {
129
+ type: "confirm",
130
+ name: "wantRelation",
131
+ message: "Add relation with an existing module?",
132
+ default: false,
133
+ },
134
+ ]);
135
+
136
+ if (!wantRelation) return null;
137
+
138
+ // On demande les détails de la relation
139
+ const relation = await actualInquirer.prompt([
140
+ {
141
+ type: "list",
142
+ name: "target",
143
+ message: "Select the target module:",
144
+ choices: modules,
145
+ },
146
+ {
147
+ type: "list",
148
+ name: "type",
149
+ message: "Relation type (NewEntity -> Target):",
150
+ choices: [
151
+ { name: "1-n (One-to-Many: This has many of those)", value: "1-n" },
152
+ {
153
+ name: "n-1 (Many-to-One: This belongs to one of those)",
154
+ value: "n-1",
155
+ },
156
+ { name: "1-1 (One-to-One)", value: "1-1" },
157
+ { name: "n-n (Many-to-Many)", value: "n-n" },
158
+ ],
159
+ },
160
+ ]);
161
+
162
+ return relation;
163
+ }
164
+
165
+ module.exports = { askEntityInputs };
@@ -1,7 +1,7 @@
1
- const { error } = require("../colors");
2
-
3
- function logError(message) {
4
- console.error(`\n${error("❌[ERROR]")} ${message}`);
5
- }
6
-
7
- module.exports = { logError };
1
+ const { error } = require("../colors");
2
+
3
+ function logError(message) {
4
+ console.error(`\n${error("❌[ERROR]")} ${message}`);
5
+ }
6
+
7
+ module.exports = { logError };
@@ -1,7 +1,7 @@
1
- const { info } = require("../colors");
2
-
3
- function logInfo(message) {
4
- console.log(`\n${info("[INFO]")} ${message}`);
5
- }
6
-
7
- module.exports = { logInfo };
1
+ const { info } = require("../colors");
2
+
3
+ function logInfo(message) {
4
+ console.log(`\n${info("[INFO]")} ${message}`);
5
+ }
6
+
7
+ module.exports = { logInfo };
@@ -1,7 +1,7 @@
1
- const { success } = require("../colors");
2
-
3
- function logSuccess(message) {
4
- console.log(`\n${success("✅[SUCCESS]")} ${message}`);
5
- }
6
-
7
- module.exports = { logSuccess };
1
+ const { success } = require("../colors");
2
+
3
+ function logSuccess(message) {
4
+ console.log(`\n${success("✅[SUCCESS]")} ${message}`);
5
+ }
6
+
7
+ module.exports = { logSuccess };
@@ -1,7 +1,7 @@
1
- const { warning } = require("../colors");
2
-
3
- function logWarning(message) {
4
- console.log(`\n${warning("⚠️[INFO]")} ${message}`);
5
- }
6
-
7
- module.exports = { logWarning };
1
+ const { warning } = require("../colors");
2
+
3
+ function logWarning(message) {
4
+ console.log(`\n${warning("⚠️[Warning]")} ${message}`);
5
+ }
6
+
7
+ module.exports = { logWarning };