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.
- package/AUDIT_ROADMAP.md +798 -0
- package/CHANGELOG.fr.md +22 -0
- package/CHANGELOG.md +22 -0
- package/PROGRESS.md +258 -0
- package/commands/demo.js +4 -0
- package/commands/generate.js +5 -4
- package/commands/help.js +29 -2
- package/commands/new.js +8 -6
- package/package.json +1 -1
- package/utils/app-module.updater.js +96 -0
- package/utils/cliParser.js +0 -45
- package/utils/configs/setupCleanArchitecture.js +2 -2
- package/utils/file-system.js +75 -0
- package/utils/generators/application/dtoGenerator.js +251 -0
- package/utils/generators/database/setupDatabase.js +32 -14
- package/utils/generators/domain/entityGenerator.js +126 -0
- package/utils/generators/infrastructure/mapperGenerator.js +114 -0
- package/utils/generators/infrastructure/middlewareGenerator.js +645 -0
- package/utils/generators/infrastructure/mongooseSchemaGenerator.js +196 -0
- package/utils/generators/infrastructure/repositoryGenerator.js +334 -0
- package/utils/generators/presentation/controllerGenerator.js +142 -0
- package/utils/helpers.js +109 -0
- package/utils/setups/setupAuth.js +5 -5
- package/utils/setups/setupDatabase.js +2 -1
- package/utils/setups/setupMongoose.js +22 -5
- package/utils/setups/setupPrisma.js +78 -132
- package/utils/shell.js +88 -8
- package/utils/userInput.js +37 -101
- package/utils/utils.js +29 -2164
|
@@ -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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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 };
|