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 +16 -0
- package/README.md +8 -0
- package/bin/cli.js +15 -0
- package/package.json +1 -1
- package/src/index.js +2 -0
- package/src/moduleConfigurator.js +339 -0
- package/src/moduleGenerator.js +63 -74
- package/templates/base-crud/src/common/base/index.ts +1 -0
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
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 };
|
package/src/moduleGenerator.js
CHANGED
|
@@ -80,11 +80,13 @@ async function detectOrm(targetDir) {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
|
-
* Ensures src/common/base
|
|
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
|
|
1324
|
-
const
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
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
|