nestcraftx 0.3.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/.github/workflows/ci.yml +35 -0
  2. package/CHANGELOG.fr.md +74 -0
  3. package/CHANGELOG.md +74 -0
  4. package/PROGRESS.md +70 -67
  5. package/README.fr.md +60 -69
  6. package/bin/nestcraft.js +8 -0
  7. package/commands/demo.js +12 -46
  8. package/commands/generate.js +551 -2
  9. package/commands/generateConf.js +4 -4
  10. package/commands/help.js +11 -0
  11. package/commands/info.js +2 -3
  12. package/commands/list.js +93 -0
  13. package/commands/new.js +28 -56
  14. package/jest.config.js +9 -0
  15. package/package.json +7 -2
  16. package/readme.md +59 -68
  17. package/tests/e2e/generator.spec.js +71 -0
  18. package/tests/unit/appModuleUpdater.spec.js +111 -0
  19. package/tests/unit/cliParser.spec.js +74 -0
  20. package/tests/unit/controllerGenerator.spec.js +145 -0
  21. package/tests/unit/dtoGenerator.spec.js +87 -0
  22. package/tests/unit/entityGenerator.spec.js +65 -0
  23. package/tests/unit/generateCommand.spec.js +100 -0
  24. package/tests/unit/health.spec.js +69 -0
  25. package/tests/unit/listCommand.spec.js +91 -0
  26. package/tests/unit/middlewareGenerator.spec.js +64 -0
  27. package/tests/unit/newCommand.spec.js +88 -0
  28. package/tests/unit/relationCommand.spec.js +202 -0
  29. package/tests/unit/throttler.spec.js +117 -0
  30. package/utils/app-module.updater.js +6 -0
  31. package/utils/configs/setupCleanArchitecture.js +1 -1
  32. package/utils/configs/setupLightArchitecture.js +98 -26
  33. package/utils/envGenerator.js +9 -1
  34. package/utils/file-system.js +15 -0
  35. package/utils/file-utils/packageJsonUtils.js +6 -0
  36. package/utils/file-utils/saveProjectConfig.js +9 -0
  37. package/utils/fullModeInput.js +12 -229
  38. package/utils/generators/application/dtoGenerator.js +26 -8
  39. package/utils/generators/application/dtoUpdater.js +5 -0
  40. package/utils/generators/cleanModuleGenerator.js +40 -11
  41. package/utils/generators/domain/entityUpdater.js +5 -0
  42. package/utils/generators/infrastructure/mapperUpdater.js +5 -0
  43. package/utils/generators/infrastructure/middlewareGenerator.js +112 -315
  44. package/utils/generators/infrastructure/mongooseSchemaGenerator.js +114 -4
  45. package/utils/generators/infrastructure/repositoryGenerator.js +114 -14
  46. package/utils/generators/lightModuleGenerator.js +14 -0
  47. package/utils/generators/presentation/controllerGenerator.js +70 -19
  48. package/utils/helpers.js +8 -2
  49. package/utils/interactive/askEntityInputs.js +5 -68
  50. package/utils/interactive/askRelationCommand.js +98 -0
  51. package/utils/interactive/entityBuilder.js +411 -0
  52. package/utils/lightModeInput.js +22 -241
  53. package/utils/setups/orms/typeOrmSetup.js +42 -8
  54. package/utils/setups/projectSetup.js +88 -10
  55. package/utils/setups/setupAuth.js +335 -20
  56. package/utils/setups/setupDatabase.js +4 -1
  57. package/utils/setups/setupHealth.js +74 -0
  58. package/utils/setups/setupMongoose.js +84 -32
  59. package/utils/setups/setupPrisma.js +151 -4
  60. package/utils/setups/setupSwagger.js +15 -8
  61. package/utils/setups/setupThrottler.js +92 -0
  62. package/utils/shell.js +61 -9
@@ -33,42 +33,10 @@ const { createDirectory, createFile, updateFile } = require("../../file-system")
33
33
  * @returns {string}
34
34
  */
