nestcraftx 0.2.5 → 0.3.0

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