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.
@@ -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
- await fs.ensureDir(commonBaseDir);
91
-
92
- // 1. base.controller.ts
93
- await fs.writeFile(
94
- path.join(commonBaseDir, 'base.controller.ts'),
95
- `import { Type } from '@nestjs/common';
96
-
97
- export abstract class BaseController<T, CreateDto, UpdateDto> {
98
- constructor(protected readonly service: any) {}
99
- protected abstract getDtoClass(): Type<T>;
100
- async create(dto: CreateDto): Promise<any> { return this.service.create(dto); }
101
- async findAll(query: any): Promise<any> { return this.service.findAll(query); }
102
- async findOne(id: string): Promise<any> { return this.service.findOne(id); }
103
- async update(id: string, dto: UpdateDto): Promise<any> { return this.service.update(id, dto); }
104
- async remove(id: string): Promise<any> { return this.service.remove(id); }
105
- }
106
- `
107
- );
108
-
109
- // 2. base.service.ts
110
- await fs.writeFile(
111
- path.join(commonBaseDir, 'base.service.ts'),
112
- `import { Injectable } from '@nestjs/common';
113
-
114
- export interface PaginationQueryDto { page?: number; limit?: number; }
115
- export interface IBaseRepository<T, CreateDto, UpdateDto> {
116
- create(dto: CreateDto): Promise<T>;
117
- findAll(pagination: PaginationQueryDto): Promise<{ data: T[]; total: number }>;
118
- findOne(id: string): Promise<T | null>;
119
- update(id: string, dto: UpdateDto): Promise<T>;
120
- remove(id: string): Promise<T>;
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
- @Injectable()
124
- export abstract class BaseService<T, CreateDto, UpdateDto> {
125
- protected abstract getRepository(): IBaseRepository<T, CreateDto, UpdateDto>;
126
- async create(dto: CreateDto): Promise<T> { return this.getRepository().create(dto); }
127
- async findAll(pagination: PaginationQueryDto): Promise<{ data: T[]; total: number }> { return this.getRepository().findAll(pagination); }
128
- async findOne(id: string): Promise<T | null> { return this.getRepository().findOne(id); }
129
- async update(id: string, dto: UpdateDto): Promise<T> { return this.getRepository().update(id, dto); }
130
- async remove(id: string): Promise<T> { return this.getRepository().remove(id); }
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
- 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
- console.log(chalk.green(' Auto-generated missing src/common/base architecture'));
151
- }
152
- } catch (error) {
153
- console.warn(chalk.yellow(` ⚠️ Could not verify/create base architecture: ${error.message}`));
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
- // Interactive Field Builder Loop
203
- // Interactive Field Builder Loop
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 building = true;
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
- // Helper untuk input field baru / edit
217
- const promptSingleField = async (initialValues = {}) => {
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: (answers) => `Select field type for '${answers.fieldName}':`,
236
- choices: ['String', 'Number', 'Boolean', 'Date'],
237
- default: initialValues.type || 'String',
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: (answers) => `Is '${answers.fieldName}' optional?`,
243
- default: initialValues.isOptional !== undefined ? initialValues.isOptional : false,
365
+ message: (ans) => `Is '${ans.fieldName}' optional?`,
366
+ default: false,
244
367
  },
245
368
  ]);
246
- };
247
369
 
248
- // Tambah field pertama
249
- console.log(chalk.cyan('\n--- Add Field 1 ---'));
250
- const firstField = await promptSingleField();
251
- fields.push({
252
- name: firstField.fieldName.trim(),
253
- type: firstField.fieldType,
254
- isOptional: firstField.isOptional,
255
- });
370
+ fields.push({
371
+ name: fieldAnswers.fieldName.trim(),
372
+ type: fieldAnswers.fieldType,
373
+ isOptional: fieldAnswers.isOptional,
374
+ });
256
375
 
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
- }]);
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
- const { fieldToEditIndex } = await inquirer.prompt([{
288
- type: 'list',
289
- name: 'fieldToEditIndex',
290
- message: 'Select field to edit:',
291
- choices: fields.map((f, index) => ({
292
- name: `${f.name} (${f.type}${f.isOptional ? '?' : ''})`,
293
- value: index,
294
- })),
295
- }]);
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
- console.log(chalk.cyan(`\n--- Editing Field '${fields[fieldToEditIndex].name}' ---`));
298
- const editedField = await promptSingleField(fields[fieldToEditIndex]);
299
- fields[fieldToEditIndex] = {
300
- name: editedField.fieldName.trim(),
301
- type: editedField.fieldType,
302
- isOptional: editedField.isOptional,
303
- };
304
- console.log(chalk.green(`✓ Field '${fields[fieldToEditIndex].name}' updated successfully.`));
305
- } else if (action === 'delete') {
306
- if (fields.length === 0) {
307
- console.log(chalk.yellow('⚠️ No fields available to delete.'));
308
- continue;
309
- }
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
- const { fieldToDeleteIndex } = await inquirer.prompt([{
404
+ if (addRelation) {
405
+ let addingRel = true;
406
+ while (addingRel) {
407
+ const relAnswers = await inquirer.prompt([
408
+ {
312
409
  type: 'list',
313
- name: 'fieldToDeleteIndex',
314
- message: 'Select field to delete:',
315
- choices: fields.map((f, index) => ({
316
- name: `${f.name} (${f.type}${f.isOptional ? '?' : ''})`,
317
- value: index,
318
- })),
319
- }]);
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
- const deletedName = fields[fieldToDeleteIndex].name;
322
- fields.splice(fieldToDeleteIndex, 1);
323
- console.log(chalk.red(`🗑️ Field '${deletedName}' removed.`));
324
- } else if (action === 'done') {
325
- building = false;
326
- }
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
- // Fallback if no custom fields added
331
- if (fields.length === 0) {
332
- fields.push({ name: 'name', type: 'String', isOptional: false });
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
- if (field.type === 'String') {
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
- if (field.type === 'Number') {
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 === 'Number') {
389
- const nameLower = f.name.toLowerCase();
390
- if (
391
- nameLower.includes('price') ||
392
- nameLower.includes('amount') ||
393
- nameLower.includes('total') ||
394
- nameLower.includes('cost') ||
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
- id String @id @default(uuid())
540
+ ${primaryKey} String @id @default(uuid())
414
541
  ${fieldLines.join('\n')}
415
- status String @default("ACTIVE")
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(` ✓ Updated prisma/schema.prisma with model ${singularPascal}`));
552
+ console.log(chalk.green(` ✓ Updated prisma/schema.prisma with model ${singularPascal}`));
427
553
  } catch (error) {
428
- console.warn(chalk.yellow(` ⚠️ Could not sync prisma/schema.prisma: ${error.message}`));
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 (f.type === 'Number') {
571
+ if (['decimal', 'numeric', 'Decimal'].includes(f.type)) {
446
572
  colDecorator = `@Column('decimal', { precision: 10, scale: 2, nullable: ${f.isOptional} })`;
447
- tsType = 'number';
448
- } else if (f.type === 'Boolean') {
573
+ } else if (['boolean', 'Boolean'].includes(f.type)) {
449
574
  colDecorator = `@Column({ default: false, nullable: ${f.isOptional} })`;
450
- tsType = 'boolean';
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
- tsType = 'Date';
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 entityContent = `import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, DeleteDateColumn } from 'typeorm';
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
- id: string;
602
+ ${primaryKey}: string;
465
603
 
