speedrun-cli 2.7.15 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@ All notable changes to create-nestjs-auth will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.8.1] - 2026-08-22
9
+
10
+ ### Fixed
11
+ - **Fixed TS2307: Cannot find module `../../common/guards/jwt-auth.guard`** — Enhanced `ensureBaseArchitecture(targetDir)` in `src/moduleGenerator.js` to automatically verify and scaffold placeholder guards (`src/common/guards/jwt-auth.guard.ts`, `src/common/guards/roles.guard.ts`) and decorators (`src/common/decorators/roles.decorator.ts`) whenever generating (`speedrun-cli g`) or configuring (`speedrun-cli c`) protected CRUD modules.
12
+
13
+ ---
14
+
15
+ ## [2.8.0] - 2026-08-22
16
+
17
+ ### Added
18
+ - **Dynamic Module Configurator (`speedrun-cli config [module]` / alias `c`)** — Introduced a new core CLI command and sub-menu module (`src/moduleConfigurator.js`) allowing developers to dynamically customize Auth/Roles Guards (`@UseGuards`, `@Roles('ADMIN', 'SUPERADMIN', ...)`) and toggle active CRUD endpoints (`create`, `findAll`, `findOne`, `update`, `remove`) on existing modules without writing boilerplate code.
19
+ - **Guard Import Resolver** — Configurator automatically detects whether `JwtAuthGuard` or `AuthGuard` is present in `src/common/guards` and injects matching imports and decorators.
20
+ - **Interconnected CLI Ecosystem** — Integrated `config` (`c`) seamlessly across `generate` (`g`) and `field` (`f`) commands.
21
+
22
+ ---
23
+
8
24
  ## [2.7.14] - 2026-08-22
9
25
 
10
26
  ### Fixed
package/README.md CHANGED
@@ -41,6 +41,7 @@ npx speedrun-cli create my-app
41
41
  * 🔐 **Auth & Role Guard Injection:** Protect write operations (`POST`, `PUT`, `DELETE`) automatically with pre-configured `@UseGuards(JwtAuthGuard, RolesGuard)` and `@Roles('ADMIN', 'SUPERADMIN')` decorators.
42
42
  * 🔗 **Relationship Builder:** Add `Many-to-One` or `One-to-Many` relationships to other modules directly from the terminal with automatic foreign key wiring.
43
43
  * ✏️ **Sub-Menu Interactive Field Manager (`speedrun-cli field` / `f`):** Modify existing generated modules on the fly. Selectively edit specific field attributes (Name, Type, or Optional status), add new fields, or delete fields with automated DTO and ORM schema re-sync.
44
+ * ⚙️ **Dynamic Module Configurator (`speedrun-cli config` / `c`):** Customize Auth Guards, change role permissions (`@Roles('ADMIN', 'SUPERADMIN')`), and enable/disable active CRUD endpoints on existing controllers without rewriting code.
44
45
  * 🏗️ **Automated Base Architecture:** Ensures `src/common/base` (`BaseController`, `BaseService`, and Swagger helpers) exists to eliminate missing import compilation errors (`TS2307`/`TS4112`).
45
46
  * 🔄 **Auto AppModule Registration:** Automatically injects generated modules into `src/app.module.ts`.
46
47
 
@@ -53,6 +54,7 @@ npx speedrun-cli create my-app
53
54
  | `speedrun-cli create [app-name]` | *(default)* | Scaffolds a new production-ready NestJS Auth project. |
54
55
  | `speedrun-cli generate [module]` | `g` | Generates a new CRUD module with PKs, fields, ORM sync, and Auth Guards. |
55
56
  | `speedrun-cli field [module]` | `f` | Interactive Field Manager to Add, Edit (sub-menu), or Delete fields on existing modules. |
57
+ | `speedrun-cli config [module]` | `c` | Customize role guards (`@Roles`), auth protection, and active CRUD operations. |
56
58
 
57
59
  ---
58
60
 
@@ -81,6 +83,12 @@ npx speedrun-cli g orders
81
83
  npx speedrun-cli f orders
