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.
- package/AUDIT_ROADMAP.md +798 -0
- package/CHANGELOG.fr.md +22 -0
- package/CHANGELOG.md +22 -0
- package/CLI_USAGE.fr.md +331 -331
- package/CLI_USAGE.md +364 -364
- package/LICENSE +21 -21
- package/PROGRESS.md +258 -0
- package/bin/nestcraft.js +84 -64
- package/commands/demo.js +337 -330
- package/commands/generate.js +94 -0
- package/commands/generateConf.js +91 -0
- package/commands/help.js +29 -2
- package/commands/info.js +48 -48
- package/commands/new.js +340 -335
- package/commands/start.js +19 -19
- package/commands/test.js +7 -7
- package/package.json +1 -1
- package/utils/app-module.updater.js +96 -0
- package/utils/cliParser.js +88 -76
- package/utils/colors.js +62 -62
- package/utils/configs/configureDocker.js +120 -120
- package/utils/configs/setupCleanArchitecture.js +17 -15
- package/utils/configs/setupLightArchitecture.js +15 -9
- package/utils/file-system.js +75 -0
- package/utils/file-utils/saveProjectConfig.js +36 -0
- package/utils/fullModeInput.js +607 -607
- package/utils/generators/application/dtoGenerator.js +251 -0
- package/utils/generators/application/dtoUpdater.js +54 -0
- package/utils/generators/cleanModuleGenerator.js +475 -0
- package/utils/generators/database/setupDatabase.js +49 -0
- package/utils/generators/domain/entityGenerator.js +126 -0
- package/utils/generators/domain/entityUpdater.js +78 -0
- package/utils/generators/infrastructure/mapperGenerator.js +114 -0
- package/utils/generators/infrastructure/mapperUpdater.js +65 -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/lightModuleGenerator.js +131 -0
- package/utils/generators/presentation/controllerGenerator.js +142 -0
- package/utils/generators/relation/relation.engine.js +64 -0
- package/utils/helpers.js +109 -0
- package/utils/interactive/askEntityInputs.js +165 -0
- package/utils/loggers/logError.js +7 -7
- package/utils/loggers/logInfo.js +7 -7
- package/utils/loggers/logSuccess.js +7 -7
- package/utils/loggers/logWarning.js +7 -7
- package/utils/setups/orms/typeOrmSetup.js +630 -630
- package/utils/setups/setupAuth.js +33 -20
- package/utils/setups/setupDatabase.js +76 -75
- package/utils/setups/setupMongoose.js +22 -5
- package/utils/setups/setupPrisma.js +748 -630
- package/utils/shell.js +112 -32
- package/utils/spinner.js +57 -57
- package/utils/systemCheck.js +124 -124
- package/utils/userInput.js +357 -421
- package/utils/utils.js +29 -2164
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
const {
|
|
2
|
+
createDirectory,
|
|
3
|
+
createFile,
|
|
4
|
+
safeUpdateAppModule,
|
|
5
|
+
capitalize,
|
|
6
|
+
decapitalize,
|
|
7
|
+
} = require("../userInput");
|
|
8
|
+
const { logInfo } = require("../loggers/logInfo");
|
|
9
|
+
const { logSuccess } = require("../loggers/logSuccess");
|
|
10
|
+
const {
|
|
11
|
+
generateEntityFileContent,
|
|
12
|
+
generateDto,
|
|
13
|
+
generateMapper,
|
|
14
|
+
generateController,
|
|
15
|
+
generateRepository,
|
|
16
|
+
generateMongooseSchemaFileContent,
|
|
17
|
+
} = require("../utils");
|
|
18
|
+
const { updateExistingEntityRelation } = require("./domain/entityUpdater");
|
|
19
|
+
const { applyRelationPatches } = require("./relation/relation.engine");
|
|
20
|
+
const setupDatabase = require("./database/setupDatabase");
|
|
21
|
+
|
|
22
|
+
async function generateCleanModule(name, config, entityData) {
|
|
23
|
+
const entityNameCapitalized = capitalize(name);
|
|
24
|
+
const entityNameLower = decapitalize(name);
|
|
25
|
+
const entityPath = `src/${entityNameLower}`;
|
|
26
|
+
const mode = "full";
|
|
27
|
+
|
|
28
|
+
// Chemins de la Clean Architecture
|
|
29
|
+
const folders = [
|
|
30
|
+
"application/use-cases",
|
|
31
|
+
"application/dtos",
|
|
32
|
+
"domain/interfaces",
|
|
33
|
+
"domain/entities",
|
|
34
|
+
"infrastructure/mappers",
|
|
35
|
+
"infrastructure/repositories",
|
|
36
|
+
"application/services",
|
|
37
|
+
"infrastructure/adapters",
|
|
38
|
+
"presentation/controllers",
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
logInfo(
|
|
42
|
+
` Building Clean Architecture layers for module: ${entityNameCapitalized}`,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// 1. Création des dossiers
|
|
46
|
+
for (const folder of folders) {
|
|
47
|
+
await createDirectory(`${entityPath}/${folder}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 2. Génération de l'Entité de Domaine
|
|
51
|
+
const entityContent = await generateEntityFileContent(entityData);
|
|
52
|
+
await createFile({
|
|
53
|
+
path: `${entityPath}/domain/entities/${entityNameLower}.entity.ts`,
|
|
54
|
+
contente: entityContent,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// 3. Génération de l'Interface Repository
|
|
58
|
+
await createFile({
|
|
59
|
+
path: `${entityPath}/domain/interfaces/${entityNameLower}.repository.interface.ts`,
|
|
60
|
+
contente: getRepositoryInterfaceTemplate(
|
|
61
|
+
entityNameCapitalized,
|
|
62
|
+
entityNameLower,
|
|
63
|
+
),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// 4. Génération du Repository (Implementation)
|
|
67
|
+
await generateRepository(name, config.orm);
|
|
68
|
+
|
|
69
|
+
// 5. Génération des Use Cases (Create, Get, Update, Delete)
|
|
70
|
+
await generateUseCases(entityPath, entityNameCapitalized, entityNameLower);
|
|
71
|
+
|
|
72
|
+
// 6. Génération des DTOs
|
|
73
|
+
const dtoContent = await generateDto(entityData, config.swagger);
|
|
74
|
+
await createFile({
|
|
75
|
+
path: `${entityPath}/application/dtos/${entityNameLower}.dto.ts`,
|
|
76
|
+
contente: dtoContent,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// 7. Génération du Mapper
|
|
80
|
+
const mapperContent = await generateMapper(entityData);
|
|
81
|
+
await createFile({
|
|
82
|
+
path: `${entityPath}/infrastructure/mappers/${entityNameLower}.mapper.ts`,
|
|
83
|
+
contente: mapperContent,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ÉTAPE RELATIONS : Patching des fichiers si une relation existe
|
|
87
|
+
if (
|
|
88
|
+
entityData.relation &&
|
|
89
|
+
(entityData.relation.type != "1-n" || entityData.relation.type != "n-n")
|
|
90
|
+
) {
|
|
91
|
+
const { target, type } = entityData.relation;
|
|
92
|
+
logInfo(
|
|
93
|
+
`🔗 Linking ${entityNameCapitalized} with ${capitalize(target)} (${type})...`,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
await updateExistingEntityRelation(target, name, type);
|
|
97
|
+
|
|
98
|
+
/* if (type === "n-1" || type === "1-1") {
|
|
99
|
+
await patchDtoWithRelation(name, target, config.swagger);
|
|
100
|
+
await patchMapperWithRelation(name, target);
|
|
101
|
+
} */
|
|
102
|
+
|
|
103
|
+
await applyRelationPatches(name, target, type, config.swagger, mode);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 8. Génération du Service, Adapter et Controller
|
|
107
|
+
await generateServiceAndAdapter(
|
|
108
|
+
entityPath,
|
|
109
|
+
entityNameCapitalized,
|
|
110
|
+
entityNameLower,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const controllerContent = await generateController(
|
|
114
|
+
name,
|
|
115
|
+
entityPath,
|
|
116
|
+
config.swagger,
|
|
117
|
+
);
|
|
118
|
+
await createFile({
|
|
119
|
+
path: `${entityPath}/presentation/controllers/${entityNameLower}.controller.ts`,
|
|
120
|
+
contente: controllerContent,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// 9. Génération du Module NestJS
|
|
124
|
+
const moduleContent = getModuleTemplate(
|
|
125
|
+
entityNameCapitalized,
|
|
126
|
+
entityNameLower,
|
|
127
|
+
entityPath,
|
|
128
|
+
config,
|
|
129
|
+
);
|
|
130
|
+
await createFile({
|
|
131
|
+
path: `${entityPath}/${entityNameLower}.module.ts`,
|
|
132
|
+
contente: moduleContent,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// 10. Auto-enregistrement dans AppModule
|
|
136
|
+
await safeUpdateAppModule(entityNameLower);
|
|
137
|
+
|
|
138
|
+
// 11.
|
|
139
|
+
await setupDatabase(config, entityData);
|
|
140
|
+
|
|
141
|
+
logSuccess(
|
|
142
|
+
` ✨ Module ${entityNameCapitalized} generated and registered successfully!`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// --- HELPERS DE TEMPLATES (Extraits de ton setup original) ---
|
|
147
|
+
|
|
148
|
+
function getRepositoryInterfaceTemplate(cap, low) {
|
|
149
|
+
return `import { Create${cap}Dto, Update${cap}Dto } from 'src/${low}/application/dtos/${low}.dto';
|
|
150
|
+
import { ${cap}Entity } from 'src/${low}/domain/entities/${low}.entity';
|
|
151
|
+
|
|
152
|
+
export const I${cap}RepositoryName = 'I${cap}Repository';
|
|
153
|
+
|
|
154
|
+
export interface I${cap}Repository {
|
|
155
|
+
create(data: Create${cap}Dto): Promise<${cap}Entity>;
|
|
156
|
+
findById(id: string): Promise<${cap}Entity | null>;
|
|
157
|
+
findAll(): Promise<${cap}Entity[]>;
|
|
158
|
+
update(id: string, data: Update${cap}Dto): Promise<${cap}Entity | null>;
|
|
159
|
+
delete(id: string): Promise<void>;
|
|
160
|
+
}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
///////////////////
|
|
164
|
+
async function generateUseCases(entityPath, cap, low) {
|
|
165
|
+
const useCases = ["Create", "GetById", "GetAll", "Update", "Delete"];
|
|
166
|
+
|
|
167
|
+
for (const useCase of useCases) {
|
|
168
|
+
let content = "";
|
|
169
|
+
const fileName = `${decapitalize(useCase)}-${low}.use-case.ts`;
|
|
170
|
+
const fullPath = `${entityPath}/application/use-cases/${fileName}`;
|
|
171
|
+
|
|
172
|
+
switch (useCase) {
|
|
173
|
+
case "Create":
|
|
174
|
+
content = `
|
|
175
|
+
import { Inject, Logger } from '@nestjs/common';
|
|
176
|
+
import { Create${cap}Dto } from 'src/${low}/application/dtos/${low}.dto';
|
|
177
|
+
import { I${cap}RepositoryName, type I${cap}Repository } from 'src/${low}/domain/interfaces/${low}.repository.interface';
|
|
178
|
+
import { ${cap}Entity } from 'src/${low}/domain/entities/${low}.entity';
|
|
179
|
+
|
|
180
|
+
export class Create${cap}UseCase {
|
|
181
|
+
private readonly logger = new Logger(Create${cap}UseCase.name);
|
|
182
|
+
|
|
183
|
+
constructor(
|
|
184
|
+
@Inject(I${cap}RepositoryName)
|
|
185
|
+
private readonly repository: I${cap}Repository,
|
|
186
|
+
) {}
|
|
187
|
+
|
|
188
|
+
async execute(data: Create${cap}Dto): Promise<${cap}Entity> {
|
|
189
|
+
this.logger.log('Starting creation process for ${cap}');
|
|
190
|
+
const result = await this.repository.create(data);
|
|
191
|
+
this.logger.log(\`Successfully created ${cap} (ID: \${result.getId()})\`);
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
}`;
|
|
195
|
+
break;
|
|
196
|
+
|
|
197
|
+
case "GetById":
|
|
198
|
+
content = `
|
|
199
|
+
import { Inject, Logger, NotFoundException } from '@nestjs/common';
|
|
200
|
+
import { I${cap}RepositoryName, type I${cap}Repository } from 'src/${low}/domain/interfaces/${low}.repository.interface';
|
|
201
|
+
import { ${cap}Entity } from 'src/${low}/domain/entities/${low}.entity';
|
|
202
|
+
|
|
203
|
+
export class GetById${cap}UseCase {
|
|
204
|
+
private readonly logger = new Logger(GetById${cap}UseCase.name);
|
|
205
|
+
|
|
206
|
+
constructor(
|
|
207
|
+
@Inject(I${cap}RepositoryName)
|
|
208
|
+
private readonly repository: I${cap}Repository,
|
|
209
|
+
) {}
|
|
210
|
+
|
|
211
|
+
async execute(id: string): Promise<${cap}Entity> {
|
|
212
|
+
this.logger.log(\`Fetching ${cap} (ID: \${id})\`);
|
|
213
|
+
const result = await this.repository.findById(id);
|
|
214
|
+
|
|
215
|
+
if (!result) {
|
|
216
|
+
this.logger.warn(\`${cap} \${id} not found\`);
|
|
217
|
+
throw new NotFoundException(\`${cap} not found\`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
this.logger.log(\`Successfully retrieved ${cap}\`);
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
}`;
|
|
224
|
+
break;
|
|
225
|
+
|
|
226
|
+
case "GetAll":
|
|
227
|
+
content = `
|
|
228
|
+
import { Inject, Logger } from '@nestjs/common';
|
|
229
|
+
import { I${cap}RepositoryName, type I${cap}Repository } from 'src/${low}/domain/interfaces/${low}.repository.interface';
|
|
230
|
+
import { ${cap}Entity } from 'src/${low}/domain/entities/${low}.entity';
|
|
231
|
+
|
|
232
|
+
export class GetAll${cap}UseCase {
|
|
233
|
+
private readonly logger = new Logger(GetAll${cap}UseCase.name);
|
|
234
|
+
|
|
235
|
+
constructor(
|
|
236
|
+
@Inject(I${cap}RepositoryName)
|
|
237
|
+
private readonly repository: I${cap}Repository,
|
|
238
|
+
) {}
|
|
239
|
+
|
|
240
|
+
async execute(): Promise<${cap}Entity[]> {
|
|
241
|
+
this.logger.log('Requesting all ${cap} records');
|
|
242
|
+
const results = await this.repository.findAll();
|
|
243
|
+
this.logger.log(\`Retrieved \${results.length} ${cap}(s)\`);
|
|
244
|
+
return results;
|
|
245
|
+
}
|
|
246
|
+
}`;
|
|
247
|
+
break;
|
|
248
|
+
|
|
249
|
+
case "Update":
|
|
250
|
+
content = `
|
|
251
|
+
import { Inject, Logger, NotFoundException } from '@nestjs/common';
|
|
252
|
+
import { Update${cap}Dto } from 'src/${low}/application/dtos/${low}.dto';
|
|
253
|
+
import { I${cap}RepositoryName, type I${cap}Repository } from 'src/${low}/domain/interfaces/${low}.repository.interface';
|
|
254
|
+
import { ${cap}Entity } from 'src/${low}/domain/entities/${low}.entity';
|
|
255
|
+
|
|
256
|
+
export class Update${cap}UseCase {
|
|
257
|
+
private readonly logger = new Logger(Update${cap}UseCase.name);
|
|
258
|
+
|
|
259
|
+
constructor(
|
|
260
|
+
@Inject(I${cap}RepositoryName)
|
|
261
|
+
private readonly repository: I${cap}Repository,
|
|
262
|
+
) {}
|
|
263
|
+
|
|
264
|
+
async execute(id: string, data: Update${cap}Dto): Promise<${cap}Entity> {
|
|
265
|
+
this.logger.log(\`Updating ${cap} (ID: \${id})\`);
|
|
266
|
+
const existing = await this.repository.findById(id);
|
|
267
|
+
if (!existing) throw new NotFoundException(\`${cap} not found\`);
|
|
268
|
+
|
|
269
|
+
const result = await this.repository.update(id, data);
|
|
270
|
+
if (!result) {
|
|
271
|
+
this.logger.error(\`Update failed for ${cap} \${id} - entity disappeared\`);
|
|
272
|
+
throw new NotFoundException(\`${cap} update failed\`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
this.logger.log(\`Successfully updated ${cap} \${id}\`);
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
}`;
|
|
279
|
+
break;
|
|
280
|
+
|
|
281
|
+
case "Delete":
|
|
282
|
+
content = `
|
|
283
|
+
import { Inject, Logger, NotFoundException } from '@nestjs/common';
|
|
284
|
+
import { I${cap}RepositoryName, type I${cap}Repository } from 'src/${low}/domain/interfaces/${low}.repository.interface';
|
|
285
|
+
|
|
286
|
+
export class Delete${cap}UseCase {
|
|
287
|
+
private readonly logger = new Logger(Delete${cap}UseCase.name);
|
|
288
|
+
|
|
289
|
+
constructor(
|
|
290
|
+
@Inject(I${cap}RepositoryName)
|
|
291
|
+
private readonly repository: I${cap}Repository,
|
|
292
|
+
) {}
|
|
293
|
+
|
|
294
|
+
async execute(id: string): Promise<void> {
|
|
295
|
+
this.logger.log(\`Deleting ${cap} (ID: \${id})\`);
|
|
296
|
+
const existing = await this.repository.findById(id);
|
|
297
|
+
if (!existing) throw new NotFoundException(\`${cap} not found\`);
|
|
298
|
+
|
|
299
|
+
await this.repository.delete(id);
|
|
300
|
+
this.logger.log(\`Successfully deleted ${cap} \${id}\`);
|
|
301
|
+
}
|
|
302
|
+
}`;
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
await createFile({
|
|
307
|
+
path: fullPath,
|
|
308
|
+
contente: content.trim(),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
//////////////////
|
|
314
|
+
function getModuleTemplate(cap, low, entityPath, config) {
|
|
315
|
+
let importsBlock = [];
|
|
316
|
+
let providersBlock = [];
|
|
317
|
+
let extraImports = "";
|
|
318
|
+
let forwardRefImport = "";
|
|
319
|
+
|
|
320
|
+
// Gestion de l'ORM pour les imports du Module
|
|
321
|
+
if (config.orm === "prisma") {
|
|
322
|
+
extraImports += `import { PrismaModule } from 'src/prisma/prisma.module';\n`;
|
|
323
|
+
importsBlock.push("PrismaModule");
|
|
324
|
+
} else if (config.orm === "typeorm") {
|
|
325
|
+
extraImports += `import { ${cap} } from 'src/entities/${cap}.entity';\nimport { TypeOrmModule } from '@nestjs/typeorm';\n`;
|
|
326
|
+
importsBlock.push(`TypeOrmModule.forFeature([${cap}])`);
|
|
327
|
+
} else if (config.orm === "mongoose") {
|
|
328
|
+
extraImports += `import { MongooseModule } from '@nestjs/mongoose';\nimport { ${cap}, ${cap}Schema } from 'src/${low}/infrastructure/persistence/mongoose/${low}.schema';\n`;
|
|
329
|
+
importsBlock.push(
|
|
330
|
+
`MongooseModule.forFeature([{ name: ${cap}.name, schema: ${cap}Schema }])`,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Support Auth si nécessaire (User module)
|
|
335
|
+
if (low === "user" && config.auth) {
|
|
336
|
+
extraImports += "import { AuthModule } from 'src/auth/auth.module';\n";
|
|
337
|
+
importsBlock.push("forwardRef(() => AuthModule)");
|
|
338
|
+
forwardRefImport = " forwardRef,";
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Providers standard pour la Clean Arch
|
|
342
|
+
providersBlock.push(
|
|
343
|
+
`{ provide: 'I${cap}Repository', useClass: ${cap}Repository }`,
|
|
344
|
+
`${cap}Service`,
|
|
345
|
+
`Create${cap}UseCase`,
|
|
346
|
+
`Update${cap}UseCase`,
|
|
347
|
+
`GetById${cap}UseCase`,
|
|
348
|
+
`GetAll${cap}UseCase`,
|
|
349
|
+
`Delete${cap}UseCase`,
|
|
350
|
+
`${cap}Mapper`,
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
return `
|
|
354
|
+
import {${forwardRefImport} Module } from '@nestjs/common';
|
|
355
|
+
${extraImports}
|
|
356
|
+
import { ${cap}Service } from 'src/${low}/application/services/${low}.service';
|
|
357
|
+
import { ${cap}Controller } from 'src/${low}/presentation/controllers/${low}.controller';
|
|
358
|
+
import { ${cap}Repository } from 'src/${low}/infrastructure/repositories/${low}.repository';
|
|
359
|
+
import { Create${cap}UseCase } from 'src/${low}/application/use-cases/create-${low}.use-case';
|
|
360
|
+
import { Update${cap}UseCase } from 'src/${low}/application/use-cases/update-${low}.use-case';
|
|
361
|
+
import { GetById${cap}UseCase } from 'src/${low}/application/use-cases/getById-${low}.use-case';
|
|
362
|
+
import { GetAll${cap}UseCase } from 'src/${low}/application/use-cases/getAll-${low}.use-case';
|
|
363
|
+
import { Delete${cap}UseCase } from 'src/${low}/application/use-cases/delete-${low}.use-case';
|
|
364
|
+
import { ${cap}Mapper } from 'src/${low}/infrastructure/mappers/${low}.mapper';
|
|
365
|
+
|
|
366
|
+
@Module({
|
|
367
|
+
imports: [
|
|
368
|
+
${importsBlock.join(",\n ")}
|
|
369
|
+
],
|
|
370
|
+
controllers: [
|
|
371
|
+
${cap}Controller
|
|
372
|
+
],
|
|
373
|
+
providers: [
|
|
374
|
+
${providersBlock.join(",\n ")}
|
|
375
|
+
],
|
|
376
|
+
exports: [
|
|
377
|
+
${cap}Service,
|
|
378
|
+
'I${cap}Repository'
|
|
379
|
+
]
|
|
380
|
+
})
|
|
381
|
+
export class ${cap}Module {}
|
|
382
|
+
`.trim();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
////////////////
|
|
386
|
+
async function generateServiceAndAdapter(
|
|
387
|
+
entityPath,
|
|
388
|
+
entityNameCapitalized,
|
|
389
|
+
entityNameLower,
|
|
390
|
+
) {
|
|
391
|
+
// 8. Service
|
|
392
|
+
await createFile({
|
|
393
|
+
path: `${entityPath}/application/services/${entityNameLower}.service.ts`,
|
|
394
|
+
contente: `/**
|
|
395
|
+
* PostService handles business logic
|
|
396
|
+
* for the Post entity.
|
|
397
|
+
*
|
|
398
|
+
* It acts as a bridge between the Controller and the Repository.
|
|
399
|
+
* Responsibilities include:
|
|
400
|
+
* - Data validation and transformation
|
|
401
|
+
* - Orchestrating use cases
|
|
402
|
+
* - Managing transactions
|
|
403
|
+
*/
|
|
404
|
+
|
|
405
|
+
import { Injectable } from '@nestjs/common';
|
|
406
|
+
import { Create${entityNameCapitalized}UseCase } from 'src/${entityNameLower}/application/use-cases/create-${entityNameLower}.use-case';
|
|
407
|
+
import { Update${entityNameCapitalized}UseCase } from 'src/${entityNameLower}/application/use-cases/update-${entityNameLower}.use-case';
|
|
408
|
+
import { GetById${entityNameCapitalized}UseCase } from 'src/${entityNameLower}/application/use-cases/getById-${entityNameLower}.use-case';
|
|
409
|
+
import { GetAll${entityNameCapitalized}UseCase } from 'src/${entityNameLower}/application/use-cases/getAll-${entityNameLower}.use-case';
|
|
410
|
+
import { Delete${entityNameCapitalized}UseCase } from 'src/${entityNameLower}/application/use-cases/delete-${entityNameLower}.use-case';
|
|
411
|
+
import { Create${entityNameCapitalized}Dto, Update${entityNameCapitalized}Dto } from 'src/${entityNameLower}/application/dtos/${entityNameLower}.dto';
|
|
412
|
+
import { ${entityNameCapitalized}Entity } from 'src/${entityNameLower}/domain/entities/${entityNameLower}.entity';
|
|
413
|
+
|
|
414
|
+
@Injectable()
|
|
415
|
+
export class ${entityNameCapitalized}Service {
|
|
416
|
+
constructor(
|
|
417
|
+
private readonly createUseCase: Create${entityNameCapitalized}UseCase,
|
|
418
|
+
private readonly updateUseCase: Update${entityNameCapitalized}UseCase,
|
|
419
|
+
private readonly getByIdUseCase: GetById${entityNameCapitalized}UseCase,
|
|
420
|
+
private readonly getAllUseCase: GetAll${entityNameCapitalized}UseCase,
|
|
421
|
+
private readonly deleteUseCase: Delete${entityNameCapitalized}UseCase,
|
|
422
|
+
) {}
|
|
423
|
+
|
|
424
|
+
async create(dto: Create${entityNameCapitalized}Dto): Promise<${entityNameCapitalized}Entity> {
|
|
425
|
+
return await this.createUseCase.execute(dto);
|
|
426
|
+
}
|
|
427
|
+
async update(id: string, dto: Update${entityNameCapitalized}Dto): Promise<${entityNameCapitalized}Entity | null> {
|
|
428
|
+
return await this.updateUseCase.execute(id, dto);
|
|
429
|
+
}
|
|
430
|
+
async getById(id: string): Promise<${entityNameCapitalized}Entity | null> {
|
|
431
|
+
return await this.getByIdUseCase.execute(id);
|
|
432
|
+
}
|
|
433
|
+
async getAll(): Promise<${entityNameCapitalized}Entity[]> {
|
|
434
|
+
return await this.getAllUseCase.execute();
|
|
435
|
+
}
|
|
436
|
+
async delete(id: string): Promise<void> {
|
|
437
|
+
return await this.deleteUseCase.execute(id);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
`.trim(),
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// 9. Adapter
|
|
444
|
+
await createFile({
|
|
445
|
+
path: `${entityPath}/infrastructure/adapters/${entityNameLower}.adapter.ts`,
|
|
446
|
+
contente: `
|
|
447
|
+
// L'adaptateur permet de transformer ou d'adapter les données d'un format source vers un format cible.
|
|
448
|
+
// Cela est particulièrement utile lorsque nous devons interagir avec des API externes ou des services ayant des structures de données différentes.
|
|
449
|
+
|
|
450
|
+
export class ${entityNameCapitalized}Adapter {
|
|
451
|
+
// La méthode 'adapt' prend des données brutes d'un format spécifique et les transforme
|
|
452
|
+
// en un format qui est attendu par le système de notre domaine.
|
|
453
|
+
adapt(data: any) {
|
|
454
|
+
// Exemple d'adaptation des données - ceci est un exemple générique.
|
|
455
|
+
// Nous transformons les données pour que le format interne du système soit respecté.
|
|
456
|
+
|
|
457
|
+
const adaptedData = {
|
|
458
|
+
// Assurez-vous que vous mappez les propriétés nécessaires et les transformez.
|
|
459
|
+
id: data.id, // Mapping de l'ID de la donnée source à notre format interne
|
|
460
|
+
name: data.fullName || data.name, // Exemple de transformation de champ
|
|
461
|
+
description: data.details || data.description, // Gestion des données optionnelles
|
|
462
|
+
createdAt: new Date(data.createdAt), // Transformation de la date
|
|
463
|
+
updatedAt: new Date(data.updatedAt), // Idem pour la date de mise à jour
|
|
464
|
+
// Vous pouvez adapter d'autres champs en fonction des exigences spécifiques
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
// Retournez les données adaptées dans un format compréhensible pour le système
|
|
468
|
+
return adaptedData;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
`,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
module.exports = generateCleanModule;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const { logInfo } = require("../../loggers/logInfo");
|
|
2
|
+
const { updatePrismaSchema } = require("../../setups/setupPrisma");
|
|
3
|
+
const { createDirectory, createFile } = require("../../userInput");
|
|
4
|
+
const { generateMongooseSchemaFileContent } = require("../../utils");
|
|
5
|
+
|
|
6
|
+
async function setupDatabase(config, entityData) {
|
|
7
|
+
logInfo("Configuring the database...");
|
|
8
|
+
|
|
9
|
+
switch (config.orm) {
|
|
10
|
+
case "prisma":
|
|
11
|
+
await updatePrismaSchema(entityData);
|
|
12
|
+
break;
|
|
13
|
+
case "mongoose":
|
|
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
|
+
});
|
|
42
|
+
break;
|
|
43
|
+
default:
|
|
44
|
+
// Other database types don't require database-level updates on module generation
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = setupDatabase;
|
|
@@ -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 };
|