speedrun-cli 2.7.10 → 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
@@ -503,15 +503,19 @@ function getFieldExampleValue(field, pascalName) {
503
503
 
504
504
  /**
505
505
  * Dynamic ORM Schema Synchronization: Prisma
506
+ * @param {boolean} forceUpdate - When true, replace existing model block instead of skipping
506
507
  */
507
- async function syncPrismaSchema(targetDir, singularPascal, kebabName, primaryKey, fields, relations, includeStatus = true) {
508
+ async function syncPrismaSchema(targetDir, singularPascal, kebabName, primaryKey, fields, relations, includeStatus = true, forceUpdate = false) {
508
509
  try {
509
510
  const schemaPath = path.join(targetDir, 'prisma', 'schema.prisma');
510
511
  if (!(await fs.pathExists(schemaPath))) return;
511
512
 
512
513
  let content = await fs.readFile(schemaPath, 'utf8');
513
514
 
514
- 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) {
515
519
  return;
516
520
  }
517
521
 
@@ -547,9 +551,15 @@ ${relLines.length > 0 ? relLines.join('\n') + '\n' : ''}${statusLine} createdAt
547
551
  }
548
552
  `;
549
553
 
550
- 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
+
551
562
  await fs.writeFile(schemaPath, content, 'utf8');
552
- console.log(chalk.green(` ✓ Updated prisma/schema.prisma with model ${singularPascal}`));
553
563
  } catch (error) {
554
564
  console.warn(chalk.yellow(` ⚠️ Could not sync prisma/schema.prisma: ${error.message}`));
555
565
  }
@@ -835,7 +845,7 @@ class TypeOrm${pascalName}Repository implements IBaseRepository<${pascalName}Ent
835
845
  }` : `async findOne(${primaryKey}: string): Promise<${pascalName}Entity | null> { throw new Error('Not implemented'); }`}
836
846
 
837
847
  ${ops.update ? `async update(${primaryKey}: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> {
838
- await this.repo.update(${primaryKey}, dto as any);
848
+ await this.repo.update({ ${primaryKey} } as any, dto as any);
839
849
  return this.repo.findOneOrFail({ where: { ${primaryKey} } as any });
840
850
  }` : `async update(${primaryKey}: string, dto: ${updateDtoName}): Promise<${pascalName}Entity> { throw new Error('Not implemented'); }`}
841
851
 
@@ -1206,17 +1216,27 @@ async function generateModule(providedModuleName, targetDir = process.cwd(), spe
1206
1216
  return ` ${swaggerDecorator}\n ${valDecorators.join('\n ')}\n ${f.name}${f.isOptional ? '?' : ''}: ${details.tsType};`;
1207
1217
  }).join('\n\n');
1208
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
+
1209
1224
  const hasDateFields = options.fields.some((f) => ['DateTime', 'timestamp', 'Date'].includes(f.type));
1225
+ const hasRelations = (options.relations || []).length > 0;
1210
1226
 
1211
- // Validator import gathering
1227
+ // Validator import gathering — only collect 'Is*' decorators (class-validator)
1212
1228
  const allValDecorators = new Set(['IsOptional', 'IsNotEmpty']);
1213
1229
  options.fields.forEach((f) => {
1214
1230
  const details = getFieldDetails(f.type);
1215
1231
  details.valDecorators.forEach((dec) => {
1216
1232
  const name = dec.replace('@', '').replace(/\(.*\)/, '');
1217
- 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);
1218
1235
  });
1219
1236
  });
1237
+ if (hasRelations) allValDecorators.add('IsUUID');
1238
+
1239
+ const createDtoBody = [createFieldsText, relationFkFields].filter(Boolean).join('\n\n');
1220
1240
 
