nestcraftx 0.4.0 → 1.0.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/.github/workflows/ci.yml +35 -0
- package/CHANGELOG.fr.md +74 -0
- package/CHANGELOG.md +74 -0
- package/PROGRESS.md +59 -57
- package/README.fr.md +60 -69
- package/bin/nestcraft.js +8 -0
- package/commands/demo.js +10 -0
- package/commands/generate.js +551 -2
- package/commands/generateConf.js +4 -4
- package/commands/help.js +11 -0
- package/commands/info.js +2 -3
- package/commands/list.js +93 -0
- package/commands/new.js +26 -9
- package/jest.config.js +9 -0
- package/package.json +7 -2
- package/readme.md +59 -68
- package/tests/e2e/generator.spec.js +71 -0
- package/tests/unit/appModuleUpdater.spec.js +111 -0
- package/tests/unit/cliParser.spec.js +74 -0
- package/tests/unit/controllerGenerator.spec.js +145 -0
- package/tests/unit/dtoGenerator.spec.js +87 -0
- package/tests/unit/entityGenerator.spec.js +65 -0
- package/tests/unit/generateCommand.spec.js +100 -0
- package/tests/unit/health.spec.js +69 -0
- package/tests/unit/listCommand.spec.js +91 -0
- package/tests/unit/middlewareGenerator.spec.js +64 -0
- package/tests/unit/newCommand.spec.js +88 -0
- package/tests/unit/relationCommand.spec.js +202 -0
- package/tests/unit/throttler.spec.js +117 -0
- package/utils/configs/setupCleanArchitecture.js +1 -1
- package/utils/configs/setupLightArchitecture.js +98 -26
- package/utils/envGenerator.js +3 -1
- package/utils/file-utils/saveProjectConfig.js +3 -0
- package/utils/fullModeInput.js +4 -0
- package/utils/generators/application/dtoGenerator.js +20 -3
- package/utils/generators/cleanModuleGenerator.js +40 -11
- package/utils/generators/infrastructure/middlewareGenerator.js +112 -315
- package/utils/generators/infrastructure/mongooseSchemaGenerator.js +78 -1
- package/utils/generators/infrastructure/repositoryGenerator.js +114 -14
- package/utils/generators/lightModuleGenerator.js +14 -0
- package/utils/generators/presentation/controllerGenerator.js +70 -19
- package/utils/interactive/askRelationCommand.js +98 -0
- package/utils/interactive/entityBuilder.js +11 -0
- package/utils/lightModeInput.js +14 -0
- package/utils/setups/orms/typeOrmSetup.js +11 -4
- package/utils/setups/projectSetup.js +10 -2
- package/utils/setups/setupAuth.js +334 -18
- package/utils/setups/setupDatabase.js +4 -1
- package/utils/setups/setupHealth.js +74 -0
- package/utils/setups/setupMongoose.js +4 -1
- package/utils/setups/setupPrisma.js +110 -1
- package/utils/setups/setupSwagger.js +4 -1
- package/utils/setups/setupThrottler.js +92 -0
- package/utils/shell.js +21 -4
package/commands/generate.js
CHANGED
|
@@ -3,10 +3,13 @@ const path = require("path");
|
|
|
3
3
|
const { logError } = require("../utils/loggers/logError");
|
|
4
4
|
const { logInfo } = require("../utils/loggers/logInfo");
|
|
5
5
|
const { logWarning } = require("../utils/loggers/logWarning");
|
|
6
|
+
const { logSuccess } = require("../utils/loggers/logSuccess");
|
|
6
7
|
const generateCleanModule = require("../utils/generators/cleanModuleGenerator");
|
|
7
8
|
const { askEntityInputs } = require("../utils/interactive/askEntityInputs");
|
|
9
|
+
const { askRelationCommand } = require("../utils/interactive/askRelationCommand");
|
|
8
10
|
const lightModuleGenerator = require("../utils/generators/lightModuleGenerator");
|
|
9
|
-
const { printSetupWarnings, clearSetupWarnings } = require("../utils/shell");
|
|
11
|
+
const { printSetupWarnings, clearSetupWarnings, runCommand } = require("../utils/shell");
|
|
12
|
+
const { generateSecret } = require("../utils/envGenerator");
|
|
10
13
|
|
|
11
14
|
async function generate(subCommand, targetName, flags) {
|
|
12
15
|
clearSetupWarnings();
|
|
@@ -60,11 +63,17 @@ async function generate(subCommand, targetName, flags) {
|
|
|
60
63
|
await handleAuthGeneration(projectConfig);
|
|
61
64
|
break;
|
|
62
65
|
|
|
66
|
+
case "relation":
|
|
67
|
+
case "r":
|
|
68
|
+
await handleRelationGeneration(projectConfig);
|
|
69
|
+
break;
|
|
70
|
+
|
|
63
71
|
default:
|
|
64
72
|
logError(`Sous-commande inconnue : ${subCommand}`);
|
|
65
73
|
console.log("\nUtilisations possibles :");
|
|
66
74
|
console.log(" nestcraftx g module <name>");
|
|
67
75
|
console.log(" nestcraftx g auth");
|
|
76
|
+
console.log(" nestcraftx g relation");
|
|
68
77
|
break;
|
|
69
78
|
}
|
|
70
79
|
}
|
|
@@ -84,11 +93,551 @@ async function handleModuleGeneration(name, config) {
|
|
|
84
93
|
await lightModuleGenerator(entityData, config);
|
|
85
94
|
}
|
|
86
95
|
|
|
96
|
+
// 2. Mettre à jour .nestcraftxrc avec la nouvelle entité
|
|
97
|
+
if (!config.entities) config.entities = [];
|
|
98
|
+
if (!config.relations) config.relations = [];
|
|
99
|
+
|
|
100
|
+
// Éviter les doublons
|
|
101
|
+
config.entities = config.entities.filter(e => e.name.toLowerCase() !== name.toLowerCase());
|
|
102
|
+
config.entities.push({
|
|
103
|
+
name: entityData.name,
|
|
104
|
+
fields: entityData.fields
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (entityData.relation) {
|
|
108
|
+
config.relations = config.relations.filter(
|
|
109
|
+
r => !(r.from.toLowerCase() === entityData.name.toLowerCase() && r.to.toLowerCase() === entityData.relation.target.toLowerCase())
|
|
110
|
+
);
|
|
111
|
+
config.relations.push({
|
|
112
|
+
from: entityData.name,
|
|
113
|
+
to: entityData.relation.target,
|
|
114
|
+
type: entityData.relation.type
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const configPath = path.join(process.cwd(), ".nestcraftx", ".nestcraftxrc");
|
|
119
|
+
const isDryRun = process.argv.includes("--dry-run");
|
|
120
|
+
if (!isDryRun) {
|
|
121
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
printModuleGenerationReport(name, config, entityData);
|
|
125
|
+
|
|
87
126
|
printSetupWarnings();
|
|
88
127
|
}
|
|
89
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Crée une relation entre deux modules EXISTANTS.
|
|
131
|
+
* Patche : entité cible, DTO source, Mapper source, schema DB (Prisma/Mongoose/TypeORM).
|
|
132
|
+
* Enregistre la relation dans .nestcraftxrc.
|
|
133
|
+
*/
|
|
134
|
+
async function handleRelationGeneration(config) {
|
|
135
|
+
const isDryRun = process.argv.includes("--dry-run");
|
|
136
|
+
|
|
137
|
+
// 1. Prompt interactif — source, cible, type
|
|
138
|
+
const relationData = await askRelationCommand(config);
|
|
139
|
+
if (!relationData) return; // Annulé ou déjà existant
|
|
140
|
+
|
|
141
|
+
const { source, target, type } = relationData;
|
|
142
|
+
const { capitalize } = require("../utils/userInput");
|
|
143
|
+
|
|
144
|
+
logInfo(`🔗 Création de la relation ${capitalize(source)} →(${type})→ ${capitalize(target)}...`);
|
|
145
|
+
|
|
146
|
+
// 2. Patcher l'entité CIBLE (ajoute FK dans la couche domaine)
|
|
147
|
+
const { updateExistingEntityRelation } = require("../utils/generators/domain/entityUpdater");
|
|
148
|
+
if (!isDryRun) {
|
|
149
|
+
await updateExistingEntityRelation(target, source, type, config.mode);
|
|
150
|
+
logInfo(" ✔ Entité cible patchée (domain layer)");
|
|
151
|
+
} else {
|
|
152
|
+
console.log(`[DRY-RUN] Simulated: patch entity ${target} with relation to ${source}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 3. Patcher le DTO SOURCE + Mapper SOURCE via relation.engine
|
|
156
|
+
const { applyRelationPatches } = require("../utils/generators/relation/relation.engine");
|
|
157
|
+
if (!isDryRun) {
|
|
158
|
+
await applyRelationPatches(source, target, type, config.swagger, config.mode);
|
|
159
|
+
logInfo(" ✔ DTO + Mapper source patchés (application & infrastructure layers)");
|
|
160
|
+
} else {
|
|
161
|
+
console.log(`[DRY-RUN] Simulated: patch DTO and mapper for ${source} with relation to ${target}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 4. Mise à jour du schema de base de données selon l'ORM
|
|
165
|
+
if (config.orm === "prisma") {
|
|
166
|
+
const { addPrismaRelation } = require("../utils/setups/setupPrisma");
|
|
167
|
+
if (!isDryRun) {
|
|
168
|
+
await addPrismaRelation(source, target, type);
|
|
169
|
+
logInfo(" ✔ schema.prisma mis à jour + migration lancée");
|
|
170
|
+
} else {
|
|
171
|
+
console.log(`[DRY-RUN] Simulated: addPrismaRelation(${source}, ${target}, ${type})`);
|
|
172
|
+
}
|
|
173
|
+
} else if (config.orm === "mongoose") {
|
|
174
|
+
const { addMongooseRelation } = require("../utils/generators/infrastructure/mongooseSchemaGenerator");
|
|
175
|
+
if (!isDryRun) {
|
|
176
|
+
await addMongooseRelation(source, target, type, config.mode);
|
|
177
|
+
logInfo(" ✔ Schémas Mongoose mis à jour");
|
|
178
|
+
} else {
|
|
179
|
+
console.log(`[DRY-RUN] Simulated: addMongooseRelation(${source}, ${target}, ${type})`);
|
|
180
|
+
}
|
|
181
|
+
} else if (config.orm === "typeorm") {
|
|
182
|
+
// TypeORM : patch FK @Column dans les entités src/entities/
|
|
183
|
+
// Note : les annotations @ManyToOne/@OneToMany doivent être ajoutées manuellement
|
|
184
|
+
// pour un typage ORM complet — le champ FK est injecté automatiquement.
|
|
185
|
+
logWarning(
|
|
186
|
+
"⚠️ TypeORM détecté : le champ FK est injecté dans l'entité source.\n" +
|
|
187
|
+
" Pour des annotations ORM complètes (@ManyToOne/@OneToMany), éditez manuellement les entités.",
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 5. Mise à jour de .nestcraftxrc
|
|
192
|
+
if (!config.relations) config.relations = [];
|
|
193
|
+
config.relations = config.relations.filter(
|
|
194
|
+
(r) => !(
|
|
195
|
+
r.from.toLowerCase() === source &&
|
|
196
|
+
r.to.toLowerCase() === target &&
|
|
197
|
+
r.type === type
|
|
198
|
+
),
|
|
199
|
+
);
|
|
200
|
+
config.relations.push({ from: source, to: target, type });
|
|
201
|
+
|
|
202
|
+
const configPath = path.join(process.cwd(), ".nestcraftx", ".nestcraftxrc");
|
|
203
|
+
if (!isDryRun) {
|
|
204
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8");
|
|
205
|
+
logInfo(" ✔ .nestcraftxrc mis à jour");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 6. Résumé final
|
|
209
|
+
logSuccess(
|
|
210
|
+
`\n✅ Relation ${capitalize(source)} →(${type})→ ${capitalize(target)} créée avec succès !\n` +
|
|
211
|
+
`\n📋 Résumé :\n` +
|
|
212
|
+
` ✔ Entité ${capitalize(target)} : champ FK ajouté\n` +
|
|
213
|
+
` ✔ DTO ${capitalize(source)} : ${target}Id ajouté dans CreateDto\n` +
|
|
214
|
+
` ✔ Mapper ${capitalize(source)} : toDomain/toPersistence mis à jour\n` +
|
|
215
|
+
(config.orm === "prisma" ? ` ✔ schema.prisma : champs @relation des deux côtés ajoutés\n` : "") +
|
|
216
|
+
(config.orm === "mongoose" ? ` ✔ Schémas Mongoose : champs de référence ajoutés\n` : "") +
|
|
217
|
+
` ✔ .nestcraftxrc : relation enregistrée`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Affiche un rapport de génération complet à la fin du scaffolding d'un module.
|
|
223
|
+
*/
|
|
224
|
+
function printModuleGenerationReport(name, config, entityData) {
|
|
225
|
+
const { capitalize, decapitalize } = require("../utils/userInput");
|
|
226
|
+
const entityCap = capitalize(name);
|
|
227
|
+
const entityLow = decapitalize(name);
|
|
228
|
+
const isFull = config.mode === "full";
|
|
229
|
+
|
|
230
|
+
console.log("\n╔" + "═".repeat(60) + "╗");
|
|
231
|
+
console.log("║" + " ".repeat(15) + "🏗️ NESTCRAFTX GENERATION REPORT 🏗️" + " ".repeat(11) + "║");
|
|
232
|
+
console.log("╠" + "═".repeat(60) + "╣");
|
|
233
|
+
|
|
234
|
+
console.log(`║ Module generated : ${entityCap}`);
|
|
235
|
+
console.log(`║ Mode : ${config.mode.toUpperCase()}`);
|
|
236
|
+
console.log(`║ ORM / Database : ${config.orm} / ${config.database}`);
|
|
237
|
+
console.log("╠" + "═".repeat(60) + "╣");
|
|
238
|
+
|
|
239
|
+
console.log("║ 📁 Generated Files :");
|
|
240
|
+
if (isFull) {
|
|
241
|
+
console.log(`║ • Domain layer :`);
|
|
242
|
+
console.log(`║ - src/${entityLow}/domain/entities/${entityLow}.entity.ts`);
|
|
243
|
+
console.log(`║ - src/${entityLow}/domain/interfaces/${entityLow}.repository.interface.ts`);
|
|
244
|
+
console.log(`║ • Application layer :`);
|
|
245
|
+
console.log(`║ - src/${entityLow}/application/dtos/${entityLow}.dto.ts`);
|
|
246
|
+
console.log(`║ - src/${entityLow}/application/services/${entityLow}.service.ts`);
|
|
247
|
+
console.log(`║ - src/${entityLow}/application/use-cases/create-${entityLow}.use-case.ts`);
|
|
248
|
+
console.log(`║ - src/${entityLow}/application/use-cases/getById-${entityLow}.use-case.ts`);
|
|
249
|
+
console.log(`║ - src/${entityLow}/application/use-cases/getAll-${entityLow}.use-case.ts`);
|
|
250
|
+
console.log(`║ - src/${entityLow}/application/use-cases/update-${entityLow}.use-case.ts`);
|
|
251
|
+
console.log(`║ - src/${entityLow}/application/use-cases/delete-${entityLow}.use-case.ts`);
|
|
252
|
+
console.log(`║ • Infrastructure layer :`);
|
|
253
|
+
console.log(`║ - src/${entityLow}/infrastructure/mappers/${entityLow}.mapper.ts`);
|
|
254
|
+
console.log(`║ - src/${entityLow}/infrastructure/repositories/${entityLow}.repository.ts`);
|
|
255
|
+
console.log(`║ • Presentation layer :`);
|
|
256
|
+
console.log(`║ - src/${entityLow}/presentation/controllers/${entityLow}.controller.ts`);
|
|
257
|
+
console.log(`║ • Nest Module :`);
|
|
258
|
+
console.log(`║ - src/${entityLow}/${entityLow}.module.ts`);
|
|
259
|
+
} else {
|
|
260
|
+
console.log(`║ • Entities : src/${entityLow}/entities/${entityLow}.entity.ts`);
|
|
261
|
+
console.log(`║ • DTOs : src/${entityLow}/dtos/${entityLow}.dto.ts`);
|
|
262
|
+
console.log(`║ • Services : src/${entityLow}/services/${entityLow}.service.ts`);
|
|
263
|
+
console.log(`║ • Repos : src/${entityLow}/repositories/${entityLow}.repository.ts`);
|
|
264
|
+
console.log(`║ • Control. : src/${entityLow}/controllers/${entityLow}.controller.ts`);
|
|
265
|
+
console.log(`║ • Module : src/${entityLow}/${entityLow}.module.ts`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
console.log("╠" + "═".repeat(60) + "╣");
|
|
269
|
+
console.log("║ ⚙️ Integration :");
|
|
270
|
+
console.log("║ ✔ Registered in src/app.module.ts");
|
|
271
|
+
console.log(`║ ✔ Database schema updated (${config.orm})`);
|
|
272
|
+
|
|
273
|
+
if (entityData.relation) {
|
|
274
|
+
const { target, type } = entityData.relation;
|
|
275
|
+
console.log("╠" + "═".repeat(60) + "╣");
|
|
276
|
+
console.log(`║ 🔗 Relation established :`);
|
|
277
|
+
console.log(`║ • ${entityCap} →(${type})→ ${capitalize(target)}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
console.log("╠" + "═".repeat(60) + "╣");
|
|
281
|
+
console.log("║ 🚀 Next Steps :");
|
|
282
|
+
console.log(`║ 1. Inspect the entity validation rules in the DTO.`);
|
|
283
|
+
if (config.orm === "prisma") {
|
|
284
|
+
console.log(`║ 2. Run Prisma sync command to update client :`);
|
|
285
|
+
console.log(`║ - npx prisma format && npx prisma generate`);
|
|
286
|
+
} else {
|
|
287
|
+
console.log(`║ 2. Review your DB configuration in .env`);
|
|
288
|
+
}
|
|
289
|
+
console.log("║ 3. Start your dev server :");
|
|
290
|
+
console.log(`║ - ${config.packageManager || "npm"} run start:dev`);
|
|
291
|
+
|
|
292
|
+
console.log("╚" + "═".repeat(60) + "╝\n");
|
|
293
|
+
}
|
|
294
|
+
|
|
90
295
|
async function handleAuthGeneration(config) {
|
|
91
|
-
|
|
296
|
+
const isDryRun = process.argv.includes("--dry-run");
|
|
297
|
+
logInfo("🚀 Initialisation de la configuration de l'Authentification...");
|
|
298
|
+
|
|
299
|
+
const isFull = config.mode === "full";
|
|
300
|
+
const orm = config.orm;
|
|
301
|
+
|
|
302
|
+
// Vérifier la présence de l'entité User — si absente, auto-scaffold
|
|
303
|
+
let userExists = false;
|
|
304
|
+
|
|
305
|
+
if (orm === "prisma") {
|
|
306
|
+
const schemaPath = path.join(process.cwd(), "prisma", "schema.prisma");
|
|
307
|
+
if (!fs.existsSync(schemaPath)) {
|
|
308
|
+
logError("Le fichier 'prisma/schema.prisma' est introuvable.");
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const schemaContent = fs.readFileSync(schemaPath, "utf8");
|
|
312
|
+
userExists = schemaContent.includes("model User");
|
|
313
|
+
} else if (orm === "typeorm") {
|
|
314
|
+
const userEntityPath = path.join(process.cwd(), "src", "entities", "User.entity.ts");
|
|
315
|
+
userExists = fs.existsSync(userEntityPath);
|
|
316
|
+
} else if (orm === "mongoose") {
|
|
317
|
+
const userSchemaPath = isFull
|
|
318
|
+
? path.join(process.cwd(), "src", "user", "infrastructure", "persistence", "mongoose", "user.schema.ts")
|
|
319
|
+
: path.join(process.cwd(), "src", "user", "persistence", "user.schema.ts");
|
|
320
|
+
userExists = fs.existsSync(userSchemaPath);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (!userExists) {
|
|
324
|
+
logInfo("⚙️ Entité 'User' absente — création automatique d'un module User minimal...");
|
|
325
|
+
|
|
326
|
+
// Construire un entityData minimal pour User (sans champs customs — les champs
|
|
327
|
+
// auth (email, password, role, isActive) seront ajoutés juste après par le bloc
|
|
328
|
+
// de mise à jour du schéma, exactement comme pour Session/Otp/PasswordReset)
|
|
329
|
+
const minimalUserEntity = {
|
|
330
|
+
name: "User",
|
|
331
|
+
fields: [
|
|
332
|
+
{
|
|
333
|
+
name: "email",
|
|
334
|
+
type: "string",
|
|
335
|
+
unique: true,
|
|
336
|
+
nullable: false,
|
|
337
|
+
default: null,
|
|
338
|
+
description: "User email address for login"
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
name: "password",
|
|
342
|
+
type: "string",
|
|
343
|
+
unique: false,
|
|
344
|
+
nullable: false,
|
|
345
|
+
default: null,
|
|
346
|
+
description: "User password"
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
name: "role",
|
|
350
|
+
type: "string",
|
|
351
|
+
unique: false,
|
|
352
|
+
nullable: false,
|
|
353
|
+
default: "USER",
|
|
354
|
+
description: "User role in the application"
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
name: "isActive",
|
|
358
|
+
type: "boolean",
|
|
359
|
+
unique: false,
|
|
360
|
+
nullable: false,
|
|
361
|
+
default: "true",
|
|
362
|
+
description: "Flag indicating whether user is active"
|
|
363
|
+
}
|
|
364
|
+
],
|
|
365
|
+
relation: null,
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// Générer le module User via les générateurs existants
|
|
369
|
+
if (config.mode === "full") {
|
|
370
|
+
await generateCleanModule("User", config, minimalUserEntity);
|
|
371
|
+
} else {
|
|
372
|
+
await lightModuleGenerator(minimalUserEntity, config);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Enregistrer l'entité User dans .nestcraftxrc
|
|
376
|
+
if (!config.entities) config.entities = [];
|
|
377
|
+
if (!config.entities.find(e => e.name.toLowerCase() === "user")) {
|
|
378
|
+
config.entities.push({ name: "User", fields: minimalUserEntity.fields });
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const configPath = path.join(process.cwd(), ".nestcraftx", ".nestcraftxrc");
|
|
382
|
+
if (!isDryRun) {
|
|
383
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8");
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
logSuccess("✅ Module User minimal créé avec succès !");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// 1. Mise à jour de .env et .env.example
|
|
390
|
+
const jwtSecret = generateSecret(64);
|
|
391
|
+
const jwtRefreshSecret = generateSecret(64);
|
|
392
|
+
const envPath = path.join(process.cwd(), ".env");
|
|
393
|
+
const examplePath = path.join(process.cwd(), ".env.example");
|
|
394
|
+
|
|
395
|
+
const authEnvBlock = `
|
|
396
|
+
# ------------------------------------------------------------------------------
|
|
397
|
+
# AUTHENTICATION (JWT)
|
|
398
|
+
# ------------------------------------------------------------------------------
|
|
399
|
+
JWT_SECRET=${jwtSecret}
|
|
400
|
+
JWT_REFRESH_SECRET=${jwtRefreshSecret}
|
|
401
|
+
JWT_EXPIRES_IN=15m
|
|
402
|
+
JWT_REFRESH_EXPIRES_IN=7d
|
|
403
|
+
`;
|
|
404
|
+
|
|
405
|
+
const authExampleBlock = `
|
|
406
|
+
# ------------------------------------------------------------------------------
|
|
407
|
+
# AUTHENTICATION (JWT)
|
|
408
|
+
# ------------------------------------------------------------------------------
|
|
409
|
+
JWT_SECRET=votre_cle_secrete_ici
|
|
410
|
+
JWT_REFRESH_SECRET=votre_cle_secrete_ici
|
|
411
|
+
JWT_EXPIRES_IN=15m
|
|
412
|
+
JWT_REFRESH_EXPIRES_IN=7d
|
|
413
|
+
`;
|
|
414
|
+
|
|
415
|
+
if (fs.existsSync(envPath)) {
|
|
416
|
+
const envContent = fs.readFileSync(envPath, "utf8");
|
|
417
|
+
if (!envContent.includes("JWT_SECRET")) {
|
|
418
|
+
if (isDryRun) {
|
|
419
|
+
console.log(`[DRY-RUN] Simulated: append JWT secrets to ${envPath}`);
|
|
420
|
+
} else {
|
|
421
|
+
fs.appendFileSync(envPath, authEnvBlock, "utf8");
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (fs.existsSync(examplePath)) {
|
|
427
|
+
const exampleContent = fs.readFileSync(examplePath, "utf8");
|
|
428
|
+
if (!exampleContent.includes("JWT_SECRET")) {
|
|
429
|
+
if (isDryRun) {
|
|
430
|
+
console.log(`[DRY-RUN] Simulated: append JWT secrets template to ${examplePath}`);
|
|
431
|
+
} else {
|
|
432
|
+
fs.appendFileSync(examplePath, authExampleBlock, "utf8");
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// 2. Mise à jour du schéma User et des tables d'Auth selon l'ORM
|
|
438
|
+
if (orm === "prisma") {
|
|
439
|
+
const schemaPath = path.join(process.cwd(), "prisma", "schema.prisma");
|
|
440
|
+
if (fs.existsSync(schemaPath)) {
|
|
441
|
+
let schemaContent = fs.readFileSync(schemaPath, "utf8");
|
|
442
|
+
let updated = false;
|
|
443
|
+
|
|
444
|
+
// Ajouter l'enum Role si absente
|
|
445
|
+
if (!schemaContent.includes("enum Role")) {
|
|
446
|
+
schemaContent += `
|
|
447
|
+
enum Role {
|
|
448
|
+
USER
|
|
449
|
+
ADMIN
|
|
450
|
+
SUPER_ADMIN
|
|
451
|
+
}
|
|
452
|
+
`;
|
|
453
|
+
updated = true;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Ajouter PasswordReset, Otp, Session si absents
|
|
457
|
+
if (!schemaContent.includes("model PasswordReset")) {
|
|
458
|
+
schemaContent += `
|
|
459
|
+
model PasswordReset {
|
|
460
|
+
id String @id @default(uuid())
|
|
461
|
+
email String
|
|
462
|
+
token String @unique
|
|
463
|
+
expiresAt DateTime
|
|
464
|
+
createdAt DateTime @default(now())
|
|
465
|
+
}
|
|
466
|
+
`;
|
|
467
|
+
updated = true;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (!schemaContent.includes("model Otp")) {
|
|
471
|
+
schemaContent += `
|
|
472
|
+
model Otp {
|
|
473
|
+
id String @id @default(uuid())
|
|
474
|
+
email String
|
|
475
|
+
otp String
|
|
476
|
+
expiresAt DateTime
|
|
477
|
+
createdAt DateTime @default(now())
|
|
478
|
+
}
|
|
479
|
+
`;
|
|
480
|
+
updated = true;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (!schemaContent.includes("model Session")) {
|
|
484
|
+
schemaContent += `
|
|
485
|
+
model Session {
|
|
486
|
+
id String @id @default(uuid())
|
|
487
|
+
refreshToken String @unique
|
|
488
|
+
userId String
|
|
489
|
+
expiresAt DateTime
|
|
490
|
+
createdAt DateTime @default(now())
|
|
491
|
+
user User @relation(fields: [userId], references: [id])
|
|
492
|
+
}
|
|
493
|
+
`;
|
|
494
|
+
updated = true;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Mettre à jour User
|
|
498
|
+
const userModelMatch = schemaContent.match(/model User \{([\s\S]*?)\}/);
|
|
499
|
+
if (userModelMatch) {
|
|
500
|
+
let userFields = userModelMatch[1];
|
|
501
|
+
let userUpdated = false;
|
|
502
|
+
|
|
503
|
+
if (!userFields.includes("email")) {
|
|
504
|
+
userFields = `\n email String @unique` + userFields;
|
|
505
|
+
userUpdated = true;
|
|
506
|
+
}
|
|
507
|
+
if (!userFields.includes("password")) {
|
|
508
|
+
userFields = `\n password String` + userFields;
|
|
509
|
+
userUpdated = true;
|
|
510
|
+
}
|
|
511
|
+
if (!userFields.includes("role")) {
|
|
512
|
+
userFields = `\n role Role @default(USER)` + userFields;
|
|
513
|
+
userUpdated = true;
|
|
514
|
+
}
|
|
515
|
+
if (!userFields.includes("isActive")) {
|
|
516
|
+
userFields = `\n isActive Boolean @default(true)` + userFields;
|
|
517
|
+
userUpdated = true;
|
|
518
|
+
}
|
|
519
|
+
if (!userFields.includes("sessions")) {
|
|
520
|
+
userFields = `\n sessions Session[]` + userFields;
|
|
521
|
+
userUpdated = true;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (userUpdated) {
|
|
525
|
+
schemaContent = schemaContent.replace(/model User \{([\s\S]*?)\}/, `model User {${userFields}}`);
|
|
526
|
+
updated = true;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (updated) {
|
|
531
|
+
if (isDryRun) {
|
|
532
|
+
console.log(`[DRY-RUN] Simulated: update prisma/schema.prisma with Auth models`);
|
|
533
|
+
} else {
|
|
534
|
+
fs.writeFileSync(schemaPath, schemaContent, "utf8");
|
|
535
|
+
await runCommand("npx prisma format", "Prisma format failed", { critical: false });
|
|
536
|
+
await runCommand("npx prisma generate", "Prisma generate failed", { critical: false });
|
|
537
|
+
await runCommand("npx prisma migrate dev --name add_auth", "Prisma migration failed", { critical: false });
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
} else if (orm === "typeorm") {
|
|
542
|
+
// S'assurer que src/entities/User.entity.ts a les champs requis
|
|
543
|
+
const userEntityPath = path.join(process.cwd(), "src", "entities", "User.entity.ts");
|
|
544
|
+
if (fs.existsSync(userEntityPath)) {
|
|
545
|
+
let content = fs.readFileSync(userEntityPath, "utf8");
|
|
546
|
+
let updated = false;
|
|
547
|
+
|
|
548
|
+
if (!content.includes("email")) {
|
|
549
|
+
content = content.replace("export class User {", `export class User {\n @Column({ type: 'varchar', unique: true })\n email: string;\n`);
|
|
550
|
+
updated = true;
|
|
551
|
+
}
|
|
552
|
+
if (!content.includes("password")) {
|
|
553
|
+
content = content.replace("export class User {", `export class User {\n @Column({ type: 'varchar' })\n password: string;\n`);
|
|
554
|
+
updated = true;
|
|
555
|
+
}
|
|
556
|
+
if (!content.includes("role")) {
|
|
557
|
+
content = content.replace("export class User {", `export class User {\n @Column({ type: 'varchar', default: 'USER' })\n role: string;\n`);
|
|
558
|
+
updated = true;
|
|
559
|
+
}
|
|
560
|
+
if (!content.includes("isActive")) {
|
|
561
|
+
content = content.replace("export class User {", `export class User {\n @Column({ type: 'boolean', default: true })\n isActive: boolean;\n`);
|
|
562
|
+
updated = true;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (updated) {
|
|
566
|
+
if (isDryRun) {
|
|
567
|
+
console.log(`[DRY-RUN] Simulated: update ${userEntityPath} with Auth columns`);
|
|
568
|
+
} else {
|
|
569
|
+
fs.writeFileSync(userEntityPath, content, "utf8");
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
} else if (orm === "mongoose") {
|
|
574
|
+
// S'assurer que le schéma User Mongoose a les champs
|
|
575
|
+
const userSchemaPath = isFull
|
|
576
|
+
? path.join(process.cwd(), "src", "user", "infrastructure", "persistence", "mongoose", "user.schema.ts")
|
|
577
|
+
: path.join(process.cwd(), "src", "user", "persistence", "user.schema.ts");
|
|
578
|
+
if (fs.existsSync(userSchemaPath)) {
|
|
579
|
+
let content = fs.readFileSync(userSchemaPath, "utf8");
|
|
580
|
+
let updated = false;
|
|
581
|
+
|
|
582
|
+
if (!content.includes("email")) {
|
|
583
|
+
content = content.replace("export class User {", `export class User {\n @Prop({ required: true, unique: true })\n email: string;\n`);
|
|
584
|
+
updated = true;
|
|
585
|
+
}
|
|
586
|
+
if (!content.includes("password")) {
|
|
587
|
+
content = content.replace("export class User {", `export class User {\n @Prop({ required: true })\n password: string;\n`);
|
|
588
|
+
updated = true;
|
|
589
|
+
}
|
|
590
|
+
if (!content.includes("role")) {
|
|
591
|
+
content = content.replace("export class User {", `export class User {\n @Prop({ required: true, default: 'USER' })\n role: string;\n`);
|
|
592
|
+
updated = true;
|
|
593
|
+
}
|
|
594
|
+
if (!content.includes("isActive")) {
|
|
595
|
+
content = content.replace("export class User {", `export class User {\n @Prop({ required: true, default: true })\n isActive: boolean;\n`);
|
|
596
|
+
updated = true;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (updated) {
|
|
600
|
+
if (isDryRun) {
|
|
601
|
+
console.log(`[DRY-RUN] Simulated: update ${userSchemaPath} with Auth properties`);
|
|
602
|
+
} else {
|
|
603
|
+
fs.writeFileSync(userSchemaPath, content, "utf8");
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// 3. Appel à setupAuth
|
|
610
|
+
const { setupAuth } = require("../utils/setups/setupAuth");
|
|
611
|
+
|
|
612
|
+
// Reconstituer l'objet inputs attendu par setupAuth
|
|
613
|
+
const authInputs = {
|
|
614
|
+
projectName: config.name,
|
|
615
|
+
mode: config.mode,
|
|
616
|
+
selectedDB: config.database,
|
|
617
|
+
dbConfig: {
|
|
618
|
+
orm: config.orm
|
|
619
|
+
},
|
|
620
|
+
useSwagger: config.swagger,
|
|
621
|
+
useAuth: true,
|
|
622
|
+
packageManager: config.packageManager
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
await setupAuth(authInputs);
|
|
626
|
+
|
|
627
|
+
// 4. Enregistrer dans AppModule
|
|
628
|
+
const { safeUpdateAppModule } = require("../utils/app-module.updater");
|
|
629
|
+
await safeUpdateAppModule("auth");
|
|
630
|
+
|
|
631
|
+
// 5. Mettre à jour .nestcraftxrc
|
|
632
|
+
config.auth = true;
|
|
633
|
+
const configPath = path.join(process.cwd(), ".nestcraftx", ".nestcraftxrc");
|
|
634
|
+
if (isDryRun) {
|
|
635
|
+
console.log(`[DRY-RUN] Simulated: update .nestcraftxrc with auth: true`);
|
|
636
|
+
} else {
|
|
637
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8");
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
logSuccess("✅ Authentification configurée et déployée avec succès dans le projet !");
|
|
92
641
|
}
|
|
93
642
|
|
|
94
643
|
module.exports = generate;
|
package/commands/generateConf.js
CHANGED
|
@@ -27,14 +27,14 @@ async function generateConf() {
|
|
|
27
27
|
fs.existsSync(path.join(root, "src", "common", "domain"));
|
|
28
28
|
const mode = isFull ? "full" : "light";
|
|
29
29
|
|
|
30
|
-
// 2. Détecter l'ORM
|
|
30
|
+
// 2. Détecter l'ORM (vérifier les fichiers physiques d'abord, puis les dépendances)
|
|
31
31
|
let orm = "prisma"; // par défaut
|
|
32
|
-
if (
|
|
32
|
+
if (fs.existsSync(path.join(root, "prisma"))) {
|
|
33
|
+
orm = "prisma";
|
|
34
|
+
} else if (deps["@nestjs/mongoose"] || deps["mongoose"]) {
|
|
33
35
|
orm = "mongoose";
|
|
34
36
|
} else if (deps["@nestjs/typeorm"] || deps["typeorm"]) {
|
|
35
37
|
orm = "typeorm";
|
|
36
|
-
} else if (fs.existsSync(path.join(root, "prisma"))) {
|
|
37
|
-
orm = "prisma";
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
// 3. Détecter les options
|
package/commands/help.js
CHANGED
|
@@ -59,6 +59,17 @@ async function helpCommand() {
|
|
|
59
59
|
--auth Adds JWT Auth
|
|
60
60
|
--swagger Adds Swagger UI
|
|
61
61
|
|
|
62
|
+
nestcraftx generate, g <subcommand> [target]
|
|
63
|
+
Generates modules or auth post-scaffolding
|
|
64
|
+
Subcommands:
|
|
65
|
+
module <name> Generates a new module (MVP or Clean Architecture)
|
|
66
|
+
auth Enables JWT Auth on an existing project
|
|
67
|
+
relation, r Creates a relation between two existing modules
|
|
68
|
+
(interactive: select source, target, and relation type)
|
|
69
|
+
|
|
70
|
+
nestcraftx list, l
|
|
71
|
+
Lists all scaffolded entities and their relationships
|
|
72
|
+
|
|
62
73
|
nestcraftx test
|
|
63
74
|
Checks system environment (Node, npm, Nest CLI, Docker, etc.)
|
|
64
75
|
|
package/commands/info.js
CHANGED
|
@@ -27,11 +27,10 @@ async function infoCommand() {
|
|
|
27
27
|
console.log(" " + packageJson.repository.url);
|
|
28
28
|
|
|
29
29
|
console.log("\n📅 Upcoming:");
|
|
30
|
-
console.log(" •
|
|
30
|
+
console.log(" • MySQL & SQLite support");
|
|
31
|
+
console.log(" • Custom templates from GitHub");
|
|
31
32
|
console.log(" • Microservices support");
|
|
32
|
-
console.log(" • CI/CD Templates");
|
|
33
33
|
console.log(" • GraphQL integration");
|
|
34
|
-
console.log(" • Automated Tests");
|
|
35
34
|
|
|
36
35
|
console.log("\n💡 Available Commands:");
|
|
37
36
|
console.log(" nestcraftx new <name> [options] Create a project");
|