speedrun-cli 2.7.8 → 2.7.10
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 -22
- package/package.json +1 -1
- package/src/index.js +22 -13
- package/src/moduleGenerator.js +631 -342
- package/src/postSetup.js +4 -4
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
|
-
|
|
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
117
|
|
|
145
|
-
|
|
146
|
-
|
|
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,212 +237,309 @@ 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
|
+
}]);
|
|
261
|
+
|
|
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
|
+
}
|
|
215
337
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return await inquirer.prompt([
|
|
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
|
-
});
|
|
370
|
+
fields.push({
|
|
371
|
+
name: fieldAnswers.fieldName.trim(),
|
|
372
|
+
type: fieldAnswers.fieldType,
|
|
373
|
+
isOptional: fieldAnswers.isOptional,
|
|
374
|
+
});
|
|
256
375
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
}]);
|
|
272
|
-
|
|
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;
|
|
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. Status Field Prompt
|
|
448
|
+
const { includeStatus } = await inquirer.prompt([{
|
|
449
|
+
type: 'confirm',
|
|
450
|
+
name: 'includeStatus',
|
|
451
|
+
message: "Include default 'status' field (e.g. ACTIVE)?",
|
|
452
|
+
default: true,
|
|
453
|
+
}]);
|
|
454
|
+
|
|
455
|
+
// 5. Role & Auth Guard Protection Prompt
|
|
456
|
+
const { protectWriteOps } = await inquirer.prompt([{
|
|
457
|
+
type: 'confirm',
|
|
458
|
+
name: 'protectWriteOps',
|
|
459
|
+
message: 'Protect write operations (POST, PUT, DELETE) with Auth/Roles Guard?',
|
|
460
|
+
default: true,
|
|
461
|
+
}]);
|
|
462
|
+
|
|
463
|
+
let selectedRoles = [];
|
|
464
|
+
if (protectWriteOps) {
|
|
465
|
+
const { roles } = await inquirer.prompt([{
|
|
466
|
+
type: 'checkbox',
|
|
467
|
+
name: 'roles',
|
|
468
|
+
message: 'Select allowed roles:',
|
|
469
|
+
choices: [
|
|
470
|
+
{ name: 'ADMIN', value: 'ADMIN', checked: true },
|
|
471
|
+
{ name: 'USER', value: 'USER' },
|
|
472
|
+
{ name: 'MANAGER', value: 'MANAGER' },
|
|
473
|
+
],
|
|
474
|
+
}]);
|
|
475
|
+
selectedRoles = roles.length > 0 ? roles : ['ADMIN'];
|
|
333
476
|
}
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
moduleName,
|
|
480
|
+
operations: selectedOperations,
|
|
481
|
+
primaryKey,
|
|
482
|
+
fields,
|
|
483
|
+
relations,
|
|
484
|
+
includeStatus,
|
|
485
|
+
protectWriteOps,
|
|
486
|
+
roles: selectedRoles,
|
|
487
|
+
};
|
|
334
488
|
}
|
|
335
489
|
|
|
336
490
|
function getFieldExampleValue(field, pascalName) {
|
|
491
|
+
const details = getFieldDetails(field.type);
|
|
337
492
|
const name = field.name.toLowerCase();
|
|
338
|
-
|
|
493
|
+
|
|
494
|
+
if (details.tsType === 'string') {
|
|
339
495
|
if (name.includes('email')) return 'user@example.com';
|
|
340
496
|
if (name.includes('phone')) return '+1234567890';
|
|
341
497
|
if (name.includes('url')) return 'https://example.com';
|
|
342
498
|
if (name.includes('sku') || name.includes('code')) return 'SKU-1001';
|
|
343
499
|
return `Sample ${field.name}`;
|
|
344
500
|
}
|
|
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
|
-
}
|
|
501
|
+
return details.ex;
|
|
369
502
|
}
|
|
370
503
|
|
|
371
504
|
/**
|
|
372
505
|
* Dynamic ORM Schema Synchronization: Prisma
|
|
373
506
|
*/
|
|
374
|
-
async function syncPrismaSchema(targetDir, singularPascal, kebabName, fields) {
|
|
507
|
+
async function syncPrismaSchema(targetDir, singularPascal, kebabName, primaryKey, fields, relations, includeStatus = true) {
|
|
375
508
|
try {
|
|
376
509
|
const schemaPath = path.join(targetDir, 'prisma', 'schema.prisma');
|
|
377
510
|
if (!(await fs.pathExists(schemaPath))) return;
|
|
378
511
|
|
|
379
512
|
let content = await fs.readFile(schemaPath, 'utf8');
|
|
380
513
|
|
|
381
|
-
// Avoid duplicate model definition
|
|
382
514
|
if (new RegExp(`\\bmodel\\s+${singularPascal}\\b`).test(content)) {
|
|
383
515
|
return;
|
|
384
516
|
}
|
|
385
517
|
|
|
386
518
|
const fieldLines = fields.map((f) => {
|
|
387
519
|
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
|
-
}
|
|
520
|
+
if (['Int', 'int', 'integer'].includes(f.type)) pType = 'Int';
|
|
521
|
+
else if (['Float', 'float'].includes(f.type)) pType = 'Float';
|
|
522
|
+
else if (['Decimal', 'decimal', 'numeric'].includes(f.type)) pType = 'Decimal';
|
|
523
|
+
else if (['Boolean', 'boolean'].includes(f.type)) pType = 'Boolean';
|
|
524
|
+
else if (['DateTime', 'timestamp', 'Date'].includes(f.type)) pType = 'DateTime';
|
|
525
|
+
else if (['Json', 'json', 'Object'].includes(f.type)) pType = 'Json';
|
|
526
|
+
|
|
408
527
|
return ` ${f.name} ${pType}${f.isOptional ? '?' : ''}`;
|
|
409
528
|
});
|
|
410
529
|
|
|
530
|
+
const relLines = relations.map((r) => {
|
|
531
|
+
const targetPascal = toSingularPascal(toPascalCase(r.targetModule));
|
|
532
|
+
const targetCamel = toSingularCamel(r.targetModule);
|
|
533
|
+
return ` ${targetCamel} ${targetPascal}? @relation(fields: [${r.fkField}], references: [id])\n ${r.fkField} String?`;
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
const statusLine = includeStatus ? ' status String @default("ACTIVE")\n' : '';
|
|
537
|
+
|
|
411
538
|
const modelDefinition = `
|
|
412
539
|
model ${singularPascal} {
|
|
413
|
-
|
|
540
|
+
${primaryKey} String @id @default(uuid())
|
|
414
541
|
${fieldLines.join('\n')}
|
|
415
|
-
|
|
416
|
-
createdAt DateTime @default(now())
|
|
542
|
+
${relLines.length > 0 ? relLines.join('\n') + '\n' : ''}${statusLine} createdAt DateTime @default(now())
|
|
417
543
|
updatedAt DateTime @updatedAt
|
|
418
544
|
deletedAt DateTime?
|
|
419
545
|
|
|
@@ -423,52 +549,61 @@ ${fieldLines.join('\n')}
|
|
|
423
549
|
|
|
424
550
|
content += modelDefinition;
|
|
425
551
|
await fs.writeFile(schemaPath, content, 'utf8');
|
|
426
|
-
console.log(chalk.green(`
|
|
552
|
+
console.log(chalk.green(` ✓ Updated prisma/schema.prisma with model ${singularPascal}`));
|
|
427
553
|
} catch (error) {
|
|
428
|
-
console.warn(chalk.yellow(`
|
|
554
|
+
console.warn(chalk.yellow(` ⚠️ Could not sync prisma/schema.prisma: ${error.message}`));
|
|
429
555
|
}
|
|
430
556
|
}
|
|
431
557
|
|
|
432
558
|
/**
|
|
433
559
|
* Dynamic ORM Schema Synchronization: TypeORM
|
|
434
560
|
*/
|
|
435
|
-
async function syncTypeOrmSchema(moduleDir, singularPascal, kebabName, fields) {
|
|
561
|
+
async function syncTypeOrmSchema(moduleDir, singularPascal, kebabName, primaryKey, fields, relations, includeStatus = true) {
|
|
436
562
|
try {
|
|
437
563
|
const entityDir = path.join(moduleDir, 'entities');
|
|
438
564
|
await fs.ensureDir(entityDir);
|
|
439
565
|
const entityPath = path.join(entityDir, `${toSingularKebab(kebabName)}.entity.ts`);
|
|
440
566
|
|
|
441
567
|
const fieldLines = fields.map((f) => {
|
|
568
|
+
const details = getFieldDetails(f.type);
|
|
442
569
|
let colDecorator = `@Column({ nullable: ${f.isOptional} })`;
|
|
443
|
-
let tsType = 'string';
|
|
444
570
|
|
|
445
|
-
if (
|
|
571
|
+
if (['decimal', 'numeric', 'Decimal'].includes(f.type)) {
|
|
446
572
|
colDecorator = `@Column('decimal', { precision: 10, scale: 2, nullable: ${f.isOptional} })`;
|
|
447
|
-
|
|
448
|
-
} else if (f.type === 'Boolean') {
|
|
573
|
+
} else if (['boolean', 'Boolean'].includes(f.type)) {
|
|
449
574
|
colDecorator = `@Column({ default: false, nullable: ${f.isOptional} })`;
|
|
450
|
-
|
|
451
|
-
} else if (f.type === 'Date') {
|
|
575
|
+
} else if (['timestamp', 'DateTime', 'Date'].includes(f.type)) {
|
|
452
576
|
colDecorator = `@Column({ type: 'timestamp', nullable: ${f.isOptional} })`;
|
|
453
|
-
|
|
577
|
+
} else if (['json', 'Json', 'Object'].includes(f.type)) {
|
|
578
|
+
colDecorator = `@Column({ type: 'json', nullable: ${f.isOptional} })`;
|
|
579
|
+
} else if (['text'].includes(f.type)) {
|
|
580
|
+
colDecorator = `@Column({ type: 'text', nullable: ${f.isOptional} })`;
|
|
454
581
|
}
|
|
455
582
|
|
|
456
|
-
return ` ${colDecorator}\n ${f.name}${f.isOptional ? '?' : ''}: ${tsType};`;
|
|
583
|
+
return ` ${colDecorator}\n ${f.name}${f.isOptional ? '?' : ''}: ${details.tsType};`;
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
const relLines = relations.map((r) => {
|
|
587
|
+
const targetPascal = toSingularPascal(toPascalCase(r.targetModule));
|
|
588
|
+
const targetCamel = toSingularCamel(r.targetModule);
|
|
589
|
+
return ` @ManyToOne(() => ${targetPascal}, { nullable: true })\n @JoinColumn({ name: '${r.fkField}' })\n ${targetCamel}?: any;\n\n @Column({ nullable: true })\n ${r.fkField}?: string;`;
|
|
457
590
|
});
|
|
458
591
|
|
|
459
|
-
const
|
|
592
|
+
const hasRelations = relations.length > 0;
|
|
593
|
+
const imports = [`Entity`, `PrimaryGeneratedColumn`, `Column`, `CreateDateColumn`, `UpdateDateColumn`, `DeleteDateColumn`].concat(hasRelations ? [`ManyToOne`, `JoinColumn`] : []);
|
|
594
|
+
|
|
595
|
+
const statusLine = includeStatus ? ' @Column({ default: \'ACTIVE\' })\n status: string;\n\n' : '';
|
|
596
|
+
|
|
597
|
+
const entityContent = `import { ${imports.join(', ')} } from 'typeorm';
|
|
460
598
|
|
|
461
599
|
@Entity('${toSnakeCase(kebabName)}')
|
|
462
600
|
export class ${singularPascal} {
|
|
463
601
|
@PrimaryGeneratedColumn('uuid')
|
|
464
|
-
|
|
602
|
+
${primaryKey}: string;
|
|
465
603
|
|
|
466
604
|
${fieldLines.join('\n\n')}
|
|
467
605
|
|
|
468
|
-
|
|
469
|
-
status: string;
|
|
470
|
-
|
|
471
|
-
@CreateDateColumn()
|
|
606
|
+
${relLines.length > 0 ? relLines.join('\n\n') + '\n\n' : ''}${statusLine} @CreateDateColumn()
|
|
472
607
|
createdAt: Date;
|
|
473
608
|
|
|
474
609
|
@UpdateDateColumn()
|
|
@@ -480,52 +615,59 @@ ${fieldLines.join('\n\n')}
|
|
|
480
615
|
`;
|
|
481
616
|
|
|
482
617
|
await fs.writeFile(entityPath, entityContent, 'utf8');
|
|
483
|
-
console.log(chalk.green(`
|
|
618
|
+
console.log(chalk.green(` ✓ Generated TypeORM entity at src/modules/${kebabName}/entities/${toSingularKebab(kebabName)}.entity.ts`));
|
|
484
619
|
} catch (error) {
|
|
485
|
-
console.warn(chalk.yellow(`
|
|
620
|
+
console.warn(chalk.yellow(` ⚠️ Could not generate TypeORM entity: ${error.message}`));
|
|
486
621
|
}
|
|
487
622
|
}
|
|
488
623
|
|
|
489
624
|
/**
|
|
490
625
|
* Dynamic ORM Schema Synchronization: Mongoose
|
|
491
626
|
*/
|
|
492
|
-
async function syncMongooseSchema(moduleDir, singularPascal, kebabName, fields) {
|
|
627
|
+
async function syncMongooseSchema(moduleDir, singularPascal, kebabName, primaryKey, fields, relations, includeStatus = true) {
|
|
493
628
|
try {
|
|
494
629
|
const schemaDir = path.join(moduleDir, 'schemas');
|
|
495
630
|
await fs.ensureDir(schemaDir);
|
|
496
631
|
const schemaPath = path.join(schemaDir, `${toSingularKebab(kebabName)}.schema.ts`);
|
|
497
632
|
|
|
498
633
|
const fieldLines = fields.map((f) => {
|
|
634
|
+
const details = getFieldDetails(f.type);
|
|
499
635
|
let propDecorator = `@Prop({ required: ${!f.isOptional} })`;
|
|
500
|
-
let tsType = 'string';
|
|
501
636
|
|
|
502
|
-
if (f.type
|
|
503
|
-
propDecorator = `@Prop({ required: ${!f.isOptional} })`;
|
|
504
|
-
tsType = 'number';
|
|
505
|
-
} else if (f.type === 'Boolean') {
|
|
637
|
+
if (['boolean', 'Boolean'].includes(f.type)) {
|
|
506
638
|
propDecorator = `@Prop({ default: false })`;
|
|
507
|
-
|
|
508
|
-
} else if (f.type === 'Date') {
|
|
639
|
+
} else if (['timestamp', 'DateTime', 'Date'].includes(f.type)) {
|
|
509
640
|
propDecorator = `@Prop({ type: Date, required: ${!f.isOptional} })`;
|
|
510
|
-
|
|
641
|
+
} else if (['json', 'Json', 'Object'].includes(f.type)) {
|
|
642
|
+
propDecorator = `@Prop({ type: Object, required: ${!f.isOptional} })`;
|
|
643
|
+
} else if (f.type === 'Array') {
|
|
644
|
+
propDecorator = `@Prop({ type: [String], default: [] })`;
|
|
511
645
|
}
|
|
512
646
|
|
|
513
|
-
return ` ${propDecorator}\n ${f.name}${f.isOptional ? '?' : ''}: ${tsType};`;
|
|
647
|
+
return ` ${propDecorator}\n ${f.name}${f.isOptional ? '?' : ''}: ${details.tsType};`;
|
|
514
648
|
});
|
|
515
649
|
|
|
650
|
+
const relLines = relations.map((r) => {
|
|
651
|
+
const targetPascal = toSingularPascal(toPascalCase(r.targetModule));
|
|
652
|
+
return ` @Prop({ type: SchemaTypes.ObjectId, ref: '${targetPascal}', default: null })\n ${r.fkField}?: string;`;
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
const pkLine = primaryKey !== 'id'
|
|
656
|
+
? ` @Prop({ default: () => new Types.ObjectId().toString() })\n ${primaryKey}: string;\n\n`
|
|
657
|
+
: '';
|
|
658
|
+
|
|
659
|
+
const statusLine = includeStatus ? ' @Prop({ default: \'ACTIVE\' })\n status: string;\n\n' : '';
|
|
660
|
+
|
|
516
661
|
const schemaContent = `import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
|
517
|
-
import { HydratedDocument } from 'mongoose';
|
|
662
|
+
import { HydratedDocument, SchemaTypes, Types } from 'mongoose';
|
|
518
663
|
|
|
519
664
|
export type ${singularPascal}Document = HydratedDocument<${singularPascal}>;
|
|
520
665
|
|
|
521
666
|
@Schema({ timestamps: true })
|
|
522
667
|
export class ${singularPascal} {
|
|
523
|
-
${fieldLines.join('\n\n')}
|
|
668
|
+
${pkLine}${fieldLines.join('\n\n')}
|
|
524
669
|
|
|
525
|
-
@Prop({ default:
|
|
526
|
-
status: string;
|
|
527
|
-
|
|
528
|
-
@Prop({ type: Date, default: null })
|
|
670
|
+
${relLines.length > 0 ? relLines.join('\n\n') + '\n\n' : ''}${statusLine} @Prop({ type: Date, default: null })
|
|
529
671
|
deletedAt?: Date | null;
|
|
530
672
|
}
|
|
531
673
|
|
|
@@ -533,16 +675,16 @@ export const ${singularPascal}Schema = SchemaFactory.createForClass(${singularPa
|
|
|
533
675
|
`;
|
|
534
676
|
|
|
535
677
|
await fs.writeFile(schemaPath, schemaContent, 'utf8');
|
|
536
|
-
console.log(chalk.green(`
|
|
678
|
+
console.log(chalk.green(` ✓ Generated Mongoose schema at src/modules/${kebabName}/schemas/${toSingularKebab(kebabName)}.schema.ts`));
|
|
537
679
|
} catch (error) {
|
|
538
|
-
console.warn(chalk.yellow(`
|
|
680
|
+
console.warn(chalk.yellow(` ⚠️ Could not generate Mongoose schema: ${error.message}`));
|
|
539
681
|
}
|
|
540
682
|
}
|
|
541
683
|
|
|
542
684
|
/**
|
|
543
685
|
* Dynamic ORM Schema Synchronization: Drizzle
|
|
544
686
|
*/
|
|
545
|
-
async function syncDrizzleSchema(moduleDir, singularPascal, kebabName, fields) {
|
|
687
|
+
async function syncDrizzleSchema(moduleDir, singularPascal, kebabName, primaryKey, fields, relations, includeStatus = true) {
|
|
546
688
|
try {
|
|
547
689
|
const schemaDir = path.join(moduleDir, 'schema');
|
|
548
690
|
await fs.ensureDir(schemaDir);
|
|
@@ -550,12 +692,18 @@ async function syncDrizzleSchema(moduleDir, singularPascal, kebabName, fields) {
|
|
|
550
692
|
|
|
551
693
|
const fieldLines = fields.map((f) => {
|
|
552
694
|
let colDef = `varchar('${toSnakeCase(f.name)}', { length: 255 })`;
|
|
553
|
-
if (
|
|
695
|
+
if (['int', 'integer', 'Int'].includes(f.type)) {
|
|
696
|
+
colDef = `integer('${toSnakeCase(f.name)}')`;
|
|
697
|
+
} else if (['float', 'decimal', 'numeric', 'Float', 'Decimal', 'Number'].includes(f.type)) {
|
|
554
698
|
colDef = `numeric('${toSnakeCase(f.name)}')`;
|
|
555
|
-
} else if (
|
|
699
|
+
} else if (['boolean', 'Boolean'].includes(f.type)) {
|
|
556
700
|
colDef = `boolean('${toSnakeCase(f.name)}').default(false)`;
|
|
557
|
-
} else if (
|
|
701
|
+
} else if (['timestamp', 'DateTime', 'Date'].includes(f.type)) {
|
|
558
702
|
colDef = `timestamp('${toSnakeCase(f.name)}')`;
|
|
703
|
+
} else if (['json', 'Json', 'Object'].includes(f.type)) {
|
|
704
|
+
colDef = `json('${toSnakeCase(f.name)}')`;
|
|
705
|
+
} else if (f.type === 'text') {
|
|
706
|
+
colDef = `text('${toSnakeCase(f.name)}')`;
|
|
559
707
|
}
|
|
560
708
|
|
|
561
709
|
if (!f.isOptional) {
|
|
@@ -565,13 +713,18 @@ async function syncDrizzleSchema(moduleDir, singularPascal, kebabName, fields) {
|
|
|
565
713
|
return ` ${f.name}: ${colDef},`;
|
|
566
714
|
});
|
|
567
715
|
|
|
568
|
-
const
|
|
716
|
+
const relLines = relations.map((r) => {
|
|
717
|
+
return ` ${r.fkField}: varchar('${toSnakeCase(r.fkField)}', { length: 36 }),`;
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
const statusLine = includeStatus ? " status: varchar('status', { length: 50 }).default('ACTIVE').notNull(),\n" : '';
|
|
721
|
+
|
|
722
|
+
const schemaContent = `import { pgTable, varchar, text, integer, numeric, boolean, timestamp, json } from 'drizzle-orm/pg-core';
|
|
569
723
|
|
|
570
724
|
export const ${toCamelCase(kebabName)}s = pgTable('${toSnakeCase(kebabName)}', {
|
|
571
|
-
|
|
725
|
+
${primaryKey}: varchar('${toSnakeCase(primaryKey)}', { length: 36 }).primaryKey().$defaultFn(() => crypto.randomUUID()),
|
|
572
726
|
${fieldLines.join('\n')}
|
|
573
|
-
|
|
574
|
-
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
727
|
+
${relLines.length > 0 ? relLines.join('\n') + '\n' : ''}${statusLine} createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
575
728
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
576
729
|
deletedAt: timestamp('deleted_at'),
|
|
577
730
|
});
|
|
@@ -581,24 +734,26 @@ export type New${singularPascal} = typeof ${toCamelCase(kebabName)}s.$inferInser
|
|
|
581
734
|
`;
|
|
582
735
|
|
|
583
736
|
await fs.writeFile(schemaPath, schemaContent, 'utf8');
|
|
584
|
-
console.log(chalk.green(`
|
|
737
|
+
console.log(chalk.green(` ✓ Generated Drizzle schema at src/modules/${kebabName}/schema/${kebabName}.schema.ts`));
|
|
585
738
|
} catch (error) {
|
|
586
|
-
console.warn(chalk.yellow(`
|
|
739
|
+
console.warn(chalk.yellow(` ⚠️ Could not generate Drizzle schema: ${error.message}`));
|
|
587
740
|
}
|
|
588
741
|
}
|
|
589
742
|
|
|
590
743
|
/**
|
|
591
744
|
* Auto-Generate Starter Seed File Template
|
|
592
745
|
*/
|
|
593
|
-
async function generateSeedFileTemplate(targetDir, orm, singularPascal, kebabName, fields) {
|
|
746
|
+
async function generateSeedFileTemplate(targetDir, orm, singularPascal, kebabName, primaryKey, fields, includeStatus = true) {
|
|
594
747
|
try {
|
|
595
748
|
const singularCamel = toSingularCamel(kebabName);
|
|
596
749
|
const dummyObjFields = fields.map((f) => {
|
|
597
750
|
const ex = getFieldExampleValue(f, singularPascal);
|
|
598
|
-
const valStr = typeof ex === 'string' ? `'${ex}'` : ex;
|
|
751
|
+
const valStr = typeof ex === 'string' ? `'${ex}'` : JSON.stringify(ex);
|
|
599
752
|
return ` ${f.name}: ${valStr},`;
|
|
600
753
|
}).join('\n');
|
|
601
754
|
|
|
755
|
+
const statusProp = includeStatus ? '\n status: \'ACTIVE\',' : '';
|
|
756
|
+
|
|
602
757
|
if (orm === 'prisma') {
|
|
603
758
|
const seedsDir = path.join(targetDir, 'prisma', 'seeds');
|
|
604
759
|
await fs.ensureDir(seedsDir);
|
|
@@ -610,14 +765,13 @@ export async function seed${singularPascal}(prisma: PrismaClient) {
|
|
|
610
765
|
console.log('🌱 Seeding ${singularPascal}...');
|
|
611
766
|
await (prisma as any).${singularCamel}.create({
|
|
612
767
|
data: {
|
|
613
|
-
${dummyObjFields}
|
|
614
|
-
status: 'ACTIVE',
|
|
768
|
+
${dummyObjFields}${statusProp}
|
|
615
769
|
},
|
|
616
770
|
});
|
|
617
771
|
}
|
|
618
772
|
`;
|
|
619
773
|
await fs.writeFile(seedPath, seedContent, 'utf8');
|
|
620
|
-
console.log(chalk.green(`
|
|
774
|
+
console.log(chalk.green(` ✓ Generated Prisma seed template at prisma/seeds/${kebabName}.seed.ts`));
|
|
621
775
|
} else {
|
|
622
776
|
const seedsDir = path.join(targetDir, 'src', 'database', 'seeds');
|
|
623
777
|
await fs.ensureDir(seedsDir);
|
|
@@ -641,14 +795,14 @@ ${dummyObjFields}
|
|
|
641
795
|
}
|
|
642
796
|
`;
|
|
643
797
|
await fs.writeFile(seedPath, seedContent, 'utf8');
|
|
644
|
-
console.log(chalk.green(`
|
|
798
|
+
console.log(chalk.green(` ✓ Generated seed template at src/database/seeds/${kebabName}.seed.ts`));
|
|
645
799
|
}
|
|
646
800
|
} catch (error) {
|
|
647
|
-
console.warn(chalk.yellow(`
|
|
801
|
+
console.warn(chalk.yellow(` ⚠️ Could not generate seed template: ${error.message}`));
|
|
648
802
|
}
|
|
649
803
|
}
|
|
650
804
|
|
|
651
|
-
function getServiceContent(orm, pascalName, singularPascal, camelName, kebabName, createDtoName, updateDtoName, ops) {
|
|
805
|
+
function getServiceContent(orm, pascalName, singularPascal, camelName, kebabName, primaryKey, createDtoName, updateDtoName, ops) {
|
|
652
806
|
const singularKebab = toSingularKebab(kebabName);
|
|
653
807
|
|
|
654
808
|
if (orm === 'typeorm') {
|
|
@@ -676,20 +830,20 @@ class TypeOrm${pascalName}Repository implements IBaseRepository<${pascalName}Ent
|
|
|
676
830
|
return { data, total };
|
|
677
831
|
}` : `async findAll(p: PaginationQueryDto): Promise<{ data: ${pascalName}Entity[]; total: number }> { throw new Error('Not implemented'); }`}
|
|
678
832
|
|
|
679
|
-
${ops.findOne ? `async findOne(
|
|
680
|
-
return this.repo.findOne({ where: {
|
|
681
|
-
}` : `async findOne(
|
|
833
|
+
${ops.findOne ? `async findOne(${primaryKey}: string): Promise<${pascalName}Entity | null> {
|
|
834
|
+
return this.repo.findOne({ where: { ${primaryKey}, deletedAt: IsNull() } as any });
|
|
835
|
+
}` : `async findOne(${primaryKey}: string): Promise<${pascalName}Entity | null> { throw new Error('Not implemented'); }`}
|
|
682
836
|
|
|
683
|
-
${ops.update ? `async update(
|
|
684
|
-
await this.repo.update(
|
|
685
|
-
return this.repo.findOneOrFail({ where: {
|
|
686
|
-
}` : `async update(
|
|
837
|
+
${ops.update ? `async update(${primaryKey}: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> {
|
|
838
|
+
await this.repo.update(${primaryKey}, dto as any);
|
|
839
|
+
return this.repo.findOneOrFail({ where: { ${primaryKey} } as any });
|
|
840
|
+
}` : `async update(${primaryKey}: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
|
|
687
841
|
|
|
688
|
-
${ops.remove ? `async remove(
|
|
689
|
-
const entity = await this.repo.findOneOrFail({ where: {
|
|
842
|
+
${ops.remove ? `async remove(${primaryKey}: string): Promise<${pascalName}Entity> {
|
|
843
|
+
const entity = await this.repo.findOneOrFail({ where: { ${primaryKey} } as any });
|
|
690
844
|
entity.deletedAt = new Date();
|
|
691
845
|
return this.repo.save(entity);
|
|
692
|
-
}` : `async remove(
|
|
846
|
+
}` : `async remove(${primaryKey}: string): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
|
|
693
847
|
}
|
|
694
848
|
|
|
695
849
|
@Injectable()
|
|
@@ -734,17 +888,17 @@ class Mongoose${pascalName}Repository implements IBaseRepository<${pascalName}En
|
|
|
734
888
|
return { data, total };
|
|
735
889
|
}` : `async findAll(p: PaginationQueryDto): Promise<{ data: ${pascalName}Entity[]; total: number }> { throw new Error('Not implemented'); }`}
|
|
736
890
|
|
|
737
|
-
${ops.findOne ? `async findOne(
|
|
738
|
-
return this.model.findOne({ _id:
|
|
739
|
-
}` : `async findOne(
|
|
891
|
+
${ops.findOne ? `async findOne(${primaryKey}: string): Promise<${pascalName}Entity | null> {
|
|
892
|
+
return this.model.findOne({ ${primaryKey === 'id' ? '_id' : primaryKey}: ${primaryKey}, deletedAt: null }).exec();
|
|
893
|
+
}` : `async findOne(${primaryKey}: string): Promise<${pascalName}Entity | null> { throw new Error('Not implemented'); }`}
|
|
740
894
|
|
|
741
|
-
${ops.update ? `async update(
|
|
742
|
-
return this.model.
|
|
743
|
-
}` : `async update(
|
|
895
|
+
${ops.update ? `async update(${primaryKey}: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> {
|
|
896
|
+
return this.model.findOneAndUpdate({ ${primaryKey === 'id' ? '_id' : primaryKey}: ${primaryKey} }, dto as any, { new: true }).exec() as Promise<${pascalName}Entity>;
|
|
897
|
+
}` : `async update(${primaryKey}: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
|
|
744
898
|
|
|
745
|
-
${ops.remove ? `async remove(
|
|
746
|
-
return this.model.
|
|
747
|
-
}` : `async remove(
|
|
899
|
+
${ops.remove ? `async remove(${primaryKey}: string): Promise<${pascalName}Entity> {
|
|
900
|
+
return this.model.findOneAndUpdate({ ${primaryKey === 'id' ? '_id' : primaryKey}: ${primaryKey} }, { deletedAt: new Date() }, { new: true }).exec() as Promise<${pascalName}Entity>;
|
|
901
|
+
}` : `async remove(${primaryKey}: string): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
|
|
748
902
|
}
|
|
749
903
|
|
|
750
904
|
@Injectable()
|
|
@@ -790,21 +944,21 @@ class Drizzle${pascalName}Repository implements IBaseRepository<${pascalName}Ent
|
|
|
790
944
|
return { data, total: 0 };
|
|
791
945
|
}` : `async findAll(p: PaginationQueryDto): Promise<{ data: ${pascalName}Entity[]; total: number }> { throw new Error('Not implemented'); }`}
|
|
792
946
|
|
|
793
|
-
${ops.findOne ? `async findOne(
|
|
947
|
+
${ops.findOne ? `async findOne(${primaryKey}: string): Promise<${pascalName}Entity | null> {
|
|
794
948
|
const [result] = await this.db.select().from(${camelName}s)
|
|
795
|
-
.where(eq(${camelName}s
|
|
949
|
+
.where(eq(${camelName}s.${primaryKey}, ${primaryKey}));
|
|
796
950
|
return result || null;
|
|
797
|
-
}` : `async findOne(
|
|
951
|
+
}` : `async findOne(${primaryKey}: string): Promise<${pascalName}Entity | null> { throw new Error('Not implemented'); }`}
|
|
798
952
|
|
|
799
|
-
${ops.update ? `async update(
|
|
800
|
-
const [result] = await this.db.update(${camelName}s).set(dto).where(eq(${camelName}s
|
|
953
|
+
${ops.update ? `async update(${primaryKey}: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> {
|
|
954
|
+
const [result] = await this.db.update(${camelName}s).set(dto).where(eq(${camelName}s.${primaryKey}, ${primaryKey})).returning();
|
|
801
955
|
return result;
|
|
802
|
-
}` : `async update(
|
|
956
|
+
}` : `async update(${primaryKey}: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
|
|
803
957
|
|
|
804
|
-
${ops.remove ? `async remove(
|
|
805
|
-
const [result] = await this.db.update(${camelName}s).set({ deletedAt: new Date() }).where(eq(${camelName}s
|
|
958
|
+
${ops.remove ? `async remove(${primaryKey}: string): Promise<${pascalName}Entity> {
|
|
959
|
+
const [result] = await this.db.update(${camelName}s).set({ deletedAt: new Date() }).where(eq(${camelName}s.${primaryKey}, ${primaryKey})).returning();
|
|
806
960
|
return result;
|
|
807
|
-
}` : `async remove(
|
|
961
|
+
}` : `async remove(${primaryKey}: string): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
|
|
808
962
|
}
|
|
809
963
|
|
|
810
964
|
@Injectable()
|
|
@@ -850,17 +1004,17 @@ class Prisma${pascalName}Repository implements IBaseRepository<${pascalName}Enti
|
|
|
850
1004
|
return { data, total };
|
|
851
1005
|
}` : `async findAll(p: PaginationQueryDto): Promise<{ data: ${pascalName}Entity[]; total: number }> { throw new Error('Not implemented'); }`}
|
|
852
1006
|
|
|
853
|
-
${ops.findOne ? `async findOne(
|
|
854
|
-
return (this.prisma as any).${singularCamel}.findFirst({ where: {
|
|
855
|
-
}` : `async findOne(
|
|
1007
|
+
${ops.findOne ? `async findOne(${primaryKey}: string): Promise<${pascalName}Entity | null> {
|
|
1008
|
+
return (this.prisma as any).${singularCamel}.findFirst({ where: { ${primaryKey}, deletedAt: null } });
|
|
1009
|
+
}` : `async findOne(${primaryKey}: string): Promise<${pascalName}Entity | null> { throw new Error('Not implemented'); }`}
|
|
856
1010
|
|
|
857
|
-
${ops.update ? `async update(
|
|
858
|
-
return (this.prisma as any).${singularCamel}.update({ where: {
|
|
859
|
-
}` : `async update(
|
|
1011
|
+
${ops.update ? `async update(${primaryKey}: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> {
|
|
1012
|
+
return (this.prisma as any).${singularCamel}.update({ where: { ${primaryKey} }, data: dto });
|
|
1013
|
+
}` : `async update(${primaryKey}: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
|
|
860
1014
|
|
|
861
|
-
${ops.remove ? `async remove(
|
|
862
|
-
return (this.prisma as any).${singularCamel}.update({ where: {
|
|
863
|
-
}` : `async remove(
|
|
1015
|
+
${ops.remove ? `async remove(${primaryKey}: string): Promise<${pascalName}Entity> {
|
|
1016
|
+
return (this.prisma as any).${singularCamel}.update({ where: { ${primaryKey} }, data: { deletedAt: new Date() } });
|
|
1017
|
+
}` : `async remove(${primaryKey}: string): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
|
|
864
1018
|
}
|
|
865
1019
|
|
|
866
1020
|
@Injectable()
|
|
@@ -982,17 +1136,18 @@ async function registerInAppModule(targetDir, pascalName, kebabName) {
|
|
|
982
1136
|
*/
|
|
983
1137
|
async function generateModule(providedModuleName, targetDir = process.cwd(), specifiedOrm = null) {
|
|
984
1138
|
try {
|
|
985
|
-
// Ensure src/common/base
|
|
1139
|
+
// 0. Ensure Base Architecture exists at src/common/base
|
|
986
1140
|
await ensureBaseArchitecture(targetDir);
|
|
987
1141
|
|
|
988
|
-
|
|
1142
|
+
// Detect ORM first so prompt choices match
|
|
1143
|
+
const orm = specifiedOrm || (await detectOrm(targetDir));
|
|
1144
|
+
|
|
1145
|
+
const options = await promptForModuleOptions(providedModuleName, orm);
|
|
989
1146
|
const kebabName = toKebabCase(options.moduleName);
|
|
990
1147
|
const pascalName = toPascalCase(options.moduleName);
|
|
991
1148
|
const camelName = toCamelCase(options.moduleName);
|
|
992
1149
|
const singularPascal = toSingularPascal(pascalName);
|
|
993
|
-
|
|
994
|
-
// Detect ORM
|
|
995
|
-
const orm = specifiedOrm || (await detectOrm(targetDir));
|
|
1150
|
+
const primaryKey = options.primaryKey || 'id';
|
|
996
1151
|
|
|
997
1152
|
// Ensure inside a NestJS project structure
|
|
998
1153
|
const srcDir = path.join(targetDir, 'src');
|
|
@@ -1018,13 +1173,13 @@ async function generateModule(providedModuleName, targetDir = process.cwd(), spe
|
|
|
1018
1173
|
|
|
1019
1174
|
// 1. Dynamic ORM Schema Synchronization
|
|
1020
1175
|
if (orm === 'prisma') {
|
|
1021
|
-
await syncPrismaSchema(targetDir, singularPascal, kebabName, options.fields);
|
|
1176
|
+
await syncPrismaSchema(targetDir, singularPascal, kebabName, primaryKey, options.fields, options.relations, options.includeStatus);
|
|
1022
1177
|
} else if (orm === 'typeorm') {
|
|
1023
|
-
await syncTypeOrmSchema(moduleDir, singularPascal, kebabName, options.fields);
|
|
1178
|
+
await syncTypeOrmSchema(moduleDir, singularPascal, kebabName, primaryKey, options.fields, options.relations, options.includeStatus);
|
|
1024
1179
|
} else if (orm === 'mongoose') {
|
|
1025
|
-
await syncMongooseSchema(moduleDir, singularPascal, kebabName, options.fields);
|
|
1180
|
+
await syncMongooseSchema(moduleDir, singularPascal, kebabName, primaryKey, options.fields, options.relations, options.includeStatus);
|
|
1026
1181
|
} else if (orm === 'drizzle') {
|
|
1027
|
-
await syncDrizzleSchema(moduleDir, singularPascal, kebabName, options.fields);
|
|
1182
|
+
await syncDrizzleSchema(moduleDir, singularPascal, kebabName, primaryKey, options.fields, options.relations, options.includeStatus);
|
|
1028
1183
|
}
|
|
1029
1184
|
|
|
1030
1185
|
// 2. Generate DTOs
|
|
@@ -1033,39 +1188,41 @@ async function generateModule(providedModuleName, targetDir = process.cwd(), spe
|
|
|
1033
1188
|
const responseDtoName = `${pascalName}Dto`;
|
|
1034
1189
|
|
|
1035
1190
|
const createFieldsText = options.fields.map((f) => {
|
|
1036
|
-
const
|
|
1191
|
+
const details = getFieldDetails(f.type);
|
|
1037
1192
|
const ex = getFieldExampleValue(f, pascalName);
|
|
1038
|
-
const exValStr = typeof ex === 'string' ? `'${ex}'` : ex;
|
|
1193
|
+
const exValStr = typeof ex === 'string' ? `'${ex}'` : JSON.stringify(ex);
|
|
1039
1194
|
|
|
1040
1195
|
const swaggerDecorator = f.isOptional
|
|
1041
1196
|
? `@ApiPropertyOptional({ description: '${f.name} property', example: ${exValStr} })`
|
|
1042
1197
|
: `@ApiProperty({ description: '${f.name} property', example: ${exValStr} })`;
|
|
1043
1198
|
|
|
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
|
-
|
|
1199
|
+
const valDecorators = [...details.valDecorators];
|
|
1053
1200
|
if (f.isOptional) {
|
|
1054
1201
|
valDecorators.push('@IsOptional()');
|
|
1055
1202
|
} else {
|
|
1056
1203
|
valDecorators.push('@IsNotEmpty()');
|
|
1057
1204
|
}
|
|
1058
1205
|
|
|
1059
|
-
return ` ${swaggerDecorator}\n ${valDecorators.join('\n ')}\n ${f.name}${f.isOptional ? '?' : ''}: ${tsType};`;
|
|
1206
|
+
return ` ${swaggerDecorator}\n ${valDecorators.join('\n ')}\n ${f.name}${f.isOptional ? '?' : ''}: ${details.tsType};`;
|
|
1060
1207
|
}).join('\n\n');
|
|
1061
1208
|
|
|
1062
|
-
const hasDateFields = options.fields.some((f) =>
|
|
1209
|
+
const hasDateFields = options.fields.some((f) => ['DateTime', 'timestamp', 'Date'].includes(f.type));
|
|
1210
|
+
|
|
1211
|
+
// Validator import gathering
|
|
1212
|
+
const allValDecorators = new Set(['IsOptional', 'IsNotEmpty']);
|
|
1213
|
+
options.fields.forEach((f) => {
|
|
1214
|
+
const details = getFieldDetails(f.type);
|
|
1215
|
+
details.valDecorators.forEach((dec) => {
|
|
1216
|
+
const name = dec.replace('@', '').replace(/\(.*\)/, '');
|
|
1217
|
+
if (name) allValDecorators.add(name);
|
|
1218
|
+
});
|
|
1219
|
+
});
|
|
1063
1220
|
|
|
1064
1221
|
if (ops.create || ops.update) {
|
|
1065
1222
|
await fs.writeFile(
|
|
1066
1223
|
path.join(dtoDir, `create-${kebabName}.dto.ts`),
|
|
1067
1224
|
`import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
1068
|
-
import {
|
|
1225
|
+
import { ${Array.from(allValDecorators).join(', ')} } from 'class-validator';
|
|
1069
1226
|
${hasDateFields ? `import { Type } from 'class-transformer';\n` : ''}
|
|
1070
1227
|
export class ${createDtoName} {
|
|
1071
1228
|
${createFieldsText}
|
|
@@ -1084,31 +1241,32 @@ export class ${updateDtoName} extends PartialType(${createDtoName}) {}
|
|
|
1084
1241
|
}
|
|
1085
1242
|
|
|
1086
1243
|
const responseFieldsText = options.fields.map((f) => {
|
|
1087
|
-
const
|
|
1244
|
+
const details = getFieldDetails(f.type);
|
|
1088
1245
|
const ex = getFieldExampleValue(f, pascalName);
|
|
1089
|
-
const exValStr = typeof ex === 'string' ? `'${ex}'` : ex;
|
|
1246
|
+
const exValStr = typeof ex === 'string' ? `'${ex}'` : JSON.stringify(ex);
|
|
1090
1247
|
|
|
1091
1248
|
const swaggerDecorator = f.isOptional
|
|
1092
1249
|
? `@ApiPropertyOptional({ example: ${exValStr} })`
|
|
1093
1250
|
: `@ApiProperty({ example: ${exValStr} })`;
|
|
1094
1251
|
|
|
1095
|
-
return ` ${swaggerDecorator}\n ${f.name}${f.isOptional ? '?' : ''}: ${tsType};`;
|
|
1252
|
+
return ` ${swaggerDecorator}\n ${f.name}${f.isOptional ? '?' : ''}: ${details.tsType};`;
|
|
1096
1253
|
}).join('\n\n');
|
|
1097
1254
|
|
|
1255
|
+
const statusDtoField = options.includeStatus !== false
|
|
1256
|
+
? ` @ApiProperty({ example: 'ACTIVE' })\n status: string;\n\n`
|
|
1257
|
+
: '';
|
|
1258
|
+
|
|
1098
1259
|
await fs.writeFile(
|
|
1099
1260
|
path.join(dtoDir, `${kebabName}.dto.ts`),
|
|
1100
1261
|
`import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
1101
1262
|
|
|
1102
1263
|
export class ${responseDtoName} {
|
|
1103
1264
|
@ApiProperty({ example: '123e4567-e89b-12d3-a456-426614174000', format: 'uuid' })
|
|
1104
|
-
|
|
1265
|
+
${primaryKey}: string;
|
|
1105
1266
|
|
|
1106
1267
|
${responseFieldsText}
|
|
1107
1268
|
|
|
1108
|
-
@ApiProperty({ example: '
|
|
1109
|
-
status: string;
|
|
1110
|
-
|
|
1111
|
-
@ApiProperty({ example: '2025-01-01T00:00:00.000Z' })
|
|
1269
|
+
${statusDtoField} @ApiProperty({ example: '2025-01-01T00:00:00.000Z' })
|
|
1112
1270
|
createdAt: Date;
|
|
1113
1271
|
|
|
1114
1272
|
@ApiProperty({ example: '2025-01-01T00:00:00.000Z' })
|
|
@@ -1127,14 +1285,20 @@ ${responseFieldsText}
|
|
|
1127
1285
|
singularPascal,
|
|
1128
1286
|
camelName,
|
|
1129
1287
|
kebabName,
|
|
1288
|
+
primaryKey,
|
|
1130
1289
|
createDtoName,
|
|
1131
1290
|
updateDtoName,
|
|
1132
1291
|
ops
|
|
1133
1292
|
);
|
|
1134
1293
|
await fs.writeFile(path.join(moduleDir, `${kebabName}.service.ts`), serviceContent);
|
|
1135
1294
|
|
|
1136
|
-
// 4. Generate Controller
|
|
1137
|
-
const
|
|
1295
|
+
// 4. Generate Controller
|
|
1296
|
+
const guardImports = options.protectWriteOps ? `, UseGuards` : '';
|
|
1297
|
+
const guardDecorator = options.protectWriteOps && options.roles?.length > 0
|
|
1298
|
+
? `\n @UseGuards()\n // Roles: ${options.roles.join(', ')}`
|
|
1299
|
+
: '';
|
|
1300
|
+
|
|
1301
|
+
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
1302
|
import { ApiTags, ApiBearerAuth, ApiExtraModels, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';
|
|
1139
1303
|
import { BaseController, ApiResponseDto, ApiResponseSchema, PaginatedResponseDto, PaginatedResponseSchema, PaginationQueryDto } from '../../common/base';
|
|
1140
1304
|
import { ${pascalName}Service } from './${kebabName}.service';
|
|
@@ -1156,7 +1320,7 @@ export class ${pascalName}Controller extends BaseController<${pascalName}Entity,
|
|
|
1156
1320
|
return ${responseDtoName} as unknown as Type<${pascalName}Entity>;
|
|
1157
1321
|
}
|
|
1158
1322
|
${ops.create ? `
|
|
1159
|
-
@Post()
|
|
1323
|
+
@Post()${guardDecorator}
|
|
1160
1324
|
@ApiOperation({ summary: 'Create a new ${kebabName}' })
|
|
1161
1325
|
@ApiResponse({ status: HttpStatus.CREATED, schema: ApiResponseSchema(${responseDtoName}) })
|
|
1162
1326
|
override async create(@Body() dto: ${createDtoName}): Promise<ApiResponseDto<${pascalName}Entity>> {
|
|
@@ -1170,34 +1334,34 @@ ${ops.create ? `
|
|
|
1170
1334
|
return super.findAll(pagination);
|
|
1171
1335
|
}
|
|
1172
1336
|
` : ''}${ops.findOne ? `
|
|
1173
|
-
@Get('
|
|
1337
|
+
@Get(':${primaryKey}')
|
|
1174
1338
|
@ApiOperation({ summary: 'Get ${kebabName} by ID' })
|
|
1175
|
-
@ApiParam({ name: '
|
|
1339
|
+
@ApiParam({ name: '${primaryKey}', format: 'uuid' })
|
|
1176
1340
|
@ApiResponse({ status: HttpStatus.OK, schema: ApiResponseSchema(${responseDtoName}) })
|
|
1177
1341
|
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: '${pascalName} not found' })
|
|
1178
|
-
override async findOne(@Param('
|
|
1179
|
-
return super.findOne(
|
|
1342
|
+
override async findOne(@Param('${primaryKey}', new ParseUUIDPipe({ version: '4', errorHttpStatusCode: HttpStatus.BAD_REQUEST })) ${primaryKey}: string): Promise<ApiResponseDto<${pascalName}Entity>> {
|
|
1343
|
+
return super.findOne(${primaryKey});
|
|
1180
1344
|
}
|
|
1181
1345
|
` : ''}${ops.update ? `
|
|
1182
|
-
@Put('
|
|
1346
|
+
@Put(':${primaryKey}')${guardDecorator}
|
|
1183
1347
|
@ApiOperation({ summary: 'Update ${kebabName} by ID' })
|
|
1184
|
-
@ApiParam({ name: '
|
|
1348
|
+
@ApiParam({ name: '${primaryKey}', format: 'uuid' })
|
|
1185
1349
|
@ApiResponse({ status: HttpStatus.OK, schema: ApiResponseSchema(${responseDtoName}) })
|
|
1186
1350
|
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: '${pascalName} not found' })
|
|
1187
1351
|
override async update(
|
|
1188
|
-
@Param('
|
|
1352
|
+
@Param('${primaryKey}', new ParseUUIDPipe({ version: '4', errorHttpStatusCode: HttpStatus.BAD_REQUEST })) ${primaryKey}: string,
|
|
1189
1353
|
@Body() dto: ${updateDtoName}
|
|
1190
1354
|
): Promise<ApiResponseDto<${pascalName}Entity>> {
|
|
1191
|
-
return super.update(
|
|
1355
|
+
return super.update(${primaryKey}, dto);
|
|
1192
1356
|
}
|
|
1193
1357
|
` : ''}${ops.remove ? `
|
|
1194
|
-
@Delete('
|
|
1358
|
+
@Delete(':${primaryKey}')${guardDecorator}
|
|
1195
1359
|
@ApiOperation({ summary: 'Delete ${kebabName} by ID' })
|
|
1196
|
-
@ApiParam({ name: '
|
|
1360
|
+
@ApiParam({ name: '${primaryKey}', format: 'uuid' })
|
|
1197
1361
|
@ApiResponse({ status: HttpStatus.OK, schema: ApiResponseSchema(${responseDtoName}) })
|
|
1198
1362
|
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: '${pascalName} not found' })
|
|
1199
|
-
override async remove(@Param('
|
|
1200
|
-
return super.remove(
|
|
1363
|
+
override async remove(@Param('${primaryKey}', new ParseUUIDPipe({ version: '4', errorHttpStatusCode: HttpStatus.BAD_REQUEST })) ${primaryKey}: string): Promise<ApiResponseDto<${pascalName}Entity>> {
|
|
1364
|
+
return super.remove(${primaryKey});
|
|
1201
1365
|
}
|
|
1202
1366
|
` : ''}
|
|
1203
1367
|
}
|
|
@@ -1212,10 +1376,10 @@ ${ops.create ? `
|
|
|
1212
1376
|
const isAutoRegistered = await registerInAppModule(targetDir, pascalName, kebabName);
|
|
1213
1377
|
|
|
1214
1378
|
// 7. Auto-Generate Starter Seed File Template
|
|
1215
|
-
await generateSeedFileTemplate(targetDir, orm, singularPascal, kebabName, options.fields);
|
|
1379
|
+
await generateSeedFileTemplate(targetDir, orm, singularPascal, kebabName, primaryKey, options.fields, options.includeStatus);
|
|
1216
1380
|
|
|
1217
1381
|
console.log(chalk.green(`\n✅ Module "${kebabName}" successfully generated in ${path.relative(process.cwd(), moduleDir)}`));
|
|
1218
|
-
console.log(chalk.gray(`
|
|
1382
|
+
console.log(chalk.gray(` Detected ORM: ${orm}`));
|
|
1219
1383
|
|
|
1220
1384
|
if (!isAutoRegistered) {
|
|
1221
1385
|
console.log(chalk.yellow(`\n⚠️ Please manually register ${pascalName}Module in src/app.module.ts:`));
|
|
@@ -1239,9 +1403,134 @@ export class AppModule {}
|
|
|
1239
1403
|
}
|
|
1240
1404
|
}
|
|
1241
1405
|
|
|
1406
|
+
/**
|
|
1407
|
+
* Regenerate module DTOs and ORM schemas when fields are updated via fieldManager
|
|
1408
|
+
*/
|
|
1409
|
+
async function regenerateModuleComponents(moduleName, fields, targetDir = process.cwd()) {
|
|
1410
|
+
const orm = await detectOrm(targetDir);
|
|
1411
|
+
const kebabName = toKebabCase(moduleName);
|
|
1412
|
+
const pascalName = toPascalCase(moduleName);
|
|
1413
|
+
const singularPascal = toSingularPascal(pascalName);
|
|
1414
|
+
const primaryKey = 'id';
|
|
1415
|
+
|
|
1416
|
+
const srcDir = path.join(targetDir, 'src');
|
|
1417
|
+
const moduleDir = (await fs.pathExists(srcDir))
|
|
1418
|
+
? path.join(srcDir, 'modules', kebabName)
|
|
1419
|
+
: path.join(targetDir, 'modules', kebabName);
|
|
1420
|
+
const dtoDir = path.join(moduleDir, 'dto');
|
|
1421
|
+
|
|
1422
|
+
await fs.ensureDir(moduleDir);
|
|
1423
|
+
await fs.ensureDir(dtoDir);
|
|
1424
|
+
|
|
1425
|
+
// 1. Sync ORM Schema
|
|
1426
|
+
if (orm === 'prisma') {
|
|
1427
|
+
await syncPrismaSchema(targetDir, singularPascal, kebabName, primaryKey, fields, [], true);
|
|
1428
|
+
} else if (orm === 'typeorm') {
|
|
1429
|
+
await syncTypeOrmSchema(moduleDir, singularPascal, kebabName, primaryKey, fields, [], true);
|
|
1430
|
+
} else if (orm === 'mongoose') {
|
|
1431
|
+
await syncMongooseSchema(moduleDir, singularPascal, kebabName, primaryKey, fields, [], true);
|
|
1432
|
+
} else if (orm === 'drizzle') {
|
|
1433
|
+
await syncDrizzleSchema(moduleDir, singularPascal, kebabName, primaryKey, fields, [], true);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// 2. Regenerate DTOs
|
|
1437
|
+
const createDtoName = `Create${singularPascal}Dto`;
|
|
1438
|
+
const updateDtoName = `Update${singularPascal}Dto`;
|
|
1439
|
+
const responseDtoName = `${pascalName}Dto`;
|
|
1440
|
+
|
|
1441
|
+
const createFieldsText = fields.map((f) => {
|
|
1442
|
+
const details = getFieldDetails(f.type);
|
|
1443
|
+
const ex = getFieldExampleValue(f, pascalName);
|
|
1444
|
+
const exValStr = typeof ex === 'string' ? `'${ex}'` : JSON.stringify(ex);
|
|
1445
|
+
|
|
1446
|
+
const swaggerDecorator = f.isOptional
|
|
1447
|
+
? `@ApiPropertyOptional({ description: '${f.name} property', example: ${exValStr} })`
|
|
1448
|
+
: `@ApiProperty({ description: '${f.name} property', example: ${exValStr} })`;
|
|
1449
|
+
|
|
1450
|
+
const valDecorators = [...details.valDecorators];
|
|
1451
|
+
if (f.isOptional) {
|
|
1452
|
+
valDecorators.push('@IsOptional()');
|
|
1453
|
+
} else {
|
|
1454
|
+
valDecorators.push('@IsNotEmpty()');
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
return ` ${swaggerDecorator}\n ${valDecorators.join('\n ')}\n ${f.name}${f.isOptional ? '?' : ''}: ${details.tsType};`;
|
|
1458
|
+
}).join('\n\n');
|
|
1459
|
+
|
|
1460
|
+
const hasDateFields = fields.some((f) => ['DateTime', 'timestamp', 'Date'].includes(f.type));
|
|
1461
|
+
|
|
1462
|
+
const allValDecorators = new Set(['IsOptional', 'IsNotEmpty']);
|
|
1463
|
+
fields.forEach((f) => {
|
|
1464
|
+
const details = getFieldDetails(f.type);
|
|
1465
|
+
details.valDecorators.forEach((dec) => {
|
|
1466
|
+
const name = dec.replace('@', '').replace(/\(.*\)/, '');
|
|
1467
|
+
if (name) allValDecorators.add(name);
|
|
1468
|
+
});
|
|
1469
|
+
});
|
|
1470
|
+
|
|
1471
|
+
await fs.writeFile(
|
|
1472
|
+
path.join(dtoDir, `create-${kebabName}.dto.ts`),
|
|
1473
|
+
`import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
1474
|
+
import { ${Array.from(allValDecorators).join(', ')} } from 'class-validator';
|
|
1475
|
+
${hasDateFields ? `import { Type } from 'class-transformer';\n` : ''}
|
|
1476
|
+
export class ${createDtoName} {
|
|
1477
|
+
${createFieldsText}
|
|
1478
|
+
}
|
|
1479
|
+
`
|
|
1480
|
+
);
|
|
1481
|
+
|
|
1482
|
+
await fs.writeFile(
|
|
1483
|
+
path.join(dtoDir, `update-${kebabName}.dto.ts`),
|
|
1484
|
+
`import { PartialType } from '@nestjs/swagger';
|
|
1485
|
+
import { ${createDtoName} } from './create-${kebabName}.dto';
|
|
1486
|
+
|
|
1487
|
+
export class ${updateDtoName} extends PartialType(${createDtoName}) {}
|
|
1488
|
+
`
|
|
1489
|
+
);
|
|
1490
|
+
|
|
1491
|
+
const responseFieldsText = fields.map((f) => {
|
|
1492
|
+
const details = getFieldDetails(f.type);
|
|
1493
|
+
const ex = getFieldExampleValue(f, pascalName);
|
|
1494
|
+
const exValStr = typeof ex === 'string' ? `'${ex}'` : JSON.stringify(ex);
|
|
1495
|
+
|
|
1496
|
+
const swaggerDecorator = f.isOptional
|
|
1497
|
+
? `@ApiPropertyOptional({ example: ${exValStr} })`
|
|
1498
|
+
: `@ApiProperty({ example: ${exValStr} })`;
|
|
1499
|
+
|
|
1500
|
+
return ` ${swaggerDecorator}\n ${f.name}${f.isOptional ? '?' : ''}: ${details.tsType};`;
|
|
1501
|
+
}).join('\n\n');
|
|
1502
|
+
|
|
1503
|
+
await fs.writeFile(
|
|
1504
|
+
path.join(dtoDir, `${kebabName}.dto.ts`),
|
|
1505
|
+
`import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
1506
|
+
|
|
1507
|
+
export class ${responseDtoName} {
|
|
1508
|
+
@ApiProperty({ example: '123e4567-e89b-12d3-a456-426614174000', format: 'uuid' })
|
|
1509
|
+
id: string;
|
|
1510
|
+
|
|
1511
|
+
${responseFieldsText}
|
|
1512
|
+
|
|
1513
|
+
@ApiProperty({ example: 'ACTIVE' })
|
|
1514
|
+
status: string;
|
|
1515
|
+
|
|
1516
|
+
@ApiProperty({ example: '2025-01-01T00:00:00.000Z' })
|
|
1517
|
+
createdAt: Date;
|
|
1518
|
+
|
|
1519
|
+
@ApiProperty({ example: '2025-01-01T00:00:00.000Z' })
|
|
1520
|
+
updatedAt: Date;
|
|
1521
|
+
|
|
1522
|
+
@ApiPropertyOptional({ nullable: true, example: null })
|
|
1523
|
+
deletedAt?: Date | null;
|
|
1524
|
+
}
|
|
1525
|
+
`
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1242
1529
|
module.exports = {
|
|
1243
1530
|
generateModule,
|
|
1244
1531
|
promptForModuleOptions,
|
|
1245
1532
|
detectOrm,
|
|
1246
1533
|
registerInAppModule,
|
|
1534
|
+
ensureBaseArchitecture,
|
|
1535
|
+
regenerateModuleComponents,
|
|
1247
1536
|
};
|