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.
- package/CHANGELOG.md +45 -29
- package/README.md +128 -491
- package/package.json +1 -1
- package/src/fieldManager.js +338 -191
- package/src/index.js +22 -13
- package/src/moduleGenerator.js +230 -38
- package/src/postSetup.js +4 -4
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
|
|
@@ -444,7 +444,15 @@ async function promptForModuleOptions(providedModuleName, detectedOrm = 'prisma'
|
|
|
444
444
|
}
|
|
445
445
|
}
|
|
446
446
|
|
|
447
|
-
// 4.
|
|
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
|
-
|
|
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' : ''}
|
|
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
|
-
|
|
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' : ''} @
|
|
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:
|
|
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' : ''}
|
|
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
|
-
|
|
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
|
-
${
|
|
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
|
-
|
|
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:
|
|
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
|
|