35
35
  function getExceptionFilterContent(orm) {
36
+ let dbSpecificChecks = "";
36
37
  if (orm === "prisma") {
37
- return `
38
- import {
39
- ExceptionFilter,
40
- Catch,
41
- ArgumentsHost,
42
- HttpException,
43
- HttpStatus,
44
- Logger,
45
- } from '@nestjs/common';
46
- import { Request, Response } from 'express';
47
-
48
- @Catch()
49
- export class AllExceptionsFilter implements ExceptionFilter {
50
- private readonly logger = new Logger(AllExceptionsFilter.name);
51
-
52
- catch(exception: unknown, host: ArgumentsHost) {
53
- const ctx = host.switchToHttp();
54
- const response = ctx.getResponse<Response>();
55
- const request = ctx.getRequest<Request>();
56
-
57
- let status = HttpStatus.INTERNAL_SERVER_ERROR;
58
- let message: string | string[] = 'Internal server error';
59
- let errorDetails: any = null;
60
-
61
- if (exception instanceof HttpException) {
62
- status = exception.getStatus();
63
- const res = exception.getResponse();
64
- if (typeof res === 'string') {
65
- message = res;
66
- } else if (typeof res === 'object' && res !== null) {
67
- const resObj = res as any;
68
- message = resObj.message || resObj.error || 'HttpException';
69
- errorDetails = resObj;
70
- }
71
- } else if (
38
+ dbSpecificChecks = `
39
+ else if (
72
40
  typeof exception === 'object' &&
73
41
  exception &&
74
42
  exception.constructor &&
@@ -78,75 +46,13 @@ export class AllExceptionsFilter implements ExceptionFilter {
78
46
  )
79
47
  ) {
80
48
  status = HttpStatus.BAD_REQUEST;
81
- message = (exception as any).message || 'Prisma error';
49
+ message = (exception as any).message || 'Prisma database error';
50
+ errorResponseName = exception.constructor.name;
82
51
  errorDetails = exception;
83
- } else if (exception instanceof Error) {
84
- message = exception.message;
85
- errorDetails = {
86
- name: exception.name,
87
- stack: exception.stack,
88
- };
89
- } else {
90
- message = 'Une erreur inattendue est survenue';
91
- errorDetails = exception;
92
- }
93
-
94
- if (process.env.NODE_ENV !== 'production') {
95
- this.logger.error(
96
- \`Exception on \${request.method} \${request.url}\`,
97
- JSON.stringify({ message, status, errorDetails, exception }),
98
- );
99
- }
100
-
101
- response.status(status).json({
102
- statusCode: status,
103
- timestamp: new Date().toISOString(),
104
- path: request.url,
105
- method: request.method,
106
- message,
107
- error: errorDetails,
108
- });
109
- }
110
- }
111
- `.trim();
112
- }
113
-
114
- if (orm === "mongoose") {
115
- return `
116
- import {
117
- ExceptionFilter,
118
- Catch,
119
- ArgumentsHost,
120
- HttpException,
121
- HttpStatus,
122
- Logger,
123
- } from '@nestjs/common';
124
- import { Request, Response } from 'express';
125
-
126
- @Catch()
127
- export class AllExceptionsFilter implements ExceptionFilter {
128
- private readonly logger = new Logger(AllExceptionsFilter.name);
129
-
130
- catch(exception: unknown, host: ArgumentsHost) {
131
- const ctx = host.switchToHttp();
132
- const response = ctx.getResponse<Response>();
133
- const request = ctx.getRequest<Request>();
134
-
135
- let status = HttpStatus.INTERNAL_SERVER_ERROR;
136
- let message: string | string[] = 'Internal server error';
137
- let errorDetails: any = null;
138
-
139
- if (exception instanceof HttpException) {
140
- status = exception.getStatus();
141
- const res = exception.getResponse();
142
- if (typeof res === 'string') {
143
- message = res;
144
- } else if (typeof res === 'object' && res !== null) {
145
- const resObj = res as any;
146
- message = resObj.message || resObj.error || 'HttpException';
147
- errorDetails = resObj;
148
- }
149
- } else if (
52
+ }`;
53
+ } else if (orm === "mongoose") {
54
+ dbSpecificChecks = `
55
+ else if (
150
56
  typeof exception === 'object' &&
151
57
  exception &&
152
58
  'name' in exception &&
@@ -156,75 +62,78 @@ export class AllExceptionsFilter implements ExceptionFilter {
156
62
  )
157
63
  ) {
158
64
  status = HttpStatus.BAD_REQUEST;
159
- message = (exception as any).message || 'MongoDB error';
65
+ message = (exception as any).message || 'Mongoose database error';
66
+ errorResponseName = (exception as any).name;
160
67
  errorDetails = exception;
161
- } else if (exception instanceof Error) {
162
- message = exception.message;
163
- errorDetails = {
164
- name: exception.name,
165
- stack: exception.stack,
166
- };
167
- } else {
168
- message = 'Une erreur inattendue est survenue';
68
+ }`;
69
+ } else if (orm === "typeorm") {
70
+ dbSpecificChecks = `
71
+ else if (
72
+ typeof exception === 'object' &&
73
+ exception &&
74
+ 'name' in exception &&
75
+ (
76
+ (exception as any).name === 'QueryFailedError' ||
77
+ (exception as any).name === 'EntityNotFoundError' ||
78
+ (exception as any).name === 'CannotCreateEntityIdMapError'
79
+ )
80
+ ) {
81
+ status = HttpStatus.BAD_REQUEST;
82
+ message = (exception as any).message || 'TypeORM database error';
83
+ errorResponseName = (exception as any).name;
84
+ errorDetails = exception;
85
+ }`;
86
+ } else if (orm === "sequelize") {
87
+ dbSpecificChecks = `
88
+ else if (
89
+ typeof exception === 'object' &&
90
+ exception &&
91
+ exception.constructor &&
92
+ (
93
+ exception.constructor.name === 'SequelizeDatabaseError' ||
94
+ exception.constructor.name === 'SequelizeValidationError'
95
+ )
96
+ ) {
97
+ status = HttpStatus.BAD_REQUEST;
98
+ message = (exception as any).message || 'Sequelize database error';
99
+ errorResponseName = exception.constructor.name;
100
+ errorDetails = exception;
101
+ }`;
102
+ } else {
103
+ // fallback global / universal
104
+ dbSpecificChecks = `
105
+ // Prisma
106
+ else if (
107
+ typeof exception === 'object' &&
108
+ exception &&
109
+ exception.constructor &&
110
+ (
111
+ exception.constructor.name === 'PrismaClientKnownRequestError' ||
112
+ exception.constructor.name === 'PrismaClientValidationError'
113
+ )
114
+ ) {
115
+ status = HttpStatus.BAD_REQUEST;
116
+ message = (exception as any).message || 'Prisma database error';
117
+ errorResponseName = exception.constructor.name;
169
118
  errorDetails = exception;
170
119
  }
171
-
172
- if (process.env.NODE_ENV !== 'production') {
173
- this.logger.error(
174
- \`Exception on \${request.method} \${request.url}\`,
175
- JSON.stringify({ message, status, errorDetails, exception }),
176
- );
120
+ // Mongoose/Mongo
121
+ else if (
122
+ typeof exception === 'object' &&
123
+ exception &&
124
+ 'name' in exception &&
125
+ (
126
+ (exception as any).name === 'MongoError' ||
127
+ (exception as any).name === 'MongooseError'
128
+ )
129
+ ) {
130
+ status = HttpStatus.BAD_REQUEST;
131
+ message = (exception as any).message || 'Mongoose database error';
132
+ errorResponseName = (exception as any).name;
133
+ errorDetails = exception;
177
134
  }
178
-
179
- response.status(status).json({
180
- statusCode: status,
181
- timestamp: new Date().toISOString(),
182
- path: request.url,
183
- method: request.method,
184
- message,
185
- error: errorDetails,
186
- });
187
- }
188
- }
189
- `.trim();
190
- }
191
-
192
- if (orm === "typeorm") {
193
- return `
194
- import {
195
- ExceptionFilter,
196
- Catch,
197
- ArgumentsHost,
198
- HttpException,
199
- HttpStatus,
200
- Logger,
201
- } from '@nestjs/common';
202
- import { Request, Response } from 'express';
203
-
204
- @Catch()
205
- export class AllExceptionsFilter implements ExceptionFilter {
206
- private readonly logger = new Logger(AllExceptionsFilter.name);
207
-
208
- catch(exception: unknown, host: ArgumentsHost) {
209
- const ctx = host.switchToHttp();
210
- const response = ctx.getResponse<Response>();
211
- const request = ctx.getRequest<Request>();
212
-
213
- let status = HttpStatus.INTERNAL_SERVER_ERROR;
214
- let message: string | string[] = 'Internal server error';
215
- let errorDetails: any = null;
216
-
217
- if (exception instanceof HttpException) {
218
- status = exception.getStatus();
219
- const res = exception.getResponse();
220
- if (typeof res === 'string') {
221
- message = res;
222
- } else if (typeof res === 'object' && res !== null) {
223
- const resObj = res as any;
224
- message = resObj.message || resObj.error || 'HttpException';
225
- errorDetails = resObj;
226
- }
227
- } else if (
135
+ // TypeORM
136
+ else if (
228
137
  typeof exception === 'object' &&
229
138
  exception &&
230
139
  'name' in exception &&
@@ -235,75 +144,12 @@ export class AllExceptionsFilter implements ExceptionFilter {
235
144
  )
236
145
  ) {
237
146
  status = HttpStatus.BAD_REQUEST;
238
- message = (exception as any).message || 'TypeORM error';
239
- errorDetails = exception;
240
- } else if (exception instanceof Error) {
241
- message = exception.message;
242
- errorDetails = {
243
- name: exception.name,
244
- stack: exception.stack,
245
- };
246
- } else {
247
- message = 'Une erreur inattendue est survenue';
147
+ message = (exception as any).message || 'TypeORM database error';
148
+ errorResponseName = (exception as any).name;
248
149
  errorDetails = exception;
249
150
  }
250
-
251
- if (process.env.NODE_ENV !== 'production') {
252
- this.logger.error(
253
- \`Exception on \${request.method} \${request.url}\`,
254
- JSON.stringify({ message, status, errorDetails, exception }),
255
- );
256
- }
257
-
258
- response.status(status).json({
259
- statusCode: status,
260
- timestamp: new Date().toISOString(),
261
- path: request.url,
262
- method: request.method,
263
- message,
264
- error: errorDetails,
265
- });
266
- }
267
- }
268
- `.trim();
269
- }
270
-
271
- if (orm === "sequelize") {
272
- return `
273
- import {
274
- ExceptionFilter,
275
- Catch,
276
- ArgumentsHost,
277
- HttpException,
278
- HttpStatus,
279
- Logger,
280
- } from '@nestjs/common';
281
- import { Request, Response } from 'express';
282
-
283
- @Catch()
284
- export class AllExceptionsFilter implements ExceptionFilter {
285
- private readonly logger = new Logger(AllExceptionsFilter.name);
286
-
287
- catch(exception: unknown, host: ArgumentsHost) {
288
- const ctx = host.switchToHttp();
289
- const response = ctx.getResponse<Response>();
290
- const request = ctx.getRequest<Request>();
291
-
292
- let status = HttpStatus.INTERNAL_SERVER_ERROR;
293
- let message: string | string[] = 'Internal server error';
294
- let errorDetails: any = null;
295
-
296
- if (exception instanceof HttpException) {
297
- status = exception.getStatus();
298
- const res = exception.getResponse();
299
- if (typeof res === 'string') {
300
- message = res;
301
- } else if (typeof res === 'object' && res !== null) {
302
- const resObj = res as any;
303
- message = resObj.message || resObj.error || 'HttpException';
304
- errorDetails = resObj;
305
- }
306
- } else if (
151
+ // Sequelize
152
+ else if (
307
153
  typeof exception === 'object' &&
308
154
  exception &&
309
155
  exception.constructor &&
@@ -313,40 +159,12 @@ export class AllExceptionsFilter implements ExceptionFilter {
313
159
  )
314
160
  ) {
315
161
  status = HttpStatus.BAD_REQUEST;
316
- message = (exception as any).message || 'Sequelize error';
317
- errorDetails = exception;
318
- } else if (exception instanceof Error) {
319
- message = exception.message;
320
- errorDetails = {
321
- name: exception.name,
322
- stack: exception.stack,
323
- };
324
- } else {
325
- message = 'Une erreur inattendue est survenue';
162
+ message = (exception as any).message || 'Sequelize database error';
163
+ errorResponseName = exception.constructor.name;
326
164
  errorDetails = exception;
327
- }
328
-
329
- if (process.env.NODE_ENV !== 'production') {
330
- this.logger.error(
331
- \`Exception on \${request.method} \${request.url}\`,
332
- JSON.stringify({ message, status, errorDetails, exception }),
333
- );
334
- }
335
-
336
- response.status(status).json({
337
- statusCode: status,
338
- timestamp: new Date().toISOString(),
339
- path: request.url,
340
- method: request.method,
341
- message,
342
- error: errorDetails,
343
- });
344
- }
345
- }
346
- `.trim();
165
+ }`;
347
166
  }
348
167
 
349
- // Version universelle (multi-ORM / fallback)
350
168
  return `
351
169
  import {
352
170
  ExceptionFilter,
@@ -358,6 +176,15 @@ import {
358
176
  } from '@nestjs/common';
359
177
  import { Request, Response } from 'express';
360
178
 
179
+ export interface ErrorResponse {
180
+ statusCode: number;
181
+ timestamp: string;
182
+ path: string;
183
+ method: string;
184
+ message: string | string[];
185
+ error: string;
186
+ }
187
+
361
188
  @Catch()
362
189
  export class AllExceptionsFilter implements ExceptionFilter {
363
190
  private readonly logger = new Logger(AllExceptionsFilter.name);
@@ -369,11 +196,13 @@ export class AllExceptionsFilter implements ExceptionFilter {
369
196
 
370
197
  let status = HttpStatus.INTERNAL_SERVER_ERROR;
371
198
  let message: string | string[] = 'Internal server error';
199
+ let errorResponseName = 'InternalServerError';
372
200
  let errorDetails: any = null;
373
201
 
374
202
  if (exception instanceof HttpException) {
375
203
  status = exception.getStatus();
376
204
  const res = exception.getResponse();
205
+ errorResponseName = exception.name || 'HttpException';
377
206
  if (typeof res === 'string') {
378
207
  message = res;
379
208
  } else if (typeof res === 'object' && res !== null) {
@@ -381,51 +210,10 @@ export class AllExceptionsFilter implements ExceptionFilter {
381
210
  message = resObj.message || resObj.error || 'HttpException';
382
211
  errorDetails = resObj;
383
212
  }
384
- }
385
- // Prisma
386
- else if (
387
- typeof exception === 'object' &&
388
- exception &&
389
- exception.constructor &&
390
- (
391
- exception.constructor.name === 'PrismaClientKnownRequestError' ||
392
- exception.constructor.name === 'PrismaClientValidationError'
393
- )
394
- ) {
395
- status = HttpStatus.BAD_REQUEST;
396
- message = (exception as any).message || 'Prisma error';
397
- errorDetails = exception;
398
- }
399
- // Mongoose/Mongo
400
- else if (
401
- typeof exception === 'object' &&
402
- exception &&
403
- 'name' in exception &&
404
- (
405
- (exception as any).name === 'MongoError' ||
406
- (exception as any).name === 'MongooseError'
407
- )
408
- ) {
409
- status = HttpStatus.BAD_REQUEST;
410
- message = (exception as any).message || 'MongoDB error';
411
- errorDetails = exception;
412
- }
413
- // Sequelize
414
- else if (
415
- typeof exception === 'object' &&
416
- exception &&
417
- exception.constructor &&
418
- (
419
- exception.constructor.name === 'SequelizeDatabaseError' ||
420
- exception.constructor.name === 'SequelizeValidationError'
421
- )
422
- ) {
423
- status = HttpStatus.BAD_REQUEST;
424
- message = (exception as any).message || 'Sequelize error';
425
- errorDetails = exception;
426
- }
213
+ }${dbSpecificChecks}
427
214
  else if (exception instanceof Error) {
428
215
  message = exception.message;
216
+ errorResponseName = exception.name;
429
217
  errorDetails = {
430
218
  name: exception.name,
431
219
  stack: exception.stack,
@@ -436,18 +224,27 @@ export class AllExceptionsFilter implements ExceptionFilter {
436
224
  }
437
225
 
438
226
  this.logger.error(
439
- \`Exception on \${request.method} \${request.url}\`,
440
- JSON.stringify({ message, status, errorDetails, exception }),
227
+ \`[\${request.method}] \${request.url} - Error status: \${status}\`,
228
+ exception instanceof Error ? exception.stack : JSON.stringify(exception),
441
229
  );
442
230
 
443
- response.status(status).json({
231
+ const isProduction = process.env.NODE_ENV === 'production';
232
+ const payload: ErrorResponse = {
444
233
  statusCode: status,
445
234
  timestamp: new Date().toISOString(),
446
235
  path: request.url,
447
236
  method: request.method,
448
- message,
449
- error: errorDetails,
450
- });
237
+ message: (status === HttpStatus.INTERNAL_SERVER_ERROR && isProduction)
238
+ ? 'Internal server error'
239
+ : message,
240
+ error: isProduction ? errorResponseName : (errorDetails?.error || errorResponseName),
241
+ };
242
+
243
+ if (!isProduction && errorDetails) {
244
+ (payload as any).details = errorDetails;
245
+ }
246
+
247
+ response.status(status).json(payload);
451
248
  }
452
249
  }
453
250
  `.trim();
@@ -119,6 +119,7 @@ async function generateMongooseSchemaFileContent(
119
119
  "number",
120
120
  "int",
121
121
  "float",
122
+ "decimal",
122
123
  "boolean",
123
124
  "date",
124
125
  "uuid",
@@ -144,35 +145,67 @@ async function generateMongooseSchemaFileContent(
144
145
 
145
146
  // 2. Mapping des types CLI -> TypeScript/Mongoose
146
147
  let tsType = "string";
147
- let propOptions = "required: true";
148
+ let propOpts = [];
148
149
 
149
150
  switch (rawType) {
150
151
  case "text":
151
152
  case "uuid":
152
153
  case "string":
153
154
  tsType = "string";
155
+ propOpts.push("type: String");
154
156
  break;
155
157
  case "int":
156
158
  case "number":
157
159
  case "float":
158
160
  case "decimal":
159
161
  tsType = "number";
162
+ propOpts.push("type: Number");
160
163
  break;
161
164
  case "boolean":
162
165
  tsType = "boolean";
166
+ propOpts.push("type: Boolean");
163
167
  break;
164
168
  case "date":
165
169
  tsType = "Date";
170
+ propOpts.push("type: Date");
166
171
  break;
167
172
  case "json":
168
173
  tsType = "Record<string, any>";
169
- propOptions = "type: Object, required: true";
174
+ propOpts.push("type: Object");
170
175
  break;
171
176
  default:
172
177
  tsType = "any";
173
178
  }
174
179
 
175
- return ` @Prop({ ${propOptions} })\n ${fieldName}: ${tsType};`;
180
+ const isRequired = f.nullable !== undefined ? !f.nullable : (fieldName.toLowerCase() !== "isactive" && fieldName.toLowerCase() !== "password");
181
+ if (isRequired) {
182
+ propOpts.push("required: true");
183
+ }
184
+ if (f.unique) {
185
+ propOpts.push("unique: true");
186
+ }
187
+ if (f.default !== undefined && f.default !== null && f.default !== "") {
188
+ let defaultVal = f.default;
189
+ if (typeof defaultVal === "string") {
190
+ if (defaultVal === "true") defaultVal = true;
191
+ else if (defaultVal === "false") defaultVal = false;
192
+ else if (defaultVal.toLowerCase() === "now" || defaultVal.toLowerCase() === "now()") {
193
+ defaultVal = "Date.now";
194
+ } else if (!isNaN(defaultVal) && defaultVal.trim() !== "") {
195
+ defaultVal = Number(defaultVal);
196
+ }
197
+ }
198
+ if (defaultVal === "Date.now") {
199
+ propOpts.push("default: Date.now");
200
+ } else if (typeof defaultVal === "boolean" || typeof defaultVal === "number") {
201
+ propOpts.push(`default: ${defaultVal}`);
202
+ } else {
203
+ propOpts.push(`default: '${defaultVal}'`);
204
+ }
205
+ }
206
+
207
+ const propOptions = propOpts.join(", ");
208
+ return ` @Prop({ ${propOptions} })\n ${fieldName}${f.nullable ? "?" : ""}: ${tsType};`;
176
209
  });
177
210
 
178
211
  const allFields = [...new Set([...directFields, ...dynamicRelations])].join(
@@ -193,4 +226,81 @@ ${allFields}
193
226
  export const ${entityName}Schema = SchemaFactory.createForClass(${entityName});`.trim();
194
227
  }
195
228
 
196
- module.exports = { generateMongooseSchemaFileContent };
229
+ /**
230
+ * Ajoute une relation entre deux schémas Mongoose EXISTANTS.
231
+ * Patche les deux fichiers .schema.ts pour ajouter les champs de référence.
232
+ *
233
+ * @param {string} source - Nom du module source (ex: "post")
234
+ * @param {string} target - Nom du module cible (ex: "user")
235
+ * @param {string} relationType - "n-1" | "1-n" | "1-1" | "n-n"
236
+ * @param {string} mode - "full" | "light"
237
+ */
238
+ async function addMongooseRelation(source, target, relationType, mode = "full") {
239
+ const fs = require("fs");
240
+ const { updateFile } = require("../../userInput");
241
+
242
+ const sourceLow = source.toLowerCase();
243
+ const targetLow = target.toLowerCase();
244
+ const sourceCap = capitalize(source);
245
+ const targetCap = capitalize(target);
246
+
247
+ const isFull = mode === "full";
248
+
249
+ const sourceSchemaPath = isFull
250
+ ? `src/${sourceLow}/infrastructure/persistence/mongoose/${sourceLow}.schema.ts`
251
+ : `src/${sourceLow}/entities/${sourceLow}.schema.ts`;
252
+
253
+ const targetSchemaPath = isFull
254
+ ? `src/${targetLow}/infrastructure/persistence/mongoose/${targetLow}.schema.ts`
255
+ : `src/${targetLow}/entities/${targetLow}.schema.ts`;
256
+
257
+ // --- Champs à injecter dans SOURCE ---
258
+ let sourceField = "";
259
+ switch (relationType) {
260
+ case "n-1":
261
+ case "1-1":
262
+ // FK : référence vers un document cible
263
+ sourceField = `\n @Prop({ type: 'ObjectId', ref: '${targetCap}' })\n ${targetLow}Id: string;\n`;
264
+ break;
265
+ case "1-n":
266
+ case "n-n":
267
+ // Tableau de références vers la cible
268
+ sourceField = `\n @Prop([{ type: 'ObjectId', ref: '${targetCap}' }])\n ${targetLow}Ids: string[];\n`;
269
+ break;
270
+ }
271
+
272
+ // --- Champs inverses à injecter dans TARGET ---
273
+ let targetField = "";
274
+ switch (relationType) {
275
+ case "n-1":
276
+ case "n-n":
277
+ // Le target a un tableau de sources
278
+ targetField = `\n @Prop([{ type: 'ObjectId', ref: '${sourceCap}' }])\n ${sourceLow}Ids: string[];\n`;
279
+ break;
280
+ case "1-n":
281
+ case "1-1":
282
+ // Le target a une référence unique vers source
283
+ targetField = `\n @Prop({ type: 'ObjectId', ref: '${sourceCap}' })\n ${sourceLow}Id: string;\n`;
284
+ break;
285
+ }
286
+
287
+ // Patch source schema
288
+ if (fs.existsSync(sourceSchemaPath) && sourceField) {
289
+ await updateFile({
290
+ path: sourceSchemaPath,
291
+ pattern: new RegExp(`(export class ${sourceCap} \\{)`),
292
+ replacement: `$1${sourceField}`,
293
+ });
294
+ }
295
+
296
+ // Patch target schema
297
+ if (fs.existsSync(targetSchemaPath) && targetField) {
298
+ await updateFile({
299
+ path: targetSchemaPath,
300
+ pattern: new RegExp(`(export class ${targetCap} \\{)`),
301
+ replacement: `$1${targetField}`,
302
+ });
303
+ }
304
+ }
305
+
306
+ module.exports = { generateMongooseSchemaFileContent, addMongooseRelation };