nestcraftx 0.3.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 +70 -67
- package/README.fr.md +60 -69
- package/bin/nestcraft.js +8 -0
- package/commands/demo.js +12 -46
- 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 +28 -56
- 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/app-module.updater.js +6 -0
- package/utils/configs/setupCleanArchitecture.js +1 -1
- package/utils/configs/setupLightArchitecture.js +98 -26
- package/utils/envGenerator.js +9 -1
- package/utils/file-system.js +15 -0
- package/utils/file-utils/packageJsonUtils.js +6 -0
- package/utils/file-utils/saveProjectConfig.js +9 -0
- package/utils/fullModeInput.js +12 -229
- package/utils/generators/application/dtoGenerator.js +26 -8
- package/utils/generators/application/dtoUpdater.js +5 -0
- package/utils/generators/cleanModuleGenerator.js +40 -11
- package/utils/generators/domain/entityUpdater.js +5 -0
- package/utils/generators/infrastructure/mapperUpdater.js +5 -0
- package/utils/generators/infrastructure/middlewareGenerator.js +112 -315
- package/utils/generators/infrastructure/mongooseSchemaGenerator.js +114 -4
- 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/helpers.js +8 -2
- package/utils/interactive/askEntityInputs.js +5 -68
- package/utils/interactive/askRelationCommand.js +98 -0
- package/utils/interactive/entityBuilder.js +411 -0
- package/utils/lightModeInput.js +22 -241
- package/utils/setups/orms/typeOrmSetup.js +42 -8
- package/utils/setups/projectSetup.js +88 -10
- package/utils/setups/setupAuth.js +335 -20
- package/utils/setups/setupDatabase.js +4 -1
- package/utils/setups/setupHealth.js +74 -0
- package/utils/setups/setupMongoose.js +84 -32
- package/utils/setups/setupPrisma.js +151 -4
- package/utils/setups/setupSwagger.js +15 -8
- package/utils/setups/setupThrottler.js +92 -0
- package/utils/shell.js +61 -9
|
@@ -63,11 +63,23 @@ async function setupMongoose(inputs) {
|
|
|
63
63
|
|
|
64
64
|
logInfo("📦 Installing Mongoose and @nestjs/mongoose...");
|
|
65
65
|
|
|
66
|
+
const pkgManager = inputs.packageManager || "npm";
|
|
67
|
+
const installCmd = pkgManager === "yarn" || pkgManager === "pnpm" ? `${pkgManager} add` : "npm install";
|
|
68
|
+
|
|
66
69
|
await runCommand(
|
|
67
|
-
`${
|
|
70
|
+
`${installCmd} @nestjs/mongoose@^10.0.6 mongoose@^8.3.1`,
|
|
68
71
|
"Mongoose and its dependencies successfully installed!"
|
|
69
72
|
);
|
|
70
73
|
|
|
74
|
+
const isDryRun = process.argv.includes("--dry-run");
|
|
75
|
+
if (isDryRun) {
|
|
76
|
+
console.log("[DRY-RUN] Simulated: write Mongoose configuration, app.module.ts config, and schemas");
|
|
77
|
+
logSuccess(
|
|
78
|
+
"Mongoose configuration complete. Schemas are generated in src/schemas!"
|
|
79
|
+
);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
71
83
|
// --- Base Configuration (app.module.ts and .env) --- // Generating the .env file
|
|
72
84
|
const envPath = ".env";
|
|
73
85
|
const mongoUriLine = `MONGO_URI=${inputs.dbConfig.MONGO_URI}`;
|
|
@@ -78,14 +90,10 @@ async function setupMongoose(inputs) {
|
|
|
78
90
|
await createFile({ path: envPath, contente: envContent });
|
|
79
91
|
} else {
|
|
80
92
|
let content = fs.readFileSync(envPath, "utf8");
|
|
81
|
-
if (content.includes("MONGO_URI=")) {
|
|
82
|
-
content = content.replace(/MONGO_URI=.*/g, mongoUriLine);
|
|
83
|
-
} else {
|
|
93
|
+
if (!content.includes("MONGO_URI=")) {
|
|
84
94
|
content += `\n${mongoUriLine}`;
|
|
85
95
|
}
|
|
86
|
-
if (content.includes("MONGO_DB=")) {
|
|
87
|
-
content = content.replace(/MONGO_DB=.*/g, mongoDbLine);
|
|
88
|
-
} else {
|
|
96
|
+
if (!content.includes("MONGO_DB=")) {
|
|
89
97
|
content += `\n${mongoDbLine}`;
|
|
90
98
|
}
|
|
91
99
|
fs.writeFileSync(envPath, content, "utf8");
|
|
@@ -98,21 +106,30 @@ async function setupMongoose(inputs) {
|
|
|
98
106
|
dbName: process.env.MONGO_DB,
|
|
99
107
|
}),`;
|
|
100
108
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
109
|
+
if (fs.existsSync(appModulePath)) {
|
|
110
|
+
let appModuleContent = fs.readFileSync(appModulePath, "utf8");
|
|
111
|
+
|
|
112
|
+
// 1. Adding MongooseModule import
|
|
113
|
+
if (!appModuleContent.includes("@nestjs/mongoose")) {
|
|
114
|
+
await updateFile({
|
|
115
|
+
path: appModulePath,
|
|
116
|
+
pattern: /import {[\s\S]*?} from '@nestjs\/config';/,
|
|
117
|
+
replacement: (match) => `${match}\n${mongooseImport}`,
|
|
118
|
+
});
|
|
119
|
+
appModuleContent = fs.readFileSync(appModulePath, "utf8");
|
|
120
|
+
}
|
|
107
121
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
122
|
+
// 2. Adding MongooseModule.forRoot() configuration
|
|
123
|
+
if (!appModuleContent.includes("MongooseModule.forRoot")) {
|
|
124
|
+
const importsPattern =
|
|
125
|
+
/imports:\s*\[[\s\S]*?ConfigModule\.forRoot\([\s\S]*?\),/;
|
|
126
|
+
await updateFile({
|
|
127
|
+
path: appModulePath,
|
|
128
|
+
pattern: importsPattern,
|
|
129
|
+
replacement: (match) => `${match}${mongooseForRoot}`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
116
133
|
|
|
117
134
|
// --- Generating Mongoose Entities (Schemas) ---
|
|
118
135
|
logInfo("📁 Generating Mongoose schemas (src/schemas)...");
|
|
@@ -161,10 +178,34 @@ role: Role;
|
|
|
161
178
|
const mongooseType = mapTypeToMongoose(field.type);
|
|
162
179
|
const tsType = field.type;
|
|
163
180
|
|
|
164
|
-
const isRequired =
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
181
|
+
const isRequired = field.nullable !== undefined ? !field.nullable : (field.name.toLowerCase() !== "isactive" && field.name.toLowerCase() !== "password");
|
|
182
|
+
let propOpts = [`type: ${mongooseType}`];
|
|
183
|
+
if (isRequired) {
|
|
184
|
+
propOpts.push("required: true");
|
|
185
|
+
}
|
|
186
|
+
if (field.unique) {
|
|
187
|
+
propOpts.push("unique: true");
|
|
188
|
+
}
|
|
189
|
+
if (field.default !== undefined && field.default !== null && field.default !== "") {
|
|
190
|
+
let defaultVal = field.default;
|
|
191
|
+
if (typeof defaultVal === "string") {
|
|
192
|
+
if (defaultVal === "true") defaultVal = true;
|
|
193
|
+
else if (defaultVal === "false") defaultVal = false;
|
|
194
|
+
else if (defaultVal.toLowerCase() === "now" || defaultVal.toLowerCase() === "now()") {
|
|
195
|
+
defaultVal = "Date.now";
|
|
196
|
+
} else if (!isNaN(defaultVal) && defaultVal.trim() !== "") {
|
|
197
|
+
defaultVal = Number(defaultVal);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (defaultVal === "Date.now") {
|
|
201
|
+
propOpts.push("default: Date.now");
|
|
202
|
+
} else if (typeof defaultVal === "boolean" || typeof defaultVal === "number") {
|
|
203
|
+
propOpts.push(`default: ${defaultVal}`);
|
|
204
|
+
} else {
|
|
205
|
+
propOpts.push(`default: '${defaultVal}'`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const propOptionsStr = propOpts.join(", ");
|
|
168
209
|
|
|
169
210
|
// Import des enums partagés si nécessaire
|
|
170
211
|
if (
|
|
@@ -176,8 +217,8 @@ role: Role;
|
|
|
176
217
|
}
|
|
177
218
|
|
|
178
219
|
fieldsContent += `
|
|
179
|
-
@Prop({
|
|
180
|
-
${field.name}: ${tsType};
|
|
220
|
+
@Prop({ ${propOptionsStr} })
|
|
221
|
+
${field.name}${field.nullable ? "?" : ""}: ${tsType};
|
|
181
222
|
`;
|
|
182
223
|
}
|
|
183
224
|
|
|
@@ -203,11 +244,22 @@ role: Role;
|
|
|
203
244
|
", "
|
|
204
245
|
)}]),`;
|
|
205
246
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
247
|
+
if (fs.existsSync(appModulePath)) {
|
|
248
|
+
const appModuleContent = fs.readFileSync(appModulePath, "utf8");
|
|
249
|
+
if (appModuleContent.includes("MongooseModule.forFeature")) {
|
|
250
|
+
await updateFile({
|
|
251
|
+
path: appModulePath,
|
|
252
|
+
pattern: /MongooseModule\.forFeature\(\[[\s\S]*?\]\),/,
|
|
253
|
+
replacement: forFeatureBlock,
|
|
254
|
+
});
|
|
255
|
+
} else {
|
|
256
|
+
await updateFile({
|
|
257
|
+
path: appModulePath,
|
|
258
|
+
pattern: new RegExp(mongooseForRoot.trim().replace(/[\n\r]/g, "\\s*"), "g"),
|
|
259
|
+
replacement: (match) => `${match}\n\t${forFeatureBlock}`,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
211
263
|
|
|
212
264
|
if (inputs.isDemo) {
|
|
213
265
|
// The generateSampleData function must be adapted for 'new Date()' usage and Mongoose types
|
|
@@ -147,7 +147,7 @@ async function setupPrisma(inputs) {
|
|
|
147
147
|
!fieldsToExclude.includes(fieldNameLower) &&
|
|
148
148
|
!addedFields.has(fieldNameLower)
|
|
149
149
|
) {
|
|
150
|
-
schemaContent += `\n ${field.name} ${
|
|
150
|
+
schemaContent += `\n ${field.name} ${formatPrismaField(field)}`;
|
|
151
151
|
addedFields.add(fieldNameLower);
|
|
152
152
|
}
|
|
153
153
|
}
|
|
@@ -281,6 +281,32 @@ async function setupPrisma(inputs) {
|
|
|
281
281
|
}
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
if (inputs.useAuth) {
|
|
285
|
+
schemaContent += `
|
|
286
|
+
/**
|
|
287
|
+
* PasswordReset Model
|
|
288
|
+
*/
|
|
289
|
+
model PasswordReset {
|
|
290
|
+
id String @id @default(uuid())
|
|
291
|
+
email String
|
|
292
|
+
token String @unique
|
|
293
|
+
expiresAt DateTime
|
|
294
|
+
createdAt DateTime @default(now())
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Otp Model
|
|
299
|
+
*/
|
|
300
|
+
model Otp {
|
|
301
|
+
id String @id @default(uuid())
|
|
302
|
+
email String
|
|
303
|
+
otp String
|
|
304
|
+
expiresAt DateTime
|
|
305
|
+
createdAt DateTime @default(now())
|
|
306
|
+
}
|
|
307
|
+
`;
|
|
308
|
+
}
|
|
309
|
+
|
|
284
310
|
logInfo("Updating schema.prisma");
|
|
285
311
|
const baseSchema = `
|
|
286
312
|
generator client {
|
|
@@ -431,6 +457,44 @@ function mapTypeToPrisma(type) {
|
|
|
431
457
|
}
|
|
432
458
|
}
|
|
433
459
|
|
|
460
|
+
/**
|
|
461
|
+
* Formats a field definition for the Prisma schema including constraints.
|
|
462
|
+
* @param {object} field
|
|
463
|
+
* @returns {string} Formatted field line content (e.g. "String? @unique")
|
|
464
|
+
*/
|
|
465
|
+
function formatPrismaField(field) {
|
|
466
|
+
let prismaType = mapTypeToPrisma(field.type);
|
|
467
|
+
if (field.nullable) {
|
|
468
|
+
if (!prismaType.endsWith("?") && !prismaType.endsWith("[]")) {
|
|
469
|
+
prismaType += "?";
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
let attributes = "";
|
|
473
|
+
if (field.unique) {
|
|
474
|
+
attributes += " @unique";
|
|
475
|
+
}
|
|
476
|
+
if (field.default !== undefined && field.default !== null && field.default !== "") {
|
|
477
|
+
let defaultVal = field.default;
|
|
478
|
+
if (typeof defaultVal === "string") {
|
|
479
|
+
if (defaultVal === "true") defaultVal = true;
|
|
480
|
+
else if (defaultVal === "false") defaultVal = false;
|
|
481
|
+
else if (defaultVal.toLowerCase() === "now" || defaultVal.toLowerCase() === "now()") {
|
|
482
|
+
defaultVal = "now()";
|
|
483
|
+
} else if (!isNaN(defaultVal) && defaultVal.trim() !== "") {
|
|
484
|
+
defaultVal = Number(defaultVal);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (defaultVal === "now()") {
|
|
488
|
+
attributes += ` @default(now())`;
|
|
489
|
+
} else if (typeof defaultVal === "boolean" || typeof defaultVal === "number") {
|
|
490
|
+
attributes += ` @default(${defaultVal})`;
|
|
491
|
+
} else {
|
|
492
|
+
attributes += ` @default("${defaultVal}")`;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return `${prismaType}${attributes}`;
|
|
496
|
+
}
|
|
497
|
+
|
|
434
498
|
async function setupPrismaSeeding(inputs) {
|
|
435
499
|
logInfo("⚙️ Configuring seeding for Prisma...");
|
|
436
500
|
|
|
@@ -681,7 +745,7 @@ model ${capitalizedName} {
|
|
|
681
745
|
updatedAt DateTime @updatedAt${relationFields}`;
|
|
682
746
|
|
|
683
747
|
fields.forEach((field) => {
|
|
684
|
-
modelBlock += `\n ${field.name} ${
|
|
748
|
+
modelBlock += `\n ${field.name} ${formatPrismaField(field)}`;
|
|
685
749
|
});
|
|
686
750
|
|
|
687
751
|
modelBlock += `\n}\n`;
|
|
@@ -738,11 +802,94 @@ model ${capitalizedName} {
|
|
|
738
802
|
);
|
|
739
803
|
|
|
740
804
|
await runCommand(
|
|
741
|
-
`npx prisma migrate dev --name add_module_${entityData.name}
|
|
805
|
+
`npx prisma migrate dev --name add_module_${entityData.name}`,
|
|
742
806
|
`❌ Failed to run prisma migration — run 'npx prisma migrate dev --name add_module_${entityData.name}' manually`,
|
|
743
807
|
{ critical: false }
|
|
744
808
|
);
|
|
745
809
|
}
|
|
746
810
|
|
|
747
|
-
|
|
811
|
+
/**
|
|
812
|
+
* Ajoute une relation entre deux modèles Prisma EXISTANTS.
|
|
813
|
+
* Contrairement à updatePrismaSchema, ne crée PAS un nouveau modèle —
|
|
814
|
+
* patche uniquement les champs FK/inverse dans les deux modèles déjà présents.
|
|
815
|
+
*
|
|
816
|
+
* @param {string} source - Nom de l'entité source (ex: "post")
|
|
817
|
+
* @param {string} target - Nom de l'entité cible (ex: "user")
|
|
818
|
+
* @param {string} relationType - "n-1" | "1-n" | "1-1" | "n-n"
|
|
819
|
+
*/
|
|
820
|
+
async function addPrismaRelation(source, target, relationType) {
|
|
821
|
+
const schemaPath = "prisma/schema.prisma";
|
|
822
|
+
const sourceCap = capitalize(source);
|
|
823
|
+
const targetCap = capitalize(target);
|
|
824
|
+
const sourceLow = source.toLowerCase();
|
|
825
|
+
const targetLow = target.toLowerCase();
|
|
826
|
+
|
|
827
|
+
// 1️⃣ Champs à injecter dans le modèle SOURCE
|
|
828
|
+
let sourceFields = "";
|
|
829
|
+
switch (relationType) {
|
|
830
|
+
case "n-1":
|
|
831
|
+
sourceFields = ` ${targetLow} ${targetCap} @relation(fields: [${targetLow}Id], references: [id])\n ${targetLow}Id String`;
|
|
832
|
+
break;
|
|
833
|
+
case "1-n":
|
|
834
|
+
sourceFields = ` ${targetLow}s ${targetCap}[]`;
|
|
835
|
+
break;
|
|
836
|
+
case "1-1":
|
|
837
|
+
sourceFields = ` ${targetLow} ${targetCap}? @relation(fields: [${targetLow}Id], references: [id])\n ${targetLow}Id String? @unique`;
|
|
838
|
+
break;
|
|
839
|
+
case "n-n":
|
|
840
|
+
sourceFields = ` ${targetLow}s ${targetCap}[]`;
|
|
841
|
+
break;
|
|
842
|
+
default:
|
|
843
|
+
throw new Error(`❌ Type de relation inconnu : ${relationType}`);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// 2️⃣ Champs inverses à injecter dans le modèle TARGET
|
|
847
|
+
let targetFields = "";
|
|
848
|
+
switch (relationType) {
|
|
849
|
+
case "n-1":
|
|
850
|
+
targetFields = ` ${sourceLow}s ${sourceCap}[]`;
|
|
851
|
+
break;
|
|
852
|
+
case "1-n":
|
|
853
|
+
targetFields = ` ${sourceLow} ${sourceCap} @relation(fields: [${sourceLow}Id], references: [id])\n ${sourceLow}Id String`;
|
|
854
|
+
break;
|
|
855
|
+
case "1-1":
|
|
856
|
+
targetFields = ` ${sourceLow} ${sourceCap}?`;
|
|
857
|
+
break;
|
|
858
|
+
case "n-n":
|
|
859
|
+
targetFields = ` ${sourceLow}s ${sourceCap}[]`;
|
|
860
|
+
break;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// 3️⃣ Patch du modèle SOURCE
|
|
864
|
+
await updateFile({
|
|
865
|
+
path: schemaPath,
|
|
866
|
+
pattern: new RegExp(`model ${sourceCap} \\{([\\s\\S]*?)\\}`, "g"),
|
|
867
|
+
replacement: `model ${sourceCap} {$1\n${sourceFields}\n}`,
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
// 4️⃣ Patch du modèle TARGET (inverse)
|
|
871
|
+
await updateFile({
|
|
872
|
+
path: schemaPath,
|
|
873
|
+
pattern: new RegExp(`model ${targetCap} \\{([\\s\\S]*?)\\}`, "g"),
|
|
874
|
+
replacement: `model ${targetCap} {$1\n${targetFields}\n}`,
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
// 5️⃣ Synchronisation Prisma
|
|
878
|
+
await runCommand(
|
|
879
|
+
"npx prisma format",
|
|
880
|
+
"❌ Failed to format prisma schema — run 'npx prisma format' manually",
|
|
881
|
+
{ critical: false },
|
|
882
|
+
);
|
|
883
|
+
await runCommand(
|
|
884
|
+
"npx prisma generate",
|
|
885
|
+
"❌ Failed to generate prisma client — run 'npx prisma generate' manually",
|
|
886
|
+
{ critical: false },
|
|
887
|
+
);
|
|
888
|
+
await runCommand(
|
|
889
|
+
`npx prisma migrate dev --name add_relation_${sourceLow}_${targetLow}`,
|
|
890
|
+
`❌ Failed to run prisma migration — run 'npx prisma migrate dev' manually`,
|
|
891
|
+
{ critical: false },
|
|
892
|
+
);
|
|
893
|
+
}
|
|
748
894
|
|
|
895
|
+
module.exports = { setupPrisma, updatePrismaSchema, addPrismaRelation };
|
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
|
-
const {
|
|
2
|
+
const { runCommand } = require("../shell");
|
|
3
3
|
const { logInfo } = require("../loggers/logInfo");
|
|
4
4
|
const { logSuccess } = require("../loggers/logSuccess");
|
|
5
5
|
|
|
6
6
|
async function setupSwagger(inputs) {
|
|
7
7
|
logInfo("Installation et Configuration de Swagger...");
|
|
8
8
|
|
|
9
|
+
const pkgManager = inputs.packageManager || "npm";
|
|
10
|
+
const installCmd = pkgManager === "yarn" || pkgManager === "pnpm" ? `${pkgManager} add` : "npm install";
|
|
11
|
+
|
|
9
12
|
// installation de swagger
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
await runCommand(
|
|
14
|
+
`${installCmd} @nestjs/swagger@^7.3.0 swagger-ui-express@^5.0.0`,
|
|
15
|
+
"Échec de l'installation de Swagger",
|
|
16
|
+
{ spinner: "Installing Swagger..." }
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const isDryRun = process.argv.includes("--dry-run");
|
|
20
|
+
if (isDryRun) {
|
|
21
|
+
console.log("[DRY-RUN] Simulated: write Swagger configuration to src/main.ts");
|
|
22
|
+
logSuccess(" Swagger configuré avec succès !");
|
|
23
|
+
return;
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
// Modification de main.ts pour intégrer Swagger
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// utils/setups/setupThrottler.js
|
|
2
|
+
//
|
|
3
|
+
// Script d'installation et de configuration de @nestjs/throttler.
|
|
4
|
+
// Injecte le module et le guard dans app.module.ts de manière idempotente.
|
|
5
|
+
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const { logInfo } = require("../loggers/logInfo");
|
|
8
|
+
const { logSuccess } = require("../loggers/logSuccess");
|
|
9
|
+
const { runCommand } = require("../shell");
|
|
10
|
+
const { updateFile } = require("../userInput");
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Configure le rate limiting global dans le projet.
|
|
14
|
+
*
|
|
15
|
+
* @param {object} inputs - Configuration du projet
|
|
16
|
+
*/
|
|
17
|
+
async function setupThrottler(inputs) {
|
|
18
|
+
logInfo("Installation et Configuration du Rate Limiting (Throttler)...");
|
|
19
|
+
|
|
20
|
+
const pkgManager = inputs.packageManager || "npm";
|
|
21
|
+
const installCmd = pkgManager === "yarn" || pkgManager === "pnpm" ? `${pkgManager} add` : "npm install";
|
|
22
|
+
|
|
23
|
+
// Installation de @nestjs/throttler
|
|
24
|
+
await runCommand(
|
|
25
|
+
`${installCmd} @nestjs/throttler@^6.0.0`,
|
|
26
|
+
"Échec de l'installation de @nestjs/throttler",
|
|
27
|
+
{ spinner: "Installing @nestjs/throttler..." }
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const isDryRun = process.argv.includes("--dry-run");
|
|
31
|
+
if (isDryRun) {
|
|
32
|
+
console.log("[DRY-RUN] Simulated: write Throttler configuration to src/app.module.ts");
|
|
33
|
+
logSuccess(" Throttler configuré avec succès !");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const appModulePath = "src/app.module.ts";
|
|
38
|
+
if (!fs.existsSync(appModulePath)) {
|
|
39
|
+
console.warn(`⚠️ Fichier ${appModulePath} introuvable. Skip configuration.`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let content = fs.readFileSync(appModulePath, "utf8");
|
|
44
|
+
|
|
45
|
+
if (!content.includes("ThrottlerModule")) {
|
|
46
|
+
// 1. Ajouter les imports
|
|
47
|
+
content = content.replace(
|
|
48
|
+
"import { Module } from '@nestjs/common';",
|
|
49
|
+
"import { Module } from '@nestjs/common';\nimport { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';\nimport { APP_GUARD } from '@nestjs/core';"
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// 2. Ajouter ThrottlerModule.forRoot dans imports: [
|
|
53
|
+
content = content.replace(
|
|
54
|
+
"imports: [",
|
|
55
|
+
`imports: [
|
|
56
|
+
ThrottlerModule.forRoot([{
|
|
57
|
+
ttl: 60000,
|
|
58
|
+
limit: 10,
|
|
59
|
+
}]),`
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// 3. Ajouter ThrottlerGuard dans providers: [
|
|
63
|
+
if (content.includes("providers: [")) {
|
|
64
|
+
content = content.replace(
|
|
65
|
+
"providers: [",
|
|
66
|
+
`providers: [
|
|
67
|
+
{
|
|
68
|
+
provide: APP_GUARD,
|
|
69
|
+
useClass: ThrottlerGuard,
|
|
70
|
+
},`
|
|
71
|
+
);
|
|
72
|
+
} else {
|
|
73
|
+
content = content.replace(
|
|
74
|
+
"@Module({",
|
|
75
|
+
`@Module({
|
|
76
|
+
providers: [
|
|
77
|
+
{
|
|
78
|
+
provide: APP_GUARD,
|
|
79
|
+
useClass: ThrottlerGuard,
|
|
80
|
+
},
|
|
81
|
+
],`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fs.writeFileSync(appModulePath, content, "utf8");
|
|
86
|
+
logSuccess(" Throttler configuré avec succès dans app.module.ts !");
|
|
87
|
+
} else {
|
|
88
|
+
logInfo(" Throttler déjà présent dans app.module.ts. Skip.");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { setupThrottler };
|
package/utils/shell.js
CHANGED
|
@@ -20,44 +20,96 @@ const _warnings = [];
|
|
|
20
20
|
* @param {boolean} [options.critical=true] - If true (default), a failure calls process.exit(1).
|
|
21
21
|
* If false, the failure is logged as a warning and execution continues.
|
|
22
22
|
* @param {string|null} [options.spinner] - Optional spinner label shown during execution.
|
|
23
|
+
* @param {number} [options.timeout] - Custom timeout in milliseconds.
|
|
23
24
|
* @returns {Promise<boolean>} Resolves to true on success, false on non-critical failure.
|
|
24
25
|
*/
|
|
25
26
|
async function runCommand(command, errorMessage, options = {}) {
|
|
26
|
-
// Support old call signature: runCommand(cmd, msg, spinnerText)
|
|
27
|
-
// New signature: runCommand(cmd, msg, { critical, spinner })
|
|
28
27
|
let critical = true;
|
|
29
28
|
let spinnerText = null;
|
|
29
|
+
let customTimeout = undefined;
|
|
30
30
|
|
|
31
31
|
if (typeof options === "string") {
|
|
32
|
-
// Legacy: third arg was spinnerText string
|
|
33
32
|
spinnerText = options;
|
|
34
33
|
} else if (typeof options === "object" && options !== null) {
|
|
35
|
-
critical = options.critical !== false;
|
|
34
|
+
critical = options.critical !== false;
|
|
36
35
|
spinnerText = options.spinner || null;
|
|
36
|
+
customTimeout = options.timeout;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Determine Timeout
|
|
40
|
+
let timeoutVal = 30000; // default 30 seconds
|
|
41
|
+
if (
|
|
42
|
+
command.includes("install") ||
|
|
43
|
+
command.includes("add") ||
|
|
44
|
+
command.includes(" new ") ||
|
|
45
|
+
command.includes("prisma migrate")
|
|
46
|
+
) {
|
|
47
|
+
timeoutVal = 600000; // 10 minutes for heavy setup commands
|
|
48
|
+
}
|
|
49
|
+
if (customTimeout !== undefined) {
|
|
50
|
+
timeoutVal = customTimeout;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Intercept npm installation commands and append --legacy-peer-deps, --no-audit, --no-fund if missing
|
|
54
|
+
let finalCommand = command;
|
|
55
|
+
if (
|
|
56
|
+
command.startsWith("npm ") &&
|
|
57
|
+
(command.includes("install") || command.includes("add") || command.includes(" i "))
|
|
58
|
+
) {
|
|
59
|
+
if (!command.includes("--legacy-peer-deps")) {
|
|
60
|
+
finalCommand = `${finalCommand} --legacy-peer-deps`;
|
|
61
|
+
}
|
|
62
|
+
if (!command.includes("--no-audit")) {
|
|
63
|
+
finalCommand = `${finalCommand} --no-audit`;
|
|
64
|
+
}
|
|
65
|
+
if (!command.includes("--no-fund")) {
|
|
66
|
+
finalCommand = `${finalCommand} --no-fund`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Dry-Run Simulation Check
|
|
71
|
+
const isDryRun = process.argv.includes("--dry-run");
|
|
72
|
+
if (isDryRun) {
|
|
73
|
+
if (spinnerText) {
|
|
74
|
+
const spin = spinner(spinnerText);
|
|
75
|
+
spin.start();
|
|
76
|
+
spin.succeed(`[DRY-RUN] Simulated: ${finalCommand}`);
|
|
77
|
+
} else {
|
|
78
|
+
console.log(`[DRY-RUN] Simulated: ${finalCommand}`);
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
37
81
|
}
|
|
38
82
|
|
|
39
83
|
const spin = spinnerText ? spinner(spinnerText) : null;
|
|
40
84
|
|
|
41
85
|
try {
|
|
42
86
|
if (spin) spin.start();
|
|
43
|
-
execSync(
|
|
87
|
+
execSync(finalCommand, {
|
|
88
|
+
stdio: spinnerText ? "pipe" : "inherit",
|
|
89
|
+
timeout: timeoutVal,
|
|
90
|
+
});
|
|
44
91
|
if (spin) spin.succeed(spinnerText);
|
|
45
92
|
return true;
|
|
46
93
|
} catch (error) {
|
|
47
94
|
if (spin) spin.fail(errorMessage);
|
|
48
95
|
|
|
49
|
-
|
|
96
|
+
let errorDetail = error.message;
|
|
97
|
+
if (error.code === "ETIMEDOUT" || error.signal === "SIGTERM") {
|
|
98
|
+
errorDetail = `Command timed out after ${timeoutVal / 1000}s`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const logLine = `[${new Date().toISOString()}] ${errorMessage}: ${errorDetail}\n`;
|
|
50
102
|
fs.appendFileSync("setup.log", logLine);
|
|
51
103
|
|
|
52
104
|
if (critical) {
|
|
53
|
-
// Fatal — stop everything
|
|
54
105
|
logError(`${errorMessage}`);
|
|
55
106
|
logError(`Run: ${command}`);
|
|
107
|
+
logError(`Detail: ${errorDetail}`);
|
|
56
108
|
process.exit(1);
|
|
57
109
|
} else {
|
|
58
|
-
// Non-critical — warn and continue
|
|
59
110
|
logWarning(`${errorMessage} (non-fatal, continuing...)`);
|
|
60
111
|
logWarning(` Command: ${command}`);
|
|
112
|
+
logWarning(` Detail: ${errorDetail}`);
|
|
61
113
|
logWarning(` See setup.log for details`);
|
|
62
114
|
_warnings.push({ command, errorMessage });
|
|
63
115
|
return false;
|
|
@@ -72,7 +124,7 @@ async function runCommand(command, errorMessage, options = {}) {
|
|
|
72
124
|
*/
|
|
73
125
|
async function runCommandSilent(command) {
|
|
74
126
|
try {
|
|
75
|
-
return execSync(command, { stdio: "pipe" }).toString();
|
|
127
|
+
return execSync(command, { stdio: "pipe", timeout: 10000 }).toString();
|
|
76
128
|
} catch (error) {
|
|
77
129
|
return null;
|
|
78
130
|
}
|