nestcraftx 0.2.4 → 0.2.6

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.
Files changed (63) hide show
  1. package/.gitattributes +6 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +33 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.md +19 -0
  4. package/.github/ISSUE_TEMPLATE/pull_request_template.md +24 -0
  5. package/CHANGELOG.fr.md +97 -97
  6. package/CHANGELOG.md +98 -98
  7. package/CLI_USAGE.fr.md +331 -331
  8. package/CLI_USAGE.md +364 -364
  9. package/DEMO.fr.md +292 -292
  10. package/DEMO.md +294 -294
  11. package/LICENSE +21 -21
  12. package/MIGRATION_GUIDE.fr.md +127 -127
  13. package/MIGRATION_GUIDE.md +124 -124
  14. package/QUICK_START.fr.md +152 -152
  15. package/QUICK_START.md +169 -169
  16. package/README.fr.md +653 -659
  17. package/SECURITY.md +10 -0
  18. package/bin/nestcraft.js +84 -64
  19. package/commands/demo.js +333 -330
  20. package/commands/generate.js +93 -0
  21. package/commands/generateConf.js +91 -0
  22. package/commands/help.js +78 -78
  23. package/commands/info.js +48 -48
  24. package/commands/new.js +338 -335
  25. package/commands/start.js +19 -19
  26. package/commands/test.js +7 -7
  27. package/package.json +41 -41
  28. package/readme.md +638 -643
  29. package/utils/cliParser.js +133 -76
  30. package/utils/colors.js +62 -62
  31. package/utils/configs/configureDocker.js +120 -120
  32. package/utils/configs/setupCleanArchitecture.js +563 -557
  33. package/utils/configs/setupLightArchitecture.js +701 -660
  34. package/utils/envGenerator.js +122 -122
  35. package/utils/file-utils/packageJsonUtils.js +49 -55
  36. package/utils/file-utils/saveProjectConfig.js +36 -0
  37. package/utils/fullModeInput.js +607 -607
  38. package/utils/generators/application/dtoUpdater.js +54 -0
  39. package/utils/generators/cleanModuleGenerator.js +475 -0
  40. package/utils/generators/database/setupDatabase.js +31 -0
  41. package/utils/generators/domain/entityUpdater.js +78 -0
  42. package/utils/generators/infrastructure/mapperUpdater.js +65 -0
  43. package/utils/generators/lightModuleGenerator.js +131 -0
  44. package/utils/generators/relation/relation.engine.js +64 -0
  45. package/utils/interactive/askEntityInputs.js +165 -0
  46. package/utils/lightModeInput.js +460 -460
  47. package/utils/loggers/logError.js +7 -7
  48. package/utils/loggers/logInfo.js +7 -7
  49. package/utils/loggers/logSuccess.js +7 -7
  50. package/utils/loggers/logWarning.js +7 -7
  51. package/utils/setups/orms/typeOrmSetup.js +630 -630
  52. package/utils/setups/projectSetup.js +46 -46
  53. package/utils/setups/setupAuth.js +973 -926
  54. package/utils/setups/setupDatabase.js +75 -75
  55. package/utils/setups/setupLogger.js +69 -59
  56. package/utils/setups/setupMongoose.js +377 -432
  57. package/utils/setups/setupPrisma.js +802 -630
  58. package/utils/setups/setupSwagger.js +97 -88
  59. package/utils/shell.js +32 -32
  60. package/utils/spinner.js +57 -57
  61. package/utils/systemCheck.js +124 -124
  62. package/utils/userInput.js +421 -421
  63. package/utils/utils.js +2197 -1762
