speedrun-cli 2.7.8 → 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/package.json +1 -1
- 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/package.json
CHANGED
package/src/moduleGenerator.js
CHANGED
|
@@ -87,77 +87,72 @@ async function ensureBaseArchitecture(targetDir) {
|
|
|
87
87
|
const commonBaseDir = path.join(targetDir, 'src', 'common', 'base');
|
|
88
88
|
|
|
89
89
|
if (!(await fs.pathExists(commonBaseDir))) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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>;
|
|
90
|
+
const templateBaseDir = path.join(__dirname, '..', 'templates', 'base-crud', 'src', 'common', 'base');
|
|
91
|
+
if (await fs.pathExists(templateBaseDir)) {
|
|
92
|
+
await fs.copy(templateBaseDir, commonBaseDir);
|
|
93
|
+
console.log(chalk.green(' ✓ Scaffolded Base CRUD architecture at src/common/base'));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.warn(chalk.yellow(` ⚠️ Could not scaffold Base CRUD architecture: ${error.message}`));
|
|
98
|
+
}
|
|
121
99
|
}
|
|
122
100
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Returns field type choices based on ORM
|
|
103
|
+
*/
|
|
104
|
+
function getOrmFieldChoices(orm) {
|
|
105
|
+
switch (orm) {
|
|
106
|
+
case 'typeorm':
|
|
107
|
+
return ['varchar', 'text', 'int', 'float', 'decimal', 'boolean', 'timestamp', 'json'];
|
|
108
|
+
case 'mongoose':
|
|
109
|
+
return ['String', 'Number', 'Boolean', 'Date', 'Array', 'Object'];
|
|
110
|
+
case 'drizzle':
|
|
111
|
+
return ['varchar', 'text', 'integer', 'numeric', 'boolean', 'timestamp', 'json'];
|
|
112
|
+
case 'prisma':
|
|
113
|
+
default:
|
|
114
|
+
return ['String', 'Int', 'Float', 'Decimal', 'Boolean', 'DateTime', 'Json'];
|
|
115
|
+
}
|
|
131
116
|
}
|
|
132
|
-
`
|
|
133
|
-
);
|
|
134
117
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
);
|
|
118
|
+
/**
|
|
119
|
+
* Helper to map field type to TypeScript type, validator decorators, and sample example value
|
|
120
|
+
*/
|
|
121
|
+
function getFieldDetails(fieldType) {
|
|
122
|
+
const ft = fieldType;
|
|
149
123
|
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
} catch (error) {
|
|
153
|
-
console.warn(chalk.yellow(` ⚠️ Could not verify/create base architecture: ${error.message}`));
|
|
124
|
+
if (['String', 'varchar', 'text'].includes(ft)) {
|
|
125
|
+
return { tsType: 'string', valDecorators: ['@IsString()'], ex: 'Sample value' };
|
|
154
126
|
}
|
|
127
|
+
if (['Int', 'int', 'integer'].includes(ft)) {
|
|
128
|
+
return { tsType: 'number', valDecorators: ['@IsInt()'], ex: 10 };
|
|
129
|
+
}
|
|
130
|
+
if (['Float', 'Decimal', 'float', 'decimal', 'numeric', 'Number'].includes(ft)) {
|
|
131
|
+
return { tsType: 'number', valDecorators: ['@IsNumber()'], ex: 99.99 };
|
|
132
|
+
}
|
|
133
|
+
if (['Boolean', 'boolean'].includes(ft)) {
|
|
134
|
+
return { tsType: 'boolean', valDecorators: ['@IsBoolean()'], ex: true };
|
|
135
|
+
}
|
|
136
|
+
if (['DateTime', 'timestamp', 'Date'].includes(ft)) {
|
|
137
|
+
return {
|
|
138
|
+
tsType: 'Date',
|
|
139
|
+
valDecorators: ['@IsDate()', '@Type(() => Date)'],
|
|
140
|
+
ex: '2025-01-01T00:00:00.000Z',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (['Json', 'json', 'Object'].includes(ft)) {
|
|
144
|
+
return { tsType: 'object', valDecorators: ['@IsObject()'], ex: { key: 'value' } };
|
|
145
|
+
}
|
|
146
|
+
if (ft === 'Array') {
|
|
147
|
+
return { tsType: 'string[]', valDecorators: ['@IsArray()'], ex: ['item1', 'item2'] };
|
|
148
|
+
}
|
|
149
|
+
return { tsType: 'string', valDecorators: ['@IsString()'], ex: 'Sample value' };
|
|
155
150
|
}
|
|
156
151
|
|
|
157
152
|
/**
|
|
158
|
-
* Interactive prompt for module options (Name, CRUD Mode, Fields)
|
|
153
|
+
* Interactive prompt for module options (Name, CRUD Mode, PK, Fields, Relations, Auth Guards)
|
|
159
154
|
*/
|
|
160
|
-
async function promptForModuleOptions(providedModuleName) {
|
|
155
|
+
async function promptForModuleOptions(providedModuleName, detectedOrm = 'prisma') {
|
|
161
156
|
let moduleName = providedModuleName;
|
|
162
157
|
|
|
163
158
|
if (!moduleName) {
|
|
@@ -170,6 +165,13 @@ async function promptForModuleOptions(providedModuleName) {
|
|
|
170
165
|
moduleName = nameAnswer.moduleName.trim();
|
|
171
166
|
}
|
|
172
167
|
|
|
168
|
+
const kebabName = toKebabCase(moduleName);
|
|
169
|
+
const pascalName = toPascalCase(moduleName);
|
|
170
|
+
const singularPascal = toSingularPascal(pascalName);
|
|
171
|
+
const singularSnake = toSnakeCase(singularPascal);
|
|
172
|
+
const singularCamel = toSingularCamel(kebabName);
|
|
173
|
+
|
|
174
|
+
// CRUD mode selection
|
|
173
175
|
const { crudMode } = await inquirer.prompt([{
|
|
174
176
|
type: 'list',
|
|
175
177
|
name: 'crudMode',
|
|
@@ -182,7 +184,6 @@ async function promptForModuleOptions(providedModuleName) {
|
|
|
182
184
|
}]);
|
|
183
185
|
|
|
184
186
|
let selectedOperations = ['create', 'findAll', 'findOne', 'update', 'remove'];
|
|
185
|
-
|
|
186
187
|
if (crudMode === 'custom') {
|
|
187
188
|
const { customOps } = await inquirer.prompt([{
|
|
188
189
|
type: 'checkbox',
|
|
@@ -199,8 +200,36 @@ async function promptForModuleOptions(providedModuleName) {
|
|
|
199
200
|
selectedOperations = customOps;
|
|
200
201
|
}
|
|
201
202
|
|
|
202
|
-
//
|
|
203
|
-
|
|
203
|
+
// 1. Primary Key Selection
|
|
204
|
+
const { pkChoice } = await inquirer.prompt([{
|
|
205
|
+
type: 'list',
|
|
206
|
+
name: 'pkChoice',
|
|
207
|
+
message: `Select primary key format for '${moduleName}':`,
|
|
208
|
+
choices: [
|
|
209
|
+
{ name: 'id (Default UUID)', value: 'id' },
|
|
210
|
+
{ name: `${singularSnake}_id (e.g., ${singularSnake}_id)`, value: `${singularSnake}_id` },
|
|
211
|
+
{ name: `${singularCamel}Id (e.g., ${singularCamel}Id)`, value: `${singularCamel}Id` },
|
|
212
|
+
{ name: 'Custom Primary Key Name...', value: 'custom' },
|
|
213
|
+
],
|
|
214
|
+
default: 'id',
|
|
215
|
+
}]);
|
|
216
|
+
|
|
217
|
+
let primaryKey = pkChoice;
|
|
218
|
+
if (pkChoice === 'custom') {
|
|
219
|
+
const { customPk } = await inquirer.prompt([{
|
|
220
|
+
type: 'input',
|
|
221
|
+
name: 'customPk',
|
|
222
|
+
message: 'Enter custom primary key name:',
|
|
223
|
+
default: 'id',
|
|
224
|
+
validate: (input) => (input && /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(input.trim()) ? true : 'Invalid identifier for primary key'),
|
|
225
|
+
}]);
|
|
226
|
+
primaryKey = customPk.trim();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 2. Interactive Field Builder Loop
|
|
230
|
+
const fields = [];
|
|
231
|
+
const ormTypeChoices = getOrmFieldChoices(detectedOrm);
|
|
232
|
+
|
|
204
233
|
const { addCustomFields } = await inquirer.prompt([{
|
|
205
234
|
type: 'confirm',
|
|
206
235
|
name: 'addCustomFields',
|
|
@@ -208,211 +237,298 @@ async function promptForModuleOptions(providedModuleName) {
|
|
|
208
237
|
default: true,
|
|
209
238
|
}]);
|
|
210
239
|
|
|
211
|
-
const fields = [];
|
|
212
|
-
|
|
213
240
|
if (addCustomFields) {
|
|
214
|
-
let
|
|
241
|
+
let managingFields = true;
|
|
242
|
+
while (managingFields) {
|
|
243
|
+
if (fields.length > 0) {
|
|
244
|
+
console.log(chalk.cyan(`\n📋 Current fields for '${moduleName}':`));
|
|
245
|
+
fields.forEach((f, idx) => {
|
|
246
|
+
console.log(chalk.gray(` ${idx + 1}. ${f.name}: ${f.type} (${f.isOptional ? 'Optional' : 'Required'})`));
|
|
247
|
+
});
|
|
248
|
+
console.log('');
|
|
249
|
+
|
|
250
|
+
const { fieldAction } = await inquirer.prompt([{
|
|
251
|
+
type: 'list',
|
|
252
|
+
name: 'fieldAction',
|
|
253
|
+
message: 'Choose an action:',
|
|
254
|
+
choices: [
|
|
255
|
+
{ name: '➕ Add another field', value: 'add' },
|
|
256
|
+
{ name: '✏️ Edit an existing field', value: 'edit' },
|
|
257
|
+
{ name: '🗑️ Delete a field', value: 'delete' },
|
|
258
|
+
{ name: '✅ Finish defining fields', value: 'finish' },
|
|
259
|
+
],
|
|
260
|
+
}]);
|
|
215
261
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
262
|
+
if (fieldAction === 'finish') {
|
|
263
|
+
managingFields = false;
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (fieldAction === 'delete') {
|
|
268
|
+
const { fieldToDelete } = await inquirer.prompt([{
|
|
269
|
+
type: 'list',
|
|
270
|
+
name: 'fieldToDelete',
|
|
271
|
+
message: 'Select field to delete:',
|
|
272
|
+
choices: fields.map((f, i) => ({ name: `${f.name} (${f.type})`, value: i })),
|
|
273
|
+
}]);
|
|
274
|
+
const removed = fields.splice(fieldToDelete, 1);
|
|
275
|
+
console.log(chalk.yellow(` Removed field '${removed[0].name}'`));
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (fieldAction === 'edit') {
|
|
280
|
+
const { fieldToEditIndex } = await inquirer.prompt([{
|
|
281
|
+
type: 'list',
|
|
282
|
+
name: 'fieldToEditIndex',
|
|
283
|
+
message: 'Select field to edit:',
|
|
284
|
+
choices: fields.map((f, i) => ({ name: `${f.name}: ${f.type} (${f.isOptional ? 'Optional' : 'Required'})`, value: i })),
|
|
285
|
+
}]);
|
|
286
|
+
|
|
287
|
+
const targetField = fields[fieldToEditIndex];
|
|
288
|
+
|
|
289
|
+
const { editPropChoice } = await inquirer.prompt([{
|
|
290
|
+
type: 'list',
|
|
291
|
+
name: 'editPropChoice',
|
|
292
|
+
message: `Select property to edit for '${targetField.name}':`,
|
|
293
|
+
choices: [
|
|
294
|
+
{ name: '1. Change Field Name', value: 'name' },
|
|
295
|
+
{ name: '2. Change Field Type', value: 'type' },
|
|
296
|
+
{ name: '3. Change Optional Status', value: 'optional' },
|
|
297
|
+
{ name: '4. Edit All Properties', value: 'all' },
|
|
298
|
+
],
|
|
299
|
+
}]);
|
|
300
|
+
|
|
301
|
+
if (editPropChoice === 'name' || editPropChoice === 'all') {
|
|
302
|
+
const { newName } = await inquirer.prompt([{
|
|
303
|
+
type: 'input',
|
|
304
|
+
name: 'newName',
|
|
305
|
+
message: 'Enter field name:',
|
|
306
|
+
default: targetField.name,
|
|
307
|
+
validate: (input) => (input && /^[a-zA-Z][a-zA-Z0-9_]*$/.test(input.trim()) ? true : 'Field name must be a valid identifier'),
|
|
308
|
+
}]);
|
|
309
|
+
targetField.name = newName.trim();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (editPropChoice === 'type' || editPropChoice === 'all') {
|
|
313
|
+
const { newType } = await inquirer.prompt([{
|
|
314
|
+
type: 'list',
|
|
315
|
+
name: 'newType',
|
|
316
|
+
message: `Select field type for '${targetField.name}':`,
|
|
317
|
+
choices: ormTypeChoices,
|
|
318
|
+
default: targetField.type,
|
|
319
|
+
}]);
|
|
320
|
+
targetField.type = newType;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (editPropChoice === 'optional' || editPropChoice === 'all') {
|
|
324
|
+
const { newOpt } = await inquirer.prompt([{
|
|
325
|
+
type: 'confirm',
|
|
326
|
+
name: 'newOpt',
|
|
327
|
+
message: `Is '${targetField.name}' optional?`,
|
|
328
|
+
default: targetField.isOptional,
|
|
329
|
+
}]);
|
|
330
|
+
targetField.isOptional = newOpt;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
console.log(chalk.green(` ✓ Updated field '${targetField.name}'`));
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Add field path
|
|
339
|
+
const fieldAnswers = await inquirer.prompt([
|
|
219
340
|
{
|
|
220
341
|
type: 'input',
|
|
221
342
|
name: 'fieldName',
|
|
222
343
|
message: 'Enter field name (e.g., totalAmount, title):',
|
|
223
|
-
default: initialValues.name,
|
|
224
344
|
validate: (input) => {
|
|
225
345
|
if (!input || !input.trim()) return 'Field name is required';
|
|
226
346
|
if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(input.trim())) {
|
|
227
347
|
return 'Field name must be a valid identifier (e.g., totalAmount)';
|
|
228
348
|
}
|
|
349
|
+
if (input.trim() === primaryKey) {
|
|
350
|
+
return `Primary key '${primaryKey}' is already defined. Please choose another field name.`;
|
|
351
|
+
}
|
|
229
352
|
return true;
|
|
230
353
|
},
|
|
231
354
|
},
|
|
232
355
|
{
|
|
233
356
|
type: 'list',
|
|
234
357
|
name: 'fieldType',
|
|
235
|
-
message: (
|
|
236
|
-
choices:
|
|
237
|
-
default:
|
|
358
|
+
message: (ans) => `Select field type for '${ans.fieldName}':`,
|
|
359
|
+
choices: ormTypeChoices,
|
|
360
|
+
default: ormTypeChoices[0],
|
|
238
361
|
},
|
|
239
362
|
{
|
|
240
363
|
type: 'confirm',
|
|
241
364
|
name: 'isOptional',
|
|
242
|
-
message: (
|
|
243
|
-
default:
|
|
365
|
+
message: (ans) => `Is '${ans.fieldName}' optional?`,
|
|
366
|
+
default: false,
|
|
244
367
|
},
|
|
245
368
|
]);
|
|
246
|
-
};
|
|
247
369
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
type: firstField.fieldType,
|
|
254
|
-
isOptional: firstField.isOptional,
|
|
255
|
-
});
|
|
256
|
-
|
|
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
|
-
],
|
|
271
|
-
}]);
|
|
370
|
+
fields.push({
|
|
371
|
+
name: fieldAnswers.fieldName.trim(),
|
|
372
|
+
type: fieldAnswers.fieldType,
|
|
373
|
+
isOptional: fieldAnswers.isOptional,
|
|
374
|
+
});
|
|
272
375
|
|
|
273
|
-
if (
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if (fields.length === 0) {
|
|
283
|
-
console.log(chalk.yellow('⚠️ No fields available to edit.'));
|
|
284
|
-
continue;
|
|
376
|
+
if (fields.length === 1) {
|
|
377
|
+
const { continueLoop } = await inquirer.prompt([{
|
|
378
|
+
type: 'confirm',
|
|
379
|
+
name: 'continueLoop',
|
|
380
|
+
message: 'Do you want to add another field?',
|
|
381
|
+
default: false,
|
|
382
|
+
}]);
|
|
383
|
+
if (!continueLoop) {
|
|
384
|
+
managingFields = false;
|
|
285
385
|
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
286
389
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
choices: fields.map((f, index) => ({
|
|
292
|
-
name: `${f.name} (${f.type}${f.isOptional ? '?' : ''})`,
|
|
293
|
-
value: index,
|
|
294
|
-
})),
|
|
295
|
-
}]);
|
|
390
|
+
// Fallback if no custom fields added
|
|
391
|
+
if (fields.length === 0) {
|
|
392
|
+
fields.push({ name: 'name', type: ormTypeChoices[0] || 'String', isOptional: false });
|
|
393
|
+
}
|
|
296
394
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
} else if (action === 'delete') {
|
|
306
|
-
if (fields.length === 0) {
|
|
307
|
-
console.log(chalk.yellow('⚠️ No fields available to delete.'));
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
395
|
+
// 3. Relationships Prompt
|
|
396
|
+
const relations = [];
|
|
397
|
+
const { addRelation } = await inquirer.prompt([{
|
|
398
|
+
type: 'confirm',
|
|
399
|
+
name: 'addRelation',
|
|
400
|
+
message: 'Do you want to add a relation to another module?',
|
|
401
|
+
default: false,
|
|
402
|
+
}]);
|
|
310
403
|
|
|
311
|
-
|
|
404
|
+
if (addRelation) {
|
|
405
|
+
let addingRel = true;
|
|
406
|
+
while (addingRel) {
|
|
407
|
+
const relAnswers = await inquirer.prompt([
|
|
408
|
+
{
|
|
312
409
|
type: 'list',
|
|
313
|
-
name: '
|
|
314
|
-
message: 'Select
|
|
315
|
-
choices:
|
|
316
|
-
name:
|
|
317
|
-
value:
|
|
318
|
-
|
|
319
|
-
}
|
|
410
|
+
name: 'relType',
|
|
411
|
+
message: 'Select relation type:',
|
|
412
|
+
choices: [
|
|
413
|
+
{ name: 'Many-to-One (e.g. Order belongs to User)', value: 'Many-to-One' },
|
|
414
|
+
{ name: 'One-to-Many', value: 'One-to-Many' },
|
|
415
|
+
],
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
type: 'input',
|
|
419
|
+
name: 'targetModule',
|
|
420
|
+
message: 'Target module name (e.g., users, categories):',
|
|
421
|
+
validate: (input) => (input && input.trim() ? true : 'Target module name is required'),
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
type: 'input',
|
|
425
|
+
name: 'fkField',
|
|
426
|
+
message: (ans) => `Foreign key field name (e.g., ${toSingularCamel(ans.targetModule)}Id):`,
|
|
427
|
+
default: (ans) => `${toSingularCamel(ans.targetModule)}Id`,
|
|
428
|
+
},
|
|
429
|
+
]);
|
|
320
430
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
431
|
+
relations.push({
|
|
432
|
+
type: relAnswers.relType,
|
|
433
|
+
targetModule: relAnswers.targetModule.trim(),
|
|
434
|
+
fkField: relAnswers.fkField.trim(),
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const { continueRel } = await inquirer.prompt([{
|
|
438
|
+
type: 'confirm',
|
|
439
|
+
name: 'continueRel',
|
|
440
|
+
message: 'Do you want to add another relation?',
|
|
441
|
+
default: false,
|
|
442
|
+
}]);
|
|
443
|
+
addingRel = continueRel;
|
|
327
444
|
}
|
|
328
445
|
}
|
|
329
446
|
|
|
330
|
-
//
|
|
331
|
-
|
|
332
|
-
|
|
447
|
+
// 4. Role & Auth Guard Protection Prompt
|
|
448
|
+
const { protectWriteOps } = await inquirer.prompt([{
|
|
449
|
+
type: 'confirm',
|
|
450
|
+
name: 'protectWriteOps',
|
|
451
|
+
message: 'Protect write operations (POST, PUT, DELETE) with Auth/Roles Guard?',
|
|
452
|
+
default: true,
|
|
453
|
+
}]);
|
|
454
|
+
|
|
455
|
+
let selectedRoles = [];
|
|
456
|
+
if (protectWriteOps) {
|
|
457
|
+
const { roles } = await inquirer.prompt([{
|
|
458
|
+
type: 'checkbox',
|
|
459
|
+
name: 'roles',
|
|
460
|
+
message: 'Select allowed roles:',
|
|
461
|
+
choices: [
|
|
462
|
+
{ name: 'ADMIN', value: 'ADMIN', checked: true },
|
|
463
|
+
{ name: 'USER', value: 'USER' },
|
|
464
|
+
{ name: 'MANAGER', value: 'MANAGER' },
|
|
465
|
+
],
|
|
466
|
+
}]);
|
|
467
|
+
selectedRoles = roles.length > 0 ? roles : ['ADMIN'];
|
|
333
468
|
}
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
moduleName,
|
|
472
|
+
operations: selectedOperations,
|
|
473
|
+
primaryKey,
|
|
474
|
+
fields,
|
|
475
|
+
relations,
|
|
476
|
+
protectWriteOps,
|
|
477
|
+
roles: selectedRoles,
|
|
478
|
+
};
|
|
334
479
|
}
|
|
335
480
|
|
|
336
481
|
function getFieldExampleValue(field, pascalName) {
|
|
482
|
+
const details = getFieldDetails(field.type);
|
|
337
483
|
const name = field.name.toLowerCase();
|
|
338
|
-
|
|
484
|
+
|
|
485
|
+
if (details.tsType === 'string') {
|
|
339
486
|
if (name.includes('email')) return 'user@example.com';
|
|
340
487
|
if (name.includes('phone')) return '+1234567890';
|
|
341
488
|
if (name.includes('url')) return 'https://example.com';
|
|
342
489
|
if (name.includes('sku') || name.includes('code')) return 'SKU-1001';
|
|
343
490
|
return `Sample ${field.name}`;
|
|
344
491
|
}
|
|
345
|
-
|
|
346
|
-
if (name.includes('price') || name.includes('amount') || name.includes('total') || name.includes('cost')) {
|
|
347
|
-
return 99.99;
|
|
348
|
-
}
|
|
349
|
-
return 10;
|
|
350
|
-
}
|
|
351
|
-
if (field.type === 'Boolean') return true;
|
|
352
|
-
if (field.type === 'Date') return '2025-01-01T00:00:00.000Z';
|
|
353
|
-
return 'Example value';
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function getTsType(fieldType) {
|
|
357
|
-
switch (fieldType) {
|
|
358
|
-
case 'String':
|
|
359
|
-
return 'string';
|
|
360
|
-
case 'Number':
|
|
361
|
-
return 'number';
|
|
362
|
-
case 'Boolean':
|
|
363
|
-
return 'boolean';
|
|
364
|
-
case 'Date':
|
|
365
|
-
return 'Date';
|
|
366
|
-
default:
|
|
367
|
-
return 'string';
|
|
368
|
-
}
|
|
492
|
+
return details.ex;
|
|
369
493
|
}
|
|
370
494
|
|
|
371
495
|
/**
|
|
372
496
|
* Dynamic ORM Schema Synchronization: Prisma
|
|
373
497
|
*/
|
|
374
|
-
async function syncPrismaSchema(targetDir, singularPascal, kebabName, fields) {
|
|
498
|
+
async function syncPrismaSchema(targetDir, singularPascal, kebabName, primaryKey, fields, relations) {
|
|
375
499
|
try {
|
|
376
500
|
const schemaPath = path.join(targetDir, 'prisma', 'schema.prisma');
|
|
377
501
|
if (!(await fs.pathExists(schemaPath))) return;
|
|
378
502
|
|
|
379
503
|
let content = await fs.readFile(schemaPath, 'utf8');
|
|
380
504
|
|
|
381
|
-
// Avoid duplicate model definition
|
|
382
505
|
if (new RegExp(`\\bmodel\\s+${singularPascal}\\b`).test(content)) {
|
|
383
506
|
return;
|
|
384
507
|
}
|
|
385
508
|
|
|
386
509
|
const fieldLines = fields.map((f) => {
|
|
387
510
|
let pType = 'String';
|
|
388
|
-
if (f.type
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
nameLower.includes('fee') ||
|
|
396
|
-
nameLower.includes('rate') ||
|
|
397
|
-
nameLower.includes('score')
|
|
398
|
-
) {
|
|
399
|
-
pType = 'Float';
|
|
400
|
-
} else {
|
|
401
|
-
pType = 'Int';
|
|
402
|
-
}
|
|
403
|
-
} else if (f.type === 'Boolean') {
|
|
404
|
-
pType = 'Boolean';
|
|
405
|
-
} else if (f.type === 'Date') {
|
|
406
|
-
pType = 'DateTime';
|
|
407
|
-
}
|
|
511
|
+
if (['Int', 'int', 'integer'].includes(f.type)) pType = 'Int';
|
|
512
|
+
else if (['Float', 'float'].includes(f.type)) pType = 'Float';
|
|
513
|
+
else if (['Decimal', 'decimal', 'numeric'].includes(f.type)) pType = 'Decimal';
|
|
514
|
+
else if (['Boolean', 'boolean'].includes(f.type)) pType = 'Boolean';
|
|
515
|
+
else if (['DateTime', 'timestamp', 'Date'].includes(f.type)) pType = 'DateTime';
|
|
516
|
+
else if (['Json', 'json', 'Object'].includes(f.type)) pType = 'Json';
|
|
517
|
+
|
|
408
518
|
return ` ${f.name} ${pType}${f.isOptional ? '?' : ''}`;
|
|
409
519
|
});
|
|
410
520
|
|
|
521
|
+
const relLines = relations.map((r) => {
|
|
522
|
+
const targetPascal = toSingularPascal(toPascalCase(r.targetModule));
|
|
523
|
+
const targetCamel = toSingularCamel(r.targetModule);
|
|
524
|
+
return ` ${targetCamel} ${targetPascal}? @relation(fields: [${r.fkField}], references: [id])\n ${r.fkField} String?`;
|
|
525
|
+
});
|
|
526
|
+
|
|
411
527
|
const modelDefinition = `
|
|
412
528
|
model ${singularPascal} {
|
|
413
|
-
|
|
529
|
+
${primaryKey} String @id @default(uuid())
|
|
414
530
|
${fieldLines.join('\n')}
|
|
415
|
-
status String @default("ACTIVE")
|
|
531
|
+
${relLines.length > 0 ? relLines.join('\n') + '\n' : ''} status String @default("ACTIVE")
|
|
416
532
|
createdAt DateTime @default(now())
|
|
417
533
|
updatedAt DateTime @updatedAt
|
|
418
534
|
deletedAt DateTime?
|
|
@@ -423,49 +539,59 @@ ${fieldLines.join('\n')}
|
|
|
423
539
|
|
|
424
540
|
content += modelDefinition;
|
|
425
541
|
await fs.writeFile(schemaPath, content, 'utf8');
|
|
426
|
-
console.log(chalk.green(`
|
|
542
|
+
console.log(chalk.green(` ✓ Updated prisma/schema.prisma with model ${singularPascal}`));
|
|
427
543
|
} catch (error) {
|
|
428
|
-
console.warn(chalk.yellow(`
|
|
544
|
+
console.warn(chalk.yellow(` ⚠️ Could not sync prisma/schema.prisma: ${error.message}`));
|
|
429
545
|
}
|
|
430
546
|
}
|
|
431
547
|
|
|
432
548
|
/**
|
|
433
549
|
* Dynamic ORM Schema Synchronization: TypeORM
|
|
434
550
|
*/
|
|
435
|
-
async function syncTypeOrmSchema(moduleDir, singularPascal, kebabName, fields) {
|
|
551
|
+
async function syncTypeOrmSchema(moduleDir, singularPascal, kebabName, primaryKey, fields, relations) {
|
|
436
552
|
try {
|
|
437
553
|
const entityDir = path.join(moduleDir, 'entities');
|
|
438
554
|
await fs.ensureDir(entityDir);
|
|
439
555
|
const entityPath = path.join(entityDir, `${toSingularKebab(kebabName)}.entity.ts`);
|
|
440
556
|
|
|
441
557
|
const fieldLines = fields.map((f) => {
|
|
558
|
+
const details = getFieldDetails(f.type);
|
|
442
559
|
let colDecorator = `@Column({ nullable: ${f.isOptional} })`;
|
|
443
|
-
let tsType = 'string';
|
|
444
560
|
|
|
445
|
-
if (
|
|
561
|
+
if (['decimal', 'numeric', 'Decimal'].includes(f.type)) {
|
|
446
562
|
colDecorator = `@Column('decimal', { precision: 10, scale: 2, nullable: ${f.isOptional} })`;
|
|
447
|
-
|
|
448
|
-
} else if (f.type === 'Boolean') {
|
|
563
|
+
} else if (['boolean', 'Boolean'].includes(f.type)) {
|
|
449
564
|
colDecorator = `@Column({ default: false, nullable: ${f.isOptional} })`;
|
|
450
|
-
|
|
451
|
-
} else if (f.type === 'Date') {
|
|
565
|
+
} else if (['timestamp', 'DateTime', 'Date'].includes(f.type)) {
|
|
452
566
|
colDecorator = `@Column({ type: 'timestamp', nullable: ${f.isOptional} })`;
|
|
453
|
-
|
|
567
|
+
} else if (['json', 'Json', 'Object'].includes(f.type)) {
|
|
568
|
+
colDecorator = `@Column({ type: 'json', nullable: ${f.isOptional} })`;
|
|
569
|
+
} else if (['text'].includes(f.type)) {
|
|
570
|
+
colDecorator = `@Column({ type: 'text', nullable: ${f.isOptional} })`;
|
|
454
571
|
}
|
|
455
572
|
|
|
456
|
-
return ` ${colDecorator}\n ${f.name}${f.isOptional ? '?' : ''}: ${tsType};`;
|
|
573
|
+
return ` ${colDecorator}\n ${f.name}${f.isOptional ? '?' : ''}: ${details.tsType};`;
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
const relLines = relations.map((r) => {
|
|
577
|
+
const targetPascal = toSingularPascal(toPascalCase(r.targetModule));
|
|
578
|
+
const targetCamel = toSingularCamel(r.targetModule);
|
|
579
|
+
return ` @ManyToOne(() => ${targetPascal}, { nullable: true })\n @JoinColumn({ name: '${r.fkField}' })\n ${targetCamel}?: any;\n\n @Column({ nullable: true })\n ${r.fkField}?: string;`;
|
|
457
580
|
});
|
|
458
581
|
|
|
459
|
-
const
|
|
582
|
+
const hasRelations = relations.length > 0;
|
|
583
|
+
const imports = [`Entity`, `PrimaryGeneratedColumn`, `Column`, `CreateDateColumn`, `UpdateDateColumn`, `DeleteDateColumn`].concat(hasRelations ? [`ManyToOne`, `JoinColumn`] : []);
|
|
584
|
+
|
|
585
|
+
const entityContent = `import { ${imports.join(', ')} } from 'typeorm';
|
|
460
586
|
|
|
461
587
|
@Entity('${toSnakeCase(kebabName)}')
|
|
462
588
|
export class ${singularPascal} {
|
|
463
589
|
@PrimaryGeneratedColumn('uuid')
|
|
464
|
-
|
|
590
|
+
${primaryKey}: string;
|
|
465
591
|
|
|
466
592
|
${fieldLines.join('\n\n')}
|
|
467
593
|
|
|
468
|
-
@Column({ default: 'ACTIVE' })
|
|
594
|
+
${relLines.length > 0 ? relLines.join('\n\n') + '\n\n' : ''} @Column({ default: 'ACTIVE' })
|
|
469
595
|
status: string;
|
|
470
596
|
|
|
471
597
|
@CreateDateColumn()
|
|
@@ -480,49 +606,57 @@ ${fieldLines.join('\n\n')}
|
|
|
480
606
|
`;
|
|
481
607
|
|
|
482
608
|
await fs.writeFile(entityPath, entityContent, 'utf8');
|
|
483
|
-
console.log(chalk.green(`
|
|
609
|
+
console.log(chalk.green(` ✓ Generated TypeORM entity at src/modules/${kebabName}/entities/${toSingularKebab(kebabName)}.entity.ts`));
|
|
484
610
|
} catch (error) {
|
|
485
|
-
console.warn(chalk.yellow(`
|
|
611
|
+
console.warn(chalk.yellow(` ⚠️ Could not generate TypeORM entity: ${error.message}`));
|
|
486
612
|
}
|
|
487
613
|
}
|
|
488
614
|
|
|
489
615
|
/**
|
|
490
616
|
* Dynamic ORM Schema Synchronization: Mongoose
|
|
491
617
|
*/
|
|
492
|
-
async function syncMongooseSchema(moduleDir, singularPascal, kebabName, fields) {
|
|
618
|
+
async function syncMongooseSchema(moduleDir, singularPascal, kebabName, primaryKey, fields, relations) {
|
|
493
619
|
try {
|
|
494
620
|
const schemaDir = path.join(moduleDir, 'schemas');
|
|
495
621
|
await fs.ensureDir(schemaDir);
|
|
496
622
|
const schemaPath = path.join(schemaDir, `${toSingularKebab(kebabName)}.schema.ts`);
|
|
497
623
|
|
|
498
624
|
const fieldLines = fields.map((f) => {
|
|
625
|
+
const details = getFieldDetails(f.type);
|
|
499
626
|
let propDecorator = `@Prop({ required: ${!f.isOptional} })`;
|
|
500
|
-
let tsType = 'string';
|
|
501
627
|
|
|
502
|
-
if (f.type
|
|
503
|
-
propDecorator = `@Prop({ required: ${!f.isOptional} })`;
|
|
504
|
-
tsType = 'number';
|
|
505
|
-
} else if (f.type === 'Boolean') {
|
|
628
|
+
if (['boolean', 'Boolean'].includes(f.type)) {
|
|
506
629
|
propDecorator = `@Prop({ default: false })`;
|
|
507
|
-
|
|
508
|
-
} else if (f.type === 'Date') {
|
|
630
|
+
} else if (['timestamp', 'DateTime', 'Date'].includes(f.type)) {
|
|
509
631
|
propDecorator = `@Prop({ type: Date, required: ${!f.isOptional} })`;
|
|
510
|
-
|
|
632
|
+
} else if (['json', 'Json', 'Object'].includes(f.type)) {
|
|
633
|
+
propDecorator = `@Prop({ type: Object, required: ${!f.isOptional} })`;
|
|
634
|
+
} else if (f.type === 'Array') {
|
|
635
|
+
propDecorator = `@Prop({ type: [String], default: [] })`;
|
|
511
636
|
}
|
|
512
637
|
|
|
513
|
-
return ` ${propDecorator}\n ${f.name}${f.isOptional ? '?' : ''}: ${tsType};`;
|
|
638
|
+
return ` ${propDecorator}\n ${f.name}${f.isOptional ? '?' : ''}: ${details.tsType};`;
|
|
514
639
|
});
|
|
515
640
|
|
|
641
|
+
const relLines = relations.map((r) => {
|
|
642
|
+
const targetPascal = toSingularPascal(toPascalCase(r.targetModule));
|
|
643
|
+
return ` @Prop({ type: SchemaTypes.ObjectId, ref: '${targetPascal}', default: null })\n ${r.fkField}?: string;`;
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
const pkLine = primaryKey !== 'id'
|
|
647
|
+
? ` @Prop({ default: () => new Types.ObjectId().toString() })\n ${primaryKey}: string;\n\n`
|
|
648
|
+
: '';
|
|
649
|
+
|
|
516
650
|
const schemaContent = `import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
|
517
|
-
import { HydratedDocument } from 'mongoose';
|
|
651
|
+
import { HydratedDocument, SchemaTypes, Types } from 'mongoose';
|
|
518
652
|
|
|
519
653
|
export type ${singularPascal}Document = HydratedDocument<${singularPascal}>;
|
|
520
654
|
|
|
521
655
|
@Schema({ timestamps: true })
|
|
522
656
|
export class ${singularPascal} {
|
|
523
|
-
${fieldLines.join('\n\n')}
|
|
657
|
+
${pkLine}${fieldLines.join('\n\n')}
|
|
524
658
|
|
|
525
|
-
@Prop({ default: 'ACTIVE' })
|
|
659
|
+
${relLines.length > 0 ? relLines.join('\n\n') + '\n\n' : ''} @Prop({ default: 'ACTIVE' })
|
|
526
660
|
status: string;
|
|
527
661
|
|
|
528
662
|
@Prop({ type: Date, default: null })
|
|
@@ -533,16 +667,16 @@ export const ${singularPascal}Schema = SchemaFactory.createForClass(${singularPa
|
|
|
533
667
|
`;
|
|
534
668
|
|
|
535
669
|
await fs.writeFile(schemaPath, schemaContent, 'utf8');
|
|
536
|
-
console.log(chalk.green(`
|
|
670
|
+
console.log(chalk.green(` ✓ Generated Mongoose schema at src/modules/${kebabName}/schemas/${toSingularKebab(kebabName)}.schema.ts`));
|
|
537
671
|
} catch (error) {
|
|
538
|
-
console.warn(chalk.yellow(`
|
|
672
|
+
console.warn(chalk.yellow(` ⚠️ Could not generate Mongoose schema: ${error.message}`));
|
|
539
673
|
}
|
|
540
674
|
}
|
|
541
675
|
|
|
542
676
|
/**
|
|
543
677
|
* Dynamic ORM Schema Synchronization: Drizzle
|
|
544
678
|
*/
|
|
545
|
-
async function syncDrizzleSchema(moduleDir, singularPascal, kebabName, fields) {
|
|
679
|
+
async function syncDrizzleSchema(moduleDir, singularPascal, kebabName, primaryKey, fields, relations) {
|
|
546
680
|
try {
|
|
547
681
|
const schemaDir = path.join(moduleDir, 'schema');
|
|
548
682
|
await fs.ensureDir(schemaDir);
|
|
@@ -550,12 +684,18 @@ async function syncDrizzleSchema(moduleDir, singularPascal, kebabName, fields) {
|
|
|
550
684
|
|
|
551
685
|
const fieldLines = fields.map((f) => {
|
|
552
686
|
let colDef = `varchar('${toSnakeCase(f.name)}', { length: 255 })`;
|
|
553
|
-
if (
|
|
687
|
+
if (['int', 'integer', 'Int'].includes(f.type)) {
|
|
688
|
+
colDef = `integer('${toSnakeCase(f.name)}')`;
|
|
689
|
+
} else if (['float', 'decimal', 'numeric', 'Float', 'Decimal', 'Number'].includes(f.type)) {
|
|
554
690
|
colDef = `numeric('${toSnakeCase(f.name)}')`;
|
|
555
|
-
} else if (
|
|
691
|
+
} else if (['boolean', 'Boolean'].includes(f.type)) {
|
|
556
692
|
colDef = `boolean('${toSnakeCase(f.name)}').default(false)`;
|
|
557
|
-
} else if (
|
|
693
|
+
} else if (['timestamp', 'DateTime', 'Date'].includes(f.type)) {
|
|
558
694
|
colDef = `timestamp('${toSnakeCase(f.name)}')`;
|
|
695
|
+
} else if (['json', 'Json', 'Object'].includes(f.type)) {
|
|
696
|
+
colDef = `json('${toSnakeCase(f.name)}')`;
|
|
697
|
+
} else if (f.type === 'text') {
|
|
698
|
+
colDef = `text('${toSnakeCase(f.name)}')`;
|
|
559
699
|
}
|
|
560
700
|
|
|
561
701
|
if (!f.isOptional) {
|
|
@@ -565,12 +705,16 @@ async function syncDrizzleSchema(moduleDir, singularPascal, kebabName, fields) {
|
|
|
565
705
|
return ` ${f.name}: ${colDef},`;
|
|
566
706
|
});
|
|
567
707
|
|
|
568
|
-
const
|
|
708
|
+
const relLines = relations.map((r) => {
|
|
709
|
+
return ` ${r.fkField}: varchar('${toSnakeCase(r.fkField)}', { length: 36 }),`;
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
const schemaContent = `import { pgTable, varchar, text, integer, numeric, boolean, timestamp, json } from 'drizzle-orm/pg-core';
|
|
569
713
|
|
|
570
714
|
export const ${toCamelCase(kebabName)}s = pgTable('${toSnakeCase(kebabName)}', {
|
|
571
|
-
|
|
715
|
+
${primaryKey}: varchar('${toSnakeCase(primaryKey)}', { length: 36 }).primaryKey().$defaultFn(() => crypto.randomUUID()),
|
|
572
716
|
${fieldLines.join('\n')}
|
|
573
|
-
status: varchar('status', { length: 50 }).default('ACTIVE').notNull(),
|
|
717
|
+
${relLines.length > 0 ? relLines.join('\n') + '\n' : ''} status: varchar('status', { length: 50 }).default('ACTIVE').notNull(),
|
|
574
718
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
575
719
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
576
720
|
deletedAt: timestamp('deleted_at'),
|
|
@@ -581,21 +725,21 @@ export type New${singularPascal} = typeof ${toCamelCase(kebabName)}s.$inferInser
|
|
|
581
725
|
`;
|
|
582
726
|
|
|
583
727
|
await fs.writeFile(schemaPath, schemaContent, 'utf8');
|
|
584
|
-
console.log(chalk.green(`
|
|
728
|
+
console.log(chalk.green(` ✓ Generated Drizzle schema at src/modules/${kebabName}/schema/${kebabName}.schema.ts`));
|
|
585
729
|
} catch (error) {
|
|
586
|
-
console.warn(chalk.yellow(`
|
|
730
|
+
console.warn(chalk.yellow(` ⚠️ Could not generate Drizzle schema: ${error.message}`));
|
|
587
731
|
}
|
|
588
732
|
}
|
|
589
733
|
|
|
590
734
|
/**
|
|
591
735
|
* Auto-Generate Starter Seed File Template
|
|
592
736
|
*/
|
|
593
|
-
async function generateSeedFileTemplate(targetDir, orm, singularPascal, kebabName, fields) {
|
|
737
|
+
async function generateSeedFileTemplate(targetDir, orm, singularPascal, kebabName, primaryKey, fields) {
|
|
594
738
|
try {
|
|
595
739
|
const singularCamel = toSingularCamel(kebabName);
|
|
596
740
|
const dummyObjFields = fields.map((f) => {
|
|
597
741
|
const ex = getFieldExampleValue(f, singularPascal);
|
|
598
|
-
const valStr = typeof ex === 'string' ? `'${ex}'` : ex;
|
|
742
|
+
const valStr = typeof ex === 'string' ? `'${ex}'` : JSON.stringify(ex);
|
|
599
743
|
return ` ${f.name}: ${valStr},`;
|
|
600
744
|
}).join('\n');
|
|
601
745
|
|
|
@@ -617,7 +761,7 @@ ${dummyObjFields}
|
|
|
617
761
|
}
|
|
618
762
|
`;
|
|
619
763
|
await fs.writeFile(seedPath, seedContent, 'utf8');
|
|
620
|
-
console.log(chalk.green(`
|
|
764
|
+
console.log(chalk.green(` ✓ Generated Prisma seed template at prisma/seeds/${kebabName}.seed.ts`));
|
|
621
765
|
} else {
|
|
622
766
|
const seedsDir = path.join(targetDir, 'src', 'database', 'seeds');
|
|
623
767
|
await fs.ensureDir(seedsDir);
|
|
@@ -641,14 +785,14 @@ ${dummyObjFields}
|
|
|
641
785
|
}
|
|
642
786
|
`;
|
|
643
787
|
await fs.writeFile(seedPath, seedContent, 'utf8');
|
|
644
|
-
console.log(chalk.green(`
|
|
788
|
+
console.log(chalk.green(` ✓ Generated seed template at src/database/seeds/${kebabName}.seed.ts`));
|
|
645
789
|
}
|
|
646
790
|
} catch (error) {
|
|
647
|
-
console.warn(chalk.yellow(`
|
|
791
|
+
console.warn(chalk.yellow(` ⚠️ Could not generate seed template: ${error.message}`));
|
|
648
792
|
}
|
|
649
793
|
}
|
|
650
794
|
|
|
651
|
-
function getServiceContent(orm, pascalName, singularPascal, camelName, kebabName, createDtoName, updateDtoName, ops) {
|
|
795
|
+
function getServiceContent(orm, pascalName, singularPascal, camelName, kebabName, primaryKey, createDtoName, updateDtoName, ops) {
|
|
652
796
|
const singularKebab = toSingularKebab(kebabName);
|
|
653
797
|
|
|
654
798
|
if (orm === 'typeorm') {
|
|
@@ -676,20 +820,20 @@ class TypeOrm${pascalName}Repository implements IBaseRepository<${pascalName}Ent
|
|
|
676
820
|
return { data, total };
|
|
677
821
|
}` : `async findAll(p: PaginationQueryDto): Promise<{ data: ${pascalName}Entity[]; total: number }> { throw new Error('Not implemented'); }`}
|
|
678
822
|
|
|
679
|
-
${ops.findOne ? `async findOne(
|
|
680
|
-
return this.repo.findOne({ where: {
|
|
681
|
-
}` : `async findOne(
|
|
823
|
+
${ops.findOne ? `async findOne(${primaryKey}: string): Promise<${pascalName}Entity | null> {
|
|
824
|
+
return this.repo.findOne({ where: { ${primaryKey}, deletedAt: IsNull() } as any });
|
|
825
|
+
}` : `async findOne(${primaryKey}: string): Promise<${pascalName}Entity | null> { throw new Error('Not implemented'); }`}
|
|
682
826
|
|
|
683
|
-
${ops.update ? `async update(
|
|
684
|
-
await this.repo.update(
|
|
685
|
-
return this.repo.findOneOrFail({ where: {
|
|
686
|
-
}` : `async update(
|
|
827
|
+
${ops.update ? `async update(${primaryKey}: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> {
|
|
828
|
+
await this.repo.update(${primaryKey}, dto as any);
|
|
829
|
+
return this.repo.findOneOrFail({ where: { ${primaryKey} } as any });
|
|
830
|
+
}` : `async update(${primaryKey}: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
|
|
687
831
|
|
|
688
|
-
${ops.remove ? `async remove(
|
|
689
|
-
const entity = await this.repo.findOneOrFail({ where: {
|
|
832
|
+
${ops.remove ? `async remove(${primaryKey}: string): Promise<${pascalName}Entity> {
|
|
833
|
+
const entity = await this.repo.findOneOrFail({ where: { ${primaryKey} } as any });
|
|
690
834
|
entity.deletedAt = new Date();
|
|
691
835
|
return this.repo.save(entity);
|
|
692
|
-
}` : `async remove(
|
|
836
|
+
}` : `async remove(${primaryKey}: string): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
|
|
693
837
|
}
|
|
694
838
|
|
|
695
839
|
@Injectable()
|
|
@@ -734,17 +878,17 @@ class Mongoose${pascalName}Repository implements IBaseRepository<${pascalName}En
|
|
|
734
878
|
return { data, total };
|
|
735
879
|
}` : `async findAll(p: PaginationQueryDto): Promise<{ data: ${pascalName}Entity[]; total: number }> { throw new Error('Not implemented'); }`}
|
|
736
880
|
|
|
737
|
-
${ops.findOne ? `async findOne(
|
|
738
|
-
return this.model.findOne({ _id:
|
|
739
|
-
}` : `async findOne(
|
|
881
|
+
${ops.findOne ? `async findOne(${primaryKey}: string): Promise<${pascalName}Entity | null> {
|
|
882
|
+
return this.model.findOne({ ${primaryKey === 'id' ? '_id' : primaryKey}: ${primaryKey}, deletedAt: null }).exec();
|
|
883
|
+
}` : `async findOne(${primaryKey}: string): Promise<${pascalName}Entity | null> { throw new Error('Not implemented'); }`}
|
|
740
884
|
|
|
741
|
-
${ops.update ? `async update(
|
|
742
|
-
return this.model.
|
|
743
|
-
}` : `async update(
|
|
885
|
+
${ops.update ? `async update(${primaryKey}: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> {
|
|
886
|
+
return this.model.findOneAndUpdate({ ${primaryKey === 'id' ? '_id' : primaryKey}: ${primaryKey} }, dto as any, { new: true }).exec() as Promise<${pascalName}Entity>;
|
|
887
|
+
}` : `async update(${primaryKey}: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
|
|
744
888
|
|
|
745
|
-
${ops.remove ? `async remove(
|
|
746
|
-
return this.model.
|
|
747
|
-
}` : `async remove(
|
|
889
|
+
${ops.remove ? `async remove(${primaryKey}: string): Promise<${pascalName}Entity> {
|
|
890
|
+
return this.model.findOneAndUpdate({ ${primaryKey === 'id' ? '_id' : primaryKey}: ${primaryKey} }, { deletedAt: new Date() }, { new: true }).exec() as Promise<${pascalName}Entity>;
|
|
891
|
+
}` : `async remove(${primaryKey}: string): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
|
|
748
892
|
}
|
|
749
893
|
|
|
750
894
|
@Injectable()
|
|
@@ -790,21 +934,21 @@ class Drizzle${pascalName}Repository implements IBaseRepository<${pascalName}Ent
|
|
|
790
934
|
return { data, total: 0 };
|
|
791
935
|
}` : `async findAll(p: PaginationQueryDto): Promise<{ data: ${pascalName}Entity[]; total: number }> { throw new Error('Not implemented'); }`}
|
|
792
936
|
|
|
793
|
-
${ops.findOne ? `async findOne(
|
|
937
|
+
${ops.findOne ? `async findOne(${primaryKey}: string): Promise<${pascalName}Entity | null> {
|
|
794
938
|
const [result] = await this.db.select().from(${camelName}s)
|
|
795
|
-
.where(eq(${camelName}s
|
|
939
|
+
.where(eq(${camelName}s.${primaryKey}, ${primaryKey}));
|
|
796
940
|
return result || null;
|
|
797
|
-
}` : `async findOne(
|
|
941
|
+
}` : `async findOne(${primaryKey}: string): Promise<${pascalName}Entity | null> { throw new Error('Not implemented'); }`}
|
|
798
942
|
|
|
799
|
-
${ops.update ? `async update(
|
|
800
|
-
const [result] = await this.db.update(${camelName}s).set(dto).where(eq(${camelName}s
|
|
943
|
+
${ops.update ? `async update(${primaryKey}: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> {
|
|
944
|
+
const [result] = await this.db.update(${camelName}s).set(dto).where(eq(${camelName}s.${primaryKey}, ${primaryKey})).returning();
|
|
801
945
|
return result;
|
|
802
|
-
}` : `async update(
|
|
946
|
+
}` : `async update(${primaryKey}: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
|
|
803
947
|
|
|
804
|
-
${ops.remove ? `async remove(
|
|
805
|
-
const [result] = await this.db.update(${camelName}s).set({ deletedAt: new Date() }).where(eq(${camelName}s
|
|
948
|
+
${ops.remove ? `async remove(${primaryKey}: string): Promise<${pascalName}Entity> {
|
|
949
|
+
const [result] = await this.db.update(${camelName}s).set({ deletedAt: new Date() }).where(eq(${camelName}s.${primaryKey}, ${primaryKey})).returning();
|
|
806
950
|
return result;
|
|
807
|
-
}` : `async remove(
|
|
951
|
+
}` : `async remove(${primaryKey}: string): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
|
|
808
952
|
}
|
|
809
953
|
|
|
810
954
|
@Injectable()
|
|
@@ -850,17 +994,17 @@ class Prisma${pascalName}Repository implements IBaseRepository<${pascalName}Enti
|
|
|
850
994
|
return { data, total };
|
|
851
995
|
}` : `async findAll(p: PaginationQueryDto): Promise<{ data: ${pascalName}Entity[]; total: number }> { throw new Error('Not implemented'); }`}
|
|
852
996
|
|
|
853
|
-
${ops.findOne ? `async findOne(
|
|
854
|
-
return (this.prisma as any).${singularCamel}.findFirst({ where: {
|
|
855
|
-
}` : `async findOne(
|
|
997
|
+
${ops.findOne ? `async findOne(${primaryKey}: string): Promise<${pascalName}Entity | null> {
|
|
998
|
+
return (this.prisma as any).${singularCamel}.findFirst({ where: { ${primaryKey}, deletedAt: null } });
|
|
999
|
+
}` : `async findOne(${primaryKey}: string): Promise<${pascalName}Entity | null> { throw new Error('Not implemented'); }`}
|
|
856
1000
|
|
|
857
|
-
${ops.update ? `async update(
|
|
858
|
-
return (this.prisma as any).${singularCamel}.update({ where: {
|
|
859
|
-
}` : `async update(
|
|
1001
|
+
${ops.update ? `async update(${primaryKey}: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> {
|
|
1002
|
+
return (this.prisma as any).${singularCamel}.update({ where: { ${primaryKey} }, data: dto });
|
|
1003
|
+
}` : `async update(${primaryKey}: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
|
|
860
1004
|
|
|
861
|
-
${ops.remove ? `async remove(
|
|
862
|
-
return (this.prisma as any).${singularCamel}.update({ where: {
|
|
863
|
-
}` : `async remove(
|
|
1005
|
+
${ops.remove ? `async remove(${primaryKey}: string): Promise<${pascalName}Entity> {
|
|
1006
|
+
return (this.prisma as any).${singularCamel}.update({ where: { ${primaryKey} }, data: { deletedAt: new Date() } });
|
|
1007
|
+
}` : `async remove(${primaryKey}: string): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
|
|
864
1008
|
}
|
|
865
1009
|
|
|
866
1010
|
@Injectable()
|
|
@@ -982,17 +1126,18 @@ async function registerInAppModule(targetDir, pascalName, kebabName) {
|
|
|
982
1126
|
*/
|
|
983
1127
|
async function generateModule(providedModuleName, targetDir = process.cwd(), specifiedOrm = null) {
|
|
984
1128
|
try {
|
|
985
|
-
// Ensure src/common/base
|
|
1129
|
+
// 0. Ensure Base Architecture exists at src/common/base
|
|
986
1130
|
await ensureBaseArchitecture(targetDir);
|
|
987
1131
|
|
|
988
|
-
|
|
1132
|
+
// Detect ORM first so prompt choices match
|
|
1133
|
+
const orm = specifiedOrm || (await detectOrm(targetDir));
|
|
1134
|
+
|
|
1135
|
+
const options = await promptForModuleOptions(providedModuleName, orm);
|
|
989
1136
|
const kebabName = toKebabCase(options.moduleName);
|
|
990
1137
|
const pascalName = toPascalCase(options.moduleName);
|
|
991
1138
|
const camelName = toCamelCase(options.moduleName);
|
|
992
1139
|
const singularPascal = toSingularPascal(pascalName);
|
|
993
|
-
|
|
994
|
-
// Detect ORM
|
|
995
|
-
const orm = specifiedOrm || (await detectOrm(targetDir));
|
|
1140
|
+
const primaryKey = options.primaryKey || 'id';
|
|
996
1141
|
|
|
997
1142
|
// Ensure inside a NestJS project structure
|
|
998
1143
|
const srcDir = path.join(targetDir, 'src');
|
|
@@ -1018,13 +1163,13 @@ async function generateModule(providedModuleName, targetDir = process.cwd(), spe
|
|
|
1018
1163
|
|
|
1019
1164
|
// 1. Dynamic ORM Schema Synchronization
|
|
1020
1165
|
if (orm === 'prisma') {
|
|
1021
|
-
await syncPrismaSchema(targetDir, singularPascal, kebabName, options.fields);
|
|
1166
|
+
await syncPrismaSchema(targetDir, singularPascal, kebabName, primaryKey, options.fields, options.relations);
|
|
1022
1167
|
} else if (orm === 'typeorm') {
|
|
1023
|
-
await syncTypeOrmSchema(moduleDir, singularPascal, kebabName, options.fields);
|
|
1168
|
+
await syncTypeOrmSchema(moduleDir, singularPascal, kebabName, primaryKey, options.fields, options.relations);
|
|
1024
1169
|
} else if (orm === 'mongoose') {
|
|
1025
|
-
await syncMongooseSchema(moduleDir, singularPascal, kebabName, options.fields);
|
|
1170
|
+
await syncMongooseSchema(moduleDir, singularPascal, kebabName, primaryKey, options.fields, options.relations);
|
|
1026
1171
|
} else if (orm === 'drizzle') {
|
|
1027
|
-
await syncDrizzleSchema(moduleDir, singularPascal, kebabName, options.fields);
|
|
1172
|
+
await syncDrizzleSchema(moduleDir, singularPascal, kebabName, primaryKey, options.fields, options.relations);
|
|
1028
1173
|
}
|
|
1029
1174
|
|
|
1030
1175
|
// 2. Generate DTOs
|
|
@@ -1033,39 +1178,41 @@ async function generateModule(providedModuleName, targetDir = process.cwd(), spe
|
|
|
1033
1178
|
const responseDtoName = `${pascalName}Dto`;
|
|
1034
1179
|
|
|
1035
1180
|
const createFieldsText = options.fields.map((f) => {
|
|
1036
|
-
const
|
|
1181
|
+
const details = getFieldDetails(f.type);
|
|
1037
1182
|
const ex = getFieldExampleValue(f, pascalName);
|
|
1038
|
-
const exValStr = typeof ex === 'string' ? `'${ex}'` : ex;
|
|
1183
|
+
const exValStr = typeof ex === 'string' ? `'${ex}'` : JSON.stringify(ex);
|
|
1039
1184
|
|
|
1040
1185
|
const swaggerDecorator = f.isOptional
|
|
1041
1186
|
? `@ApiPropertyOptional({ description: '${f.name} property', example: ${exValStr} })`
|
|
1042
1187
|
: `@ApiProperty({ description: '${f.name} property', example: ${exValStr} })`;
|
|
1043
1188
|
|
|
1044
|
-
const valDecorators = [];
|
|
1045
|
-
if (f.type === 'String') valDecorators.push('@IsString()');
|
|
1046
|
-
if (f.type === 'Number') valDecorators.push('@IsNumber()');
|
|
1047
|
-
if (f.type === 'Boolean') valDecorators.push('@IsBoolean()');
|
|
1048
|
-
if (f.type === 'Date') {
|
|
1049
|
-
valDecorators.push('@IsDate()');
|
|
1050
|
-
valDecorators.push('@Type(() => Date)');
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1189
|
+
const valDecorators = [...details.valDecorators];
|
|
1053
1190
|
if (f.isOptional) {
|
|
1054
1191
|
valDecorators.push('@IsOptional()');
|
|
1055
1192
|
} else {
|
|
1056
1193
|
valDecorators.push('@IsNotEmpty()');
|
|
1057
1194
|
}
|
|
1058
1195
|
|
|
1059
|
-
return ` ${swaggerDecorator}\n ${valDecorators.join('\n ')}\n ${f.name}${f.isOptional ? '?' : ''}: ${tsType};`;
|
|
1196
|
+
return ` ${swaggerDecorator}\n ${valDecorators.join('\n ')}\n ${f.name}${f.isOptional ? '?' : ''}: ${details.tsType};`;
|
|
1060
1197
|
}).join('\n\n');
|
|
1061
1198
|
|
|
1062
|
-
const hasDateFields = options.fields.some((f) =>
|
|
1199
|
+
const hasDateFields = options.fields.some((f) => ['DateTime', 'timestamp', 'Date'].includes(f.type));
|
|
1200
|
+
|
|
1201
|
+
// Validator import gathering
|
|
1202
|
+
const allValDecorators = new Set(['IsOptional', 'IsNotEmpty']);
|
|
1203
|
+
options.fields.forEach((f) => {
|
|
1204
|
+
const details = getFieldDetails(f.type);
|
|
1205
|
+
details.valDecorators.forEach((dec) => {
|
|
1206
|
+
const name = dec.replace('@', '').replace(/\(.*\)/, '');
|
|
1207
|
+
if (name) allValDecorators.add(name);
|
|
1208
|
+
});
|
|
1209
|
+
});
|
|
1063
1210
|
|
|
1064
1211
|
if (ops.create || ops.update) {
|
|
1065
1212
|
await fs.writeFile(
|
|
1066
1213
|
path.join(dtoDir, `create-${kebabName}.dto.ts`),
|
|
1067
1214
|
`import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
1068
|
-
import {
|
|
1215
|
+
import { ${Array.from(allValDecorators).join(', ')} } from 'class-validator';
|
|
1069
1216
|
${hasDateFields ? `import { Type } from 'class-transformer';\n` : ''}
|
|
1070
1217
|
export class ${createDtoName} {
|
|
1071
1218
|
${createFieldsText}
|
|
@@ -1084,15 +1231,15 @@ export class ${updateDtoName} extends PartialType(${createDtoName}) {}
|
|
|
1084
1231
|
}
|
|
1085
1232
|
|
|
1086
1233
|
const responseFieldsText = options.fields.map((f) => {
|
|
1087
|
-
const
|
|
1234
|
+
const details = getFieldDetails(f.type);
|
|
1088
1235
|
const ex = getFieldExampleValue(f, pascalName);
|
|
1089
|
-
const exValStr = typeof ex === 'string' ? `'${ex}'` : ex;
|
|
1236
|
+
const exValStr = typeof ex === 'string' ? `'${ex}'` : JSON.stringify(ex);
|
|
1090
1237
|
|
|
1091
1238
|
const swaggerDecorator = f.isOptional
|
|
1092
1239
|
? `@ApiPropertyOptional({ example: ${exValStr} })`
|
|
1093
1240
|
: `@ApiProperty({ example: ${exValStr} })`;
|
|
1094
1241
|
|
|
1095
|
-
return ` ${swaggerDecorator}\n ${f.name}${f.isOptional ? '?' : ''}: ${tsType};`;
|
|
1242
|
+
return ` ${swaggerDecorator}\n ${f.name}${f.isOptional ? '?' : ''}: ${details.tsType};`;
|
|
1096
1243
|
}).join('\n\n');
|
|
1097
1244
|
|
|
1098
1245
|
await fs.writeFile(
|
|
@@ -1101,7 +1248,7 @@ export class ${updateDtoName} extends PartialType(${createDtoName}) {}
|
|
|
1101
1248
|
|
|
1102
1249
|
export class ${responseDtoName} {
|
|
1103
1250
|
@ApiProperty({ example: '123e4567-e89b-12d3-a456-426614174000', format: 'uuid' })
|
|
1104
|
-
|
|
1251
|
+
${primaryKey}: string;
|
|
1105
1252
|
|
|
1106
1253
|
${responseFieldsText}
|
|
1107
1254
|
|
|
@@ -1127,14 +1274,20 @@ ${responseFieldsText}
|
|
|
1127
1274
|
singularPascal,
|
|
1128
1275
|
camelName,
|
|
1129
1276
|
kebabName,
|
|
1277
|
+
primaryKey,
|
|
1130
1278
|
createDtoName,
|
|
1131
1279
|
updateDtoName,
|
|
1132
1280
|
ops
|
|
1133
1281
|
);
|
|
1134
1282
|
await fs.writeFile(path.join(moduleDir, `${kebabName}.service.ts`), serviceContent);
|
|
1135
1283
|
|
|
1136
|
-
// 4. Generate Controller
|
|
1137
|
-
const
|
|
1284
|
+
// 4. Generate Controller
|
|
1285
|
+
const guardImports = options.protectWriteOps ? `, UseGuards` : '';
|
|
1286
|
+
const guardDecorator = options.protectWriteOps && options.roles?.length > 0
|
|
1287
|
+
? `\n @UseGuards()\n // Roles: ${options.roles.join(', ')}`
|
|
1288
|
+
: '';
|
|
1289
|
+
|
|
1290
|
+
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';
|
|
1138
1291
|
import { ApiTags, ApiBearerAuth, ApiExtraModels, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';
|
|
1139
1292
|
import { BaseController, ApiResponseDto, ApiResponseSchema, PaginatedResponseDto, PaginatedResponseSchema, PaginationQueryDto } from '../../common/base';
|
|
1140
1293
|
import { ${pascalName}Service } from './${kebabName}.service';
|
|
@@ -1156,7 +1309,7 @@ export class ${pascalName}Controller extends BaseController<${pascalName}Entity,
|
|
|
1156
1309
|
return ${responseDtoName} as unknown as Type<${pascalName}Entity>;
|
|
1157
1310
|
}
|
|
1158
1311
|
${ops.create ? `
|
|
1159
|
-
@Post()
|
|
1312
|
+
@Post()${guardDecorator}
|
|
1160
1313
|
@ApiOperation({ summary: 'Create a new ${kebabName}' })
|
|
1161
1314
|
@ApiResponse({ status: HttpStatus.CREATED, schema: ApiResponseSchema(${responseDtoName}) })
|
|
1162
1315
|
override async create(@Body() dto: ${createDtoName}): Promise<ApiResponseDto<${pascalName}Entity>> {
|
|
@@ -1170,34 +1323,34 @@ ${ops.create ? `
|
|
|
1170
1323
|
return super.findAll(pagination);
|
|
1171
1324
|
}
|
|
1172
1325
|
` : ''}${ops.findOne ? `
|
|
1173
|
-
@Get('
|
|
1326
|
+
@Get(':${primaryKey}')
|
|
1174
1327
|
@ApiOperation({ summary: 'Get ${kebabName} by ID' })
|
|
1175
|
-
@ApiParam({ name: '
|
|
1328
|
+
@ApiParam({ name: '${primaryKey}', format: 'uuid' })
|
|
1176
1329
|
@ApiResponse({ status: HttpStatus.OK, schema: ApiResponseSchema(${responseDtoName}) })
|
|
1177
1330
|
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: '${pascalName} not found' })
|
|
1178
|
-
override async findOne(@Param('
|
|
1179
|
-
return super.findOne(
|
|
1331
|
+
override async findOne(@Param('${primaryKey}', new ParseUUIDPipe({ version: '4', errorHttpStatusCode: HttpStatus.BAD_REQUEST })) ${primaryKey}: string): Promise<ApiResponseDto<${pascalName}Entity>> {
|
|
1332
|
+
return super.findOne(${primaryKey});
|
|
1180
1333
|
}
|
|
1181
1334
|
` : ''}${ops.update ? `
|
|
1182
|
-
@Put('
|
|
1335
|
+
@Put(':${primaryKey}')${guardDecorator}
|
|
1183
1336
|
@ApiOperation({ summary: 'Update ${kebabName} by ID' })
|
|
1184
|
-
@ApiParam({ name: '
|
|
1337
|
+
@ApiParam({ name: '${primaryKey}', format: 'uuid' })
|
|
1185
1338
|
@ApiResponse({ status: HttpStatus.OK, schema: ApiResponseSchema(${responseDtoName}) })
|
|
1186
1339
|
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: '${pascalName} not found' })
|
|
1187
1340
|
override async update(
|
|
1188
|
-
@Param('
|
|
1341
|
+
@Param('${primaryKey}', new ParseUUIDPipe({ version: '4', errorHttpStatusCode: HttpStatus.BAD_REQUEST })) ${primaryKey}: string,
|
|
1189
1342
|
@Body() dto: ${updateDtoName}
|
|
1190
1343
|
): Promise<ApiResponseDto<${pascalName}Entity>> {
|
|
1191
|
-
return super.update(
|
|
1344
|
+
return super.update(${primaryKey}, dto);
|
|
1192
1345
|
}
|
|
1193
1346
|
` : ''}${ops.remove ? `
|
|
1194
|
-
@Delete('
|
|
1347
|
+
@Delete(':${primaryKey}')${guardDecorator}
|
|
1195
1348
|
@ApiOperation({ summary: 'Delete ${kebabName} by ID' })
|
|
1196
|
-
@ApiParam({ name: '
|
|
1349
|
+
@ApiParam({ name: '${primaryKey}', format: 'uuid' })
|
|
1197
1350
|
@ApiResponse({ status: HttpStatus.OK, schema: ApiResponseSchema(${responseDtoName}) })
|
|
1198
1351
|
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: '${pascalName} not found' })
|
|
1199
|
-
override async remove(@Param('
|
|
1200
|
-
return super.remove(
|
|
1352
|
+
override async remove(@Param('${primaryKey}', new ParseUUIDPipe({ version: '4', errorHttpStatusCode: HttpStatus.BAD_REQUEST })) ${primaryKey}: string): Promise<ApiResponseDto<${pascalName}Entity>> {
|
|
1353
|
+
return super.remove(${primaryKey});
|
|
1201
1354
|
}
|
|
1202
1355
|
` : ''}
|
|
1203
1356
|
}
|
|
@@ -1212,10 +1365,10 @@ ${ops.create ? `
|
|
|
1212
1365
|
const isAutoRegistered = await registerInAppModule(targetDir, pascalName, kebabName);
|
|
1213
1366
|
|
|
1214
1367
|
// 7. Auto-Generate Starter Seed File Template
|
|
1215
|
-
await generateSeedFileTemplate(targetDir, orm, singularPascal, kebabName, options.fields);
|
|
1368
|
+
await generateSeedFileTemplate(targetDir, orm, singularPascal, kebabName, primaryKey, options.fields);
|
|
1216
1369
|
|
|
1217
1370
|
console.log(chalk.green(`\n✅ Module "${kebabName}" successfully generated in ${path.relative(process.cwd(), moduleDir)}`));
|
|
1218
|
-
console.log(chalk.gray(`
|
|
1371
|
+
console.log(chalk.gray(` Detected ORM: ${orm}`));
|
|
1219
1372
|
|
|
1220
1373
|
if (!isAutoRegistered) {
|
|
1221
1374
|
console.log(chalk.yellow(`\n⚠️ Please manually register ${pascalName}Module in src/app.module.ts:`));
|
|
@@ -1244,4 +1397,5 @@ module.exports = {
|
|
|
1244
1397
|
promptForModuleOptions,
|
|
1245
1398
|
detectOrm,
|
|
1246
1399
|
registerInAppModule,
|
|
1400
|
+
ensureBaseArchitecture,
|
|
1247
1401
|
};
|