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.
- package/CHANGELOG.md +38 -0
- package/README.md +128 -491
- package/package.json +1 -1
- package/src/fieldManager.js +338 -191
- package/src/moduleGenerator.js +72 -15
package/src/moduleGenerator.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
${
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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,
|