1221
1241
  if (ops.create || ops.update) {
1222
1242
  await fs.writeFile(
@@ -1225,7 +1245,7 @@ async function generateModule(providedModuleName, targetDir = process.cwd(), spe
1225
1245
  import { ${Array.from(allValDecorators).join(', ')} } from 'class-validator';
1226
1246
  ${hasDateFields ? `import { Type } from 'class-transformer';\n` : ''}
1227
1247
  export class ${createDtoName} {
1228
- ${createFieldsText}
1248
+ ${createDtoBody}
1229
1249
  }
1230
1250
  `
1231
1251
  );
@@ -1252,6 +1272,11 @@ export class ${updateDtoName} extends PartialType(${createDtoName}) {}
1252
1272
  return ` ${swaggerDecorator}\n ${f.name}${f.isOptional ? '?' : ''}: ${details.tsType};`;
1253
1273
  }).join('\n\n');
1254
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
+
1255
1280
  const statusDtoField = options.includeStatus !== false
1256
1281
  ? ` @ApiProperty({ example: 'ACTIVE' })\n status: string;\n\n`
1257
1282
  : '';
@@ -1264,7 +1289,9 @@ export class ${responseDtoName} {
1264
1289
  @ApiProperty({ example: '123e4567-e89b-12d3-a456-426614174000', format: 'uuid' })
1265
1290
  ${primaryKey}: string;
1266
1291
 
1267
- ${responseFieldsText}
1292
+ ${responseFieldsText}${
1293
+ responseFkFields ? `\n\n${responseFkFields}` : ''
1294
+ }
1268
1295
 
1269
1296
  ${statusDtoField} @ApiProperty({ example: '2025-01-01T00:00:00.000Z' })
1270
1297
  createdAt: Date;
@@ -1403,6 +1430,30 @@ export class AppModule {}
1403
1430
  }
1404
1431
  }
1405
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
+
1406
1457
  /**
1407
1458
  * Regenerate module DTOs and ORM schemas when fields are updated via fieldManager
1408
1459
  */
@@ -1411,7 +1462,6 @@ async function regenerateModuleComponents(moduleName, fields, targetDir = proces
1411
1462
  const kebabName = toKebabCase(moduleName);
1412
1463
  const pascalName = toPascalCase(moduleName);
1413
1464
  const singularPascal = toSingularPascal(pascalName);
1414
- const primaryKey = 'id';
1415
1465
 
1416
1466
  const srcDir = path.join(targetDir, 'src');
1417
1467
  const moduleDir = (await fs.pathExists(srcDir))
@@ -1422,9 +1472,12 @@ async function regenerateModuleComponents(moduleName, fields, targetDir = proces
1422
1472
  await fs.ensureDir(moduleDir);
1423
1473
  await fs.ensureDir(dtoDir);
1424
1474
 
1425
- // 1. Sync ORM Schema
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)
1426
1479
  if (orm === 'prisma') {
1427
- await syncPrismaSchema(targetDir, singularPascal, kebabName, primaryKey, fields, [], true);
1480
+ await syncPrismaSchema(targetDir, singularPascal, kebabName, primaryKey, fields, [], true, true);
1428
1481
  } else if (orm === 'typeorm') {
1429
1482
  await syncTypeOrmSchema(moduleDir, singularPascal, kebabName, primaryKey, fields, [], true);
1430
1483
  } else if (orm === 'mongoose') {
@@ -1464,7 +1517,8 @@ async function regenerateModuleComponents(moduleName, fields, targetDir = proces
1464
1517
  const details = getFieldDetails(f.type);
1465
1518
  details.valDecorators.forEach((dec) => {
1466
1519
  const name = dec.replace('@', '').replace(/\(.*\)/, '');
1467
- if (name) allValDecorators.add(name);
1520
+ // Only class-validator Is* decorators — Type comes from class-transformer, not here
1521
+ if (name && name.startsWith('Is')) allValDecorators.add(name);
1468
1522
  });
1469
1523
  });
1470
1524
 
@@ -1506,7 +1560,7 @@ export class ${updateDtoName} extends PartialType(${createDtoName}) {}
1506
1560
 
1507
1561
  export class ${responseDtoName} {
1508
1562
  @ApiProperty({ example: '123e4567-e89b-12d3-a456-426614174000', format: 'uuid' })
1509
- id: string;
1563
+ ${primaryKey}: string;
1510
1564
 
1511
1565
  ${responseFieldsText}
1512
1566
 
@@ -1530,6 +1584,9 @@ module.exports = {
1530
1584
  generateModule,
1531
1585
  promptForModuleOptions,
1532
1586
  detectOrm,
1587
+ detectModulePrimaryKey,
1588
+ getOrmFieldChoices,
1589
+ toKebabCase,
1533
1590
  registerInAppModule,
1534
1591
  ensureBaseArchitecture,
1535
1592
  regenerateModuleComponents,