466
604
  ${fieldLines.join('\n\n')}
467
605
 
468
- @Column({ default: 'ACTIVE' })
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(` ✓ Generated TypeORM entity at src/modules/${kebabName}/entities/${toSingularKebab(kebabName)}.entity.ts`));
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(` ⚠️ Could not generate TypeORM entity: ${error.message}`));
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 === 'Number') {
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
- tsType = 'boolean';
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
- tsType = 'Date';
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: 'ACTIVE' })
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(` ✓ Generated Mongoose schema at src/modules/${kebabName}/schemas/${toSingularKebab(kebabName)}.schema.ts`));
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(` ⚠️ Could not generate Mongoose schema: ${error.message}`));
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 (f.type === 'Number') {
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 (f.type === 'Boolean') {
699
+ } else if (['boolean', 'Boolean'].includes(f.type)) {
556
700
  colDef = `boolean('${toSnakeCase(f.name)}').default(false)`;
557
- } else if (f.type === 'Date') {
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 schemaContent = `import { pgTable, varchar, numeric, boolean, timestamp } from 'drizzle-orm/pg-core';
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
- id: varchar('id', { length: 36 }).primaryKey().$defaultFn(() => crypto.randomUUID()),
725
+ ${primaryKey}: varchar('${toSnakeCase(primaryKey)}', { length: 36 }).primaryKey().$defaultFn(() => crypto.randomUUID()),
572
726
  ${fieldLines.join('\n')}
573
- status: varchar('status', { length: 50 }).default('ACTIVE').notNull(),
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(` ✓ Generated Drizzle schema at src/modules/${kebabName}/schema/${kebabName}.schema.ts`));
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(` ⚠️ Could not generate Drizzle schema: ${error.message}`));
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(` ✓ Generated Prisma seed template at prisma/seeds/${kebabName}.seed.ts`));
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(` ✓ Generated seed template at src/database/seeds/${kebabName}.seed.ts`));
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(` ⚠️ Could not generate seed template: ${error.message}`));
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(id: string): Promise<${pascalName}Entity | null> {
680
- return this.repo.findOne({ where: { id, deletedAt: IsNull() } as any });
681
- }` : `async findOne(id: string): Promise<${pascalName}Entity | null> { throw new Error('Not implemented'); }`}
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(id: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> {
684
- await this.repo.update(id, dto as any);
685
- return this.repo.findOneOrFail({ where: { id } as any });
686
- }` : `async update(id: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
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(id: string): Promise<${pascalName}Entity> {
689
- const entity = await this.repo.findOneOrFail({ where: { id } as any });
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(id: string): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
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(id: string): Promise<${pascalName}Entity | null> {
738
- return this.model.findOne({ _id: id, deletedAt: null }).exec();
739
- }` : `async findOne(id: string): Promise<${pascalName}Entity | null> { throw new Error('Not implemented'); }`}
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(id: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> {
742
- return this.model.findByIdAndUpdate(id, dto as any, { new: true }).exec() as Promise<${pascalName}Entity>;
743
- }` : `async update(id: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
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(id: string): Promise<${pascalName}Entity> {
746
- return this.model.findByIdAndUpdate(id, { deletedAt: new Date() }, { new: true }).exec() as Promise<${pascalName}Entity>;
747
- }` : `async remove(id: string): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
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(id: string): Promise<${pascalName}Entity | null> {
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.id, id));
949
+ .where(eq(${camelName}s.${primaryKey}, ${primaryKey}));
796
950
  return result || null;
797
- }` : `async findOne(id: string): Promise<${pascalName}Entity | null> { throw new Error('Not implemented'); }`}
951
+ }` : `async findOne(${primaryKey}: string): Promise<${pascalName}Entity | null> { throw new Error('Not implemented'); }`}
798
952
 
799
- ${ops.update ? `async update(id: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> {
800
- const [result] = await this.db.update(${camelName}s).set(dto).where(eq(${camelName}s.id, id)).returning();
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(id: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
956
+ }` : `async update(${primaryKey}: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
803
957
 
804
- ${ops.remove ? `async remove(id: string): Promise<${pascalName}Entity> {
805
- const [result] = await this.db.update(${camelName}s).set({ deletedAt: new Date() }).where(eq(${camelName}s.id, id)).returning();
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(id: string): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
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(id: string): Promise<${pascalName}Entity | null> {
854
- return (this.prisma as any).${singularCamel}.findFirst({ where: { id, deletedAt: null } });
855
- }` : `async findOne(id: string): Promise<${pascalName}Entity | null> { throw new Error('Not implemented'); }`}
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(id: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> {
858
- return (this.prisma as any).${singularCamel}.update({ where: { id }, data: dto });
859
- }` : `async update(id: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
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(id: string): Promise<${pascalName}Entity> {
862
- return (this.prisma as any).${singularCamel}.update({ where: { id }, data: { deletedAt: new Date() } });
863
- }` : `async remove(id: string): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
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 exists before generating any module components
1139
+ // 0. Ensure Base Architecture exists at src/common/base
986
1140
  await ensureBaseArchitecture(targetDir);
987
1141
 
988
- const options = await promptForModuleOptions(providedModuleName);
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 tsType = getTsType(f.type);
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) => f.type === 'Date');
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 { IsString, IsNumber, IsBoolean, IsDate, IsNotEmpty, IsOptional } from 'class-validator';
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 tsType = getTsType(f.type);
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
- id: string;
1265
+ ${primaryKey}: string;
1105
1266
 
1106
1267
  ${responseFieldsText}
1107
1268
 
1108
- @ApiProperty({ example: 'ACTIVE' })
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 (Fixed single clean import path from common/base)
1137
- const controllerContent = `import { Controller${ops.findAll ? ', Query' : ''}${ops.findOne || ops.update || ops.remove ? ', Param, ParseUUIDPipe, HttpStatus' : ''}${ops.create ? ', Post, Body' : ''}${ops.findAll || ops.findOne ? ', Get' : ''}${ops.update ? ', Put' : ''}${ops.remove ? ', Delete' : ''}, Type } from '@nestjs/common';
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(':id')
1337
+ @Get(':${primaryKey}')
1174
1338
  @ApiOperation({ summary: 'Get ${kebabName} by ID' })
1175
- @ApiParam({ name: 'id', format: 'uuid' })
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('id', new ParseUUIDPipe({ version: '4', errorHttpStatusCode: HttpStatus.BAD_REQUEST })) id: string): Promise<ApiResponseDto<${pascalName}Entity>> {
1179
- return super.findOne(id);
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(':id')
1346
+ @Put(':${primaryKey}')${guardDecorator}
1183
1347
  @ApiOperation({ summary: 'Update ${kebabName} by ID' })
1184
- @ApiParam({ name: 'id', format: 'uuid' })
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('id', new ParseUUIDPipe({ version: '4', errorHttpStatusCode: HttpStatus.BAD_REQUEST })) id: string,
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(id, dto);
1355
+ return super.update(${primaryKey}, dto);
1192
1356
  }
1193
1357
  ` : ''}${ops.remove ? `
1194
- @Delete(':id')
1358
+ @Delete(':${primaryKey}')${guardDecorator}
1195
1359
  @ApiOperation({ summary: 'Delete ${kebabName} by ID' })
1196
- @ApiParam({ name: 'id', format: 'uuid' })
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('id', new ParseUUIDPipe({ version: '4', errorHttpStatusCode: HttpStatus.BAD_REQUEST })) id: string): Promise<ApiResponseDto<${pascalName}Entity>> {
1200
- return super.remove(id);
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(` Detected ORM: ${orm}`));
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
  };