nestcraftx 0.2.6 → 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.
@@ -0,0 +1,75 @@
1
+ /**
2
+ * utils/file-system.js
3
+ *
4
+ * Fonctions d'accès au système de fichiers utilisées par tous les générateurs.
5
+ * Extraites de userInput.js pour centraliser les opérations I/O.
6
+ *
7
+ * Les fichiers qui importent depuis userInput.js continuent de fonctionner
8
+ * sans modification grâce aux ré-exports dans userInput.js.
9
+ */
10
+
11
+ const fs = require("fs");
12
+
13
+ /**
14
+ * Crée un répertoire de manière récursive s'il n'existe pas déjà.
15
+ * @param {string} directoryPath - Le chemin du répertoire à créer.
16
+ */
17
+ async function createDirectory(directoryPath) {
18
+ try {
19
+ if (!fs.existsSync(directoryPath)) {
20
+ fs.mkdirSync(directoryPath, { recursive: true });
21
+ }
22
+ } catch (error) {
23
+ console.error(
24
+ `Erreur lors de la création du dossier ${directoryPath}:`,
25
+ error,
26
+ );
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Crée un fichier avec le contenu fourni.
32
+ * Si le fichier existe déjà :
33
+ * - Si `fileData.overwrite === false` : skip silencieusement
34
+ * - Sinon : écrase le fichier existant
35
+ *
36
+ * @param {{ path: string, contente: string, overwrite?: boolean }} fileData
37
+ */
38
+ async function createFile(fileData) {
39
+ try {
40
+ if (!fs.existsSync(fileData.path)) {
41
+ fs.writeFileSync(`${fileData.path}`, `${fileData.contente}`);
42
+ } else {
43
+ if (fileData.overwrite === false) {
44
+ console.log(`File already exists, skipping: ${fileData.path}`);
45
+ return;
46
+ }
47
+ console.log(`Existing file : ${fileData.path}`);
48
+ fs.writeFileSync(`${fileData.path}`, `${fileData.contente}`);
49
+ }
50
+ } catch (error) {
51
+ console.error(`Erreur creating file ${fileData.path}:`, error);
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Met à jour un fichier existant en remplaçant un pattern par un remplacement.
57
+ * Utilise `String.prototype.replace` : supporte les chaînes et les RegExp.
58
+ *
59
+ * @param {{ path: string, pattern: string|RegExp, replacement: string }} options
60
+ */
61
+ async function updateFile({ path, pattern, replacement }) {
62
+ try {
63
+ let mainTs = fs.readFileSync(path, "utf8");
64
+ const updatedContent = mainTs.replace(pattern, replacement);
65
+ fs.writeFileSync(path, updatedContent, "utf-8");
66
+ } catch (error) {
67
+ console.error(` Error updating file ${path}:`, error);
68
+ }
69
+ }
70
+
71
+ module.exports = {
72
+ createDirectory,
73
+ createFile,
74
+ updateFile,
75
+ };
@@ -0,0 +1,251 @@
1
+ /**
2
+ * utils/generators/application/dtoGenerator.js
3
+ *
4
+ * Générateur des DTOs (Data Transfer Objects) Create/Update.
5
+ * Extrait de utils.js — Phase 1B.
6
+ *
7
+ * Supporte les modes "full" et "light" via le paramètre `mode` :
8
+ * - full : import Role depuis src/user/domain/enums/role.enum
9
+ * - light : import Role depuis src/common/enums/role.enum
10
+ *
11
+ * Génère deux classes : Create{Entity}Dto et Update{Entity}Dto
12
+ * Gère optionnellement les décorateurs Swagger (@ApiProperty etc.)
13
+ */
14
+
15
+ const { capitalize, formatType } = require("../../helpers");
16
+
17
+ /**
18
+ * Génère le contenu TypeScript des DTOs pour une entité.
19
+ *
20
+ * @param {object} entity - L'objet entité { name, fields[] }
21
+ * @param {boolean} useSwagger - Inclure les décorateurs Swagger
22
+ * @param {boolean} isAuthDto - Mode Auth DTO (classe unique sans CRUD)
23
+ * @param {"full"|"light"} mode - Mode d'architecture (impacte l'import de Role)
24
+ * @returns {Promise<string>} Le code TypeScript des DTOs
25
+ */
26
+ async function generateDto(
27
+ entity,
28
+ useSwagger,
29
+ isAuthDto = false,
30
+ mode = "full",
31
+ ) {
32
+ const entityName = capitalize(entity.name);
33
+ const entityNameLower = entity.name.toLowerCase();
34
+
35
+ /* ===============================
36
+ ENUM IMPORT
37
+ =============================== */
38
+ let enumImport = "";
39
+ if (entityName === "User") {
40
+ enumImport =
41
+ mode === "light"
42
+ ? "\nimport { Role } from 'src/common/enums/role.enum';"
43
+ : "\nimport { Role } from 'src/user/domain/enums/role.enum';";
44
+ }
45
+
46
+ /* ===============================
47
+ SWAGGER HELPERS
48
+ =============================== */
49
+ const getFieldDescription = (f) => {
50
+ const name = f.name.toLowerCase();
51
+ if (name.includes("email")) return "The official email address of the user";
52
+ if (name.includes("password"))
53
+ return "Must contain at least 8 characters, one letter and one number";
54
+ if (name.includes("token")) return "Authentication token";
55
+ if (name.includes("id") && name !== "id")
56
+ return `Unique identifier of the related ${name.replace("id", "")}`;
57
+ return `The ${f.name} of the ${entityNameLower}`;
58
+ };
59
+
60
+ const getExampleForField = (f) => {
61
+ const fieldName = f.name.toLowerCase();
62
+ const isArray = f.type.endsWith("[]");
63
+ const cleanType = f.type.toLowerCase().replace("[]", "");
64
+
65
+ const getBaseExample = () => {
66
+ if (fieldName.includes("email")) return "user@example.com";
67
+ if (fieldName.includes("password")) return "SecurePass@2024";
68
+ if (fieldName.includes("token")) return "eyJhbGciOi...";
69
+ if (fieldName.includes("id") && fieldName !== "id")
70
+ return "550e8400-e29b-41d4-a716-446655440000";
71
+ if (fieldName.includes("title") || fieldName.includes("name"))
72
+ return `${capitalize(entityName)} Example`;
73
+ if (cleanType === "boolean") return true;
74
+ if (cleanType === "number" || cleanType === "int") return 42;
75
+ return `${f.name.toLowerCase()}_val`;
76
+ };
77
+
78
+ const base = getBaseExample();
79
+ return isArray ? [base, base] : base;
80
+ };
81
+
82
+ /* ===============================
83
+ FIELD GENERATOR
84
+ =============================== */
85
+ const generateFieldLine = (f, optional = false, forceNoSwagger = false) => {
86
+ if (entityName === "User" && f.name.toLowerCase() === "role") return null;
87
+
88
+ const name = f.name;
89
+ const cleanType = f.type.toLowerCase().replace("[]", "");
90
+ const isArray = f.type.endsWith("[]");
91
+
92
+ const SCALAR_TYPES = [
93
+ "string",
94
+ "text",
95
+ "uuid",
96
+ "json",
97
+ "number",
98
+ "decimal",
99
+ "int",
100
+ "float",
101
+ "boolean",
102
+ "date",
103
+ ];
104
+ if (!SCALAR_TYPES.includes(cleanType)) return null;
105
+
106
+ let validators = [];
107
+ if (optional) validators.push("@IsOptional()");
108
+ if (name.toLowerCase().includes("email")) validators.push("@IsEmail()");
109
+ else if (name.toLowerCase().includes("password"))
110
+ validators.push(
111
+ "@MinLength(8, { message: 'Password is too short (min 8 characters)' })",
112
+ );
113
+
114
+ switch (cleanType) {
115
+ case "string":
116
+ case "text":
117
+ validators.push(isArray ? "@IsString({ each: true })" : "@IsString()");
118
+ if (!isArray && !name.toLowerCase().includes("password"))
119
+ validators.push("@MinLength(2)");
120
+ break;
121
+ case "number":
122
+ case "int":
123
+ case "float":
124
+ validators.push(
125
+ isArray ? "@IsNumber({}, { each: true })" : "@IsNumber()",
126
+ );
127
+ break;
128
+ case "boolean":
129
+ validators.push(
130
+ isArray ? "@IsBoolean({ each: true })" : "@IsBoolean()",
131
+ );
132
+ break;
133
+ case "uuid":
134
+ validators.push("@IsUUID()");
135
+ break;
136
+ case "date":
137
+ validators.push("@IsDateString()");
138
+ break;
139
+ }
140
+ if (isArray) validators.push("@IsArray()");
141
+
142
+ let swaggerDecorator = "";
143
+ if (useSwagger && !forceNoSwagger) {
144
+ const decorator = optional ? "@ApiPropertyOptional" : "@ApiProperty";
145
+ const options = JSON.stringify(
146
+ {
147
+ example: getExampleForField(f),
148
+ description: getFieldDescription(f),
149
+ },
150
+ null,
151
+ 2,
152
+ ).replace(/"([^"]+)":/g, "$1:");
153
+ swaggerDecorator = `${decorator}(${options})\n `;
154
+ }
155
+
156
+ return `${swaggerDecorator}${validators.join("\n ")}\n ${name}${
157
+ optional ? "?" : "!"
158
+ }: ${formatType(f.type)};`;
159
+ };
160
+
161
+ /* ======================================================
162
+ AUTH DTO (Strict & Clean)
163
+ ====================================================== */
164
+ if (isAuthDto) {
165
+ const authFields = entity.fields
166
+ .map((f) => generateFieldLine(f, false))
167
+ .filter(Boolean)
168
+ .join("\n\n ");
169
+
170
+ return `import {
171
+ IsString, IsInt, IsBoolean, IsEmail, IsArray,
172
+ IsUUID, IsDateString, MinLength, IsOptional
173
+ } from 'class-validator';
174
+ ${useSwagger ? "import { ApiProperty } from '@nestjs/swagger';" : ""}
175
+
176
+ /**
177
+ * Auth DTO - Strict contract for authentication
178
+ */
179
+ export class ${entityName}Dto {
180
+ ${authFields}
181
+ }
182
+ `;
183
+ }
184
+
185
+ /* ======================================================
186
+ CRUD DTOs (Create / Update)
187
+ ====================================================== */
188
+ const createFields = entity.fields
189
+ .map((f) => generateFieldLine(f, false))
190
+ .filter(Boolean)
191
+ .join("\n\n ");
192
+
193
+ const updateFields = entity.fields
194
+ .map((f) => generateFieldLine(f, true))
195
+ .filter(Boolean)
196
+ .join("\n\n ");
197
+
198
+ // Imports Swagger dynamiques
199
+ let swaggerImports = ["ApiProperty", "ApiPropertyOptional", "PartialType"];
200
+ if (entityName === "User") swaggerImports.push("ApiHideProperty");
201
+
202
+ return `import {
203
+ IsOptional, IsString, IsInt, IsBoolean, IsEmail,
204
+ IsArray, IsUUID, IsDateString, MinLength, IsEnum
205
+ } from 'class-validator';
206
+ ${
207
+ useSwagger
208
+ ? `import { ${swaggerImports.join(", ")} } from '@nestjs/swagger';`
209
+ : ""
210
+ }
211
+ ${enumImport}
212
+
213
+ /**
214
+ * DTO for creating a ${entityName}
215
+ */
216
+ export class Create${entityName}Dto {
217
+ ${createFields}
218
+
219
+ ${
220
+ entityName === "User"
221
+ ? `
222
+ ${useSwagger ? "@ApiHideProperty()" : ""}
223
+ @IsOptional()
224
+ @IsEnum(Role)
225
+ role: Role = Role.USER;`
226
+ : ""
227
+ }
228
+ }
229
+
230
+ /**
231
+ * DTO for updating a ${entityName}
232
+ */
233
+ ${
234
+ useSwagger
235
+ ? `export class Update${entityName}Dto extends PartialType(Create${entityName}Dto) {}`
236
+ : `export class Update${entityName}Dto {
237
+ ${updateFields}
238
+
239
+ ${
240
+ entityName === "User"
241
+ ? `@IsEnum(Role)
242
+ @IsOptional()
243
+ role?: Role;`
244
+ : ""
245
+ }
246
+ }`
247
+ }
248
+ `;
249
+ }
250
+
251
+ module.exports = { generateDto };
@@ -1,5 +1,7 @@
1
1
  const { logInfo } = require("../../loggers/logInfo");
2
2
  const { updatePrismaSchema } = require("../../setups/setupPrisma");
3
+ const { createDirectory, createFile } = require("../../userInput");
4
+ const { generateMongooseSchemaFileContent } = require("../../utils");
3
5
 
4
6
  async function setupDatabase(config, entityData) {
5
7
  logInfo("Configuring the database...");
@@ -8,23 +10,39 @@ async function setupDatabase(config, entityData) {
8
10
  case "prisma":
9
11
  await updatePrismaSchema(entityData);
10
12
  break;
11
- case "typeorm":
12
- await setupMySQL(inputs);
13
- break;
14
13
  case "mongoose":
15
- await setupMongoDB(inputs); // MongoDB Configuration
16
- break;
17
- case "sqlite":
18
- await setupSQLite(inputs); // SQLite Configuration
19
- break;
20
- case "firebase":
21
- await setupFirebase(inputs); // Firebase Configuration
22
- break;
23
- case "redis":
24
- await setupRedis(inputs); // Redis Configuration
14
+ const entityNameLower = entityData.name.toLowerCase();
15
+ const isFull = config.mode === "full";
16
+ const schemaPath = isFull
17
+ ? `src/${entityNameLower}/infrastructure/persistence/mongoose`
18
+ : `src/${entityNameLower}/entities`;
19
+ await createDirectory(schemaPath);
20
+
21
+ // Reconstruct entitiesData with the single entity and relations for generateMongooseSchemaFileContent
22
+ const entitiesData = {
23
+ relations: entityData.relation ? [
24
+ {
25
+ from: entityData.name.toLowerCase(),
26
+ to: entityData.relation.target.toLowerCase(),
27
+ type: entityData.relation.type
28
+ }
29
+ ] : []
30
+ };
31
+
32
+ const mongooseSchemaContent = await generateMongooseSchemaFileContent(
33
+ entityData,
34
+ entitiesData,
35
+ config.mode
36
+ );
37
+
38
+ await createFile({
39
+ path: `${schemaPath}/${entityNameLower}.schema.ts`,
40
+ contente: mongooseSchemaContent,
41
+ });
25
42
  break;
26
43
  default:
27
- throw new Error("Unsupported database.");
44
+ // Other database types don't require database-level updates on module generation
45
+ break;
28
46
  }
29
47
  }
30
48
 
@@ -0,0 +1,126 @@
1
+ /**
2
+ * utils/generators/domain/entityGenerator.js
3
+ *
4
+ * Générateur de l'Entité de Domaine (Domain Entity).
5
+ * Extrait de utils.js — Phase 1B.
6
+ *
7
+ * Supporte les modes "full" (Clean Architecture) et "light" (MVP).
8
+ * La différence entre les deux modes porte uniquement sur le chemin
9
+ * d'import de l'enum Role :
10
+ * - full : src/user/domain/enums/role.enum
11
+ * - light : src/common/enums/role.enum
12
+ */
13
+
14
+ const { capitalize, getFormattedType } = require("../../helpers");
15
+
16
+ /**
17
+ * Génère le contenu TypeScript de l'entité de domaine.
18
+ *
19
+ * @param {object} entity - L'objet entité { name, fields[] }
20
+ * @param {"full"|"light"} mode - Le mode d'architecture cible
21
+ * @returns {Promise<string>} Le code TypeScript de l'entité
22
+ */
23
+ async function generateEntityFileContent(entity, mode = "full") {
24
+ if (!entity || !entity.name) {
25
+ throw new Error("Nom de l'entité manquant !");
26
+ }
27
+
28
+ const entityName = capitalize(entity.name);
29
+ const className = `${entityName}Entity`;
30
+
31
+ const defaultFields = [
32
+ {
33
+ name: "id",
34
+ type: "string",
35
+ comment:
36
+ "L'identifiant unique de l'entité.\n * Utilisé pour retrouver de manière unique un enregistrement dans la base de données.\n *\n * Exemple : '123e4567-e89b-12d3-a456-426614174000'",
37
+ },
38
+ {
39
+ name: "createdAt",
40
+ type: "Date",
41
+ comment:
42
+ "La date de création de l'entité.\n * Définie lors de la création et ne change pas.\n *\n * Exemple : new Date('2022-01-01T10:00:00Z')",
43
+ },
44
+ {
45
+ name: "updatedAt",
46
+ type: "Date",
47
+ comment:
48
+ "La date de dernière mise à jour de l'entité.\n * Mise à jour à chaque modification.\n *\n * Exemple : new Date('2022-02-01T15:00:00Z')",
49
+ },
50
+ ];
51
+
52
+ // Types de base autorisés dans une Entité de Domaine
53
+ const DOMAIN_SCALAR_TYPES = [
54
+ "string",
55
+ "number",
56
+ "boolean",
57
+ "date",
58
+ "json",
59
+ "text",
60
+ "uuid",
61
+ "decimal",
62
+ "float",
63
+ "int",
64
+ "role",
65
+ ];
66
+
67
+ // Filtrage : on ne garde que les types scalaires ou les IDs techniques
68
+ const filteredFields = entity.fields.filter((f) => {
69
+ const typeLower = f.type.toLowerCase().replace("[]", "");
70
+ return DOMAIN_SCALAR_TYPES.includes(typeLower) || f.name.endsWith("Id");
71
+ });
72
+
73
+ const isUserEntityWithRole =
74
+ entity.name.toLowerCase() === "user" &&
75
+ entity.fields.some((f) => f.name === "role");
76
+
77
+ const allFields = [...defaultFields, ...filteredFields];
78
+
79
+ const constructorParams = allFields
80
+ .map((f) => `\n private readonly ${f.name}: ${getFormattedType(f)},`)
81
+ .join("");
82
+
83
+ const getters = allFields
84
+ .map(
85
+ (f) => `
86
+ get${capitalize(f.name)}(): ${getFormattedType(f)}
87
+ {
88
+ return this.${f.name};
89
+ }`,
90
+ )
91
+ .join("\n");
92
+
93
+ const jsonFields = allFields
94
+ .map((f) => ` ${f.name}: this.${f.name},`)
95
+ .join("\n");
96
+
97
+ let importStatements = "";
98
+ if (isUserEntityWithRole) {
99
+ importStatements +=
100
+ mode == "full"
101
+ ? `import { Role } from 'src/user/domain/enums/role.enum';\n\n`
102
+ : `import { Role } from 'src/common/enums/role.enum'; \n\n`;
103
+ }
104
+
105
+ return `${importStatements}/**
106
+ * ${className} représente l'entité principale de ${entityName} dans le domaine.
107
+ * Elle contient les propriétés de base nécessaires à la gestion des données liées à ${entityName}.
108
+ */
109
+ export class ${className} {
110
+ constructor(${constructorParams}
111
+ ) {}
112
+ ${getters}
113
+
114
+ /**
115
+ * transforme entity data to json
116
+ */
117
+ toJSON() {
118
+ return {
119
+ ${jsonFields}
120
+ };
121
+ }
122
+ }
123
+ `;
124
+ }
125
+
126
+ module.exports = { generateEntityFileContent };
@@ -0,0 +1,114 @@
1
+ /**
2
+ * utils/generators/infrastructure/mapperGenerator.js
3
+ *
4
+ * Générateur du Mapper infrastructure (Full/Clean Architecture uniquement).
5
+ * Extrait de utils.js — Phase 1B.
6
+ *
7
+ * Le mapper n'est JAMAIS utilisé en mode light :
8
+ * - light : le repository utilise une méthode `private toEntity()` inline
9
+ * - full : le mapper découple persistence <-> domaine <-> DTO
10
+ */
11
+
12
+ const { capitalize, decapitalize } = require("../../helpers");
13
+
14
+ /**
15
+ * Génère le contenu TypeScript du Mapper pour une entité.
16
+ *
17
+ * @param {object} entity - L'objet entité { name, fields[] }
18
+ * @returns {Promise<string>} Le code TypeScript du mapper
19
+ */
20
+ async function generateMapper(entity) {
21
+ const entityName = capitalize(entity.name);
22
+
23
+ // 1. Types scalaires / primitifs autorisés dans le mapper
24
+ const SCALAR_TYPES = [
25
+ "string",
26
+ "text",
27
+ "uuid",
28
+ "json",
29
+ "number",
30
+ "int",
31
+ "float",
32
+ "decimal",
33
+ "boolean",
34
+ "date",
35
+ "role",
36
+ ];
37
+
38
+ // 2. Filtrage cohérent : scalaires + IDs techniques
39
+ const filterDomainFields = (f) => {
40
+ const typeName = f.type.toLowerCase().replace("[]", "");
41
+ return (
42
+ SCALAR_TYPES.includes(typeName) || f.name.toLowerCase().endsWith("id")
43
+ );
44
+ };
45
+
46
+ // 3. Champs filtrés pour le constructeur de l'Entity
47
+ const filteredFields = entity.fields.filter(filterDomainFields);
48
+
49
+ const domainArgs = ["data.id"]
50
+ .concat(["data.createdAt", "data.updatedAt"])
51
+ .concat(filteredFields.map((f) => `data.${f.name}`))
52
+ .join(",\n ");
53
+
54
+ // 4. Champs pour la persistence
55
+ const toPersistenceFields = filteredFields
56
+ .map((f) => `${f.name}: dto.${f.name},`)
57
+ .join("\n ");
58
+
59
+ const toUpdateFields = filteredFields
60
+ .map(
61
+ (f) => `if (dto.${f.name} !== undefined) data.${f.name} = dto.${f.name};`,
62
+ )
63
+ .join("\n ");
64
+
65
+ return `/**
66
+ * ${entityName}Mapper transforms data between
67
+ * different layers (Persistence <-> Domain <-> DTO).
68
+ *
69
+ * Ensures that the internal database structure
70
+ * never leaks into the API responses.
71
+ */
72
+
73
+ import { Injectable } from '@nestjs/common';
74
+ import { ${entityName}Entity } from 'src/${decapitalize(
75
+ entity.name,
76
+ )}/domain/entities/${decapitalize(entity.name)}.entity';
77
+ import { Create${entityName}Dto, Update${entityName}Dto } from 'src/${decapitalize(
78
+ entity.name,
79
+ )}/application/dtos/${decapitalize(entity.name)}.dto';
80
+
81
+ @Injectable()
82
+ export class ${entityName}Mapper {
83
+
84
+ /**
85
+ * Transforme les données (Prisma/TypeORM) en Entité de Domaine
86
+ */
87
+ toDomain(data: any): ${entityName}Entity {
88
+ return new ${entityName}Entity(
89
+ ${domainArgs}
90
+ );
91
+ }
92
+
93
+ /**
94
+ * Transforme le DTO en objet pour la création en base de données
95
+ */
96
+ toPersistence(dto: Create${entityName}Dto): any {
97
+ return {
98
+ ${toPersistenceFields}
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Prépare l'objet de mise à jour partielle
104
+ */
105
+ toUpdatePersistence(dto: Update${entityName}Dto): any {
106
+ const data: any = {};
107
+ ${toUpdateFields}
108
+ return data;
109
+ }
110
+ }
111
+ `;
112
+ }
113
+
114
+ module.exports = { generateMapper };