speedrun-cli 2.7.7 → 2.7.9
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 +19 -0
- package/bin/cli.js +16 -0
- package/package.json +1 -1
- package/src/fieldManager.js +192 -0
- package/src/moduleGenerator.js +482 -328
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ 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.0] - 2026-08-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Auto-Scaffold Missing Base Architecture (`ensureBaseArchitecture`)** — When generating modules via `speedrun-cli g [module]`, `src/common/base` is automatically scaffolded if missing from target project, resolving TS2307 & TS4112 compilation errors.
|
|
12
|
+
- **Custom Primary Key Selection** — Prompt to choose primary key format (`id`, `<singular_snake>_id`, `<singular_camel>Id`, or `Custom...`), applied dynamically across ORM schemas, DTOs, response decorators, and `@Param()` controller annotations.
|
|
13
|
+
- **ORM-Native Field Types** — Customized field type choices in the interactive builder according to detected ORM:
|
|
14
|
+
- **Prisma:** `[String, Int, Float, Decimal, Boolean, DateTime, Json]`
|
|
15
|
+
- **TypeORM:** `[varchar, text, int, float, decimal, boolean, timestamp, json]`
|
|
16
|
+
- **Mongoose:** `[String, Number, Boolean, Date, Array, Object]`
|
|
17
|
+
- **Drizzle:** `[varchar, text, integer, numeric, boolean, timestamp, json]`
|
|
18
|
+
- **Specific Field Editing Sub-menu** — Interactive sub-menu allows modifying specific field properties (Field Name, Field Type, Optional Status, or All) instead of forcing full re-entry.
|
|
19
|
+
- **Advanced Relationship Builder** — Interactively add relations (`Many-to-One`, `One-to-Many`) pointing to target modules and foreign keys, automatically injecting attributes into Prisma, TypeORM, Mongoose, and Drizzle schemas.
|
|
20
|
+
- **Auth & Roles Guard Protection Prompt** — Optional step to protect write operations (`POST`, `PUT`, `DELETE`) with `@UseGuards()` and role-based decorators (`ADMIN`, `USER`, `MANAGER`).
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- **Standardized Import Statements** — Controller & Service templates now fetch `BaseController`, `BaseService`, `IBaseRepository`, `ApiResponseDto`, `ApiResponseSchema`, `PaginatedResponseDto`, `PaginatedResponseSchema`, `PaginationQueryDto` cleanly from the single barrel export `../../common/base`.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
8
27
|
## [2.7.1] - 2026-08-22
|
|
9
28
|
|
|
10
29
|
### Fixed
|
package/bin/cli.js
CHANGED
|
@@ -28,6 +28,7 @@ const {
|
|
|
28
28
|
handlePostSetup,
|
|
29
29
|
printManualInstructions,
|
|
30
30
|
generateModule,
|
|
31
|
+
manageFields,
|
|
31
32
|
} = require(path.join(packageRoot, 'src'));
|
|
32
33
|
|
|
33
34
|
program
|
|
@@ -50,6 +51,21 @@ program
|
|
|
50
51
|
}
|
|
51
52
|
});
|
|
52
53
|
|
|
54
|
+
// ==================== FIELD MANAGEMENT COMMAND ====================
|
|
55
|
+
program
|
|
56
|
+
.command('field [module-name]')
|
|
57
|
+
.alias('f')
|
|
58
|
+
.description('Manage (add, edit, delete) fields of an existing module')
|
|
59
|
+
.action(async (moduleName) => {
|
|
60
|
+
try {
|
|
61
|
+
console.log(chalk.cyan(`\n😱🤯🤯 speedrun-cli v${CLI_VERSION} field manager 🤧🥶🥶🥶 (real)\n`));
|
|
62
|
+
await manageFields(moduleName, process.cwd());
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error(chalk.red('\n❌ Field management failed:'));
|
|
65
|
+
console.error(error);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
53
69
|
// ==================== MAIN SCAFFOLD COMMAND (DEFAULT) ====================
|
|
54
70
|
program
|
|
55
71
|
.command('create [app-name]', { isDefault: true })
|
package/package.json
CHANGED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive Field Manager for existing modules
|
|
3
|
+
* @module fieldManager
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const inquirer = require('inquirer');
|
|
7
|
+
const fs = require('fs-extra');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
|
|
11
|
+
// Helper untuk membaca field dari file DTO response yang sudah ada
|
|
12
|
+
async function parseExistingFields(moduleDir, kebabName) {
|
|
13
|
+
const dtoPath = path.join(moduleDir, 'dto', `${kebabName}.dto.ts`);
|
|
14
|
+
if (!(await fs.pathExists(dtoPath))) return [];
|
|
15
|
+
|
|
16
|
+
const content = await fs.readFile(dtoPath, 'utf8');
|
|
17
|
+
const fields = [];
|
|
18
|
+
|
|
19
|
+
// RegEx untuk mengekstrak property DTO NestJS (namaField?: type)
|
|
20
|
+
const regex = /^\s*([a-zA-Z0-9_]+)(\?)?:\s*([a-zA-Z]+);/gm;
|
|
21
|
+
let match;
|
|
22
|
+
|
|
23
|
+
const ignoredFields = ['id', 'status', 'createdAt', 'updatedAt', 'deletedAt'];
|
|
24
|
+
|
|
25
|
+
while ((match = regex.exec(content)) !== null) {
|
|
26
|
+
const [, name, optional, tsType] = match;
|
|
27
|
+
if (!ignoredFields.includes(name)) {
|
|
28
|
+
let type = 'String';
|
|
29
|
+
if (tsType === 'number') type = 'Number';
|
|
30
|
+
if (tsType === 'boolean') type = 'Boolean';
|
|
31
|
+
if (tsType === 'Date') type = 'Date';
|
|
32
|
+
|
|
33
|
+
fields.push({
|
|
34
|
+
name,
|
|
35
|
+
type,
|
|
36
|
+
isOptional: !!optional,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return fields;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Main Interactive Field Manager Entry Point
|
|
46
|
+
*/
|
|
47
|
+
async function manageFields(providedModuleName, targetDir = process.cwd()) {
|
|
48
|
+
try {
|
|
49
|
+
let moduleName = providedModuleName;
|
|
50
|
+
|
|
51
|
+
if (!moduleName) {
|
|
52
|
+
const nameAnswer = await inquirer.prompt([{
|
|
53
|
+
type: 'input',
|
|
54
|
+
name: 'moduleName',
|
|
55
|
+
message: 'Which module do you want to manage fields for? (e.g., orders, products)',
|
|
56
|
+
validate: (input) => (input && input.trim() ? true : 'Module name is required'),
|
|
57
|
+
}]);
|
|
58
|
+
moduleName = nameAnswer.moduleName.trim();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const kebabName = moduleName.toLowerCase();
|
|
62
|
+
const moduleDir = path.join(targetDir, 'src', 'modules', kebabName);
|
|
63
|
+
|
|
64
|
+
if (!(await fs.pathExists(moduleDir))) {
|
|
65
|
+
console.error(chalk.red(`\n❌ Module "${kebabName}" not found at src/modules/${kebabName}`));
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Load existing fields
|
|
70
|
+
let fields = await parseExistingFields(moduleDir, kebabName);
|
|
71
|
+
console.log(chalk.cyan(`\n📦 Managing fields for module: ${chalk.bold(kebabName)}`));
|
|
72
|
+
|
|
73
|
+
let managing = true;
|
|
74
|
+
|
|
75
|
+
const promptSingleField = async (initialValues = {}) => {
|
|
76
|
+
return await inquirer.prompt([
|
|
77
|
+
{
|
|
78
|
+
type: 'input',
|
|
79
|
+
name: 'fieldName',
|
|
80
|
+
message: 'Enter field name:',
|
|
81
|
+
default: initialValues.name,
|
|
82
|
+
validate: (input) => {
|
|
83
|
+
if (!input || !input.trim()) return 'Field name is required';
|
|
84
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(input.trim())) {
|
|
85
|
+
return 'Field name must be a valid identifier';
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
type: 'list',
|
|
92
|
+
name: 'fieldType',
|
|
93
|
+
message: (answers) => `Select field type for '${answers.fieldName}':`,
|
|
94
|
+
choices: ['String', 'Number', 'Boolean', 'Date'],
|
|
95
|
+
default: initialValues.type || 'String',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
type: 'confirm',
|
|
99
|
+
name: 'isOptional',
|
|
100
|
+
message: (answers) => `Is '${answers.fieldName}' optional?`,
|
|
101
|
+
default: initialValues.isOptional !== undefined ? initialValues.isOptional : false,
|
|
102
|
+
},
|
|
103
|
+
]);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
while (managing) {
|
|
107
|
+
console.log(
|
|
108
|
+
chalk.gray(`\nCurrent Fields (${fields.length}): `) +
|
|
109
|
+
(fields.length > 0
|
|
110
|
+
? fields.map(f => chalk.yellow(`${f.name} (${f.type}${f.isOptional ? '?' : ''})`)).join(', ')
|
|
111
|
+
: chalk.gray('None'))
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const { action } = await inquirer.prompt([{
|
|
115
|
+
type: 'list',
|
|
116
|
+
name: 'action',
|
|
117
|
+
message: 'What do you want to do?',
|
|
118
|
+
choices: [
|
|
119
|
+
{ name: '➕ Add new field', value: 'add' },
|
|
120
|
+
{ name: '✏️ Edit an existing field', value: 'edit' },
|
|
121
|
+
{ name: '🗑️ Delete a field', value: 'delete' },
|
|
122
|
+
{ name: '💾 Save changes and update files', value: 'save' },
|
|
123
|
+
{ name: '❌ Cancel', value: 'cancel' },
|
|
124
|
+
],
|
|
125
|
+
}]);
|
|
126
|
+
|
|
127
|
+
if (action === 'add') {
|
|
128
|
+
const newField = await promptSingleField();
|
|
129
|
+
fields.push({
|
|
130
|
+
name: newField.fieldName.trim(),
|
|
131
|
+
type: newField.fieldType,
|
|
132
|
+
isOptional: newField.isOptional,
|
|
133
|
+
});
|
|
134
|
+
} else if (action === 'edit') {
|
|
135
|
+
if (fields.length === 0) {
|
|
136
|
+
console.log(chalk.yellow('⚠️ No fields to edit.'));
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const { fieldToEditIndex } = await inquirer.prompt([{
|
|
141
|
+
type: 'list',
|
|
142
|
+
name: 'fieldToEditIndex',
|
|
143
|
+
message: 'Select field to edit:',
|
|
144
|
+
choices: fields.map((f, index) => ({
|
|
145
|
+
name: `${f.name} (${f.type}${f.isOptional ? '?' : ''})`,
|
|
146
|
+
value: index,
|
|
147
|
+
})),
|
|
148
|
+
}]);
|
|
149
|
+
|
|
150
|
+
const editedField = await promptSingleField(fields[fieldToEditIndex]);
|
|
151
|
+
fields[fieldToEditIndex] = {
|
|
152
|
+
name: editedField.fieldName.trim(),
|
|
153
|
+
type: editedField.fieldType,
|
|
154
|
+
isOptional: editedField.isOptional,
|
|
155
|
+
};
|
|
156
|
+
console.log(chalk.green(`✓ Field updated.`));
|
|
157
|
+
} else if (action === 'delete') {
|
|
158
|
+
if (fields.length === 0) {
|
|
159
|
+
console.log(chalk.yellow('⚠️ No fields to delete.'));
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const { fieldToDeleteIndex } = await inquirer.prompt([{
|
|
164
|
+
type: 'list',
|
|
165
|
+
name: 'fieldToDeleteIndex',
|
|
166
|
+
message: 'Select field to delete:',
|
|
167
|
+
choices: fields.map((f, index) => ({
|
|
168
|
+
name: `${f.name} (${f.type}${f.isOptional ? '?' : ''})`,
|
|
169
|
+
value: index,
|
|
170
|
+
})),
|
|
171
|
+
}]);
|
|
172
|
+
|
|
173
|
+
const deletedName = fields[fieldToDeleteIndex].name;
|
|
174
|
+
fields.splice(fieldToDeleteIndex, 1);
|
|
175
|
+
console.log(chalk.red(`🗑️ Field '${deletedName}' removed.`));
|
|
176
|
+
} else if (action === 'save') {
|
|
177
|
+
managing = false;
|
|
178
|
+
// Panggil fungsi regenerasi DTO & Sync ORM dari moduleGenerator
|
|
179
|
+
const { regenerateModuleComponents } = require('./moduleGenerator');
|
|
180
|
+
await regenerateModuleComponents(moduleName, fields, targetDir);
|
|
181
|
+
console.log(chalk.green(`\n✅ Successfully updated fields for module "${kebabName}"!`));
|
|
182
|
+
} else if (action === 'cancel') {
|
|
183
|
+
console.log(chalk.gray('Cancelled field management. No files were modified.'));
|
|
184
|
+
managing = false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} catch (error) {
|
|
188
|
+
console.error(chalk.red('\n❌ Field management failed:'), error.message);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = { manageFields };
|