speedrun-cli 2.7.9 → 2.7.15

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.
@@ -7,7 +7,7 @@ const inquirer = require('inquirer');
7
7
  const fs = require('fs-extra');
8
8
  const path = require('path');
9
9
  const chalk = require('chalk');
10
- require('./utils');
10
+ const { detectPackageManager, getRunPrefix } = require('./utils');
11
11
 
12
12
  function toPascalCase(str) {
13
13
  return str
@@ -444,7 +444,15 @@ async function promptForModuleOptions(providedModuleName, detectedOrm = 'prisma'
444
444
  }
445
445
  }
446
446
 
447
- // 4. Role & Auth Guard Protection Prompt
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
448
456
  const { protectWriteOps } = await inquirer.prompt([{
449
457
  type: 'confirm',
450
458
  name: 'protectWriteOps',
@@ -473,6 +481,7 @@ async function promptForModuleOptions(providedModuleName, detectedOrm = 'prisma'
473
481
  primaryKey,
474
482
  fields,
475
483
  relations,
484
+ includeStatus,
476
485
  protectWriteOps,
477
486
  roles: selectedRoles,
478
487
  };
@@ -494,15 +503,19 @@ function getFieldExampleValue(field, pascalName) {
494
503
 
495
504
  /**
496
505
  * Dynamic ORM Schema Synchronization: Prisma
506
+ * @param {boolean} forceUpdate - When true, replace existing model block instead of skipping
497
507
  */
498
- async function syncPrismaSchema(targetDir, singularPascal, kebabName, primaryKey, fields, relations) {
508
+ async function syncPrismaSchema(targetDir, singularPascal, kebabName, primaryKey, fields, relations, includeStatus = true, forceUpdate = false) {
499
509
  try {
500
510
  const schemaPath = path.join(targetDir, 'prisma', 'schema.prisma');
501
511
  if (!(await fs.pathExists(schemaPath))) return;
502
512
 
503
513
  let content = await fs.readFile(schemaPath, 'utf8');
504
514
 
505
- if (new RegExp(`\\bmodel\\s+${singularPascal}\\b`).test(content)) {
515
+ const modelRegex = new RegExp(`(\\bmodel\\s+${singularPascal}\\s*\\{[^}]*\\})`, 's');
516
+ const modelExists = modelRegex.test(content);
517
+
518
+ if (modelExists && !forceUpdate) {
506
519
  return;
507
520
  }
508
521
 
@@ -524,12 +537,13 @@ async function syncPrismaSchema(targetDir, singularPascal, kebabName, primaryKey
524
537
  return ` ${targetCamel} ${targetPascal}? @relation(fields: [${r.fkField}], references: [id])\n ${r.fkField} String?`;
525
538
  });
526
539
 
540
+ const statusLine = includeStatus ? ' status String @default("ACTIVE")\n' : '';
541
+
527
542
  const modelDefinition = `
528
543
  model ${singularPascal} {
529
544
  ${primaryKey} String @id @default(uuid())
530
545
  ${fieldLines.join('\n')}
531
- ${relLines.length > 0 ? relLines.join('\n') + '\n' : ''} status String @default("ACTIVE")
532
- createdAt DateTime @default(now())
546
+ ${relLines.length > 0 ? relLines.join('\n') + '\n' : ''}${statusLine} createdAt DateTime @default(now())
533
547
  updatedAt DateTime @updatedAt
534
548
  deletedAt DateTime?
535
549
 
@@ -537,9 +551,15 @@ ${relLines.length > 0 ? relLines.join('\n') + '\n' : ''} status String @de
537
551
  }
538
552
  `;
539
553
 
540
- content += modelDefinition;
554
+ if (modelExists && forceUpdate) {
555
+ content = content.replace(modelRegex, modelDefinition.trim());
556
+ console.log(chalk.green(` ✓ Updated existing Prisma model ${singularPascal} in schema.prisma`));
557
+ } else {
558
+ content += modelDefinition;
559
+ console.log(chalk.green(` ✓ Added Prisma model ${singularPascal} to schema.prisma`));
560
+ }
561
+
541
562
  await fs.writeFile(schemaPath, content, 'utf8');
542
- console.log(chalk.green(` ✓ Updated prisma/schema.prisma with model ${singularPascal}`));
543
563
  } catch (error) {
544
564
  console.warn(chalk.yellow(` ⚠️ Could not sync prisma/schema.prisma: ${error.message}`));
545
565
  }
@@ -548,7 +568,7 @@ ${relLines.length > 0 ? relLines.join('\n') + '\n' : ''} status String @de
548
568
  /**
549
569
  * Dynamic ORM Schema Synchronization: TypeORM
550
570
  */
551
- async function syncTypeOrmSchema(moduleDir, singularPascal, kebabName, primaryKey, fields, relations) {
571
+ async function syncTypeOrmSchema(moduleDir, singularPascal, kebabName, primaryKey, fields, relations, includeStatus = true) {
552
572
  try {
553
573
  const entityDir = path.join(moduleDir, 'entities');
554
574
  await fs.ensureDir(entityDir);
@@ -582,6 +602,8 @@ async function syncTypeOrmSchema(moduleDir, singularPascal, kebabName, primaryKe
582
602
  const hasRelations = relations.length > 0;
583
603
  const imports = [`Entity`, `PrimaryGeneratedColumn`, `Column`, `CreateDateColumn`, `UpdateDateColumn`, `DeleteDateColumn`].concat(hasRelations ? [`ManyToOne`, `JoinColumn`] : []);
584
604
 
605
+ const statusLine = includeStatus ? ' @Column({ default: \'ACTIVE\' })\n status: string;\n\n' : '';
606
+
585
607
  const entityContent = `import { ${imports.join(', ')} } from 'typeorm';
586
608
 
587
609
  @Entity('${toSnakeCase(kebabName)}')
@@ -591,10 +613,7 @@ export class ${singularPascal} {
591
613
 
592
614
  ${fieldLines.join('\n\n')}
593
615
 
594
- ${relLines.length > 0 ? relLines.join('\n\n') + '\n\n' : ''} @Column({ default: 'ACTIVE' })
595
- status: string;
596
-
597
- @CreateDateColumn()
616
+ ${relLines.length > 0 ? relLines.join('\n\n') + '\n\n' : ''}${statusLine} @CreateDateColumn()
598
617
  createdAt: Date;
599
618
 
600
619
  @UpdateDateColumn()
@@ -615,7 +634,7 @@ ${relLines.length > 0 ? relLines.join('\n\n') + '\n\n' : ''} @Column({ default:
615
634
  /**
616
635
  * Dynamic ORM Schema Synchronization: Mongoose
617
636
  */
618
- async function syncMongooseSchema(moduleDir, singularPascal, kebabName, primaryKey, fields, relations) {
637
+ async function syncMongooseSchema(moduleDir, singularPascal, kebabName, primaryKey, fields, relations, includeStatus = true) {
619
638
  try {
620
639
  const schemaDir = path.join(moduleDir, 'schemas');
621
640
  await fs.ensureDir(schemaDir);
@@ -647,6 +666,8 @@ async function syncMongooseSchema(moduleDir, singularPascal, kebabName, primaryK
647
666
  ? ` @Prop({ default: () => new Types.ObjectId().toString() })\n ${primaryKey}: string;\n\n`
648
667
  : '';
649
668
 
669
+ const statusLine = includeStatus ? ' @Prop({ default: \'ACTIVE\' })\n status: string;\n\n' : '';
670
+
650
671
  const schemaContent = `import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
651
672
  import { HydratedDocument, SchemaTypes, Types } from 'mongoose';
652
673
 
@@ -656,10 +677,7 @@ export type ${singularPascal}Document = HydratedDocument<${singularPascal}>;
656
677
  export class ${singularPascal} {
657
678
  ${pkLine}${fieldLines.join('\n\n')}
658
679
 
659
- ${relLines.length > 0 ? relLines.join('\n\n') + '\n\n' : ''} @Prop({ default: 'ACTIVE' })
660
- status: string;
661
-
662
- @Prop({ type: Date, default: null })
680
+ ${relLines.length > 0 ? relLines.join('\n\n') + '\n\n' : ''}${statusLine} @Prop({ type: Date, default: null })
663
681
  deletedAt?: Date | null;
664
682
  }
665
683
 
@@ -676,7 +694,7 @@ export const ${singularPascal}Schema = SchemaFactory.createForClass(${singularPa
676
694
  /**
677
695
  * Dynamic ORM Schema Synchronization: Drizzle
678
696
  */
679
- async function syncDrizzleSchema(moduleDir, singularPascal, kebabName, primaryKey, fields, relations) {
697
+ async function syncDrizzleSchema(moduleDir, singularPascal, kebabName, primaryKey, fields, relations, includeStatus = true) {
680
698
  try {
681
699
  const schemaDir = path.join(moduleDir, 'schema');
682
700
  await fs.ensureDir(schemaDir);
@@ -709,13 +727,14 @@ async function syncDrizzleSchema(moduleDir, singularPascal, kebabName, primaryKe
709
727
  return ` ${r.fkField}: varchar('${toSnakeCase(r.fkField)}', { length: 36 }),`;
710
728
  });
711
729
 
730
+ const statusLine = includeStatus ? " status: varchar('status', { length: 50 }).default('ACTIVE').notNull(),\n" : '';
731
+
712
732
  const schemaContent = `import { pgTable, varchar, text, integer, numeric, boolean, timestamp, json } from 'drizzle-orm/pg-core';
713
733
 
714
734
  export const ${toCamelCase(kebabName)}s = pgTable('${toSnakeCase(kebabName)}', {
715
735
  ${primaryKey}: varchar('${toSnakeCase(primaryKey)}', { length: 36 }).primaryKey().$defaultFn(() => crypto.randomUUID()),
716
736
  ${fieldLines.join('\n')}
717
- ${relLines.length > 0 ? relLines.join('\n') + '\n' : ''} status: varchar('status', { length: 50 }).default('ACTIVE').notNull(),
718
- createdAt: timestamp('created_at').defaultNow().notNull(),
737
+ ${relLines.length > 0 ? relLines.join('\n') + '\n' : ''}${statusLine} createdAt: timestamp('created_at').defaultNow().notNull(),
719
738
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
720
739
  deletedAt: timestamp('deleted_at'),
721
740
  });
@@ -734,7 +753,7 @@ export type New${singularPascal} = typeof ${toCamelCase(kebabName)}s.$inferInser
734
753
  /**
735
754
  * Auto-Generate Starter Seed File Template
736
755
  */
737
- async function generateSeedFileTemplate(targetDir, orm, singularPascal, kebabName, primaryKey, fields) {
756
+ async function generateSeedFileTemplate(targetDir, orm, singularPascal, kebabName, primaryKey, fields, includeStatus = true) {
738
757
  try {
739
758
  const singularCamel = toSingularCamel(kebabName);
740
759
  const dummyObjFields = fields.map((f) => {
@@ -743,6 +762,8 @@ async function generateSeedFileTemplate(targetDir, orm, singularPascal, kebabNam
743
762
  return ` ${f.name}: ${valStr},`;
744
763
  }).join('\n');
745
764
 
765
+ const statusProp = includeStatus ? '\n status: \'ACTIVE\',' : '';
766
+
746
767
  if (orm === 'prisma') {
747
768
  const seedsDir = path.join(targetDir, 'prisma', 'seeds');
748
769
  await fs.ensureDir(seedsDir);
@@ -754,8 +775,7 @@ export async function seed${singularPascal}(prisma: PrismaClient) {
754
775
  console.log('🌱 Seeding ${singularPascal}...');
755
776
  await (prisma as any).${singularCamel}.create({
756
777
  data: {
757
- ${dummyObjFields}
758
- status: 'ACTIVE',
778
+ ${dummyObjFields}${statusProp}
759
779
  },
760
780
  });
761
781
  }
@@ -825,7 +845,7 @@ class TypeOrm${pascalName}Repository implements IBaseRepository<${pascalName}Ent
825
845
  }` : `async findOne(${primaryKey}: string): Promise<${pascalName}Entity | null> { throw new Error('Not implemented'); }`}
826
846
 
827
847
  ${ops.update ? `async update(${primaryKey}: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> {
828
- await this.repo.update(${primaryKey}, dto as any);
848
+ await this.repo.update({ ${primaryKey} } as any, dto as any);
829
849
  return this.repo.findOneOrFail({ where: { ${primaryKey} } as any });
830
850
  }` : `async update(${primaryKey}: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
831
851
 
@@ -1163,13 +1183,13 @@ async function generateModule(providedModuleName, targetDir = process.cwd(), spe
1163
1183
 
1164
1184
  // 1. Dynamic ORM Schema Synchronization
1165
1185
  if (orm === 'prisma') {
1166
- await syncPrismaSchema(targetDir, singularPascal, kebabName, primaryKey, options.fields, options.relations);
1186
+ await syncPrismaSchema(targetDir, singularPascal, kebabName, primaryKey, options.fields, options.relations, options.includeStatus);
1167
1187
  } else if (orm === 'typeorm') {
1168
- await syncTypeOrmSchema(moduleDir, singularPascal, kebabName, primaryKey, options.fields, options.relations);
1188
+ await syncTypeOrmSchema(moduleDir, singularPascal, kebabName, primaryKey, options.fields, options.relations, options.includeStatus);
1169
1189
  } else if (orm === 'mongoose') {
1170
- await syncMongooseSchema(moduleDir, singularPascal, kebabName, primaryKey, options.fields, options.relations);
1190
+ await syncMongooseSchema(moduleDir, singularPascal, kebabName, primaryKey, options.fields, options.relations, options.includeStatus);
1171
1191
  } else if (orm === 'drizzle') {
1172
- await syncDrizzleSchema(moduleDir, singularPascal, kebabName, primaryKey, options.fields, options.relations);
1192
+ await syncDrizzleSchema(moduleDir, singularPascal, kebabName, primaryKey, options.fields, options.relations, options.includeStatus);
1173
1193
  }
1174
1194
 
1175
1195
  // 2. Generate DTOs
@@ -1196,17 +1216,27 @@ async function generateModule(providedModuleName, targetDir = process.cwd(), spe
1196
1216
  return ` ${swaggerDecorator}\n ${valDecorators.join('\n ')}\n ${f.name}${f.isOptional ? '?' : ''}: ${details.tsType};`;
1197
1217
  }).join('\n\n');
1198
1218
 
1219
+ // Build FK fields for Create DTO from relations (e.g., userId, categoryId)
1220
+ const relationFkFields = (options.relations || []).map((r) => {
1221
+ return ` @ApiPropertyOptional({ description: 'Foreign key linking to ${r.targetModule}', example: '123e4567-e89b-12d3-a456-426614174000', format: 'uuid' })\n @IsOptional()\n @IsUUID()\n ${r.fkField}?: string;`;
1222
+ }).join('\n\n');
1223
+
1199
1224
  const hasDateFields = options.fields.some((f) => ['DateTime', 'timestamp', 'Date'].includes(f.type));
1225
+ const hasRelations = (options.relations || []).length > 0;
1200
1226
 
1201
- // Validator import gathering
1227
+ // Validator import gathering — only collect 'Is*' decorators (class-validator)
1202
1228
  const allValDecorators = new Set(['IsOptional', 'IsNotEmpty']);
1203
1229
  options.fields.forEach((f) => {
1204
1230
  const details = getFieldDetails(f.type);
1205
1231
  details.valDecorators.forEach((dec) => {
1206
1232
  const name = dec.replace('@', '').replace(/\(.*\)/, '');
1207
- if (name) allValDecorators.add(name);
1233
+ // Only include class-validator decorators (Is* prefix), not Type from class-transformer
1234
+ if (name && name.startsWith('Is')) allValDecorators.add(name);
1208
1235
  });
1209
1236
  });
1237
+ if (hasRelations) allValDecorators.add('IsUUID');
1238
+
1239
+ const createDtoBody = [createFieldsText, relationFkFields].filter(Boolean).join('\n\n');
1210
1240
 
1211
1241
  if (ops.create || ops.update) {
1212
1242
  await fs.writeFile(
@@ -1215,7 +1245,7 @@ async function generateModule(providedModuleName, targetDir = process.cwd(), spe
1215
1245
  import { ${Array.from(allValDecorators).join(', ')} } from 'class-validator';
1216
1246
  ${hasDateFields ? `import { Type } from 'class-transformer';\n` : ''}
1217
1247
  export class ${createDtoName} {
1218
- ${createFieldsText}
1248
+ ${createDtoBody}
1219
1249
  }
1220
1250
  `
1221
1251
  );
@@ -1242,6 +1272,15 @@ export class ${updateDtoName} extends PartialType(${createDtoName}) {}
1242
1272
  return ` ${swaggerDecorator}\n ${f.name}${f.isOptional ? '?' : ''}: ${details.tsType};`;
1243
1273
  }).join('\n\n');
1244
1274
 
1275
+ // FK relation fields in response DTO (always optional — may be null if not populated)
1276
+ const responseFkFields = (options.relations || []).map((r) => {
1277
+ return ` @ApiPropertyOptional({ example: '123e4567-e89b-12d3-a456-426614174000', format: 'uuid' })\n ${r.fkField}?: string;`;
1278
+ }).join('\n\n');
1279
+
1280
+ const statusDtoField = options.includeStatus !== false
1281
+ ? ` @ApiProperty({ example: 'ACTIVE' })\n status: string;\n\n`
1282
+ : '';
1283
+
1245
1284
  await fs.writeFile(
1246
1285
  path.join(dtoDir, `${kebabName}.dto.ts`),
1247
1286
  `import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
@@ -1250,12 +1289,11 @@ export class ${responseDtoName} {
1250
1289
  @ApiProperty({ example: '123e4567-e89b-12d3-a456-426614174000', format: 'uuid' })
1251
1290
  ${primaryKey}: string;
1252
1291
 
1253
- ${responseFieldsText}
1254
-
1255
- @ApiProperty({ example: 'ACTIVE' })
1256
- status: string;
1292
+ ${responseFieldsText}${
1293
+ responseFkFields ? `\n\n${responseFkFields}` : ''
1294
+ }
1257
1295
 
1258
- @ApiProperty({ example: '2025-01-01T00:00:00.000Z' })
1296
+ ${statusDtoField} @ApiProperty({ example: '2025-01-01T00:00:00.000Z' })
1259
1297
  createdAt: Date;
1260
1298
 
1261
1299
  @ApiProperty({ example: '2025-01-01T00:00:00.000Z' })
@@ -1365,7 +1403,7 @@ ${ops.create ? `
1365
1403
  const isAutoRegistered = await registerInAppModule(targetDir, pascalName, kebabName);
1366
1404
 
1367
1405
  // 7. Auto-Generate Starter Seed File Template
1368
- await generateSeedFileTemplate(targetDir, orm, singularPascal, kebabName, primaryKey, options.fields);
1406
+ await generateSeedFileTemplate(targetDir, orm, singularPascal, kebabName, primaryKey, options.fields, options.includeStatus);
1369
1407
 
1370
1408
  console.log(chalk.green(`\n✅ Module "${kebabName}" successfully generated in ${path.relative(process.cwd(), moduleDir)}`));
1371
1409
  console.log(chalk.gray(` Detected ORM: ${orm}`));
@@ -1392,10 +1430,164 @@ export class AppModule {}
1392
1430
  }
1393
1431
  }
1394
1432
 
1433
+ /**
1434
+ * Detect the primary key used by a module by scanning its response DTO file.
1435
+ * Falls back to 'id' if the file is missing or no PK can be found.
1436
+ */
1437
+ async function detectModulePrimaryKey(moduleDir, kebabName) {
1438
+ try {
1439
+ const dtoPath = path.join(moduleDir, 'dto', `${kebabName}.dto.ts`);
1440
+ if (!(await fs.pathExists(dtoPath))) return 'id';
1441
+
1442
+ const content = await fs.readFile(dtoPath, 'utf8');
1443
+ // The PK is the first property after 'export class Xxx {'
1444
+ // It is decorated with @ApiProperty and has format: 'uuid'
1445
+ const pkMatch = content.match(/@ApiProperty\([^)]*format:\s*['"](uuid)['"][^)]*\)[\s\S]*?\n\s*(\w+)\s*:/m);
1446
+ if (pkMatch && pkMatch[2]) return pkMatch[2];
1447
+
1448
+ // Fallback: look for *_id or *Id as the first bare property
1449
+ const firstPropMatch = content.match(/^\s*([a-zA-Z][a-zA-Z0-9_]*)\s*:/m);
1450
+ if (firstPropMatch && firstPropMatch[1] !== 'status') return firstPropMatch[1];
1451
+ } catch {
1452
+ // swallow
1453
+ }
1454
+ return 'id';
1455
+ }
1456
+
1457
+ /**
1458
+ * Regenerate module DTOs and ORM schemas when fields are updated via fieldManager
1459
+ */
1460
+ async function regenerateModuleComponents(moduleName, fields, targetDir = process.cwd()) {
1461
+ const orm = await detectOrm(targetDir);
1462
+ const kebabName = toKebabCase(moduleName);
1463
+ const pascalName = toPascalCase(moduleName);
1464
+ const singularPascal = toSingularPascal(pascalName);
1465
+
1466
+ const srcDir = path.join(targetDir, 'src');
1467
+ const moduleDir = (await fs.pathExists(srcDir))
1468
+ ? path.join(srcDir, 'modules', kebabName)
1469
+ : path.join(targetDir, 'modules', kebabName);
1470
+ const dtoDir = path.join(moduleDir, 'dto');
1471
+
1472
+ await fs.ensureDir(moduleDir);
1473
+ await fs.ensureDir(dtoDir);
1474
+
1475
+ // Detect the existing primary key from the module's response DTO
1476
+ const primaryKey = await detectModulePrimaryKey(moduleDir, kebabName);
1477
+
1478
+ // 1. Sync ORM Schema (forceUpdate=true so existing definitions are replaced)
1479
+ if (orm === 'prisma') {
1480
+ await syncPrismaSchema(targetDir, singularPascal, kebabName, primaryKey, fields, [], true, true);
1481
+ } else if (orm === 'typeorm') {
1482
+ await syncTypeOrmSchema(moduleDir, singularPascal, kebabName, primaryKey, fields, [], true);
1483
+ } else if (orm === 'mongoose') {
1484
+ await syncMongooseSchema(moduleDir, singularPascal, kebabName, primaryKey, fields, [], true);
1485
+ } else if (orm === 'drizzle') {
1486
+ await syncDrizzleSchema(moduleDir, singularPascal, kebabName, primaryKey, fields, [], true);
1487
+ }
1488
+
1489
+ // 2. Regenerate DTOs
1490
+ const createDtoName = `Create${singularPascal}Dto`;
1491
+ const updateDtoName = `Update${singularPascal}Dto`;
1492
+ const responseDtoName = `${pascalName}Dto`;
1493
+
1494
+ const createFieldsText = fields.map((f) => {
1495
+ const details = getFieldDetails(f.type);
1496
+ const ex = getFieldExampleValue(f, pascalName);
1497
+ const exValStr = typeof ex === 'string' ? `'${ex}'` : JSON.stringify(ex);
1498
+
1499
+ const swaggerDecorator = f.isOptional
1500
+ ? `@ApiPropertyOptional({ description: '${f.name} property', example: ${exValStr} })`
1501
+ : `@ApiProperty({ description: '${f.name} property', example: ${exValStr} })`;
1502
+
1503
+ const valDecorators = [...details.valDecorators];
1504
+ if (f.isOptional) {
1505
+ valDecorators.push('@IsOptional()');
1506
+ } else {
1507
+ valDecorators.push('@IsNotEmpty()');
1508
+ }
1509
+
1510
+ return ` ${swaggerDecorator}\n ${valDecorators.join('\n ')}\n ${f.name}${f.isOptional ? '?' : ''}: ${details.tsType};`;
1511
+ }).join('\n\n');
1512
+
1513
+ const hasDateFields = fields.some((f) => ['DateTime', 'timestamp', 'Date'].includes(f.type));
1514
+
1515
+ const allValDecorators = new Set(['IsOptional', 'IsNotEmpty']);
1516
+ fields.forEach((f) => {
1517
+ const details = getFieldDetails(f.type);
1518
+ details.valDecorators.forEach((dec) => {
1519
+ const name = dec.replace('@', '').replace(/\(.*\)/, '');
1520
+ // Only class-validator Is* decorators — Type comes from class-transformer, not here
1521
+ if (name && name.startsWith('Is')) allValDecorators.add(name);
1522
+ });
1523
+ });
1524
+
1525
+ await fs.writeFile(
1526
+ path.join(dtoDir, `create-${kebabName}.dto.ts`),
1527
+ `import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
1528
+ import { ${Array.from(allValDecorators).join(', ')} } from 'class-validator';
1529
+ ${hasDateFields ? `import { Type } from 'class-transformer';\n` : ''}
1530
+ export class ${createDtoName} {
1531
+ ${createFieldsText}
1532
+ }
1533
+ `
1534
+ );
1535
+
1536
+ await fs.writeFile(
1537
+ path.join(dtoDir, `update-${kebabName}.dto.ts`),
1538
+ `import { PartialType } from '@nestjs/swagger';
1539
+ import { ${createDtoName} } from './create-${kebabName}.dto';
1540
+
1541
+ export class ${updateDtoName} extends PartialType(${createDtoName}) {}
1542
+ `
1543
+ );
1544
+
1545
+ const responseFieldsText = fields.map((f) => {
1546
+ const details = getFieldDetails(f.type);
1547
+ const ex = getFieldExampleValue(f, pascalName);
1548
+ const exValStr = typeof ex === 'string' ? `'${ex}'` : JSON.stringify(ex);
1549
+
1550
+ const swaggerDecorator = f.isOptional
1551
+ ? `@ApiPropertyOptional({ example: ${exValStr} })`
1552
+ : `@ApiProperty({ example: ${exValStr} })`;
1553
+
1554
+ return ` ${swaggerDecorator}\n ${f.name}${f.isOptional ? '?' : ''}: ${details.tsType};`;
1555
+ }).join('\n\n');
1556
+
1557
+ await fs.writeFile(
1558
+ path.join(dtoDir, `${kebabName}.dto.ts`),
1559
+ `import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
1560
+
1561
+ export class ${responseDtoName} {
1562
+ @ApiProperty({ example: '123e4567-e89b-12d3-a456-426614174000', format: 'uuid' })
1563
+ ${primaryKey}: string;
1564
+
1565
+ ${responseFieldsText}
1566
+
1567
+ @ApiProperty({ example: 'ACTIVE' })
1568
+ status: string;
1569
+
1570
+ @ApiProperty({ example: '2025-01-01T00:00:00.000Z' })
1571
+ createdAt: Date;
1572
+
1573
+ @ApiProperty({ example: '2025-01-01T00:00:00.000Z' })
1574
+ updatedAt: Date;
1575
+
1576
+ @ApiPropertyOptional({ nullable: true, example: null })
1577
+ deletedAt?: Date | null;
1578
+ }
1579
+ `
1580
+ );
1581
+ }
1582
+
1395
1583
  module.exports = {
1396
1584
  generateModule,
1397
1585
  promptForModuleOptions,
1398
1586
  detectOrm,
1587
+ detectModulePrimaryKey,
1588
+ getOrmFieldChoices,
1589
+ toKebabCase,
1399
1590
  registerInAppModule,
1400
1591
  ensureBaseArchitecture,
1592
+ regenerateModuleComponents,
1401
1593
  };
package/src/postSetup.js CHANGED
@@ -48,14 +48,14 @@ async function handlePostSetup(targetDir, appName, options) {
48
48
  // Step 1: Configure JWT secrets and database URL
49
49
  await configureEnvironment(targetDir, database);
50
50
 
51
- // Step 2: ORM-specific database setup (Schema/Migration Seed)
52
- await setupDatabase(targetDir, orm, packageManager);
53
-
54
- // Step 3: CRUD module generation
51
+ // Step 2: CRUD module generation (updates ORM schema before running migrations)
55
52
  if (generateFirstCrud !== false) {
56
53
  await promptCrudGeneration(targetDir, orm, firstModuleName);
57
54
  }
58
55
 
56
+ // Step 3: ORM-specific database setup (Generate Client → Migration → Seed)
57
+ await setupDatabase(targetDir, orm, packageManager);
58
+
59
59
  // Step 4: Display Post-Setup Summary Log with Next Steps
60
60
  printNextStepsSummary(orm);
61
61