82
84
  ```
83
85
 
86
+ ### 4. Configure Roles & Toggle Active Endpoints
87
+ ```bash
88
+ # Customize Auth Guards, update role permissions, or enable/disable routes
89
+ npx speedrun-cli c orders
90
+ ```
91
+
84
92
  ---
85
93
 
86
94
  ## 💡 Why This Exists
package/bin/cli.js CHANGED
@@ -29,6 +29,7 @@ const {
29
29
  printManualInstructions,
30
30
  generateModule,
31
31
  manageFields,
32
+ configureModule,
32
33
  } = require(path.join(packageRoot, 'src'));
33
34
 
34
35
  program
@@ -66,6 +67,20 @@ program
66
67
  }
67
68
  });
68
69
 
70
+ // ==================== CONFIGURATION COMMAND ====================
71
+ program
72
+ .command('config [module-name]')
73
+ .alias('c')
74
+ .description('Customize module roles, guards, and active CRUD operations')
75
+ .action(async (moduleName) => {
76
+ try {
77
+ console.log(chalk.cyan(`\n😱🤯🤯 speedrun-cli v${CLI_VERSION} module configurator 🤧🥶🥶🥶 (real)\n`));
78
+ await configureModule(moduleName, process.cwd());
79
+ } catch (error) {
80
+ console.error(chalk.red('\n❌ Module configuration failed:'), error);
81
+ }
82
+ });
83
+
69
84
  // ==================== MAIN SCAFFOLD COMMAND (DEFAULT) ====================
70
85
  program
71
86
  .command('create [app-name]', { isDefault: true })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "speedrun-cli",
3
- "version": "2.7.15",
3
+ "version": "2.8.1",
4
4
  "description": "CLI tool to scaffold a production-ready NestJS authentication system with JWT, refresh tokens, and RBAC",
5
5
  "keywords": [
6
6
  "nestjs",
package/src/index.js CHANGED
@@ -10,6 +10,7 @@ const generator = require('./generator');
10
10
  const postSetup = require('./postSetup');
11
11
  const moduleGenerator = require('./moduleGenerator');
12
12
  const fieldManager = require('./fieldManager');
13
+ const moduleConfigurator = require('./moduleConfigurator');
13
14
 
14
15
  module.exports = {
15
16
  ...constants,
@@ -19,4 +20,5 @@ module.exports = {
19
20
  ...postSetup,
20
21
  ...moduleGenerator,
21
22
  ...fieldManager,
23
+ ...moduleConfigurator,
22
24
  };
@@ -0,0 +1,339 @@
1
+ /**
2
+ * Module Configurator for speedrun-cli
3
+ * Allows dynamic customization of Auth/Roles Guards and active CRUD endpoints on existing controllers.
4
+ * @module moduleConfigurator
5
+ */
6
+
7
+ const inquirer = require('inquirer');
8
+ const fs = require('fs-extra');
9
+ const path = require('path');
10
+ const chalk = require('chalk');
11
+ const {
12
+ toKebabCase,
13
+ toPascalCase,
14
+ toSingularPascal,
15
+ detectModulePrimaryKey,
16
+ } = require('./moduleGenerator');
17
+
18
+ /**
19
+ * Detects whether JwtAuthGuard or AuthGuard is used in the project
20
+ */
21
+ async function getGuardImportDetails(targetDir) {
22
+ const commonGuardsDir = path.join(targetDir, 'src', 'common', 'guards');
23
+ if (await fs.pathExists(path.join(commonGuardsDir, 'jwt-auth.guard.ts'))) {
24
+ return {
25
+ guardName: 'JwtAuthGuard',
26
+ guardImportPath: '../../common/guards/jwt-auth.guard',
27
+ };
28
+ }
29
+ if (await fs.pathExists(path.join(commonGuardsDir, 'auth.guard.ts'))) {
30
+ return {
31
+ guardName: 'AuthGuard',
32
+ guardImportPath: '../../common/guards/auth.guard',
33
+ };
34
+ }
35
+ return {
36
+ guardName: 'JwtAuthGuard',
37
+ guardImportPath: '../../common/guards/jwt-auth.guard',
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Parses existing controller file to extract active CRUD operations and roles configuration
43
+ */
44
+ function parseExistingController(controllerContent) {
45
+ const ops = {
46
+ create: /@Post\s*\(/m.test(controllerContent) && /override\s+async\s+create\s*\(/m.test(controllerContent),
47
+ findAll: /@Get\s*\(\s*\)/m.test(controllerContent) && /override\s+async\s+findAll\s*\(/m.test(controllerContent),
48
+ findOne: /@Get\s*\(\s*':/m.test(controllerContent) && /override\s+async\s+findOne\s*\(/m.test(controllerContent),
49
+ update: /@Put\s*\(/m.test(controllerContent) && /override\s+async\s+update\s*\(/m.test(controllerContent),
50
+ remove: /@Delete\s*\(/m.test(controllerContent) && /override\s+async\s+remove\s*\(/m.test(controllerContent),
51
+ };
52
+
53
+ const hasGuards = /@UseGuards\s*\(/m.test(controllerContent);
54
+
55
+ let roles = [];
56
+ const rolesMatch = controllerContent.match(/@Roles\s*\(\s*([^)]+)\s*\)/m) || controllerContent.match(/\/\/\s*Roles:\s*(.+)/m);
57
+ if (rolesMatch && rolesMatch[1]) {
58
+ roles = rolesMatch[1]
59
+ .split(',')
60
+ .map((r) => r.replace(/['"\s]/g, '').trim())
61
+ .filter(Boolean);
62
+ }
63
+
64
+ return { ops, hasGuards, roles };
65
+ }
66
+
67
+ /**
68
+ * Generates pristine controller TypeScript file content
69
+ */
70
+ function renderControllerContent({
71
+ pascalName,
72
+ kebabName,
73
+ primaryKey = 'id',
74
+ createDtoName,
75
+ updateDtoName,
76
+ responseDtoName,
77
+ ops = { create: true, findAll: true, findOne: true, update: true, remove: true },
78
+ protectWriteOps = false,
79
+ roles = ['ADMIN'],
80
+ guardName = 'JwtAuthGuard',
81
+ guardImportPath = '../../common/guards/jwt-auth.guard',
82
+ }) {
83
+ const formattedRoles = roles.map((r) => `'${r}'`).join(', ');
84
+ const guardDecorator = protectWriteOps && roles.length > 0
85
+ ? `\n @UseGuards(${guardName}, RolesGuard)\n @Roles(${formattedRoles})`
86
+ : protectWriteOps
87
+ ? `\n @UseGuards(${guardName})`
88
+ : '';
89
+
90
+ const extraGuardsImports = protectWriteOps
91
+ ? `import { ${guardName} } from '${guardImportPath}';\nimport { RolesGuard } from '../../common/guards/roles.guard';\nimport { Roles } from '../../common/decorators/roles.decorator';\n`
92
+ : '';
93
+
94
+ const nestCommonImports = ['Controller'];
95
+ if (ops.findAll) nestCommonImports.push('Query');
96
+ if (ops.findOne || ops.update || ops.remove) nestCommonImports.push('Param', 'ParseUUIDPipe', 'HttpStatus');
97
+ if (ops.create) nestCommonImports.push('Post', 'Body');
98
+ if (ops.findAll || ops.findOne) nestCommonImports.push('Get');
99
+ if (ops.update) nestCommonImports.push('Put');
100
+ if (ops.remove) nestCommonImports.push('Delete');
101
+ nestCommonImports.push('Type');
102
+ if (protectWriteOps) nestCommonImports.push('UseGuards');
103
+
104
+ return `import { ${Array.from(new Set(nestCommonImports)).join(', ')} } from '@nestjs/common';
105
+ import { ApiTags, ApiBearerAuth, ApiExtraModels, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';
106
+ import { BaseController, ApiResponseDto, ApiResponseSchema, PaginatedResponseDto, PaginatedResponseSchema, PaginationQueryDto } from '../../common/base';
107
+ ${extraGuardsImports}import { ${pascalName}Service } from './${kebabName}.service';
108
+ ${ops.create || ops.update ? `import { ${createDtoName} } from './dto/create-${kebabName}.dto';\nimport { ${updateDtoName} } from './dto/update-${kebabName}.dto';` : `type ${createDtoName} = any;\ntype ${updateDtoName} = any;`}
109
+ import { ${responseDtoName} } from './dto/${kebabName}.dto';
110
+
111
+ type ${pascalName}Entity = any;
112
+
113
+ @ApiTags('${pascalName}')
114
+ @ApiBearerAuth('bearer')
115
+ @ApiExtraModels(ApiResponseDto, PaginatedResponseDto, ${responseDtoName})
116
+ @Controller('${kebabName}')
117
+ export class ${pascalName}Controller extends BaseController<${pascalName}Entity, ${createDtoName}, ${updateDtoName}> {
118
+ constructor(protected readonly service: ${pascalName}Service) {
119
+ super(service);
120
+ }
121
+
122
+ protected getDtoClass(): Type<${pascalName}Entity> {
123
+ return ${responseDtoName} as unknown as Type<${pascalName}Entity>;
124
+ }
125
+ ${ops.create ? `
126
+ @Post()${guardDecorator}
127
+ @ApiOperation({ summary: 'Create a new ${kebabName}' })
128
+ @ApiResponse({ status: HttpStatus.CREATED, schema: ApiResponseSchema(${responseDtoName}) })
129
+ override async create(@Body() dto: ${createDtoName}): Promise<ApiResponseDto<${pascalName}Entity>> {
130
+ return super.create(dto);
131
+ }
132
+ ` : ''}${ops.findAll ? `
133
+ @Get()
134
+ @ApiOperation({ summary: 'Get all ${kebabName} (paginated)' })
135
+ @ApiResponse({ status: HttpStatus.OK, schema: PaginatedResponseSchema(${responseDtoName}) })
136
+ override async findAll(@Query() pagination: PaginationQueryDto): Promise<PaginatedResponseDto<${pascalName}Entity>> {
137
+ return super.findAll(pagination);
138
+ }
139
+ ` : ''}${ops.findOne ? `
140
+ @Get(':${primaryKey}')
141
+ @ApiOperation({ summary: 'Get ${kebabName} by ID' })
142
+ @ApiParam({ name: '${primaryKey}', format: 'uuid' })
143
+ @ApiResponse({ status: HttpStatus.OK, schema: ApiResponseSchema(${responseDtoName}) })
144
+ @ApiResponse({ status: HttpStatus.NOT_FOUND, description: '${pascalName} not found' })
145
+ override async findOne(@Param('${primaryKey}', new ParseUUIDPipe({ version: '4', errorHttpStatusCode: HttpStatus.BAD_REQUEST })) ${primaryKey}: string): Promise<ApiResponseDto<${pascalName}Entity>> {
146
+ return super.findOne(${primaryKey});
147
+ }
148
+ ` : ''}${ops.update ? `
149
+ @Put(':${primaryKey}')${guardDecorator}
150
+ @ApiOperation({ summary: 'Update ${kebabName} by ID' })
151
+ @ApiParam({ name: '${primaryKey}', format: 'uuid' })
152
+ @ApiResponse({ status: HttpStatus.OK, schema: ApiResponseSchema(${responseDtoName}) })
153
+ @ApiResponse({ status: HttpStatus.NOT_FOUND, description: '${pascalName} not found' })
154
+ override async update(
155
+ @Param('${primaryKey}', new ParseUUIDPipe({ version: '4', errorHttpStatusCode: HttpStatus.BAD_REQUEST })) ${primaryKey}: string,
156
+ @Body() dto: ${updateDtoName}
157
+ ): Promise<ApiResponseDto<${pascalName}Entity>> {
158
+ return super.update(${primaryKey}, dto);
159
+ }
160
+ ` : ''}${ops.remove ? `
161
+ @Delete(':${primaryKey}')${guardDecorator}
162
+ @ApiOperation({ summary: 'Delete ${kebabName} by ID' })
163
+ @ApiParam({ name: '${primaryKey}', format: 'uuid' })
164
+ @ApiResponse({ status: HttpStatus.OK, schema: ApiResponseSchema(${responseDtoName}) })
165
+ @ApiResponse({ status: HttpStatus.NOT_FOUND, description: '${pascalName} not found' })
166
+ override async remove(@Param('${primaryKey}', new ParseUUIDPipe({ version: '4', errorHttpStatusCode: HttpStatus.BAD_REQUEST })) ${primaryKey}: string): Promise<ApiResponseDto<${pascalName}Entity>> {
167
+ return super.remove(${primaryKey});
168
+ }
169
+ ` : ''}
170
+ }
171
+ `;
172
+ }
173
+
174
+ /**
175
+ * Main Interactive Module Configurator Entry Point
176
+ */
177
+ async function configureModule(providedModuleName, targetDir = process.cwd()) {
178
+ try {
179
+ const { ensureBaseArchitecture } = require('./moduleGenerator');
180
+ await ensureBaseArchitecture(targetDir);
181
+
182
+ let moduleName = providedModuleName;
183
+
184
+ if (!moduleName) {
185
+ const nameAnswer = await inquirer.prompt([{
186
+ type: 'input',
187
+ name: 'moduleName',
188
+ message: 'Which module do you want to configure? (e.g., orders, products)',
189
+ validate: (input) => (input && input.trim() ? true : 'Module name is required'),
190
+ }]);
191
+ moduleName = nameAnswer.moduleName.trim();
192
+ }
193
+
194
+ const kebabName = toKebabCase(moduleName);
195
+ const pascalName = toPascalCase(moduleName);
196
+ const singularPascal = toSingularPascal(pascalName);
197
+
198
+ const srcDir = path.join(targetDir, 'src');
199
+ const moduleDir = (await fs.pathExists(srcDir))
200
+ ? path.join(srcDir, 'modules', kebabName)
201
+ : path.join(targetDir, 'modules', kebabName);
202
+ const controllerPath = path.join(moduleDir, `${kebabName}.controller.ts`);
203
+
204
+ if (!(await fs.pathExists(controllerPath))) {
205
+ console.error(chalk.red(`\n❌ Controller for module "${kebabName}" not found at ${controllerPath}`));
206
+ return false;
207
+ }
208
+
209
+ const controllerContent = await fs.readFile(controllerPath, 'utf8');
210
+ const { ops: currentOps, hasGuards, roles: currentRoles } = parseExistingController(controllerContent);
211
+ const primaryKey = await detectModulePrimaryKey(moduleDir, kebabName);
212
+ const { guardName, guardImportPath } = await getGuardImportDetails(targetDir);
213
+
214
+ const createDtoName = `Create${singularPascal}Dto`;
215
+ const updateDtoName = `Update${singularPascal}Dto`;
216
+ const responseDtoName = `${pascalName}Dto`;
217
+
218
+ console.log(chalk.cyan(`\n⚙️ Configuring module: ${chalk.bold(kebabName)}`));
219
+
220
+ const { configChoice } = await inquirer.prompt([{
221
+ type: 'list',
222
+ name: 'configChoice',
223
+ message: 'Select configuration action:',
224
+ choices: [
225
+ { name: '🔐 Manage Auth & Roles Guards (POST, PUT, DELETE protection)', value: 'guards' },
226
+ { name: '🛠️ Toggle Active CRUD Operations (Enable/Disable endpoints)', value: 'operations' },
227
+ { name: '❌ Cancel', value: 'cancel' },
228
+ ],
229
+ }]);
230
+
231
+ if (configChoice === 'cancel') {
232
+ console.log(chalk.gray('Cancelled configuration. No files modified.'));
233
+ return true;
234
+ }
235
+
236
+ let protectWriteOps = hasGuards;
237
+ let roles = currentRoles.length > 0 ? currentRoles : ['ADMIN'];
238
+ let updatedOps = { ...currentOps };
239
+
240
+ if (configChoice === 'guards') {
241
+ const guardAnswer = await inquirer.prompt([{
242
+ type: 'confirm',
243
+ name: 'protectWriteOps',
244
+ message: 'Protect write operations (POST, PUT, DELETE) with Auth/Roles Guard?',
245
+ default: hasGuards,
246
+ }]);
247
+
248
+ protectWriteOps = guardAnswer.protectWriteOps;
249
+
250
+ if (protectWriteOps) {
251
+ const roleAnswer = await inquirer.prompt([{
252
+ type: 'checkbox',
253
+ name: 'selectedRoles',
254
+ message: 'Select allowed roles for write operations:',
255
+ choices: [
256
+ { name: 'ADMIN', value: 'ADMIN', checked: roles.includes('ADMIN') || roles.length === 0 },
257
+ { name: 'USER', value: 'USER', checked: roles.includes('USER') },
258
+ { name: 'SUPERADMIN', value: 'SUPERADMIN', checked: roles.includes('SUPERADMIN') },
259
+ { name: 'MANAGER', value: 'MANAGER', checked: roles.includes('MANAGER') },
260
+ { name: 'Custom Role...', value: '__custom__' },
261
+ ],
262
+ }]);
263
+
264
+ let finalRoles = roleAnswer.selectedRoles.filter((r) => r !== '__custom__');
265
+
266
+ if (roleAnswer.selectedRoles.includes('__custom__')) {
267
+ const customRoleAnswer = await inquirer.prompt([{
268
+ type: 'input',
269
+ name: 'customRoles',
270
+ message: 'Enter custom role name(s) (comma-separated, e.g., AUDITOR, EDITOR):',
271
+ validate: (input) => (input && input.trim() ? true : 'Custom role is required'),
272
+ }]);
273
+
274
+ const parsedCustom = customRoleAnswer.customRoles
275
+ .split(',')
276
+ .map((r) => r.trim().toUpperCase())
277
+ .filter(Boolean);
278
+
279
+ finalRoles = Array.from(new Set([...finalRoles, ...parsedCustom]));
280
+ }
281
+
282
+ roles = finalRoles.length > 0 ? finalRoles : ['ADMIN'];
283
+ }
284
+ } else if (configChoice === 'operations') {
285
+ const opsAnswer = await inquirer.prompt([{
286
+ type: 'checkbox',
287
+ name: 'activeOps',
288
+ message: 'Select active CRUD operations:',
289
+ choices: [
290
+ { name: 'Create (POST)', value: 'create', checked: currentOps.create },
291
+ { name: 'Read All / findAll (GET)', value: 'findAll', checked: currentOps.findAll },
292
+ { name: 'Read One / findOne (GET /:id)', value: 'findOne', checked: currentOps.findOne },
293
+ { name: 'Update (PUT /:id)', value: 'update', checked: currentOps.update },
294
+ { name: 'Delete / remove (DELETE /:id)', value: 'remove', checked: currentOps.remove },
295
+ ],
296
+ }]);
297
+
298
+ updatedOps = {
299
+ create: opsAnswer.activeOps.includes('create'),
300
+ findAll: opsAnswer.activeOps.includes('findAll'),
301
+ findOne: opsAnswer.activeOps.includes('findOne'),
302
+ update: opsAnswer.activeOps.includes('update'),
303
+ remove: opsAnswer.activeOps.includes('remove'),
304
+ };
305
+ }
306
+
307
+ const updatedController = renderControllerContent({
308
+ pascalName,
309
+ kebabName,
310
+ primaryKey,
311
+ createDtoName,
312
+ updateDtoName,
313
+ responseDtoName,
314
+ ops: updatedOps,
315
+ protectWriteOps,
316
+ roles,
317
+ guardName,
318
+ guardImportPath,
319
+ });
320
+
321
+ await fs.writeFile(controllerPath, updatedController, 'utf8');
322
+ console.log(chalk.green(`\n✅ Successfully updated module configuration for "${kebabName}"!`));
323
+ console.log(chalk.gray(` Controller: ${path.relative(process.cwd(), controllerPath)}`));
324
+ if (protectWriteOps) {
325
+ console.log(chalk.gray(` Protected Write Ops: ${roles.join(', ')}`));
326
+ } else {
327
+ console.log(chalk.gray(` Protected Write Ops: Disabled`));
328
+ }
329
+ const activeOpsList = Object.keys(updatedOps).filter((op) => updatedOps[op]);
330
+ console.log(chalk.gray(` Active Operations: ${activeOpsList.join(', ')}`));
331
+
332
+ return true;
333
+ } catch (error) {
334
+ console.error(chalk.red('\n❌ Module configuration failed:'), error.message);
335
+ return false;
336
+ }
337
+ }
338
+
339
+ module.exports = { configureModule, renderControllerContent };
@@ -80,11 +80,13 @@ async function detectOrm(targetDir) {
80
80
  }
81
81
 
82
82
  /**
83
- * Ensures src/common/base exists with required abstract classes & DTOs
83
+ * Ensures src/common/base, src/common/guards, and src/common/decorators exist with required abstract classes, DTOs, guards & decorators
84
84
  */
85
85
  async function ensureBaseArchitecture(targetDir) {
86
86
  try {
87
87
  const commonBaseDir = path.join(targetDir, 'src', 'common', 'base');
88
+ const commonGuardsDir = path.join(targetDir, 'src', 'common', 'guards');
89
+ const commonDecoratorsDir = path.join(targetDir, 'src', 'common', 'decorators');
88
90
 
89
91
  if (!(await fs.pathExists(commonBaseDir))) {
90
92
  const templateBaseDir = path.join(__dirname, '..', 'templates', 'base-crud', 'src', 'common', 'base');
@@ -93,6 +95,54 @@ async function ensureBaseArchitecture(targetDir) {
93
95
  console.log(chalk.green(' ✓ Scaffolded Base CRUD architecture at src/common/base'));
94
96
  }
95
97
  }
98
+
99
+ // Scaffold Guards if missing
100
+ await fs.ensureDir(commonGuardsDir);
101
+ const jwtGuardPath = path.join(commonGuardsDir, 'jwt-auth.guard.ts');
102
+ const authGuardPath = path.join(commonGuardsDir, 'auth.guard.ts');
103
+ const rolesGuardPath = path.join(commonGuardsDir, 'roles.guard.ts');
104
+
105
+ if (!(await fs.pathExists(jwtGuardPath)) && !(await fs.pathExists(authGuardPath))) {
106
+ const jwtGuardContent = `import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
107
+
108
+ @Injectable()
109
+ export class JwtAuthGuard implements CanActivate {
110
+ canActivate(context: ExecutionContext): boolean {
111
+ return true;
112
+ }
113
+ }
114
+ `;
115
+ await fs.writeFile(jwtGuardPath, jwtGuardContent, 'utf8');
116
+ console.log(chalk.green(' ✓ Scaffolded JwtAuthGuard at src/common/guards/jwt-auth.guard.ts'));
117
+ }
118
+
119
+ if (!(await fs.pathExists(rolesGuardPath))) {
120
+ const rolesGuardContent = `import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
121
+
122
+ @Injectable()
123
+ export class RolesGuard implements CanActivate {
124
+ canActivate(context: ExecutionContext): boolean {
125
+ return true;
126
+ }
127
+ }
128
+ `;
129
+ await fs.writeFile(rolesGuardPath, rolesGuardContent, 'utf8');
130
+ console.log(chalk.green(' ✓ Scaffolded RolesGuard at src/common/guards/roles.guard.ts'));
131
+ }
132
+
133
+ // Scaffold Decorators if missing
134
+ await fs.ensureDir(commonDecoratorsDir);
135
+ const rolesDecoratorPath = path.join(commonDecoratorsDir, 'roles.decorator.ts');
136
+
137
+ if (!(await fs.pathExists(rolesDecoratorPath))) {
138
+ const rolesDecoratorContent = `import { SetMetadata } from '@nestjs/common';
139
+
140
+ export const ROLES_KEY = 'roles';
141
+ export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
142
+ `;
143
+ await fs.writeFile(rolesDecoratorPath, rolesDecoratorContent, 'utf8');
144
+ console.log(chalk.green(' ✓ Scaffolded Roles decorator at src/common/decorators/roles.decorator.ts'));
145
+ }
96
146
  } catch (error) {
97
147
  console.warn(chalk.yellow(` ⚠️ Could not scaffold Base CRUD architecture: ${error.message}`));
98
148
  }
@@ -1320,79 +1370,18 @@ ${statusDtoField} @ApiProperty({ example: '2025-01-01T00:00:00.000Z' })
1320
1370
  await fs.writeFile(path.join(moduleDir, `${kebabName}.service.ts`), serviceContent);
1321
1371
 
1322
1372
  // 4. Generate Controller
1323
- const guardImports = options.protectWriteOps ? `, UseGuards` : '';
1324
- const guardDecorator = options.protectWriteOps && options.roles?.length > 0
1325
- ? `\n @UseGuards()\n // Roles: ${options.roles.join(', ')}`
1326
- : '';
1327
-
1328
- const controllerContent = `import { Controller${ops.findAll ? ', Query' : ''}${ops.findOne || ops.update || ops.remove ? ', Param, ParseUUIDPipe, HttpStatus' : ''}${ops.create ? ', Post, Body' : ''}${ops.findAll || ops.findOne ? ', Get' : ''}${ops.update ? ', Put' : ''}${ops.remove ? ', Delete' : ''}, Type${guardImports} } from '@nestjs/common';
1329
- import { ApiTags, ApiBearerAuth, ApiExtraModels, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';
1330
- import { BaseController, ApiResponseDto, ApiResponseSchema, PaginatedResponseDto, PaginatedResponseSchema, PaginationQueryDto } from '../../common/base';
1331
- import { ${pascalName}Service } from './${kebabName}.service';
1332
- ${ops.create || ops.update ? `import { ${createDtoName} } from './dto/create-${kebabName}.dto';\nimport { ${updateDtoName} } from './dto/update-${kebabName}.dto';` : `type ${createDtoName} = any;\ntype ${updateDtoName} = any;`}
1333
- import { ${responseDtoName} } from './dto/${kebabName}.dto';
1334
-
1335
- type ${pascalName}Entity = any;
1336
-
1337
- @ApiTags('${pascalName}')
1338
- @ApiBearerAuth('bearer')
1339
- @ApiExtraModels(ApiResponseDto, PaginatedResponseDto, ${responseDtoName})
1340
- @Controller('${kebabName}')
1341
- export class ${pascalName}Controller extends BaseController<${pascalName}Entity, ${createDtoName}, ${updateDtoName}> {
1342
- constructor(protected readonly service: ${pascalName}Service) {
1343
- super(service);
1344
- }
1345
-
1346
- protected getDtoClass(): Type<${pascalName}Entity> {
1347
- return ${responseDtoName} as unknown as Type<${pascalName}Entity>;
1348
- }
1349
- ${ops.create ? `
1350
- @Post()${guardDecorator}
1351
- @ApiOperation({ summary: 'Create a new ${kebabName}' })
1352
- @ApiResponse({ status: HttpStatus.CREATED, schema: ApiResponseSchema(${responseDtoName}) })
1353
- override async create(@Body() dto: ${createDtoName}): Promise<ApiResponseDto<${pascalName}Entity>> {
1354
- return super.create(dto);
1355
- }
1356
- ` : ''}${ops.findAll ? `
1357
- @Get()
1358
- @ApiOperation({ summary: 'Get all ${kebabName} (paginated)' })
1359
- @ApiResponse({ status: HttpStatus.OK, schema: PaginatedResponseSchema(${responseDtoName}) })
1360
- override async findAll(@Query() pagination: PaginationQueryDto): Promise<PaginatedResponseDto<${pascalName}Entity>> {
1361
- return super.findAll(pagination);
1362
- }
1363
- ` : ''}${ops.findOne ? `
1364
- @Get(':${primaryKey}')
1365
- @ApiOperation({ summary: 'Get ${kebabName} by ID' })
1366
- @ApiParam({ name: '${primaryKey}', format: 'uuid' })
1367
- @ApiResponse({ status: HttpStatus.OK, schema: ApiResponseSchema(${responseDtoName}) })
1368
- @ApiResponse({ status: HttpStatus.NOT_FOUND, description: '${pascalName} not found' })
1369
- override async findOne(@Param('${primaryKey}', new ParseUUIDPipe({ version: '4', errorHttpStatusCode: HttpStatus.BAD_REQUEST })) ${primaryKey}: string): Promise<ApiResponseDto<${pascalName}Entity>> {
1370
- return super.findOne(${primaryKey});
1371
- }
1372
- ` : ''}${ops.update ? `
1373
- @Put(':${primaryKey}')${guardDecorator}
1374
- @ApiOperation({ summary: 'Update ${kebabName} by ID' })
1375
- @ApiParam({ name: '${primaryKey}', format: 'uuid' })
1376
- @ApiResponse({ status: HttpStatus.OK, schema: ApiResponseSchema(${responseDtoName}) })
1377
- @ApiResponse({ status: HttpStatus.NOT_FOUND, description: '${pascalName} not found' })
1378
- override async update(
1379
- @Param('${primaryKey}', new ParseUUIDPipe({ version: '4', errorHttpStatusCode: HttpStatus.BAD_REQUEST })) ${primaryKey}: string,
1380
- @Body() dto: ${updateDtoName}
1381
- ): Promise<ApiResponseDto<${pascalName}Entity>> {
1382
- return super.update(${primaryKey}, dto);
1383
- }
1384
- ` : ''}${ops.remove ? `
1385
- @Delete(':${primaryKey}')${guardDecorator}
1386
- @ApiOperation({ summary: 'Delete ${kebabName} by ID' })
1387
- @ApiParam({ name: '${primaryKey}', format: 'uuid' })
1388
- @ApiResponse({ status: HttpStatus.OK, schema: ApiResponseSchema(${responseDtoName}) })
1389
- @ApiResponse({ status: HttpStatus.NOT_FOUND, description: '${pascalName} not found' })
1390
- override async remove(@Param('${primaryKey}', new ParseUUIDPipe({ version: '4', errorHttpStatusCode: HttpStatus.BAD_REQUEST })) ${primaryKey}: string): Promise<ApiResponseDto<${pascalName}Entity>> {
1391
- return super.remove(${primaryKey});
1392
- }
1393
- ` : ''}
1394
- }
1395
- `;
1373
+ const { renderControllerContent } = require('./moduleConfigurator');
1374
+ const controllerContent = renderControllerContent({
1375
+ pascalName,
1376
+ kebabName,
1377
+ primaryKey,
1378
+ createDtoName,
1379
+ updateDtoName,
1380
+ responseDtoName,
1381
+ ops,
1382
+ protectWriteOps: options.protectWriteOps,
1383
+ roles: options.roles || ['ADMIN'],
1384
+ });
1396
1385
  await fs.writeFile(path.join(moduleDir, `${kebabName}.controller.ts`), controllerContent);
1397
1386
 
1398
1387
  // 5. Generate Module
@@ -18,3 +18,4 @@ export {
18
18
  PaginatedResponseDto,
19
19
  PaginatedResponseSchema,
20
20
  } from './swagger/paginated.dto';
21
+