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,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils/generators/infrastructure/mongooseSchemaGenerator.js
|
|
3
|
+
*
|
|
4
|
+
* Générateur du schéma Mongoose (Document Schema).
|
|
5
|
+
* Extrait de utils.js — Phase 1B.
|
|
6
|
+
*
|
|
7
|
+
* Supporte les modes "full" et "light" :
|
|
8
|
+
* - full : chemins d'import depuis infrastructure/persistence/mongoose/
|
|
9
|
+
* - light : chemins d'import depuis entities/
|
|
10
|
+
* - Role : full → domain/enums/role.enum | light → common/enums/role.enum
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { capitalize } = require("../../helpers");
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Vérifie si un champ est un objet relationnel (non scalaire).
|
|
17
|
+
* Utilisé pour filtrer les champs qui ne doivent pas être inclus dans le schéma.
|
|
18
|
+
*
|
|
19
|
+
* @param {object} field - Le champ { name, type }
|
|
20
|
+
* @param {object} entitiesData - Les données complètes { entities[], relations[] }
|
|
21
|
+
* @returns {boolean}
|
|
22
|
+
*/
|
|
23
|
+
function isRelationObjectField(field, entitiesData) {
|
|
24
|
+
const typeLower = field.type.toLowerCase();
|
|
25
|
+
return entitiesData.entities?.some((e) => e.name.toLowerCase() === typeLower);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Génère le contenu TypeScript du schéma Mongoose pour une entité.
|
|
30
|
+
*
|
|
31
|
+
* @param {object} entity - L'objet entité { name, fields[] }
|
|
32
|
+
* @param {object} entitiesData - { entities[], relations[] }
|
|
33
|
+
* @param {"full"|"light"} mode - Mode d'architecture
|
|
34
|
+
* @returns {Promise<string>} Le code TypeScript du schéma Mongoose
|
|
35
|
+
*/
|
|
36
|
+
async function generateMongooseSchemaFileContent(
|
|
37
|
+
entity,
|
|
38
|
+
entitiesData,
|
|
39
|
+
mode = "full",
|
|
40
|
+
) {
|
|
41
|
+
const entityName = capitalize(entity.name);
|
|
42
|
+
const entityNameLower = entity.name.toLowerCase();
|
|
43
|
+
const isFull = mode === "full";
|
|
44
|
+
|
|
45
|
+
// --- 1. IMPORTS DYNAMIQUES ---
|
|
46
|
+
const relatedEntities = entitiesData.relations
|
|
47
|
+
.filter((rel) => rel.from === entityNameLower || rel.to === entityNameLower)
|
|
48
|
+
.map((rel) => (rel.from === entityNameLower ? rel.to : rel.from));
|
|
49
|
+
|
|
50
|
+
const uniqueRelated = [...new Set(relatedEntities)].filter(
|
|
51
|
+
(e) => e !== entityNameLower,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
let extraImports = "";
|
|
55
|
+
|
|
56
|
+
uniqueRelated.forEach((target) => {
|
|
57
|
+
const targetCap = capitalize(target);
|
|
58
|
+
const moduleName = target === "session" ? "auth" : target;
|
|
59
|
+
|
|
60
|
+
let importPath = "";
|
|
61
|
+
|
|
62
|
+
if (isFull) {
|
|
63
|
+
// Mode Clean Architecture (Full)
|
|
64
|
+
importPath = `../../../../${moduleName}/infrastructure/persistence/mongoose/${target}.schema`;
|
|
65
|
+
} else {
|
|
66
|
+
// Mode Light
|
|
67
|
+
if (target === "session") {
|
|
68
|
+
importPath = `../../auth/persistence/${target}.schema`;
|
|
69
|
+
} else {
|
|
70
|
+
importPath = `../../${moduleName}/entities/${target}.schema`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
extraImports += `import { ${targetCap} } from '${importPath}';\n`;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (entityNameLower === "user") {
|
|
78
|
+
const rolePath = isFull
|
|
79
|
+
? "../../../domain/enums/role.enum"
|
|
80
|
+
: "../../common/enums/role.enum";
|
|
81
|
+
extraImports += `import { Role } from '${rolePath}';\n`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// --- 2. LOGIQUE DES RELATIONS DYNAMIQUES (Prioritaire) ---
|
|
85
|
+
const dynamicRelations = entitiesData.relations
|
|
86
|
+
.map((rel) => {
|
|
87
|
+
const isFrom = rel.from === entityNameLower;
|
|
88
|
+
const isTo = rel.to === entityNameLower;
|
|
89
|
+
if (!isFrom && !isTo) return null;
|
|
90
|
+
|
|
91
|
+
const otherEntity = isFrom ? rel.to : rel.from;
|
|
92
|
+
const otherCap = capitalize(otherEntity);
|
|
93
|
+
|
|
94
|
+
switch (rel.type) {
|
|
95
|
+
case "1-n":
|
|
96
|
+
return isTo
|
|
97
|
+
? ` @Prop({ type: mongoose.Schema.Types.ObjectId, ref: ${otherCap}.name, required: true })\n ${otherEntity}Id: mongoose.Types.ObjectId;`
|
|
98
|
+
: null;
|
|
99
|
+
case "n-1":
|
|
100
|
+
return isFrom
|
|
101
|
+
? ` @Prop({ type: mongoose.Schema.Types.ObjectId, ref: ${otherCap}.name, required: true })\n ${otherEntity}Id: mongoose.Types.ObjectId;`
|
|
102
|
+
: null;
|
|
103
|
+
case "1-1":
|
|
104
|
+
return isFrom
|
|
105
|
+
? ` @Prop({ type: mongoose.Schema.Types.ObjectId, ref: ${otherCap}.name, unique: true })\n ${otherEntity}Id: mongoose.Types.ObjectId;`
|
|
106
|
+
: null;
|
|
107
|
+
case "n-n":
|
|
108
|
+
return ` @Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: ${otherCap}.name }] })\n ${otherEntity}Ids: mongoose.Types.ObjectId[];`;
|
|
109
|
+
default:
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
.filter(Boolean);
|
|
114
|
+
|
|
115
|
+
// --- 3. FILTRAGE DES CHAMPS DIRECTS (Scalaires uniquement) ---
|
|
116
|
+
const scalarTypes = [
|
|
117
|
+
"string",
|
|
118
|
+
"text",
|
|
119
|
+
"number",
|
|
120
|
+
"int",
|
|
121
|
+
"float",
|
|
122
|
+
"boolean",
|
|
123
|
+
"date",
|
|
124
|
+
"uuid",
|
|
125
|
+
"json",
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
const directFields = entity.fields
|
|
129
|
+
.filter((f) => {
|
|
130
|
+
const nameLow = f.name.toLowerCase();
|
|
131
|
+
const isRelId = uniqueRelated.some(
|
|
132
|
+
(rel) => nameLow === rel + "id" || nameLow === rel,
|
|
133
|
+
);
|
|
134
|
+
return scalarTypes.includes(f.type.toLowerCase()) && !isRelId;
|
|
135
|
+
})
|
|
136
|
+
.map((f) => {
|
|
137
|
+
const fieldName = f.name;
|
|
138
|
+
const rawType = f.type.toLowerCase();
|
|
139
|
+
|
|
140
|
+
// 1. Gestion spécifique du Role
|
|
141
|
+
if (entityNameLower === "user" && fieldName === "role") {
|
|
142
|
+
return ` @Prop({ type: String, enum: Role, default: Role.USER })\n role: Role;`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 2. Mapping des types CLI -> TypeScript/Mongoose
|
|
146
|
+
let tsType = "string";
|
|
147
|
+
let propOptions = "required: true";
|
|
148
|
+
|
|
149
|
+
switch (rawType) {
|
|
150
|
+
case "text":
|
|
151
|
+
case "uuid":
|
|
152
|
+
case "string":
|
|
153
|
+
tsType = "string";
|
|
154
|
+
break;
|
|
155
|
+
case "int":
|
|
156
|
+
case "number":
|
|
157
|
+
case "float":
|
|
158
|
+
case "decimal":
|
|
159
|
+
tsType = "number";
|
|
160
|
+
break;
|
|
161
|
+
case "boolean":
|
|
162
|
+
tsType = "boolean";
|
|
163
|
+
break;
|
|
164
|
+
case "date":
|
|
165
|
+
tsType = "Date";
|
|
166
|
+
break;
|
|
167
|
+
case "json":
|
|
168
|
+
tsType = "Record<string, any>";
|
|
169
|
+
propOptions = "type: Object, required: true";
|
|
170
|
+
break;
|
|
171
|
+
default:
|
|
172
|
+
tsType = "any";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return ` @Prop({ ${propOptions} })\n ${fieldName}: ${tsType};`;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const allFields = [...new Set([...directFields, ...dynamicRelations])].join(
|
|
179
|
+
"\n\n",
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
return `import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
|
183
|
+
import * as mongoose from 'mongoose';
|
|
184
|
+
import { Document } from 'mongoose';
|
|
185
|
+
${extraImports}
|
|
186
|
+
export type ${entityName}Document = ${entityName} & Document;
|
|
187
|
+
|
|
188
|
+
@Schema({ timestamps: true })
|
|
189
|
+
export class ${entityName} {
|
|
190
|
+
${allFields}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export const ${entityName}Schema = SchemaFactory.createForClass(${entityName});`.trim();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = { generateMongooseSchemaFileContent };
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils/generators/infrastructure/repositoryGenerator.js
|
|
3
|
+
*
|
|
4
|
+
* Générateur du Repository (Full/Clean Architecture uniquement).
|
|
5
|
+
* Extrait de utils.js — Phase 1B.
|
|
6
|
+
*
|
|
7
|
+
* ATTENTION : Ce générateur est UNIQUEMENT pour le mode Full (Clean Architecture).
|
|
8
|
+
* Il implémente l'interface I${Entity}Repository et utilise le Mapper.
|
|
9
|
+
*
|
|
10
|
+
* Le mode Light a son propre générateur `generateLightRepository` dans
|
|
11
|
+
* setupLightArchitecture.js avec une implémentation entièrement différente :
|
|
12
|
+
* - Pas de Mapper, méthode toEntity() locale
|
|
13
|
+
* - Imports depuis '../dtos/' et '../entities/' (chemins relatifs simples)
|
|
14
|
+
* - Pas d'interface IEntityRepository
|
|
15
|
+
*
|
|
16
|
+
* Ne jamais fusionner les deux.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const { capitalize } = require("../../helpers");
|
|
20
|
+
const { createFile } = require("../../file-system");
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Génère et écrit le fichier repository pour une entité (Full Architecture).
|
|
24
|
+
*
|
|
25
|
+
* @param {string} entityName - Le nom de l'entité (ex: "post", "user")
|
|
26
|
+
* @param {"prisma"|"typeorm"|"mongoose"|"sequelize"} orm - L'ORM cible
|
|
27
|
+
* @returns {Promise<void>}
|
|
28
|
+
*/
|
|
29
|
+
async function generateRepository(entityName, orm) {
|
|
30
|
+
const entityNameCapitalized = capitalize(entityName);
|
|
31
|
+
const entityNameLower = entityName.toLowerCase();
|
|
32
|
+
const entityPath = `src/${entityNameLower}`;
|
|
33
|
+
|
|
34
|
+
// Méthode supplémentaire findByEmail pour l'entité User
|
|
35
|
+
const isUser = entityNameLower === "user";
|
|
36
|
+
const getExtraMethods = (ormType) => {
|
|
37
|
+
if (!isUser) return "";
|
|
38
|
+
|
|
39
|
+
switch (ormType) {
|
|
40
|
+
case "typeorm":
|
|
41
|
+
return `
|
|
42
|
+
async findByEmail(email: string): Promise<${entityNameCapitalized}Entity | null> {
|
|
43
|
+
const record = await this.repository.findOne({ where: { email } as any });
|
|
44
|
+
return record ? this.mapper.toDomain(record) : null;
|
|
45
|
+
}`;
|
|
46
|
+
case "prisma":
|
|
47
|
+
return `
|
|
48
|
+
async findByEmail(email: string): Promise<${entityNameCapitalized}Entity | null> {
|
|
49
|
+
const record = await this.prisma.${entityNameLower}.findFirst({ where: { email } });
|
|
50
|
+
return record ? this.mapper.toDomain(record) : null;
|
|
51
|
+
}`;
|
|
52
|
+
case "mongoose":
|
|
53
|
+
return `
|
|
54
|
+
async findByEmail(email: string): Promise<${entityNameCapitalized}Entity | null> {
|
|
55
|
+
const record = await this.model.findOne({ email }).exec();
|
|
56
|
+
return record ? this.mapper.toDomain(record) : null;
|
|
57
|
+
}`;
|
|
58
|
+
case "sequelize":
|
|
59
|
+
return `
|
|
60
|
+
async findByEmail(email: string): Promise<${entityNameCapitalized}Entity | null> {
|
|
61
|
+
const record = await this.model.findOne({ where: { email } });
|
|
62
|
+
return record ? this.mapper.toDomain(record) : null;
|
|
63
|
+
}`;
|
|
64
|
+
default:
|
|
65
|
+
return "";
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const extraMethods = getExtraMethods(orm);
|
|
70
|
+
|
|
71
|
+
switch (orm) {
|
|
72
|
+
case "typeorm":
|
|
73
|
+
await createFile({
|
|
74
|
+
path: `${entityPath}/infrastructure/repositories/${entityNameLower}.repository.ts`,
|
|
75
|
+
contente: `/**
|
|
76
|
+
* PostRepository handles data persistence
|
|
77
|
+
* for the Post entity.
|
|
78
|
+
*
|
|
79
|
+
* This layer abstracts the database engine (Prisma/TypeORM)
|
|
80
|
+
* and provides a clean interface for data operations.
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
84
|
+
import { Repository } from 'typeorm';
|
|
85
|
+
import { InjectRepository } from '@nestjs/typeorm';
|
|
86
|
+
import { ${entityNameCapitalized}Entity } from '${entityPath}/domain/entities/${entityNameLower}.entity';
|
|
87
|
+
import { ${entityNameCapitalized} } from 'src/entities/${entityNameCapitalized}.entity';
|
|
88
|
+
import type { I${entityNameCapitalized}Repository } from '${entityPath}/domain/interfaces/${entityNameLower}.repository.interface';
|
|
89
|
+
import { Create${entityNameCapitalized}Dto, Update${entityNameCapitalized}Dto } from '${entityPath}/application/dtos/${entityNameLower}.dto';
|
|
90
|
+
import { ${entityNameCapitalized}Mapper } from '${entityPath}/infrastructure/mappers/${entityNameLower}.mapper';
|
|
91
|
+
|
|
92
|
+
@Injectable()
|
|
93
|
+
export class ${entityNameCapitalized}Repository implements I${entityNameCapitalized}Repository {
|
|
94
|
+
constructor(
|
|
95
|
+
@InjectRepository(${entityNameCapitalized})
|
|
96
|
+
private readonly repository: Repository<${entityNameCapitalized}>,
|
|
97
|
+
private readonly mapper: ${entityNameCapitalized}Mapper,
|
|
98
|
+
) {}
|
|
99
|
+
|
|
100
|
+
// create
|
|
101
|
+
async create(data: Create${entityNameCapitalized}Dto): Promise<${entityNameCapitalized}Entity> {
|
|
102
|
+
const toPersist = this.mapper.toPersistence(data);
|
|
103
|
+
const created = await this.repository.save(toPersist);
|
|
104
|
+
return this.mapper.toDomain(created);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// find by id
|
|
108
|
+
async findById(id: string): Promise<${entityNameCapitalized}Entity | null> {
|
|
109
|
+
const record = await this.repository.findOne({
|
|
110
|
+
where: { id },
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (!record) return null;
|
|
114
|
+
|
|
115
|
+
return this.mapper.toDomain(record);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
${extraMethods}
|
|
119
|
+
|
|
120
|
+
// update
|
|
121
|
+
async update(id: string, data: Update${entityNameCapitalized}Dto): Promise<${entityNameCapitalized}Entity> {
|
|
122
|
+
const toUpdate = this.mapper.toUpdatePersistence(data);
|
|
123
|
+
const updated = await this.repository.save({ ...toUpdate, id });
|
|
124
|
+
return this.mapper.toDomain(updated);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// find all
|
|
128
|
+
async findAll(): Promise<${entityNameCapitalized}Entity[]> {
|
|
129
|
+
const records = await this.repository.find();
|
|
130
|
+
return records.map(record => this.mapper.toDomain(record));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// delete
|
|
134
|
+
async delete(id: string): Promise<void> {
|
|
135
|
+
await this.repository.delete(id);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
`,
|
|
139
|
+
});
|
|
140
|
+
break;
|
|
141
|
+
|
|
142
|
+
case "prisma":
|
|
143
|
+
await createFile({
|
|
144
|
+
path: `${entityPath}/infrastructure/repositories/${entityNameLower}.repository.ts`,
|
|
145
|
+
contente: `import { Injectable, NotFoundException } from '@nestjs/common';
|
|
146
|
+
import { PrismaService } from 'src/prisma/prisma.service';
|
|
147
|
+
import { Create${entityNameCapitalized}Dto, Update${entityNameCapitalized}Dto } from '${entityPath}/application/dtos/${entityNameLower}.dto';
|
|
148
|
+
import { I${entityNameCapitalized}Repository } from '${entityPath}/domain/interfaces/${entityNameLower}.repository.interface';
|
|
149
|
+
import { ${entityNameCapitalized}Entity } from '${entityPath}/domain/entities/${entityNameLower}.entity';
|
|
150
|
+
import { ${entityNameCapitalized}Mapper } from '${entityPath}/infrastructure/mappers/${entityNameLower}.mapper';
|
|
151
|
+
|
|
152
|
+
@Injectable()
|
|
153
|
+
export class ${entityNameCapitalized}Repository implements I${entityNameCapitalized}Repository {
|
|
154
|
+
constructor(
|
|
155
|
+
private readonly prisma: PrismaService,
|
|
156
|
+
private readonly mapper: ${entityNameCapitalized}Mapper,
|
|
157
|
+
) {}
|
|
158
|
+
|
|
159
|
+
// create
|
|
160
|
+
async create(data: Create${entityNameCapitalized}Dto): Promise<${entityNameCapitalized}Entity> {
|
|
161
|
+
const toPersist = this.mapper.toPersistence(data);
|
|
162
|
+
const created = await this.prisma.${entityNameLower}.create({ data: toPersist });
|
|
163
|
+
return this.mapper.toDomain(created);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// find by id
|
|
167
|
+
async findById(id: string): Promise<${entityNameCapitalized}Entity | null> {
|
|
168
|
+
const record = await this.prisma.${entityNameLower}.findUnique({
|
|
169
|
+
where: { id },
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
if (!record) return null;
|
|
173
|
+
|
|
174
|
+
return this.mapper.toDomain(record);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
${extraMethods}
|
|
178
|
+
|
|
179
|
+
// update
|
|
180
|
+
async update(id: string, data: Update${entityNameCapitalized}Dto): Promise<${entityNameCapitalized}Entity> {
|
|
181
|
+
const toUpdate = this.mapper.toUpdatePersistence(data);
|
|
182
|
+
const updated = await this.prisma.${entityNameLower}.update({
|
|
183
|
+
where: { id },
|
|
184
|
+
data: toUpdate,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return this.mapper.toDomain(updated);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// find all
|
|
191
|
+
async findAll(): Promise<${entityNameCapitalized}Entity[]> {
|
|
192
|
+
const records = await this.prisma.${entityNameLower}.findMany();
|
|
193
|
+
return records.map(record => this.mapper.toDomain(record));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// delete
|
|
197
|
+
async delete(id: string): Promise<void> {
|
|
198
|
+
await this.prisma.${entityNameLower}.delete({
|
|
199
|
+
where: { id },
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
`,
|
|
204
|
+
});
|
|
205
|
+
break;
|
|
206
|
+
|
|
207
|
+
case "mongoose":
|
|
208
|
+
await createFile({
|
|
209
|
+
path: `${entityPath}/infrastructure/repositories/${entityNameLower}.repository.ts`,
|
|
210
|
+
contente: `import { Injectable, NotFoundException } from '@nestjs/common';
|
|
211
|
+
import { InjectModel } from '@nestjs/mongoose';
|
|
212
|
+
import { Model } from 'mongoose';
|
|
213
|
+
import { ${entityNameCapitalized}Entity } from '${entityPath}/domain/entities/${entityNameLower}.entity';
|
|
214
|
+
import { Create${entityNameCapitalized}Dto, Update${entityNameCapitalized}Dto } from '${entityPath}/application/dtos/${entityNameLower}.dto';
|
|
215
|
+
import { I${entityNameCapitalized}Repository } from '${entityPath}/domain/interfaces/${entityNameLower}.repository.interface';
|
|
216
|
+
import { ${entityNameCapitalized} } from '${entityPath}/infrastructure/persistence/mongoose/${entityNameLower}.schema';
|
|
217
|
+
import { ${entityNameCapitalized}Mapper } from '${entityPath}/infrastructure/mappers/${entityNameLower}.mapper';
|
|
218
|
+
|
|
219
|
+
@Injectable()
|
|
220
|
+
export class ${entityNameCapitalized}Repository implements I${entityNameCapitalized}Repository {
|
|
221
|
+
constructor(
|
|
222
|
+
@InjectModel(${entityNameCapitalized}.name)
|
|
223
|
+
private readonly model: Model<${entityNameCapitalized}>,
|
|
224
|
+
private readonly mapper: ${entityNameCapitalized}Mapper,
|
|
225
|
+
) {}
|
|
226
|
+
|
|
227
|
+
// create
|
|
228
|
+
async create(data: Create${entityNameCapitalized}Dto): Promise<${entityNameCapitalized}Entity> {
|
|
229
|
+
const toPersist = this.mapper.toPersistence(data);
|
|
230
|
+
const created = await this.model.create(toPersist);
|
|
231
|
+
return this.mapper.toDomain(created);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// find by id
|
|
235
|
+
async findById(id: string): Promise<${entityNameCapitalized}Entity | null> {
|
|
236
|
+
const record = await this.model.findById(id).exec();
|
|
237
|
+
|
|
238
|
+
if (!record) return null;
|
|
239
|
+
|
|
240
|
+
return this.mapper.toDomain(record);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
${extraMethods}
|
|
244
|
+
|
|
245
|
+
// update
|
|
246
|
+
async update(id: string, data: Update${entityNameCapitalized}Dto): Promise<${entityNameCapitalized}Entity> {
|
|
247
|
+
const toUpdate = this.mapper.toUpdatePersistence(data);
|
|
248
|
+
const updated = await this.model.findByIdAndUpdate(id, toUpdate, { new: true }).exec();
|
|
249
|
+
return this.mapper.toDomain(updated);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// find all
|
|
253
|
+
async findAll(): Promise<${entityNameCapitalized}Entity[]> {
|
|
254
|
+
const records = await this.model.find().exec();
|
|
255
|
+
return records.map(record => this.mapper.toDomain(record));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// delete
|
|
259
|
+
async delete(id: string): Promise<void> {
|
|
260
|
+
await this.model.findByIdAndDelete(id).exec();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
`,
|
|
264
|
+
});
|
|
265
|
+
break;
|
|
266
|
+
|
|
267
|
+
case "sequelize":
|
|
268
|
+
await createFile({
|
|
269
|
+
path: `${entityPath}/infrastructure/repositories/${entityNameLower}.repository.ts`,
|
|
270
|
+
contente: `import { Injectable, NotFoundException } from '@nestjs/common';
|
|
271
|
+
import { InjectModel } from '@nestjs/sequelize';
|
|
272
|
+
import { Model } from 'sequelize-typescript';
|
|
273
|
+
import { Create${entityNameCapitalized}Dto, Update${entityNameCapitalized}Dto } from '${entityPath}/application/dtos/${entityNameLower}.dto';
|
|
274
|
+
import { I${entityNameCapitalized}Repository } from '${entityPath}/domain/interfaces/${entityNameLower}.repository.interface';
|
|
275
|
+
import { ${entityNameCapitalized}Entity } from '${entityPath}/domain/entities/${entityNameLower}.entity';
|
|
276
|
+
import { ${entityNameCapitalized}Mapper } from '${entityPath}/infrastructure/mappers/${entityNameLower}.mapper';
|
|
277
|
+
|
|
278
|
+
@Injectable()
|
|
279
|
+
export class ${entityNameCapitalized}Repository implements I${entityNameCapitalized}Repository {
|
|
280
|
+
constructor(
|
|
281
|
+
@InjectModel(${entityNameCapitalized}Entity)
|
|
282
|
+
private readonly model: Model<${entityNameCapitalized}Entity>,
|
|
283
|
+
private readonly mapper: ${entityNameCapitalized}Mapper,
|
|
284
|
+
) {}
|
|
285
|
+
|
|
286
|
+
// create
|
|
287
|
+
async create(data: Create${entityNameCapitalized}Dto): Promise<${entityNameCapitalized}Entity> {
|
|
288
|
+
const toPersist = this.mapper.toPersistence(data);
|
|
289
|
+
const created = await this.model.create(toPersist);
|
|
290
|
+
return this.mapper.toDomain(created);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// find by id
|
|
294
|
+
async findById(id: string): Promise<${entityNameCapitalized}Entity> {
|
|
295
|
+
const record = await this.model.findByPk(id);
|
|
296
|
+
|
|
297
|
+
if (!record) {
|
|
298
|
+
throw new NotFoundException(\`${entityNameCapitalized}Entity with id \${id} not found\`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return this.mapper.toDomain(record);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
${extraMethods}
|
|
305
|
+
|
|
306
|
+
// update
|
|
307
|
+
async update(id: string, data: Update${entityNameCapitalized}Dto): Promise<${entityNameCapitalized}Entity> {
|
|
308
|
+
const toUpdate = this.mapper.toUpdatePersistence(data);
|
|
309
|
+
const updated = await this.model.update(toUpdate, { where: { id } });
|
|
310
|
+
return this.mapper.toDomain(updated);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// find all
|
|
314
|
+
async findAll(): Promise<${entityNameCapitalized}Entity[]> {
|
|
315
|
+
const records = await this.model.find();
|
|
316
|
+
return records.map(record => this.mapper.toDomain(record));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// delete
|
|
320
|
+
async delete(id: string): Promise<void> {
|
|
321
|
+
await this.model.destroy({ where: { id } });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
`,
|
|
325
|
+
});
|
|
326
|
+
break;
|
|
327
|
+
|
|
328
|
+
default:
|
|
329
|
+
console.error("Unsupported ORM: " + orm);
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
module.exports = { generateRepository };
|
|
@@ -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 };
|