speedrun-cli 2.7.5 → 2.7.7
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/package.json +1 -1
- package/src/moduleGenerator.js +179 -38
package/package.json
CHANGED
package/src/moduleGenerator.js
CHANGED
|
@@ -79,6 +79,81 @@ async function detectOrm(targetDir) {
|
|
|
79
79
|
return 'prisma';
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Ensures src/common/base exists with required abstract classes & DTOs
|
|
84
|
+
*/
|
|
85
|
+
async function ensureBaseArchitecture(targetDir) {
|
|
86
|
+
try {
|
|
87
|
+
const commonBaseDir = path.join(targetDir, 'src', 'common', 'base');
|
|
88
|
+
|
|
89
|
+
if (!(await fs.pathExists(commonBaseDir))) {
|
|
90
|
+
await fs.ensureDir(commonBaseDir);
|
|
91
|
+
|
|
92
|
+
// 1. base.controller.ts
|
|
93
|
+
await fs.writeFile(
|
|
94
|
+
path.join(commonBaseDir, 'base.controller.ts'),
|
|
95
|
+
`import { Type } from '@nestjs/common';
|
|
96
|
+
|
|
97
|
+
export abstract class BaseController<T, CreateDto, UpdateDto> {
|
|
98
|
+
constructor(protected readonly service: any) {}
|
|
99
|
+
protected abstract getDtoClass(): Type<T>;
|
|
100
|
+
async create(dto: CreateDto): Promise<any> { return this.service.create(dto); }
|
|
101
|
+
async findAll(query: any): Promise<any> { return this.service.findAll(query); }
|
|
102
|
+
async findOne(id: string): Promise<any> { return this.service.findOne(id); }
|
|
103
|
+
async update(id: string, dto: UpdateDto): Promise<any> { return this.service.update(id, dto); }
|
|
104
|
+
async remove(id: string): Promise<any> { return this.service.remove(id); }
|
|
105
|
+
}
|
|
106
|
+
`
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// 2. base.service.ts
|
|
110
|
+
await fs.writeFile(
|
|
111
|
+
path.join(commonBaseDir, 'base.service.ts'),
|
|
112
|
+
`import { Injectable } from '@nestjs/common';
|
|
113
|
+
|
|
114
|
+
export interface PaginationQueryDto { page?: number; limit?: number; }
|
|
115
|
+
export interface IBaseRepository<T, CreateDto, UpdateDto> {
|
|
116
|
+
create(dto: CreateDto): Promise<T>;
|
|
117
|
+
findAll(pagination: PaginationQueryDto): Promise<{ data: T[]; total: number }>;
|
|
118
|
+
findOne(id: string): Promise<T | null>;
|
|
119
|
+
update(id: string, dto: UpdateDto): Promise<T>;
|
|
120
|
+
remove(id: string): Promise<T>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@Injectable()
|
|
124
|
+
export abstract class BaseService<T, CreateDto, UpdateDto> {
|
|
125
|
+
protected abstract getRepository(): IBaseRepository<T, CreateDto, UpdateDto>;
|
|
126
|
+
async create(dto: CreateDto): Promise<T> { return this.getRepository().create(dto); }
|
|
127
|
+
async findAll(pagination: PaginationQueryDto): Promise<{ data: T[]; total: number }> { return this.getRepository().findAll(pagination); }
|
|
128
|
+
async findOne(id: string): Promise<T | null> { return this.getRepository().findOne(id); }
|
|
129
|
+
async update(id: string, dto: UpdateDto): Promise<T> { return this.getRepository().update(id, dto); }
|
|
130
|
+
async remove(id: string): Promise<T> { return this.getRepository().remove(id); }
|
|
131
|
+
}
|
|
132
|
+
`
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// 3. index.ts (Re-exports & DTO helpers)
|
|
136
|
+
await fs.writeFile(
|
|
137
|
+
path.join(commonBaseDir, 'index.ts'),
|
|
138
|
+
`export * from './base.controller';
|
|
139
|
+
export * from './base.service';
|
|
140
|
+
|
|
141
|
+
export class ApiResponseDto<T> { statusCode: number; message: string; data: T; }
|
|
142
|
+
export class PaginatedResponseDto<T> { statusCode: number; message: string; data: T[]; total: number; page: number; limit: number; }
|
|
143
|
+
export class PaginationQueryDto { page?: number; limit?: number; }
|
|
144
|
+
|
|
145
|
+
export function ApiResponseSchema(dto: any): any { return {}; }
|
|
146
|
+
export function PaginatedResponseSchema(dto: any): any { return {}; }
|
|
147
|
+
`
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
console.log(chalk.green(' ✓ Auto-generated missing src/common/base architecture'));
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.warn(chalk.yellow(` ⚠️ Could not verify/create base architecture: ${error.message}`));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
82
157
|
/**
|
|
83
158
|
* Interactive prompt for module options (Name, CRUD Mode, Fields)
|
|
84
159
|
*/
|
|
@@ -124,6 +199,7 @@ async function promptForModuleOptions(providedModuleName) {
|
|
|
124
199
|
selectedOperations = customOps;
|
|
125
200
|
}
|
|
126
201
|
|
|
202
|
+
// Interactive Field Builder Loop
|
|
127
203
|
// Interactive Field Builder Loop
|
|
128
204
|
const { addCustomFields } = await inquirer.prompt([{
|
|
129
205
|
type: 'confirm',
|
|
@@ -135,13 +211,16 @@ async function promptForModuleOptions(providedModuleName) {
|
|
|
135
211
|
const fields = [];
|
|
136
212
|
|
|
137
213
|
if (addCustomFields) {
|
|
138
|
-
let
|
|
139
|
-
|
|
140
|
-
|
|
214
|
+
let building = true;
|
|
215
|
+
|
|
216
|
+
// Helper untuk input field baru / edit
|
|
217
|
+
const promptSingleField = async (initialValues = {}) => {
|
|
218
|
+
return await inquirer.prompt([
|
|
141
219
|
{
|
|
142
220
|
type: 'input',
|
|
143
221
|
name: 'fieldName',
|
|
144
222
|
message: 'Enter field name (e.g., totalAmount, title):',
|
|
223
|
+
default: initialValues.name,
|
|
145
224
|
validate: (input) => {
|
|
146
225
|
if (!input || !input.trim()) return 'Field name is required';
|
|
147
226
|
if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(input.trim())) {
|
|
@@ -155,30 +234,96 @@ async function promptForModuleOptions(providedModuleName) {
|
|
|
155
234
|
name: 'fieldType',
|
|
156
235
|
message: (answers) => `Select field type for '${answers.fieldName}':`,
|
|
157
236
|
choices: ['String', 'Number', 'Boolean', 'Date'],
|
|
158
|
-
default: 'String',
|
|
237
|
+
default: initialValues.type || 'String',
|
|
159
238
|
},
|
|
160
239
|
{
|
|
161
240
|
type: 'confirm',
|
|
162
241
|
name: 'isOptional',
|
|
163
242
|
message: (answers) => `Is '${answers.fieldName}' optional?`,
|
|
164
|
-
default: false,
|
|
243
|
+
default: initialValues.isOptional !== undefined ? initialValues.isOptional : false,
|
|
165
244
|
},
|
|
166
245
|
]);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Tambah field pertama
|
|
249
|
+
console.log(chalk.cyan('\n--- Add Field 1 ---'));
|
|
250
|
+
const firstField = await promptSingleField();
|
|
251
|
+
fields.push({
|
|
252
|
+
name: firstField.fieldName.trim(),
|
|
253
|
+
type: firstField.fieldType,
|
|
254
|
+
isOptional: firstField.isOptional,
|
|
255
|
+
});
|
|
167
256
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
257
|
+
// Menu Navigasi (Add, Edit, Delete, Finish)
|
|
258
|
+
while (building) {
|
|
259
|
+
console.log(chalk.gray(`\nCurrent Fields (${fields.length}): `) + fields.map(f => chalk.yellow(`${f.name} (${f.type}${f.isOptional ? '?' : ''})`)).join(', '));
|
|
260
|
+
|
|
261
|
+
const { action } = await inquirer.prompt([{
|
|
262
|
+
type: 'list',
|
|
263
|
+
name: 'action',
|
|
264
|
+
message: 'What do you want to do next?',
|
|
265
|
+
choices: [
|
|
266
|
+
{ name: '➕ Add another field', value: 'add' },
|
|
267
|
+
{ name: '✏️ Edit an existing field', value: 'edit' },
|
|
268
|
+
{ name: '🗑️ Delete a field', value: 'delete' },
|
|
269
|
+
{ name: '✅ Finish and generate module', value: 'done' },
|
|
270
|
+
],
|
|
179
271
|
}]);
|
|
180
272
|
|
|
181
|
-
|
|
273
|
+
if (action === 'add') {
|
|
274
|
+
console.log(chalk.cyan(`\n--- Add Field ${fields.length + 1} ---`));
|
|
275
|
+
const newField = await promptSingleField();
|
|
276
|
+
fields.push({
|
|
277
|
+
name: newField.fieldName.trim(),
|
|
278
|
+
type: newField.fieldType,
|
|
279
|
+
isOptional: newField.isOptional,
|
|
280
|
+
});
|
|
281
|
+
} else if (action === 'edit') {
|
|
282
|
+
if (fields.length === 0) {
|
|
283
|
+
console.log(chalk.yellow('⚠️ No fields available to edit.'));
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const { fieldToEditIndex } = await inquirer.prompt([{
|
|
288
|
+
type: 'list',
|
|
289
|
+
name: 'fieldToEditIndex',
|
|
290
|
+
message: 'Select field to edit:',
|
|
291
|
+
choices: fields.map((f, index) => ({
|
|
292
|
+
name: `${f.name} (${f.type}${f.isOptional ? '?' : ''})`,
|
|
293
|
+
value: index,
|
|
294
|
+
})),
|
|
295
|
+
}]);
|
|
296
|
+
|
|
297
|
+
console.log(chalk.cyan(`\n--- Editing Field '${fields[fieldToEditIndex].name}' ---`));
|
|
298
|
+
const editedField = await promptSingleField(fields[fieldToEditIndex]);
|
|
299
|
+
fields[fieldToEditIndex] = {
|
|
300
|
+
name: editedField.fieldName.trim(),
|
|
301
|
+
type: editedField.fieldType,
|
|
302
|
+
isOptional: editedField.isOptional,
|
|
303
|
+
};
|
|
304
|
+
console.log(chalk.green(`✓ Field '${fields[fieldToEditIndex].name}' updated successfully.`));
|
|
305
|
+
} else if (action === 'delete') {
|
|
306
|
+
if (fields.length === 0) {
|
|
307
|
+
console.log(chalk.yellow('⚠️ No fields available to delete.'));
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const { fieldToDeleteIndex } = await inquirer.prompt([{
|
|
312
|
+
type: 'list',
|
|
313
|
+
name: 'fieldToDeleteIndex',
|
|
314
|
+
message: 'Select field to delete:',
|
|
315
|
+
choices: fields.map((f, index) => ({
|
|
316
|
+
name: `${f.name} (${f.type}${f.isOptional ? '?' : ''})`,
|
|
317
|
+
value: index,
|
|
318
|
+
})),
|
|
319
|
+
}]);
|
|
320
|
+
|
|
321
|
+
const deletedName = fields[fieldToDeleteIndex].name;
|
|
322
|
+
fields.splice(fieldToDeleteIndex, 1);
|
|
323
|
+
console.log(chalk.red(`🗑️ Field '${deletedName}' removed.`));
|
|
324
|
+
} else if (action === 'done') {
|
|
325
|
+
building = false;
|
|
326
|
+
}
|
|
182
327
|
}
|
|
183
328
|
}
|
|
184
329
|
|
|
@@ -186,12 +331,6 @@ async function promptForModuleOptions(providedModuleName) {
|
|
|
186
331
|
if (fields.length === 0) {
|
|
187
332
|
fields.push({ name: 'name', type: 'String', isOptional: false });
|
|
188
333
|
}
|
|
189
|
-
|
|
190
|
-
return {
|
|
191
|
-
moduleName,
|
|
192
|
-
operations: selectedOperations,
|
|
193
|
-
fields,
|
|
194
|
-
};
|
|
195
334
|
}
|
|
196
335
|
|
|
197
336
|
function getFieldExampleValue(field, pascalName) {
|
|
@@ -284,9 +423,9 @@ ${fieldLines.join('\n')}
|
|
|
284
423
|
|
|
285
424
|
content += modelDefinition;
|
|
286
425
|
await fs.writeFile(schemaPath, content, 'utf8');
|
|
287
|
-
console.log(chalk.green(`
|
|
426
|
+
console.log(chalk.green(` ✓ Updated prisma/schema.prisma with model ${singularPascal}`));
|
|
288
427
|
} catch (error) {
|
|
289
|
-
console.warn(chalk.yellow(`
|
|
428
|
+
console.warn(chalk.yellow(` ⚠️ Could not sync prisma/schema.prisma: ${error.message}`));
|
|
290
429
|
}
|
|
291
430
|
}
|
|
292
431
|
|
|
@@ -341,9 +480,9 @@ ${fieldLines.join('\n\n')}
|
|
|
341
480
|
`;
|
|
342
481
|
|
|
343
482
|
await fs.writeFile(entityPath, entityContent, 'utf8');
|
|
344
|
-
console.log(chalk.green(`
|
|
483
|
+
console.log(chalk.green(` ✓ Generated TypeORM entity at src/modules/${kebabName}/entities/${toSingularKebab(kebabName)}.entity.ts`));
|
|
345
484
|
} catch (error) {
|
|
346
|
-
console.warn(chalk.yellow(`
|
|
485
|
+
console.warn(chalk.yellow(` ⚠️ Could not generate TypeORM entity: ${error.message}`));
|
|
347
486
|
}
|
|
348
487
|
}
|
|
349
488
|
|
|
@@ -394,9 +533,9 @@ export const ${singularPascal}Schema = SchemaFactory.createForClass(${singularPa
|
|
|
394
533
|
`;
|
|
395
534
|
|
|
396
535
|
await fs.writeFile(schemaPath, schemaContent, 'utf8');
|
|
397
|
-
console.log(chalk.green(`
|
|
536
|
+
console.log(chalk.green(` ✓ Generated Mongoose schema at src/modules/${kebabName}/schemas/${toSingularKebab(kebabName)}.schema.ts`));
|
|
398
537
|
} catch (error) {
|
|
399
|
-
console.warn(chalk.yellow(`
|
|
538
|
+
console.warn(chalk.yellow(` ⚠️ Could not generate Mongoose schema: ${error.message}`));
|
|
400
539
|
}
|
|
401
540
|
}
|
|
402
541
|
|
|
@@ -442,9 +581,9 @@ export type New${singularPascal} = typeof ${toCamelCase(kebabName)}s.$inferInser
|
|
|
442
581
|
`;
|
|
443
582
|
|
|
444
583
|
await fs.writeFile(schemaPath, schemaContent, 'utf8');
|
|
445
|
-
console.log(chalk.green(`
|
|
584
|
+
console.log(chalk.green(` ✓ Generated Drizzle schema at src/modules/${kebabName}/schema/${kebabName}.schema.ts`));
|
|
446
585
|
} catch (error) {
|
|
447
|
-
console.warn(chalk.yellow(`
|
|
586
|
+
console.warn(chalk.yellow(` ⚠️ Could not generate Drizzle schema: ${error.message}`));
|
|
448
587
|
}
|
|
449
588
|
}
|
|
450
589
|
|
|
@@ -478,7 +617,7 @@ ${dummyObjFields}
|
|
|
478
617
|
}
|
|
479
618
|
`;
|
|
480
619
|
await fs.writeFile(seedPath, seedContent, 'utf8');
|
|
481
|
-
console.log(chalk.green(`
|
|
620
|
+
console.log(chalk.green(` ✓ Generated Prisma seed template at prisma/seeds/${kebabName}.seed.ts`));
|
|
482
621
|
} else {
|
|
483
622
|
const seedsDir = path.join(targetDir, 'src', 'database', 'seeds');
|
|
484
623
|
await fs.ensureDir(seedsDir);
|
|
@@ -502,10 +641,10 @@ ${dummyObjFields}
|
|
|
502
641
|
}
|
|
503
642
|
`;
|
|
504
643
|
await fs.writeFile(seedPath, seedContent, 'utf8');
|
|
505
|
-
console.log(chalk.green(`
|
|
644
|
+
console.log(chalk.green(` ✓ Generated seed template at src/database/seeds/${kebabName}.seed.ts`));
|
|
506
645
|
}
|
|
507
646
|
} catch (error) {
|
|
508
|
-
console.warn(chalk.yellow(`
|
|
647
|
+
console.warn(chalk.yellow(` ⚠️ Could not generate seed template: ${error.message}`));
|
|
509
648
|
}
|
|
510
649
|
}
|
|
511
650
|
|
|
@@ -843,6 +982,9 @@ async function registerInAppModule(targetDir, pascalName, kebabName) {
|
|
|
843
982
|
*/
|
|
844
983
|
async function generateModule(providedModuleName, targetDir = process.cwd(), specifiedOrm = null) {
|
|
845
984
|
try {
|
|
985
|
+
// Ensure src/common/base exists before generating any module components
|
|
986
|
+
await ensureBaseArchitecture(targetDir);
|
|
987
|
+
|
|
846
988
|
const options = await promptForModuleOptions(providedModuleName);
|
|
847
989
|
const kebabName = toKebabCase(options.moduleName);
|
|
848
990
|
const pascalName = toPascalCase(options.moduleName);
|
|
@@ -991,11 +1133,10 @@ ${responseFieldsText}
|
|
|
991
1133
|
);
|
|
992
1134
|
await fs.writeFile(path.join(moduleDir, `${kebabName}.service.ts`), serviceContent);
|
|
993
1135
|
|
|
994
|
-
// 4. Generate Controller
|
|
1136
|
+
// 4. Generate Controller (Fixed single clean import path from common/base)
|
|
995
1137
|
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 } from '@nestjs/common';
|
|
996
1138
|
import { ApiTags, ApiBearerAuth, ApiExtraModels, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';
|
|
997
|
-
import { BaseController } from '../../common/base
|
|
998
|
-
import { ApiResponseDto, ApiResponseSchema, PaginatedResponseDto, PaginatedResponseSchema, PaginationQueryDto } from '../../common/base';
|
|
1139
|
+
import { BaseController, ApiResponseDto, ApiResponseSchema, PaginatedResponseDto, PaginatedResponseSchema, PaginationQueryDto } from '../../common/base';
|
|
999
1140
|
import { ${pascalName}Service } from './${kebabName}.service';
|
|
1000
1141
|
${ops.create || ops.update ? `import { ${createDtoName} } from './dto/create-${kebabName}.dto';\nimport { ${updateDtoName} } from './dto/update-${kebabName}.dto';` : `type ${createDtoName} = any;\ntype ${updateDtoName} = any;`}
|
|
1001
1142
|
import { ${responseDtoName} } from './dto/${kebabName}.dto';
|
|
@@ -1074,7 +1215,7 @@ ${ops.create ? `
|
|
|
1074
1215
|
await generateSeedFileTemplate(targetDir, orm, singularPascal, kebabName, options.fields);
|
|
1075
1216
|
|
|
1076
1217
|
console.log(chalk.green(`\n✅ Module "${kebabName}" successfully generated in ${path.relative(process.cwd(), moduleDir)}`));
|
|
1077
|
-
console.log(chalk.gray(`
|
|
1218
|
+
console.log(chalk.gray(` Detected ORM: ${orm}`));
|
|
1078
1219
|
|
|
1079
1220
|
if (!isAutoRegistered) {
|
|
1080
1221
|
console.log(chalk.yellow(`\n⚠️ Please manually register ${pascalName}Module in src/app.module.ts:`));
|