@@ -1,630 +1,802 @@
1
- const fs = require("fs");
2
- const { logInfo } = require("../loggers/logInfo");
3
- const { runCommand } = require("../shell");
4
- const { logSuccess } = require("../loggers/logSuccess");
5
- const {
6
- createDirectory,
7
- createFile,
8
- updateFile,
9
- capitalize,
10
- } = require("../userInput");
11
- const { updatePackageJson } = require("../file-utils/packageJsonUtils");
12
-
13
- async function setupPrisma(inputs) {
14
- logInfo("Configuring Prisma...");
15
-
16
- const dbConfig = inputs.dbConfig; // 📌 Path to schema.prisma
17
- const schemaPath = "prisma/schema.prisma"; // 📦 Step 1: Install Prisma and its client at version 6.5.0
18
-
19
- const prismaVersion = "6.5.0"; // Stable version for the CLI
20
- logInfo(`Installing prisma and client...`);
21
- await runCommand(
22
- `${inputs.packageManager} add -D prisma@${prismaVersion} @prisma/client@${prismaVersion}`,
23
- " Prisma installation failed"
24
- );
25
-
26
- // Step 2: Initialize Prisma
27
- logInfo("Initializing Prisma");
28
- await runCommand("npx prisma init", "❌ Prisma initialization failed");
29
-
30
- await updateFile({
31
- path: schemaPath,
32
- pattern: /generator client \{[^}]*\}/g,
33
- replacement: `generator client {
34
- provider = "prisma-client-js"
35
- output = "../node_modules/.prisma/client" //
36
- }`,
37
- });
38
-
39
- // Step 3: Environment Configuration (.env and .env.example files)
40
- const envPath = ".env";
41
- const exampleEnvPath = ".env.example";
42
- const databaseUrl = `DATABASE_URL="postgresql://${dbConfig.POSTGRES_USER}:${dbConfig.POSTGRES_PASSWORD}@${dbConfig.POSTGRES_HOST}:${dbConfig.POSTGRES_PORT}/${dbConfig.POSTGRES_DB}?schema=public"`;
43
- const exampleDatabaseUrl = `DATABASE_URL="postgresql://user:password@localhost:5432/dbName?schema=public"`;
44
-
45
- await createFile({
46
- path: envPath,
47
- contente: databaseUrl,
48
- });
49
-
50
- await createFile({
51
- path: exampleEnvPath,
52
- contente: exampleDatabaseUrl,
53
- });
54
-
55
- // Step 4: Generating Prisma models from provided entities
56
- logInfo("Adding entities");
57
- let schemaContent = "";
58
- const hasUserEntity = inputs.entitiesData.entities.some(
59
- (entity) => entity.name.toLowerCase() === "user"
60
- );
61
-
62
- // Adding the Role enum block if User is present
63
- if (hasUserEntity) {
64
- schemaContent += `
65
- /**
66
- * Role enumeration
67
- */
68
- enum Role {
69
- USER
70
- ADMIN
71
- SUPER_ADMIN
72
- }
73
- `;
74
- }
75
-
76
- const fieldsToExcludeMap = new Map();
77
- for (const entity of inputs.entitiesData.entities) {
78
- fieldsToExcludeMap.set(entity.name.toLowerCase(), []);
79
- }
80
-
81
- if (inputs.entitiesData.relations?.length > 0) {
82
- for (const relation of inputs.entitiesData.relations) {
83
- const fromLower = relation.from.toLowerCase();
84
- const toLower = relation.to.toLowerCase();
85
- const fromCapitalized = capitalize(relation.from);
86
- const toCapitalized = capitalize(relation.to); // 'from' side (source)
87
-
88
- if (relation.type === "1-n") {
89
- // 'One' side: exclude the name of the other entity's list (e.g., 'articles')
90
- fieldsToExcludeMap.get(fromLower).push(`${toLower}s`);
91
- } else if (relation.type === "n-1") {
92
- // 'Many' side: exclude the foreign key (e.g., 'articleId') and the relation name (e.g., 'article')
93
- fieldsToExcludeMap.get(fromLower).push(`${toLower}id`, toLower);
94
- } // Add other relation types (1-1, n-n) if necessary here... // 'to' side (target)
95
- if (relation.type === "1-n") {
96
- // 'Many' side: exclude the foreign key (e.g., 'userId') and the relation name (e.g., 'user')
97
- fieldsToExcludeMap.get(toLower).push(`${fromLower}id`, fromLower);
98
- } else if (relation.type === "n-1") {
99
- // 'One' side: exclude the name of the other entity's list (e.g., 'comments')
100
- fieldsToExcludeMap.get(toLower).push(`${fromLower}s`);
101
- }
102
- }
103
- }
104
- // 2. Initial generation of models WITHOUT incorrect relationship fields
105
- for (const entity of inputs.entitiesData.entities) {
106
- const entityNameLower = entity.name.toLowerCase();
107
- // On utilise un Set pour suivre les noms de champs déjà écrits dans ce modèle
108
- const addedFields = new Set(["id", "createdat", "updatedat"]);
109
-
110
- schemaContent += `
111
- /**
112
- * ${entity.name} Model
113
- */
114
- model ${entity.name} {
115
- id String @id @default(uuid())
116
- createdAt DateTime @default(now())
117
- updatedAt DateTime @updatedAt`;
118
-
119
- const fieldsToExclude = fieldsToExcludeMap.get(entityNameLower) || [];
120
-
121
- for (const field of entity.fields) {
122
- const fieldNameLower = field.name.toLowerCase();
123
-
124
- // Ajout du rôle SEULEMENT s'il n'a pas été ajouté durant la boucle ci-dessus
125
- if (entityNameLower == "user" && !addedFields.has("role")) {
126
- schemaContent += `\n role Role @default(USER)`;
127
- addedFields.add("role");
128
- }
129
-
130
- // Add email field ONLY if it was not added previously
131
- if (entityNameLower === "user" && !addedFields.has("email")) {
132
- schemaContent += `\n email String @unique`;
133
- addedFields.add("email");
134
- }
135
-
136
- // On n'ajoute le champ que s'il n'est pas exclu ET pas déjà présent (comme id/createdAt)
137
- if (
138
- !fieldsToExclude.includes(fieldNameLower) &&
139
- !addedFields.has(fieldNameLower)
140
- ) {
141
- schemaContent += `\n ${field.name} ${mapTypeToPrisma(field.type)}`;
142
- addedFields.add(fieldNameLower);
143
- }
144
- }
145
-
146
- schemaContent += `\n}\n`;
147
- }
148
-
149
- // 3. Applying relationship logic to add the CORRECT fields
150
- logInfo("Applying Prisma relations...");
151
-
152
- if (inputs.entitiesData.relations?.length > 0) {
153
- for (const relation of inputs.entitiesData.relations) {
154
- const from = relation.from;
155
- const to = relation.to;
156
- const type = relation.type;
157
-
158
- // The replacement must be done on the entire generated schemaContent // Using a replacement function to update the content of `schemaContent`
159
- if (type === "1-n") {
160
- // "One" side (source): adds the list (to[])
161
- schemaContent = schemaContent.replace(
162
- new RegExp(`model ${from} \{([\\s\\S]*?)\n\\}`, "m"),
163
- (match, content) => {
164
- const fieldLine = ` ${to}s ${to}[]`;
165
- return match.includes(fieldLine)
166
- ? match
167
- : `model ${from} {${content}\n${fieldLine}\n}`;
168
- }
169
- );
170
-
171
- // "Many" side (target): adds the relation and the foreign key
172
- schemaContent = schemaContent.replace(
173
- new RegExp(`model ${to} \{([\\s\\S]*?)\n\\}`, "m"),
174
- (match, content) => {
175
- const relationLine = ` ${from} ${from} @relation(fields: [${from}Id], references: [id])`;
176
- const fkLine = ` ${from}Id String`;
177
- let result = match.includes(relationLine)
178
- ? content
179
- : `${content}\n${relationLine}`;
180
- result = result.includes(fkLine) ? result : `${result}\n${fkLine}`;
181
- return `model ${to} {${result}\n}`;
182
- }
183
- );
184
- }
185
-
186
- if (type === "n-1") {
187
- // n-1 is the inverse of 1-n: from is the "many" and to is the "one"
188
-
189
- // "Many" side (source = from): adds the relation and the foreign key
190
- schemaContent = schemaContent.replace(
191
- new RegExp(`model ${from} \{([\\s\\S]*?)\n\\}`, "m"),
192
- (match, content) => {
193
- const relationLine = ` ${to} ${to} @relation(fields: [${to}Id], references: [id])`;
194
- const fkLine = ` ${to}Id String`;
195
- let result = match.includes(relationLine)
196
- ? content
197
- : `${content}\n${relationLine}`;
198
- result = result.includes(fkLine) ? result : `${result}\n${fkLine}`;
199
- return `model ${from} {${result}\n}`;
200
- }
201
- );
202
-
203
- // "One" side (target = to): adds the list (from[])
204
- schemaContent = schemaContent.replace(
205
- new RegExp(`model ${to} \{([\\s\\S]*?)\n\\}`, "m"),
206
- (match, content) => {
207
- const fromCapitalized = capitalize(from);
208
- const fieldLine = ` ${from}s ${from}[]`;
209
- return match.includes(fieldLine)
210
- ? match
211
- : `model ${to} {${content}\n${fieldLine}\n}`;
212
- }
213
- );
214
- }
215
-
216
- if (type === "1-1") {
217
- // 'from' side (source): adds the relation, foreign key, and @unique attribute
218
- schemaContent = schemaContent.replace(
219
- new RegExp(`model ${from} \{([\\s\\S]*?)\n\\}`, "m"),
220
- (match, content) => {
221
- const relationLine = ` ${to} ${to}? @relation(fields: [${to}Id], references: [id])`; // The foreign key must be unique in a 1-1 relationship, and optional for flexibility
222
- const fkLine = ` ${to}Id String? @unique`;
223
-
224
- let result = match.includes(relationLine)
225
- ? content
226
- : `${content}\n${relationLine}`;
227
- result = result.includes(fkLine) ? result : `${result}\n${fkLine}`;
228
- return `model ${from} {${result}\n}`;
229
- }
230
- );
231
-
232
- // 'to' side (target): adds the inverse relation (optional)
233
- schemaContent = schemaContent.replace(
234
- new RegExp(`model ${to} \{([\\s\\S]*?)\n\\}`, "m"),
235
- (match, content) => {
236
- // Inverse relation (optional because 'from' holds the FK)
237
- const fieldLine = ` ${from} ${from}?`;
238
- return match.includes(fieldLine)
239
- ? match
240
- : `model ${to} {${content}\n${fieldLine}\n}`;
241
- }
242
- );
243
- }
244
-
245
- if (type === "n-n") {
246
- // 'from' side (source): adds the list (to[])
247
- schemaContent = schemaContent.replace(
248
- new RegExp(`model ${from} \{([\\s\\S]*?)\n\\}`, "m"),
249
- (match, content) => {
250
- const fieldLine = ` ${to}s ${to}[]`;
251
- return match.includes(fieldLine)
252
- ? match
253
- : `model ${from} {${content}\n${fieldLine}\n}`;
254
- }
255
- );
256
-
257
- // 'to' side (target): adds the list (from[])
258
- schemaContent = schemaContent.replace(
259
- new RegExp(`model ${to} \{([\\s\\S]*?)\n\\}`, "m"),
260
- (match, content) => {
261
- const fieldLine = ` ${from}s ${from}[]`;
262
- return match.includes(fieldLine)
263
- ? match
264
- : `model ${to} {${content}\n${fieldLine}\n}`;
265
- }
266
- );
267
- }
268
- }
269
- }
270
-
271
- logInfo("Updating schema.prisma");
272
- const baseSchema = `
273
- generator client {
274
- provider = "prisma-client-js"
275
- }
276
-
277
- datasource db {
278
- provider = "${inputs.dbConfig.orm === "mongodb" ? "mongodb" : "postgresql"}"
279
- url = env("DATABASE_URL")
280
- }
281
-
282
- ${schemaContent}
283
- `;
284
- await createFile({
285
- path: schemaPath,
286
- contente: baseSchema,
287
- });
288
- await runCommand(`npx prisma format`, "❌ Failed to format prisma schema");
289
-
290
- // 📁 Step 6: Creating the `src/prisma` structure
291
- const defaultPatch = "src/prisma";
292
- await createDirectory(defaultPatch);
293
-
294
- // 🧩 Prisma Service
295
- await createFile({
296
- path: `${defaultPatch}/prisma.service.ts`,
297
- contente: `import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
298
- import { PrismaClient } from '@prisma/client';
299
-
300
- /**
301
- * Prisma Service to expose a global instance of the Prisma client
302
- */
303
- @Injectable()
304
- export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
305
- constructor() {
306
- super();
307
- }
308
-
309
- async onModuleInit() {
310
- await this.$connect();
311
- }
312
-
313
- async onModuleDestroy() {
314
- await this.$disconnect();
315
- }
316
- }
317
- `,
318
- });
319
-
320
- // 🧩 Prisma Module
321
- await createFile({
322
- path: `${defaultPatch}/prisma.module.ts`,
323
- contente: `import { Global, Module } from '@nestjs/common';
324
- import { PrismaService } from './prisma.service';
325
-
326
- /**
327
- * Global Prisma Module to provide the service to the entire application
328
- */
329
- @Global()
330
- @Module({
331
- providers: [PrismaService],
332
- exports: [PrismaService],
333
- })
334
- export class PrismaModule {}
335
- `,
336
- });
337
-
338
- // Installing dotenv if necessary
339
- logInfo("📦 Installing dotenv...");
340
- await runCommand(
341
- `${inputs.packageManager} add dotenv`,
342
- "❌ Failed to install dotenv"
343
- );
344
-
345
- // Creating prisma.config.ts file to load environment variables
346
- let prismaConfigPath = "prisma.config.ts";
347
- if (!fs.existsSync(prismaConfigPath)) {
348
- prismaConfigPath = "prisma/prisma.config.ts";
349
- }
350
-
351
- if (fs.existsSync(prismaConfigPath)) {
352
- logInfo(" Updating prisma.config.ts with dotenv import...");
353
- await updateFile({
354
- path: prismaConfigPath,
355
- pattern: /^/,
356
- replacement: `import 'dotenv/config';\n\n`,
357
- });
358
- }
359
-
360
- // Step 7: Generating the Prisma client
361
- await runCommand("npx prisma generate", "❌ Prisma generation failed");
362
-
363
- // Step 8: Migration (ONLY in 'new' mode)
364
- if (inputs.isDemo) {
365
- setupPrismaSeeding(inputs);
366
- }
367
-
368
- logSuccess(" Prisma configured successfully!");
369
- }
370
-
371
- /**
372
- * Maps generic entity types to Prisma data types.
373
- * @param {string} type - Generic type (e.g., 'string', 'number', 'Date', 'string[]', 'MonEnum')
374
- * @returns {string} The corresponding type in the Prisma schema.
375
- */
376
- function mapTypeToPrisma(type) {
377
- // Handles the case of arrays (e.g., 'string[]')
378
- if (type.endsWith("[]")) {
379
- const innerType = type.slice(0, -2); // Removes '[]' // Recursively calls for the inner type
380
- return `${mapTypeToPrisma(innerType)}[]`;
381
- }
382
-
383
- const typeLower = type.toLowerCase();
384
-
385
- switch (typeLower) {
386
- case "string":
387
- case "text": // Mapped to String because Prisma does not have a distinct TEXT type for PostgreSQL
388
- return "String";
389
-
390
- case "number": // A simple "number" field can be Int or Float. We default to Float.
391
- return "Float";
392
- case "int":
393
- return "Int";
394
-
395
- case "decimal": // Use Decimal for high precision, or Float for simplicity
396
- return "Decimal";
397
-
398
- case "boolean":
399
- return "Boolean";
400
-
401
- case "date":
402
- return "DateTime";
403
-
404
- case "uuid": // We use String by default for storage, the @id @default(uuid()) attribute will be managed by the ID logic. // For non-ID fields, String is the appropriate choice.
405
- return "String";
406
-
407
- case "json":
408
- return "Json";
409
- case "role":
410
- return "Role";
411
-
412
- default: // Handles cases of custom enumerations (e.g., 'StatusEnum') or named object types (e.g., 'Address')
413
- // Prisma will use the exact type name if it matches a defined 'enum' or other 'model'.
414
- // In the context of a simple non-persistent DTO/object field, it is better to revert to Json if unrecognized.
415
- // If the type is capitalized (e.g., 'Address'), we return it as is (assuming it's another Model/Enum)
416
- return type.charAt(0) === type.charAt(0).toUpperCase() ? type : "Json";
417
- }
418
- }
419
-
420
- async function setupPrismaSeeding(inputs) {
421
- logInfo("⚙️ Configuring seeding for Prisma...");
422
-
423
- // --- Dependencies ---
424
- const prismaDevDeps = [
425
- "ts-node",
426
- "@types/node",
427
- "@types/bcrypt",
428
- "dotenv-cli",
429
- ];
430
- await runCommand(
431
- `${inputs.packageManager} add -D ${prismaDevDeps.join(" ")}`,
432
- "❌ Failed to install Prisma seeding dependencies"
433
- ); // Bcrypt is often a production dependency for hashing
434
- await runCommand(
435
- `${inputs.packageManager} install bcrypt`,
436
- "❌ Failed to install bcrypt"
437
- );
438
-
439
- // --- Scripts in package.json ---
440
- const prismaScripts = {
441
- "prisma:migrate": "prisma migrate dev",
442
- "prisma:seed": "prisma db seed",
443
- "prisma:reset": "prisma migrate reset",
444
- "prisma:migrate:prod": "prisma migrate deploy",
445
- seed: "ts-node prisma/seed.ts",
446
- };
447
-
448
- await updatePackageJson(inputs, prismaScripts);
449
-
450
- // --- Configuration in schema.prisma ---
451
- await updateFile({
452
- path: "prisma/schema.prisma",
453
- pattern: /generator client \{[^}]*\}/g,
454
- replacement: `generator client {
455
- provider = "prisma-client-js"
456
- output = "../node_modules/.prisma/client"
457
- }
458
-
459
- `,
460
- });
461
-
462
- // --- Creating seed.ts file ---
463
- const seedTsContent = generatePrismaSeedContent(inputs.entitiesData.entities);
464
- await createFile({
465
- path: `prisma/seed.ts`,
466
- contente: seedTsContent,
467
- });
468
-
469
- // logSuccess("✅ Prisma seeding configured.");
470
- }
471
-
472
- function generatePrismaSeedContent(entities) {
473
- const requiresBcrypt = entities.some((e) => e.name.toLowerCase() === "user");
474
-
475
- return `
476
- import { PrismaClient } from '@prisma/client';
477
- ${requiresBcrypt ? "import * as bcrypt from 'bcrypt';" : ""}
478
-
479
- const prisma = new PrismaClient();
480
-
481
- async function main() {
482
- console.log('🌱 Starting Prisma seeding...');
483
-
484
- // --- 1. ADMIN USER ---
485
- ${
486
- requiresBcrypt
487
- ? `const salt = await bcrypt.genSalt(10);
488
- const hashedPassword = await bcrypt.hash('password123', salt);`
489
- : ""
490
- }
491
-
492
- const exists = await prisma.user.findFirst({
493
- where: { email: 'admin@nestcraft.com' },
494
- });
495
-
496
- if (!exists) {
497
- const adminUser = await prisma.user.create({
498
- data: {
499
- email: 'admin@nestcraft.com',
500
- ${
501
- requiresBcrypt
502
- ? "password: hashedPassword,"
503
- : "// Default password: password123"
504
- }
505
- username: 'NestCraftAdmin',
506
- role: 'SUPER_ADMIN',
507
- isActive: true,
508
- },
509
- });
510
-
511
- console.log(\`👑 Admin created: \${adminUser.email}\`);
512
- } else {
513
- console.log(\`👑 Admin realy exists: \${exists.email}\`);
514
- }
515
-
516
-
517
- // --- 2. DEMO USERS ---
518
- const demoUsersData = [
519
- { email: 'emma.jones@demo.com', ${
520
- requiresBcrypt ? "password: hashedPassword," : ""
521
- } username: 'EmmaJones', isActive: true },
522
- { email: 'lucas.martin@demo.com', ${
523
- requiresBcrypt ? "password: hashedPassword," : ""
524
- } username: 'LucasMartin', isActive: true },
525
- { email: 'sophia.bernard@demo.com', ${
526
- requiresBcrypt ? "password: hashedPassword," : ""
527
- } username: 'SophiaBernard', isActive: true },
528
- { email: 'alexandre.dubois@demo.com', ${
529
- requiresBcrypt ? "password: hashedPassword," : ""
530
- } username: 'AlexandreDubois', isActive: true },
531
- { email: 'chloe.moreau@demo.com', ${
532
- requiresBcrypt ? "password: hashedPassword," : ""
533
- } username: 'ChloeMoreau', isActive: true },
534
- ];
535
-
536
- const existingUsers = await prisma.user.findMany({
537
- where: {
538
- email: {
539
- in: demoUsersData.map((u) => u.email),
540
- },
541
- },
542
- select: { email: true },
543
- });
544
-
545
- const existingEmails = new Set(existingUsers.map((u) => u.email));
546
-
547
- const usersToCreate = demoUsersData.filter(
548
- (u) => !existingEmails.has(u.email),
549
- );
550
-
551
- if (usersToCreate.length === 0) {
552
- console.log(
553
- '! Demo users already exist. Reset the database if you want to reseed.',
554
- );
555
- } else {
556
- await prisma.user.createMany({
557
- data: usersToCreate,
558
- });
559
-
560
- console.log(\`👥 \${usersToCreate.length} demo users created\`);
561
- }
562
-
563
- const allUsers = await prisma.user.findMany({ select: { id: true } });
564
- const userIds = allUsers.map(u => u.id);
565
-
566
- // --- 3. BLOG POSTS ---
567
- const postsData = [
568
- {
569
- title: 'The Basics of NestJS for Modern Developers',
570
- content: 'Discover how to build a robust and maintainable API with NestJS...',
571
- published: true,
572
- userId: userIds[1],
573
- },
574
- {
575
- title: 'How to Secure Your API with JWT',
576
- content: 'JWT authentication is a standard for securing APIs...',
577
- published: true,
578
- userId: userIds[2],
579
- },
580
- {
581
- title: 'Optimizing Node.js API Performance',
582
- content: 'Discover best practices for improving performance...',
583
- published: true,
584
- userId: userIds[3],
585
- },
586
- {
587
- title: 'Introduction to Prisma ORM',
588
- content: 'Prisma is a modern ORM that simplifies interactions with the database...',
589
- published: true,
590
- userId: userIds[4],
591
- },
592
- {
593
- title: 'Understanding Clean Architecture',
594
- content: 'Clean Architecture helps separate business logic from the rest of the code...',
595
- published: false,
596
- userId: userIds[0],
597
- },
598
- ];
599
- await prisma.post.createMany({ data: postsData, skipDuplicates: true });
600
- console.log('📝 5 Articles created.');
601
-
602
- const allPosts = await prisma.post.findMany({ select: { id: true } });
603
- const postIds = allPosts.map(p => p.id);
604
-
605
- // --- 4. DEMO COMMENTS ---
606
- const commentsData = [
607
- { content: 'Excellent article! I was able to apply these tips directly to my NestJS project.', postId: postIds[0], userId: userIds[2] },
608
- { content: 'Very clear and well explained, thank you for sharing about Prisma 👏', postId: postIds[3], userId: userIds[0] },
609
- { content: "I didn\'t know about JWT before this article, it\'s a real revelation.", postId: postIds[1], userId: userIds[4] },
610
- { content: 'Clean Architecture always seemed blurry to me, this article finally enlightened me.', postId: postIds[4], userId: userIds[1] },
611
- { content: 'Thanks for the content! I would like to see a complete tutorial with NestJS + Prisma.', postId: postIds[2], userId: userIds[3] },
612
- ];
613
- await prisma.comment.createMany({ data: commentsData, skipDuplicates: true });
614
- console.log('💬 5 Comments created.');
615
-
616
- console.log('✅ Seeding finished successfully! 🚀');
617
- }
618
-
619
- main()
620
- .catch((e) => {
621
- console.error('❌ Error during Prisma seeding:', e);
622
- process.exit(1);
623
- })
624
- .finally(async () => {
625
- await prisma.$disconnect();
626
- });
627
- `;
628
- }
629
-
630
- module.exports = { setupPrisma };
1
+ const fs = require("fs");
2
+ const { execSync } = require("child_process");
3
+ const { logInfo } = require("../loggers/logInfo");
4
+ const { runCommand } = require("../shell");
5
+ const { logSuccess } = require("../loggers/logSuccess");
6
+ const {
7
+ createDirectory,
8
+ createFile,
9
+ updateFile,
10
+ capitalize,
11
+ } = require("../userInput");
12
+ const { updatePackageJson } = require("../file-utils/packageJsonUtils");
13
+ const { success, warning, error, info } = require("../colors");
14
+
15
+ const { logWarning } = require("../loggers/logWarning");
16
+
17
+ async function setupPrisma(inputs) {
18
+ logInfo("Configuring Prisma...");
19
+
20
+ const dbConfig = inputs.dbConfig; // 📌 Path to schema.prisma
21
+ const schemaPath = "prisma/schema.prisma"; // 📦 Step 1: Install Prisma and its client at version 6.5.0
22
+
23
+ const prismaVersion = "6.5.0"; // Stable version for the CLI
24
+ logInfo(`Installing prisma and client...`);
25
+ await runCommand(
26
+ `${inputs.packageManager} add -D prisma@${prismaVersion} @prisma/client@${prismaVersion}`,
27
+ " Prisma installation failed",
28
+ );
29
+
30
+ // Step 2: Initialize Prisma
31
+ logInfo("Initializing Prisma");
32
+ await runCommand("npx prisma init", "❌ Prisma initialization failed");
33
+
34
+ await updateFile({
35
+ path: schemaPath,
36
+ pattern: /generator client \{[^}]*\}/g,
37
+ replacement: `generator client {
38
+ provider = "prisma-client-js"
39
+ output = "../node_modules/.prisma/client" //
40
+ }`,
41
+ });
42
+
43
+ // Step 3: Environment Configuration (.env and .env.example files)
44
+ const envPath = ".env";
45
+ const exampleEnvPath = ".env.example";
46
+ const databaseUrl = `DATABASE_URL="postgresql://${dbConfig.POSTGRES_USER}:${dbConfig.POSTGRES_PASSWORD}@${dbConfig.POSTGRES_HOST}:${dbConfig.POSTGRES_PORT}/${dbConfig.POSTGRES_DB}?schema=public"`;
47
+ const exampleDatabaseUrl = `DATABASE_URL="postgresql://user:password@localhost:5432/dbName?schema=public"`;
48
+
49
+ await createFile({
50
+ path: envPath,
51
+ contente: databaseUrl,
52
+ });
53
+
54
+ await createFile({
55
+ path: exampleEnvPath,
56
+ contente: exampleDatabaseUrl,
57
+ });
58
+
59
+ // Step 4: Generating Prisma models from provided entities
60
+ logInfo("Adding entities");
61
+ let schemaContent = "";
62
+ const hasUserEntity = inputs.entitiesData.entities.some(
63
+ (entity) => entity.name.toLowerCase() === "user",
64
+ );
65
+
66
+ // Adding the Role enum block if User is present
67
+ if (hasUserEntity) {
68
+ schemaContent += `
69
+ /**
70
+ * Role enumeration
71
+ */
72
+ enum Role {
73
+ USER
74
+ ADMIN
75
+ SUPER_ADMIN
76
+ }
77
+ `;
78
+ }
79
+
80
+ const fieldsToExcludeMap = new Map();
81
+ for (const entity of inputs.entitiesData.entities) {
82
+ fieldsToExcludeMap.set(entity.name.toLowerCase(), []);
83
+ }
84
+
85
+ if (inputs.entitiesData.relations?.length > 0) {
86
+ for (const relation of inputs.entitiesData.relations) {
87
+ const fromLower = relation.from.toLowerCase();
88
+ const toLower = relation.to.toLowerCase();
89
+ const fromCapitalized = capitalize(relation.from);
90
+ const toCapitalized = capitalize(relation.to); // 'from' side (source)
91
+
92
+ if (relation.type === "1-n") {
93
+ // 'One' side: exclude the name of the other entity's list (e.g., 'articles')
94
+ fieldsToExcludeMap.get(fromLower).push(`${toLower}s`);
95
+ } else if (relation.type === "n-1") {
96
+ // 'Many' side: exclude the foreign key (e.g., 'articleId') and the relation name (e.g., 'article')
97
+ fieldsToExcludeMap.get(fromLower).push(`${toLower}id`, toLower);
98
+ } // Add other relation types (1-1, n-n) if necessary here... // 'to' side (target)
99
+ if (relation.type === "1-n") {
100
+ // 'Many' side: exclude the foreign key (e.g., 'userId') and the relation name (e.g., 'user')
101
+ fieldsToExcludeMap.get(toLower).push(`${fromLower}id`, fromLower);
102
+ } else if (relation.type === "n-1") {
103
+ // 'One' side: exclude the name of the other entity's list (e.g., 'comments')
104
+ fieldsToExcludeMap.get(toLower).push(`${fromLower}s`);
105
+ }
106
+ }
107
+ }
108
+ // 2. Initial generation of models WITHOUT incorrect relationship fields
109
+ for (const entity of inputs.entitiesData.entities) {
110
+ const entityNameLower = entity.name.toLowerCase();
111
+ // On utilise un Set pour suivre les noms de champs déjà écrits dans ce modèle
112
+ const addedFields = new Set(["id", "createdat", "updatedat"]);
113
+
114
+ schemaContent += `
115
+ /**
116
+ * ${entity.name} Model
117
+ */
118
+ model ${entity.name} {
119
+ id String @id @default(uuid())
120
+ createdAt DateTime @default(now())
121
+ updatedAt DateTime @updatedAt`;
122
+
123
+ const fieldsToExclude = fieldsToExcludeMap.get(entityNameLower) || [];
124
+
125
+ for (const field of entity.fields) {
126
+ const fieldNameLower = field.name.toLowerCase();
127
+
128
+ // Ajout du rôle SEULEMENT s'il n'a pas été ajouté durant la boucle ci-dessus
129
+ if (entityNameLower == "user" && !addedFields.has("role")) {
130
+ schemaContent += `\n role Role @default(USER)`;
131
+ addedFields.add("role");
132
+ }
133
+
134
+ // Add email field ONLY if it was not added previously
135
+ if (entityNameLower === "user" && !addedFields.has("email")) {
136
+ schemaContent += `\n email String @unique`;
137
+ addedFields.add("email");
138
+ }
139
+
140
+ // On n'ajoute le champ que s'il n'est pas exclu ET pas déjà présent (comme id/createdAt)
141
+ if (
142
+ !fieldsToExclude.includes(fieldNameLower) &&
143
+ !addedFields.has(fieldNameLower)
144
+ ) {
145
+ schemaContent += `\n ${field.name} ${mapTypeToPrisma(field.type)}`;
146
+ addedFields.add(fieldNameLower);
147
+ }
148
+ }
149
+
150
+ schemaContent += `\n}\n`;
151
+ }
152
+
153
+ // 3. Applying relationship logic to add the CORRECT fields
154
+ logInfo("Applying Prisma relations...");
155
+
156
+ if (inputs.entitiesData.relations?.length > 0) {
157
+ for (const relation of inputs.entitiesData.relations) {
158
+ const from = relation.from;
159
+ const to = relation.to;
160
+ const type = relation.type;
161
+
162
+ // The replacement must be done on the entire generated schemaContent // Using a replacement function to update the content of `schemaContent`
163
+ if (type === "1-n") {
164
+ // "One" side (source): adds the list (to[])
165
+ schemaContent = schemaContent.replace(
166
+ new RegExp(`model ${from} \{([\\s\\S]*?)\n\\}`, "m"),
167
+ (match, content) => {
168
+ const fieldLine = ` ${to}s ${to}[]`;
169
+ return match.includes(fieldLine)
170
+ ? match
171
+ : `model ${from} {${content}\n${fieldLine}\n}`;
172
+ },
173
+ );
174
+
175
+ // "Many" side (target): adds the relation and the foreign key
176
+ schemaContent = schemaContent.replace(
177
+ new RegExp(`model ${to} \{([\\s\\S]*?)\n\\}`, "m"),
178
+ (match, content) => {
179
+ const relationLine = ` ${from} ${from} @relation(fields: [${from}Id], references: [id])`;
180
+ const fkLine = ` ${from}Id String`;
181
+ let result = match.includes(relationLine)
182
+ ? content
183
+ : `${content}\n${relationLine}`;
184
+ result = result.includes(fkLine) ? result : `${result}\n${fkLine}`;
185
+ return `model ${to} {${result}\n}`;
186
+ },
187
+ );
188
+ }
189
+
190
+ if (type === "n-1") {
191
+ // n-1 is the inverse of 1-n: from is the "many" and to is the "one"
192
+
193
+ // "Many" side (source = from): adds the relation and the foreign key
194
+ schemaContent = schemaContent.replace(
195
+ new RegExp(`model ${from} \{([\\s\\S]*?)\n\\}`, "m"),
196
+ (match, content) => {
197
+ const relationLine = ` ${to} ${to} @relation(fields: [${to}Id], references: [id])`;
198
+ const fkLine = ` ${to}Id String`;
199
+ let result = match.includes(relationLine)
200
+ ? content
201
+ : `${content}\n${relationLine}`;
202
+ result = result.includes(fkLine) ? result : `${result}\n${fkLine}`;
203
+ return `model ${from} {${result}\n}`;
204
+ },
205
+ );
206
+
207
+ // "One" side (target = to): adds the list (from[])
208
+ schemaContent = schemaContent.replace(
209
+ new RegExp(`model ${to} \{([\\s\\S]*?)\n\\}`, "m"),
210
+ (match, content) => {
211
+ const fromCapitalized = capitalize(from);
212
+ const fieldLine = ` ${from}s ${from}[]`;
213
+ return match.includes(fieldLine)
214
+ ? match
215
+ : `model ${to} {${content}\n${fieldLine}\n}`;
216
+ },
217
+ );
218
+ }
219
+
220
+ if (type === "1-1") {
221
+ // 'from' side (source): adds the relation, foreign key, and @unique attribute
222
+ schemaContent = schemaContent.replace(
223
+ new RegExp(`model ${from} \{([\\s\\S]*?)\n\\}`, "m"),
224
+ (match, content) => {
225
+ const relationLine = ` ${to} ${to}? @relation(fields: [${to}Id], references: [id])`; // The foreign key must be unique in a 1-1 relationship, and optional for flexibility
226
+ const fkLine = ` ${to}Id String? @unique`;
227
+
228
+ let result = match.includes(relationLine)
229
+ ? content
230
+ : `${content}\n${relationLine}`;
231
+ result = result.includes(fkLine) ? result : `${result}\n${fkLine}`;
232
+ return `model ${from} {${result}\n}`;
233
+ },
234
+ );
235
+
236
+ // 'to' side (target): adds the inverse relation (optional)
237
+ schemaContent = schemaContent.replace(
238
+ new RegExp(`model ${to} \{([\\s\\S]*?)\n\\}`, "m"),
239
+ (match, content) => {
240
+ // Inverse relation (optional because 'from' holds the FK)
241
+ const fieldLine = ` ${from} ${from}?`;
242
+ return match.includes(fieldLine)
243
+ ? match
244
+ : `model ${to} {${content}\n${fieldLine}\n}`;
245
+ },
246
+ );
247
+ }
248
+
249
+ if (type === "n-n") {
250
+ // 'from' side (source): adds the list (to[])
251
+ schemaContent = schemaContent.replace(
252
+ new RegExp(`model ${from} \{([\\s\\S]*?)\n\\}`, "m"),
253
+ (match, content) => {
254
+ const fieldLine = ` ${to}s ${to}[]`;
255
+ return match.includes(fieldLine)
256
+ ? match
257
+ : `model ${from} {${content}\n${fieldLine}\n}`;
258
+ },
259
+ );
260
+
261
+ // 'to' side (target): adds the list (from[])
262
+ schemaContent = schemaContent.replace(
263
+ new RegExp(`model ${to} \{([\\s\\S]*?)\n\\}`, "m"),
264
+ (match, content) => {
265
+ const fieldLine = ` ${from}s ${from}[]`;
266
+ return match.includes(fieldLine)
267
+ ? match
268
+ : `model ${to} {${content}\n${fieldLine}\n}`;
269
+ },
270
+ );
271
+ }
272
+ }
273
+ }
274
+
275
+ logInfo("Updating schema.prisma");
276
+ const baseSchema = `
277
+ generator client {
278
+ provider = "prisma-client-js"
279
+ }
280
+
281
+ datasource db {
282
+ provider = "${inputs.dbConfig.orm === "mongodb" ? "mongodb" : "postgresql"}"
283
+ url = env("DATABASE_URL")
284
+ }
285
+
286
+ ${schemaContent}
287
+ `;
288
+ await createFile({
289
+ path: schemaPath,
290
+ contente: baseSchema,
291
+ });
292
+ await runCommand(`npx prisma format`, "❌ Failed to format prisma schema");
293
+
294
+ // 📁 Step 6: Creating the `src/prisma` structure
295
+ const defaultPatch = "src/prisma";
296
+ await createDirectory(defaultPatch);
297
+
298
+ // 🧩 Prisma Service
299
+ await createFile({
300
+ path: `${defaultPatch}/prisma.service.ts`,
301
+ contente: `import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
302
+ import { PrismaClient } from '@prisma/client';
303
+
304
+ /**
305
+ * Prisma Service to expose a global instance of the Prisma client
306
+ */
307
+ @Injectable()
308
+ export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
309
+ constructor() {
310
+ super();
311
+ }
312
+
313
+ async onModuleInit() {
314
+ await this.$connect();
315
+ }
316
+
317
+ async onModuleDestroy() {
318
+ await this.$disconnect();
319
+ }
320
+ }
321
+ `,
322
+ });
323
+
324
+ // 🧩 Prisma Module
325
+ await createFile({
326
+ path: `${defaultPatch}/prisma.module.ts`,
327
+ contente: `import { Global, Module } from '@nestjs/common';
328
+ import { PrismaService } from './prisma.service';
329
+
330
+ /**
331
+ * Global Prisma Module to provide the service to the entire application
332
+ */
333
+ @Global()
334
+ @Module({
335
+ providers: [PrismaService],
336
+ exports: [PrismaService],
337
+ })
338
+ export class PrismaModule {}
339
+ `,
340
+ });
341
+
342
+ // Installing dotenv if necessary
343
+ logInfo("📦 Installing dotenv...");
344
+ await runCommand(
345
+ `${inputs.packageManager} add dotenv`,
346
+ "❌ Failed to install dotenv",
347
+ );
348
+
349
+ // Creating prisma.config.ts file to load environment variables
350
+ let prismaConfigPath = "prisma.config.ts";
351
+ if (!fs.existsSync(prismaConfigPath)) {
352
+ prismaConfigPath = "prisma/prisma.config.ts";
353
+ }
354
+
355
+ if (fs.existsSync(prismaConfigPath)) {
356
+ logInfo(" Updating prisma.config.ts with dotenv import...");
357
+ await updateFile({
358
+ path: prismaConfigPath,
359
+ pattern: /^/,
360
+ replacement: `import 'dotenv/config';\n\n`,
361
+ });
362
+ }
363
+
364
+ // Step 7: Generating the Prisma client
365
+ await runCommand("npx prisma generate", "❌ Prisma generation failed");
366
+
367
+ // Step 8: Migration (ONLY in 'new' mode)
368
+ if (inputs.isDemo) {
369
+ setupPrismaSeeding(inputs);
370
+ }
371
+
372
+ logSuccess(" Prisma configured successfully!");
373
+ }
374
+
375
+ /**
376
+ * Maps generic entity types to Prisma data types.
377
+ * @param {string} type - Generic type (e.g., 'string', 'number', 'Date', 'string[]', 'MonEnum')
378
+ * @returns {string} The corresponding type in the Prisma schema.
379
+ */
380
+ function mapTypeToPrisma(type) {
381
+ // Handles the case of arrays (e.g., 'string[]')
382
+ if (type.endsWith("[]")) {
383
+ const innerType = type.slice(0, -2); // Removes '[]' // Recursively calls for the inner type
384
+ return `${mapTypeToPrisma(innerType)}[]`;
385
+ }
386
+
387
+ const typeLower = type.toLowerCase();
388
+
389
+ switch (typeLower) {
390
+ case "string":
391
+ case "text": // Mapped to String because Prisma does not have a distinct TEXT type for PostgreSQL
392
+ return "String";
393
+
394
+ case "number": // A simple "number" field can be Int or Float. We default to Float.
395
+ return "Float";
396
+ case "int":
397
+ return "Int";
398
+
399
+ case "decimal": // Use Decimal for high precision, or Float for simplicity
400
+ return "Decimal";
401
+
402
+ case "boolean":
403
+ return "Boolean";
404
+
405
+ case "date":
406
+ return "DateTime";
407
+
408
+ case "uuid": // We use String by default for storage, the @id @default(uuid()) attribute will be managed by the ID logic. // For non-ID fields, String is the appropriate choice.
409
+ return "String";
410
+
411
+ case "json":
412
+ return "Json";
413
+ case "role":
414
+ return "Role";
415
+
416
+ default: // Handles cases of custom enumerations (e.g., 'StatusEnum') or named object types (e.g., 'Address')
417
+ // Prisma will use the exact type name if it matches a defined 'enum' or other 'model'.
418
+ // In the context of a simple non-persistent DTO/object field, it is better to revert to Json if unrecognized.
419
+ // If the type is capitalized (e.g., 'Address'), we return it as is (assuming it's another Model/Enum)
420
+ return type.charAt(0) === type.charAt(0).toUpperCase() ? type : "Json";
421
+ }
422
+ }
423
+
424
+ async function setupPrismaSeeding(inputs) {
425
+ logInfo("⚙️ Configuring seeding for Prisma...");
426
+
427
+ // --- Dependencies ---
428
+ const prismaDevDeps = [
429
+ "ts-node",
430
+ "@types/node",
431
+ "@types/bcrypt",
432
+ "dotenv-cli",
433
+ ];
434
+ await runCommand(
435
+ `${inputs.packageManager} add -D ${prismaDevDeps.join(" ")}`,
436
+ "❌ Failed to install Prisma seeding dependencies",
437
+ ); // Bcrypt is often a production dependency for hashing
438
+ await runCommand(
439
+ `${inputs.packageManager} install bcrypt`,
440
+ "❌ Failed to install bcrypt",
441
+ );
442
+
443
+ // --- Scripts in package.json ---
444
+ const prismaScripts = {
445
+ "prisma:migrate": "prisma migrate dev",
446
+ "prisma:seed": "prisma db seed",
447
+ "prisma:reset": "prisma migrate reset",
448
+ "prisma:migrate:prod": "prisma migrate deploy",
449
+ seed: "ts-node prisma/seed.ts",
450
+ };
451
+
452
+ await updatePackageJson(inputs, prismaScripts);
453
+
454
+ // --- Configuration in schema.prisma ---
455
+ await updateFile({
456
+ path: "prisma/schema.prisma",
457
+ pattern: /generator client \{[^}]*\}/g,
458
+ replacement: `generator client {
459
+ provider = "prisma-client-js"
460
+ output = "../node_modules/.prisma/client"
461
+ }
462
+
463
+ `,
464
+ });
465
+
466
+ // --- Creating seed.ts file ---
467
+ const seedTsContent = generatePrismaSeedContent(inputs.entitiesData.entities);
468
+ await createFile({
469
+ path: `prisma/seed.ts`,
470
+ contente: seedTsContent,
471
+ });
472
+
473
+ // logSuccess("✅ Prisma seeding configured.");
474
+ }
475
+
476
+ function generatePrismaSeedContent(entities) {
477
+ const requiresBcrypt = entities.some((e) => e.name.toLowerCase() === "user");
478
+
479
+ return `
480
+ import { PrismaClient } from '@prisma/client';
481
+ ${requiresBcrypt ? "import * as bcrypt from 'bcrypt';" : ""}
482
+
483
+ const prisma = new PrismaClient();
484
+
485
+ async function main() {
486
+ console.log('🌱 Starting Prisma seeding...');
487
+
488
+ // --- 1. ADMIN USER ---
489
+ ${
490
+ requiresBcrypt
491
+ ? `const salt = await bcrypt.genSalt(10);
492
+ const hashedPassword = await bcrypt.hash('password123', salt);`
493
+ : ""
494
+ }
495
+
496
+ const exists = await prisma.user.findFirst({
497
+ where: { email: 'admin@nestcraft.com' },
498
+ });
499
+
500
+ if (!exists) {
501
+ const adminUser = await prisma.user.create({
502
+ data: {
503
+ email: 'admin@nestcraft.com',
504
+ ${
505
+ requiresBcrypt
506
+ ? "password: hashedPassword,"
507
+ : "// Default password: password123"
508
+ }
509
+ username: 'NestCraftAdmin',
510
+ role: 'SUPER_ADMIN',
511
+ isActive: true,
512
+ },
513
+ });
514
+
515
+ console.log(\`👑 Admin created: \${adminUser.email}\`);
516
+ } else {
517
+ console.log(\`👑 Admin realy exists: \${exists.email}\`);
518
+ }
519
+
520
+
521
+ // --- 2. DEMO USERS ---
522
+ const demoUsersData = [
523
+ { email: 'emma.jones@demo.com', ${
524
+ requiresBcrypt ? "password: hashedPassword," : ""
525
+ } username: 'EmmaJones', isActive: true },
526
+ { email: 'lucas.martin@demo.com', ${
527
+ requiresBcrypt ? "password: hashedPassword," : ""
528
+ } username: 'LucasMartin', isActive: true },
529
+ { email: 'sophia.bernard@demo.com', ${
530
+ requiresBcrypt ? "password: hashedPassword," : ""
531
+ } username: 'SophiaBernard', isActive: true },
532
+ { email: 'alexandre.dubois@demo.com', ${
533
+ requiresBcrypt ? "password: hashedPassword," : ""
534
+ } username: 'AlexandreDubois', isActive: true },
535
+ { email: 'chloe.moreau@demo.com', ${
536
+ requiresBcrypt ? "password: hashedPassword," : ""
537
+ } username: 'ChloeMoreau', isActive: true },
538
+ ];
539
+
540
+ const existingUsers = await prisma.user.findMany({
541
+ where: {
542
+ email: {
543
+ in: demoUsersData.map((u) => u.email),
544
+ },
545
+ },
546
+ select: { email: true },
547
+ });
548
+
549
+ const existingEmails = new Set(existingUsers.map((u) => u.email));
550
+
551
+ const usersToCreate = demoUsersData.filter(
552
+ (u) => !existingEmails.has(u.email),
553
+ );
554
+
555
+ if (usersToCreate.length === 0) {
556
+ console.log(
557
+ '! Demo users already exist. Reset the database if you want to reseed.',
558
+ );
559
+ } else {
560
+ await prisma.user.createMany({
561
+ data: usersToCreate,
562
+ });
563
+
564
+ console.log(\`👥 \${usersToCreate.length} demo users created\`);
565
+ }
566
+
567
+ const allUsers = await prisma.user.findMany({ select: { id: true } });
568
+ const userIds = allUsers.map(u => u.id);
569
+
570
+ // --- 3. BLOG POSTS ---
571
+ const postsData = [
572
+ {
573
+ title: 'The Basics of NestJS for Modern Developers',
574
+ content: 'Discover how to build a robust and maintainable API with NestJS...',
575
+ published: true,
576
+ userId: userIds[1],
577
+ },
578
+ {
579
+ title: 'How to Secure Your API with JWT',
580
+ content: 'JWT authentication is a standard for securing APIs...',
581
+ published: true,
582
+ userId: userIds[2],
583
+ },
584
+ {
585
+ title: 'Optimizing Node.js API Performance',
586
+ content: 'Discover best practices for improving performance...',
587
+ published: true,
588
+ userId: userIds[3],
589
+ },
590
+ {
591
+ title: 'Introduction to Prisma ORM',
592
+ content: 'Prisma is a modern ORM that simplifies interactions with the database...',
593
+ published: true,
594
+ userId: userIds[4],
595
+ },
596
+ {
597
+ title: 'Understanding Clean Architecture',
598
+ content: 'Clean Architecture helps separate business logic from the rest of the code...',
599
+ published: false,
600
+ userId: userIds[0],
601
+ },
602
+ ];
603
+ await prisma.post.createMany({ data: postsData, skipDuplicates: true });
604
+ console.log('📝 5 Articles created.');
605
+
606
+ const allPosts = await prisma.post.findMany({ select: { id: true } });
607
+ const postIds = allPosts.map(p => p.id);
608
+
609
+ // --- 4. DEMO COMMENTS ---
610
+ const commentsData = [
611
+ { content: 'Excellent article! I was able to apply these tips directly to my NestJS project.', postId: postIds[0], userId: userIds[2] },
612
+ { content: 'Very clear and well explained, thank you for sharing about Prisma 👏', postId: postIds[3], userId: userIds[0] },
613
+ { content: "I didn\'t know about JWT before this article, it\'s a real revelation.", postId: postIds[1], userId: userIds[4] },
614
+ { content: 'Clean Architecture always seemed blurry to me, this article finally enlightened me.', postId: postIds[4], userId: userIds[1] },
615
+ { content: 'Thanks for the content! I would like to see a complete tutorial with NestJS + Prisma.', postId: postIds[2], userId: userIds[3] },
616
+ ];
617
+ await prisma.comment.createMany({ data: commentsData, skipDuplicates: true });
618
+ console.log('💬 5 Comments created.');
619
+
620
+ console.log('✅ Seeding finished successfully! 🚀');
621
+ }
622
+
623
+ main()
624
+ .catch((e) => {
625
+ console.error('❌ Error during Prisma seeding:', e);
626
+ process.exit(1);
627
+ })
628
+ .finally(async () => {
629
+ await prisma.$disconnect();
630
+ });
631
+ `;
632
+ }
633
+
634
+ async function updatePrismaSchema(entityData) {
635
+ const schemaPath = "prisma/schema.prisma";
636
+ const { name, fields, relation } = entityData;
637
+ const capitalizedName = capitalize(name);
638
+ const lowercasedName = name.toLowerCase();
639
+
640
+ // 1️⃣ Préparation des champs de relation pour le NOUVEAU modèle
641
+ let relationFields = "";
642
+ if (relation) {
643
+ const targetCap = capitalize(relation.target);
644
+ const targetLow = relation.target.toLowerCase();
645
+
646
+ switch (relation.type) {
647
+ case "n-1": // "Many to One" : Le nouveau module appartient à une cible
648
+ relationFields += `\n ${targetLow} ${targetLow} @relation(fields: [${targetLow}Id], references: [id])`;
649
+ relationFields += `\n ${targetLow}Id String`;
650
+ break;
651
+ case "1-n": // "One to Many" : Le nouveau module possède plusieurs cibles
652
+ relationFields += `\n ${targetLow}s ${targetLow}[]`;
653
+ break;
654
+ case "1-1":
655
+ relationFields += `\n ${targetLow} ${targetLow}? @relation(fields: [${targetLow}Id], references: [id])`;
656
+ relationFields += `\n ${targetLow}Id String? @unique`;
657
+ break;
658
+ case "n-n":
659
+ relationFields += `\n ${targetLow}s ${targetLow}[]`;
660
+ break;
661
+ }
662
+ }
663
+
664
+ // 2️⃣ Construction du bloc pour le nouveau modèle
665
+ let modelBlock = `\n/**
666
+ * ${capitalizedName} Model
667
+ */
668
+ model ${lowercasedName} {
669
+ id String @id @default(uuid())
670
+ createdAt DateTime @default(now())
671
+ updatedAt DateTime @updatedAt${relationFields}`;
672
+
673
+ fields.forEach((field) => {
674
+ modelBlock += `\n ${field.name} ${mapTypeToPrisma(field.type)}`;
675
+ });
676
+
677
+ modelBlock += `\n}\n`;
678
+
679
+ // 3️⃣ Injection du nouveau modèle
680
+ await updateFile({
681
+ path: schemaPath,
682
+ pattern: /$/,
683
+ replacement: modelBlock,
684
+ });
685
+
686
+ // 4️⃣ Injection de l'INVERSE dans le modèle cible (Target)
687
+ if (relation) {
688
+ const targetCap = capitalize(relation.target);
689
+ const targetLow = capitalize(relation.target);
690
+ const newCap = capitalizedName;
691
+ const newLow = name.toLowerCase();
692
+
693
+ let inverseField = "";
694
+ switch (relation.type) {
695
+ case "n-1": // Inverse d'un Many-to-One est un One-to-Many
696
+ inverseField = ` ${newLow}s ${newLow}[]`;
697
+ break;
698
+ case "1-n": // Inverse d'un One-to-Many est un Many-to-One
699
+ inverseField = ` ${newLow} ${newLow} @relation(fields: [${newLow}Id], references: [id])\n ${newLow}Id String`;
700
+ break;
701
+ case "1-1":
702
+ inverseField = ` ${newLow} ${newLow}?`;
703
+ break;
704
+ case "n-n":
705
+ inverseField = ` ${newLow}s ${newLow}[]`;
706
+ break;
707
+ }
708
+
709
+ // On utilise une RegEx pour trouver le bloc du modèle cible et insérer avant la fermeture "}"
710
+ await updateFile({
711
+ path: schemaPath,
712
+ pattern: new RegExp(`model ${targetLow} \\{([\\s\\S]*?)\\}`, "g"),
713
+ replacement: `model ${targetLow} {$1\n${inverseField}\n}`,
714
+ });
715
+ }
716
+
717
+ // 3️⃣ Prisma Commands Sync (meilleur sur Windows)
718
+ try {
719
+ console.log(info("Running: prisma format..."));
720
+ execSync("npx prisma format", { stdio: "pipe" });
721
+
722
+ console.log(info("Running: prisma generate..."));
723
+ execSync("npx prisma generate", { stdio: "pipe" });
724
+
725
+ console.log(info("Running: prisma migrate dev..."));
726
+ execSync(
727
+ `npx prisma migrate dev --name add_module_${entityData.name} --force`,
728
+ {
729
+ stdio: "pipe",
730
+ },
731
+ );
732
+
733
+ // console.log(success("🎉 Prisma migration completed successfully!"));
734
+ } catch (err) {
735
+ // 4️⃣ Gestion spécifique Windows EPERM
736
+ if (
737
+ err.message.includes("operation not permitted") ||
738
+ err.message.includes("EPERM") ||
739
+ err.message.includes("EBUSY")
740
+ ) {
741
+ logWarning("System Lock Detected!");
742
+ console.log(
743
+ `${info("Prisma Client binary is locked because your server is running.")}`,
744
+ );
745
+ console.log(
746
+ `${info("ACTION:")} Stop NestJS (Ctrl+C) and run manually:\n`,
747
+ );
748
+ console.log(`${success("npx prisma generate")}`);
749
+ console.log(`${success("npx prisma migrate dev")}\n`);
750
+ } else {
751
+ console.log(error("❌ Prisma error: " + err.message));
752
+ }
753
+ }
754
+ }
755
+
756
+ /* async function updatePrismaSchema(entityData) {
757
+ const schemaPath = "prisma/schema.prisma";
758
+
759
+ // Construction du bloc modèle
760
+ let modelBlock = `\nmodel ${capitalize(entityData.name)} {
761
+ id String @id @default(uuid())
762
+ createdAt DateTime @default(now())
763
+ updatedAt DateTime @updatedAt`;
764
+
765
+ entityData.fields.forEach((field) => {
766
+ // Note: On réutilise ta fonction mapTypeToPrisma ici
767
+ modelBlock += `\n ${field.name} ${mapTypeToPrisma(field.type)}`;
768
+ });
769
+
770
+ modelBlock += `\n}\n`;
771
+
772
+ // Injection à la fin du fichier
773
+ await updateFile({
774
+ path: schemaPath,
775
+ pattern: /$/, // On ajoute à la fin
776
+ replacement: modelBlock,
777
+ });
778
+
779
+ // ⚠️ IMPORTANT : On formate et on régénère le client pour supprimer les erreurs TS
780
+ try {
781
+ await runCommand("npx prisma format");
782
+ await runCommand("npx prisma generate");
783
+ await runCommand(
784
+ `npx prisma migrate dev --name add_module_${entityData.name}`,
785
+ );
786
+ } catch (err) {
787
+ if (err.message.includes("EPERM")) {
788
+ console.log(
789
+ `\n${warning("⚠️ System Lock:")} Could not update Prisma Client binary because your app is currently running.`,
790
+ );
791
+ console.log(
792
+ `${info("ACTION REQUIRED:")} Stop your dev server (Ctrl+C) and run: ${success("| npx prisma generate | and | npx prisma migrate dev |")}\n`,
793
+ );
794
+ } else {
795
+ logError("Prisma error: " + err.message);
796
+ }
797
+ }
798
+
799
+ throw new Error(err);
800
+ } */
801
+
802
+ module.exports = { setupPrisma, updatePrismaSchema };