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
@@ -47,13 +47,30 @@ async function generateDto(
47
47
  SWAGGER HELPERS
48
48
  =============================== */
49
49
  const getFieldDescription = (f) => {
50
+ if (f.description) return f.description;
51
+
50
52
  const name = f.name.toLowerCase();
51
53
  if (name.includes("email")) return "The official email address of the user";
52
54
  if (name.includes("password"))
53
55
  return "Must contain at least 8 characters, one letter and one number";
54
56
  if (name.includes("token")) return "Authentication token";
55
- if (name.includes("id") && name !== "id")
56
- return `Unique identifier of the related ${name.replace("id", "")}`;
57
+ if (name.endsWith("id") && name !== "id")
58
+ return `UUID of the referenced ${capitalize(f.name.slice(0, -2))}`;
59
+ if (name.endsWith("ids"))
60
+ return `Array of UUIDs of the referenced ${capitalize(f.name.slice(0, -3))}`;
61
+
62
+ const cleanType = f.type.toLowerCase().replace("[]", "");
63
+ if (cleanType === "date" || name.endsWith("date") || name.endsWith("at")) {
64
+ return "ISO 8601 date string (e.g. 2026-06-16T15:00:00Z)";
65
+ }
66
+ if (name.includes("price") || name.includes("amount")) {
67
+ return "Decimal value in USD (e.g. 99.99)";
68
+ }
69
+ if (cleanType === "boolean") {
70
+ const defSuffix = f.default !== undefined && f.default !== null ? ` — default: ${f.default}` : "";
71
+ return `true/false flag${defSuffix}`;
72
+ }
73
+
57
74
  return `The ${f.name} of the ${entityNameLower}`;
58
75
  };
59
76
 
@@ -88,6 +105,7 @@ async function generateDto(
88
105
  const name = f.name;
89
106
  const cleanType = f.type.toLowerCase().replace("[]", "");
90
107
  const isArray = f.type.endsWith("[]");
108
+ const isOptional = optional || f.nullable;
91
109
 
92
110
  const SCALAR_TYPES = [
93
111
  "string",
@@ -104,7 +122,7 @@ async function generateDto(
104
122
  if (!SCALAR_TYPES.includes(cleanType)) return null;
105
123
 
106
124
  let validators = [];
107
- if (optional) validators.push("@IsOptional()");
125
+ if (isOptional) validators.push("@IsOptional()");
108
126
  if (name.toLowerCase().includes("email")) validators.push("@IsEmail()");
109
127
  else if (name.toLowerCase().includes("password"))
110
128
  validators.push(
@@ -141,7 +159,7 @@ async function generateDto(
141
159
 
142
160
  let swaggerDecorator = "";
143
161
  if (useSwagger && !forceNoSwagger) {
144
- const decorator = optional ? "@ApiPropertyOptional" : "@ApiProperty";
162
+ const decorator = isOptional ? "@ApiPropertyOptional" : "@ApiProperty";
145
163
  const options = JSON.stringify(
146
164
  {
147
165
  example: getExampleForField(f),
@@ -154,7 +172,7 @@ async function generateDto(
154
172
  }
155
173
 
156
174
  return `${swaggerDecorator}${validators.join("\n ")}\n ${name}${
157
- optional ? "?" : "!"
175
+ isOptional ? "?" : "!"
158
176
  }: ${formatType(f.type)};`;
159
177
  };
160
178
 
@@ -169,7 +187,7 @@ async function generateDto(
169
187
 
170
188
  return `import {
171
189
  IsString, IsInt, IsBoolean, IsEmail, IsArray,
172
- IsUUID, IsDateString, MinLength, IsOptional
190
+ IsUUID, IsDateString, MinLength, IsOptional, IsNumber
173
191
  } from 'class-validator';
174
192
  ${useSwagger ? "import { ApiProperty } from '@nestjs/swagger';" : ""}
175
193
 
@@ -200,7 +218,7 @@ export class ${entityName}Dto {
200
218
  if (entityName === "User") swaggerImports.push("ApiHideProperty");
201
219
 
202
220
  return `import {
203
- IsOptional, IsString, IsInt, IsBoolean, IsEmail,
221
+ IsOptional, IsString, IsInt, IsNumber, IsBoolean, IsEmail,
204
222
  IsArray, IsUUID, IsDateString, MinLength, IsEnum
205
223
  } from 'class-validator';
206
224
  ${
@@ -228,7 +246,7 @@ export class Create${entityName}Dto {
228
246
  }
229
247
 
230
248
  /**
231
- * DTO for updating a ${entityName}
249
+ * DTO for updating a ${entityName} (All fields are optional)
232
250
  */
233
251
  ${
234
252
  useSwagger
@@ -3,6 +3,11 @@ const fs = require("fs");
3
3
  const { updateFile, capitalize } = require("../../userInput");
4
4
 
5
5
  async function patchDtoWithRelation(source, targetName, useSwagger, mode) {
6
+ const isDryRun = process.argv.includes("--dry-run");
7
+ if (isDryRun) {
8
+ console.log(`[DRY-RUN] Simulated: patch DTO for ${source} with relation to ${targetName}`);
9
+ return;
10
+ }
6
11
  let dtoPath;
7
12
  if (mode === "full") {
8
13
  dtoPath = `src/${source.toLowerCase()}/application/dtos/${source.toLowerCase()}.dto.ts`;
@@ -64,7 +64,7 @@ async function generateCleanModule(name, config, entityData) {
64
64
  });
65
65
 
66
66
  // 4. Génération du Repository (Implementation)
67
- await generateRepository(name, config.orm);
67
+ await generateRepository(name, config.orm, entityData);
68
68
 
69
69
  // 5. Génération des Use Cases (Create, Get, Update, Delete)
70
70
  await generateUseCases(entityPath, entityNameCapitalized, entityNameLower);
@@ -83,10 +83,25 @@ async function generateCleanModule(name, config, entityData) {
83
83
  contente: mapperContent,
84
84
  });
85
85
 
86
+ if (entityNameLower === "user") {
87
+ await createDirectory(`${entityPath}/domain/enums`);
88
+ await createFile({
89
+ path: `${entityPath}/domain/enums/role.enum.ts`,
90
+ contente: `// Enumération des rôles utilisateurs
91
+ export enum Role {
92
+ USER = 'USER',
93
+ ADMIN = 'ADMIN',
94
+ SUPER_ADMIN = 'SUPER_ADMIN',
95
+ }
96
+ `,
97
+ });
98
+ }
99
+
86
100
  // ÉTAPE RELATIONS : Patching des fichiers si une relation existe
87
101
  if (
88
102
  entityData.relation &&
89
- (entityData.relation.type != "1-n" || entityData.relation.type != "n-n")
103
+ entityData.relation.type !== "1-n" &&
104
+ entityData.relation.type !== "n-n"
90
105
  ) {
91
106
  const { target, type } = entityData.relation;
92
107
  logInfo(
@@ -114,6 +129,7 @@ async function generateCleanModule(name, config, entityData) {
114
129
  name,
115
130
  entityPath,
116
131
  config.swagger,
132
+ config.auth || false,
117
133
  );
118
134
  await createFile({
119
135
  path: `${entityPath}/presentation/controllers/${entityNameLower}.controller.ts`,
@@ -146,6 +162,10 @@ async function generateCleanModule(name, config, entityData) {
146
162
  // --- HELPERS DE TEMPLATES (Extraits de ton setup original) ---
147
163
 
148
164
  function getRepositoryInterfaceTemplate(cap, low) {
165
+ let findByEmailMethod = "";
166
+ if (low === "user") {
167
+ findByEmailMethod = `\n findByEmail(email: string): Promise<${cap}Entity | null>;`;
168
+ }
149
169
  return `import { Create${cap}Dto, Update${cap}Dto } from 'src/${low}/application/dtos/${low}.dto';
150
170
  import { ${cap}Entity } from 'src/${low}/domain/entities/${low}.entity';
151
171
 
@@ -154,9 +174,9 @@ export const I${cap}RepositoryName = 'I${cap}Repository';
154
174
  export interface I${cap}Repository {
155
175
  create(data: Create${cap}Dto): Promise<${cap}Entity>;
156
176
  findById(id: string): Promise<${cap}Entity | null>;
157
- findAll(): Promise<${cap}Entity[]>;
177
+ findAll(page?: number, limit?: number, search?: string, sortBy?: string, sortOrder?: string): Promise<{ data: ${cap}Entity[], total: number }>;
158
178
  update(id: string, data: Update${cap}Dto): Promise<${cap}Entity | null>;
159
- delete(id: string): Promise<void>;
179
+ delete(id: string): Promise<void>;${findByEmailMethod}
160
180
  }`;
161
181
  }
162
182
 
@@ -237,11 +257,20 @@ export class GetAll${cap}UseCase {
237
257
  private readonly repository: I${cap}Repository,
238
258
  ) {}
239
259
 
240
- async execute(): Promise<${cap}Entity[]> {
241
- this.logger.log('Requesting all ${cap} records');
242
- const results = await this.repository.findAll();
243
- this.logger.log(\`Retrieved \${results.length} ${cap}(s)\`);
244
- return results;
260
+ async execute(page: number = 1, limit: number = 10, search?: string, sortBy?: string, sortOrder?: string): Promise<{ data: ${cap}Entity[], meta: { total: number, page: number, limit: number, totalPages: number } }> {
261
+ this.logger.log(\`Requesting page \${page} of ${cap} records (limit: \${limit}, search: \${search}, sortBy: \${sortBy}, sortOrder: \${sortOrder})\`);
262
+ const { data, total } = await this.repository.findAll(page, limit, search, sortBy, sortOrder);
263
+ const totalPages = Math.ceil(total / limit);
264
+ this.logger.log(\`Retrieved \${data.length} of \${total} ${cap}(s)\`);
265
+ return {
266
+ data,
267
+ meta: {
268
+ total,
269
+ page,
270
+ limit,
271
+ totalPages,
272
+ },
273
+ };
245
274
  }
246
275
  }`;
247
276
  break;
@@ -430,8 +459,8 @@ export class ${entityNameCapitalized}Service {
430
459
  async getById(id: string): Promise<${entityNameCapitalized}Entity | null> {
431
460
  return await this.getByIdUseCase.execute(id);
432
461
  }
433
- async getAll(): Promise<${entityNameCapitalized}Entity[]> {
434
- return await this.getAllUseCase.execute();
462
+ async getAll(page: number = 1, limit: number = 10, search?: string, sortBy?: string, sortOrder?: string): Promise<{ data: ${entityNameCapitalized}Entity[], meta: { total: number, page: number, limit: number, totalPages: number } }> {
463
+ return await this.getAllUseCase.execute(page, limit, search, sortBy, sortOrder);
435
464
  }
436
465
  async delete(id: string): Promise<void> {
437
466
  return await this.deleteUseCase.execute(id);
@@ -8,6 +8,11 @@ async function updateExistingEntityRelation(
8
8
  relationType,
9
9
  mode,
10
10
  ) {
11
+ const isDryRun = process.argv.includes("--dry-run");
12
+ if (isDryRun) {
13
+ console.log(`[DRY-RUN] Simulated: update existing entity relation between ${targetName} and ${newEntityName}`);
14
+ return;
15
+ }
11
16
  const targetLow = targetName.toLowerCase();
12
17
  const newCap = capitalize(newEntityName);
13
18
  const newLow = newEntityName.toLowerCase();
@@ -2,6 +2,11 @@ const { updateFile } = require("../../userInput");
2
2
 
3
3
  // src/utils/generators/infrastructure/mapperUpdater.js
4
4
  async function patchMapperWithRelation(source, targetName, mode = "full") {
5
+ const isDryRun = process.argv.includes("--dry-run");
6
+ if (isDryRun) {
7
+ console.log(`[DRY-RUN] Simulated: patch mapper for ${source} with relation to ${targetName}`);
8
+ return;
9
+ }
5
10
  const targetLow = targetName.toLowerCase();
6
11
 
7
12
  if (mode === "full") {