speedrun-cli 2.7.9 → 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.
- package/CHANGELOG.md +7 -29
- package/package.json +1 -1
- package/src/index.js +22 -13
- package/src/moduleGenerator.js +164 -29
- package/src/postSetup.js +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,7 @@ All notable changes to create-nestjs-auth will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [2.
|
|
8
|
+
## [2.7.10] - 2026-08-22
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
11
|
- **Auto-Scaffold Missing Base Architecture (`ensureBaseArchitecture`)** — When generating modules via `speedrun-cli g [module]`, `src/common/base` is automatically scaffolded if missing from target project, resolving TS2307 & TS4112 compilation errors.
|
|
@@ -18,38 +18,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
18
18
|
- **Specific Field Editing Sub-menu** — Interactive sub-menu allows modifying specific field properties (Field Name, Field Type, Optional Status, or All) instead of forcing full re-entry.
|
|
19
19
|
- **Advanced Relationship Builder** — Interactively add relations (`Many-to-One`, `One-to-Many`) pointing to target modules and foreign keys, automatically injecting attributes into Prisma, TypeORM, Mongoose, and Drizzle schemas.
|
|
20
20
|
- **Auth & Roles Guard Protection Prompt** — Optional step to protect write operations (`POST`, `PUT`, `DELETE`) with `@UseGuards()` and role-based decorators (`ADMIN`, `USER`, `MANAGER`).
|
|
21
|
-
|
|
22
|
-
### Changed
|
|
23
|
-
- **Standardized Import Statements** — Controller & Service templates now fetch `BaseController`, `BaseService`, `IBaseRepository`, `ApiResponseDto`, `ApiResponseSchema`, `PaginatedResponseDto`, `PaginatedResponseSchema`, `PaginationQueryDto` cleanly from the single barrel export `../../common/base`.
|
|
24
|
-
|
|
25
|
-
---
|
|
26
|
-
|
|
27
|
-
## [2.7.1] - 2026-08-22
|
|
21
|
+
- **Optional `status` Field Prompt** — Added interactive prompt `Include default 'status' field (e.g. ACTIVE)? (Y/n)` during module generation to include or omit status field.
|
|
28
22
|
|
|
29
23
|
### Fixed
|
|
30
|
-
- **
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
## [2.7.0] - 2026-08-22
|
|
35
|
-
|
|
36
|
-
### Added
|
|
37
|
-
- **Scaffolding Cleanup** — Static `src/modules/products` directory has been completely removed from all initial project templates (`templates/base-crud*`). Scaffolding now creates zero CRUD modules by default.
|
|
38
|
-
- **Interactive Field Builder Loop** — When generating a module (via guided setup or `speedrun-cli g [module_name]`), users can interactively define custom fields (`fieldName`, `fieldType`: `String`/`Number`/`Boolean`/`Date`, `isOptional`).
|
|
39
|
-
- **Dynamic ORM Schema Synchronization** — Automatically syncs definitions to the project's detected ORM:
|
|
40
|
-
- **Prisma:** Appends `model` with appropriate data types (`String`, `Float`/`Int`, `Boolean`, `DateTime`) and optional modifiers (`?`) to `prisma/schema.prisma`.
|
|
41
|
-
- **TypeORM:** Generates `@Entity()` class in `src/modules/[kebab]/entities/[singular].entity.ts`.
|
|
42
|
-
- **Mongoose:** Generates `@Schema()` class in `src/modules/[kebab]/schemas/[singular].schema.ts`.
|
|
43
|
-
- **Drizzle:** Generates `pgTable` definition in `src/modules/[kebab]/schema/[kebab].schema.ts`.
|
|
44
|
-
- **Auto-Generated Starter Seed Template** — Creates starter seed snippet populated with dummy data based on defined fields in `prisma/seeds/[kebab].seed.ts` (Prisma) or `src/database/seeds/[kebab].seed.ts` (TypeORM/Mongoose/Drizzle).
|
|
45
|
-
- **Dynamic Post-Setup Summary Log** — Displays clear post-install next steps dynamically tailored for each ORM (migration & seed commands).
|
|
24
|
+
- **Runtime Error `TypeError: manageFields is not a function` Fixed** — Exported `fieldManager` in `src/index.js` entry point so `manageFields` is properly destructured in `bin/cli.js` when executing `speedrun-cli field [module]`.
|
|
25
|
+
- **Module Field Regeneration** — Exported `regenerateModuleComponents` in `src/moduleGenerator.js` to enable automatic DTO and ORM schema updates when field definitions are edited via `speedrun-cli field`.
|
|
26
|
+
- **Automated Migration Flow for CRUD Modules & JWT Setup** — Reordered post-setup steps in `handlePostSetup` so CRUD module generation occurs *before* database migrations/seeding. Any generated CRUD modules and schema updates are now automatically included when running `prisma migrate dev` / `schema:sync` / `db:push` and seeding during CLI setup.
|
|
27
|
+
- **Strict Input Validation for CLI Confirm Prompts** — Enforced strict `y`/`n` input validation across all CLI interactive prompts.
|
|
46
28
|
|
|
47
29
|
### Changed
|
|
48
|
-
- **
|
|
49
|
-
1. `Enable Base CRUD Architecture?` (Default: `Yes`)
|
|
50
|
-
2. `Do you want to generate your first CRUD module now?` (Default: `Yes`)
|
|
51
|
-
3. Shared Module Generator execution for module name, CRUD mode, and custom field loop.
|
|
52
|
-
- **Controller Service Constructor Visibility** fixed to `protected readonly service: [Name]Service` so generated controllers safely extend `BaseController`.
|
|
30
|
+
- **Standardized Import Statements** — Controller & Service templates now fetch `BaseController`, `BaseService`, `IBaseRepository`, `ApiResponseDto`, `ApiResponseSchema`, `PaginatedResponseDto`, `PaginatedResponseSchema`, `PaginationQueryDto` cleanly from the single barrel export `../../common/base`.
|
|
53
31
|
|
|
54
32
|
---
|
|
55
33
|
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Module exports for CLI source files
|
|
3
|
-
* @module src
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Module exports for CLI source files
|
|
3
|
+
* @module src
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const constants = require('./constants');
|
|
7
|
+
const utils = require('./utils');
|
|
8
|
+
const prompts = require('./prompts');
|
|
9
|
+
const generator = require('./generator');
|
|
10
|
+
const postSetup = require('./postSetup');
|
|
11
|
+
const moduleGenerator = require('./moduleGenerator');
|
|
12
|
+
const fieldManager = require('./fieldManager');
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
...constants,
|
|
16
|
+
...utils,
|
|
17
|
+
...prompts,
|
|
18
|
+
...generator,
|
|
19
|
+
...postSetup,
|
|
20
|
+
...moduleGenerator,
|
|
21
|
+
...fieldManager,
|
|
22
|
+
};
|
package/src/moduleGenerator.js
CHANGED
|
@@ -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
|
};
|
|
@@ -495,7 +504,7 @@ function getFieldExampleValue(field, pascalName) {
|
|
|
495
504
|
/**
|
|
496
505
|
* Dynamic ORM Schema Synchronization: Prisma
|
|
497
506
|
*/
|
|
498
|
-
async function syncPrismaSchema(targetDir, singularPascal, kebabName, primaryKey, fields, relations) {
|
|
507
|
+
async function syncPrismaSchema(targetDir, singularPascal, kebabName, primaryKey, fields, relations, includeStatus = true) {
|
|
499
508
|
try {
|
|
500
509
|
const schemaPath = path.join(targetDir, 'prisma', 'schema.prisma');
|
|
501
510
|
if (!(await fs.pathExists(schemaPath))) return;
|
|
@@ -524,12 +533,13 @@ async function syncPrismaSchema(targetDir, singularPascal, kebabName, primaryKey
|
|
|
524
533
|
return ` ${targetCamel} ${targetPascal}? @relation(fields: [${r.fkField}], references: [id])\n ${r.fkField} String?`;
|
|
525
534
|
});
|
|
526
535
|
|
|
536
|
+
const statusLine = includeStatus ? ' status String @default("ACTIVE")\n' : '';
|
|
537
|
+
|
|
527
538
|
const modelDefinition = `
|
|
528
539
|
model ${singularPascal} {
|
|
529
540
|
${primaryKey} String @id @default(uuid())
|
|
530
541
|
${fieldLines.join('\n')}
|
|
531
|
-
${relLines.length > 0 ? relLines.join('\n') + '\n' : ''}
|
|
532
|
-
createdAt DateTime @default(now())
|
|
542
|
+
${relLines.length > 0 ? relLines.join('\n') + '\n' : ''}${statusLine} createdAt DateTime @default(now())
|
|
533
543
|
updatedAt DateTime @updatedAt
|
|
534
544
|
deletedAt DateTime?
|
|
535
545
|
|
|
@@ -548,7 +558,7 @@ ${relLines.length > 0 ? relLines.join('\n') + '\n' : ''} status String @de
|
|
|
548
558
|
/**
|
|
549
559
|
* Dynamic ORM Schema Synchronization: TypeORM
|
|
550
560
|
*/
|
|
551
|
-
async function syncTypeOrmSchema(moduleDir, singularPascal, kebabName, primaryKey, fields, relations) {
|
|
561
|
+
async function syncTypeOrmSchema(moduleDir, singularPascal, kebabName, primaryKey, fields, relations, includeStatus = true) {
|
|
552
562
|
try {
|
|
553
563
|
const entityDir = path.join(moduleDir, 'entities');
|
|
554
564
|
await fs.ensureDir(entityDir);
|
|
@@ -582,6 +592,8 @@ async function syncTypeOrmSchema(moduleDir, singularPascal, kebabName, primaryKe
|
|
|
582
592
|
const hasRelations = relations.length > 0;
|
|
583
593
|
const imports = [`Entity`, `PrimaryGeneratedColumn`, `Column`, `CreateDateColumn`, `UpdateDateColumn`, `DeleteDateColumn`].concat(hasRelations ? [`ManyToOne`, `JoinColumn`] : []);
|
|
584
594
|
|
|
595
|
+
const statusLine = includeStatus ? ' @Column({ default: \'ACTIVE\' })\n status: string;\n\n' : '';
|
|
596
|
+
|
|
585
597
|
const entityContent = `import { ${imports.join(', ')} } from 'typeorm';
|
|
586
598
|
|
|
587
599
|
@Entity('${toSnakeCase(kebabName)}')
|
|
@@ -591,10 +603,7 @@ export class ${singularPascal} {
|
|
|
591
603
|
|
|
592
604
|
${fieldLines.join('\n\n')}
|
|
593
605
|
|
|
594
|
-
${relLines.length > 0 ? relLines.join('\n\n') + '\n\n' : ''} @
|
|
595
|
-
status: string;
|
|
596
|
-
|
|
597
|
-
@CreateDateColumn()
|
|
606
|
+
${relLines.length > 0 ? relLines.join('\n\n') + '\n\n' : ''}${statusLine} @CreateDateColumn()
|
|
598
607
|
createdAt: Date;
|
|
599
608
|
|
|
600
609
|
@UpdateDateColumn()
|
|
@@ -615,7 +624,7 @@ ${relLines.length > 0 ? relLines.join('\n\n') + '\n\n' : ''} @Column({ default:
|
|
|
615
624
|
/**
|
|
616
625
|
* Dynamic ORM Schema Synchronization: Mongoose
|
|
617
626
|
*/
|
|
618
|
-
async function syncMongooseSchema(moduleDir, singularPascal, kebabName, primaryKey, fields, relations) {
|
|
627
|
+
async function syncMongooseSchema(moduleDir, singularPascal, kebabName, primaryKey, fields, relations, includeStatus = true) {
|
|
619
628
|
try {
|
|
620
629
|
const schemaDir = path.join(moduleDir, 'schemas');
|
|
621
630
|
await fs.ensureDir(schemaDir);
|
|
@@ -647,6 +656,8 @@ async function syncMongooseSchema(moduleDir, singularPascal, kebabName, primaryK
|
|
|
647
656
|
? ` @Prop({ default: () => new Types.ObjectId().toString() })\n ${primaryKey}: string;\n\n`
|
|
648
657
|
: '';
|
|
649
658
|
|
|
659
|
+
const statusLine = includeStatus ? ' @Prop({ default: \'ACTIVE\' })\n status: string;\n\n' : '';
|
|
660
|
+
|
|
650
661
|
const schemaContent = `import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
|
651
662
|
import { HydratedDocument, SchemaTypes, Types } from 'mongoose';
|
|
652
663
|
|
|
@@ -656,10 +667,7 @@ export type ${singularPascal}Document = HydratedDocument<${singularPascal}>;
|
|
|
656
667
|
export class ${singularPascal} {
|
|
657
668
|
${pkLine}${fieldLines.join('\n\n')}
|
|
658
669
|
|
|
659
|
-
${relLines.length > 0 ? relLines.join('\n\n') + '\n\n' : ''} @Prop({ default:
|
|
660
|
-
status: string;
|
|
661
|
-
|
|
662
|
-
@Prop({ type: Date, default: null })
|
|
670
|
+
${relLines.length > 0 ? relLines.join('\n\n') + '\n\n' : ''}${statusLine} @Prop({ type: Date, default: null })
|
|
663
671
|
deletedAt?: Date | null;
|
|
664
672
|
}
|
|
665
673
|
|
|
@@ -676,7 +684,7 @@ export const ${singularPascal}Schema = SchemaFactory.createForClass(${singularPa
|
|
|
676
684
|
/**
|
|
677
685
|
* Dynamic ORM Schema Synchronization: Drizzle
|
|
678
686
|
*/
|
|
679
|
-
async function syncDrizzleSchema(moduleDir, singularPascal, kebabName, primaryKey, fields, relations) {
|
|
687
|
+
async function syncDrizzleSchema(moduleDir, singularPascal, kebabName, primaryKey, fields, relations, includeStatus = true) {
|
|
680
688
|
try {
|
|
681
689
|
const schemaDir = path.join(moduleDir, 'schema');
|
|
682
690
|
await fs.ensureDir(schemaDir);
|
|
@@ -709,13 +717,14 @@ async function syncDrizzleSchema(moduleDir, singularPascal, kebabName, primaryKe
|
|
|
709
717
|
return ` ${r.fkField}: varchar('${toSnakeCase(r.fkField)}', { length: 36 }),`;
|
|
710
718
|
});
|
|
711
719
|
|
|
720
|
+
const statusLine = includeStatus ? " status: varchar('status', { length: 50 }).default('ACTIVE').notNull(),\n" : '';
|
|
721
|
+
|
|
712
722
|
const schemaContent = `import { pgTable, varchar, text, integer, numeric, boolean, timestamp, json } from 'drizzle-orm/pg-core';
|
|
713
723
|
|
|
714
724
|
export const ${toCamelCase(kebabName)}s = pgTable('${toSnakeCase(kebabName)}', {
|
|
715
725
|
${primaryKey}: varchar('${toSnakeCase(primaryKey)}', { length: 36 }).primaryKey().$defaultFn(() => crypto.randomUUID()),
|
|
716
726
|
${fieldLines.join('\n')}
|
|
717
|
-
${relLines.length > 0 ? relLines.join('\n') + '\n' : ''}
|
|
718
|
-
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
727
|
+
${relLines.length > 0 ? relLines.join('\n') + '\n' : ''}${statusLine} createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
719
728
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
720
729
|
deletedAt: timestamp('deleted_at'),
|
|
721
730
|
});
|
|
@@ -734,7 +743,7 @@ export type New${singularPascal} = typeof ${toCamelCase(kebabName)}s.$inferInser
|
|
|
734
743
|
/**
|
|
735
744
|
* Auto-Generate Starter Seed File Template
|
|
736
745
|
*/
|
|
737
|
-
async function generateSeedFileTemplate(targetDir, orm, singularPascal, kebabName, primaryKey, fields) {
|
|
746
|
+
async function generateSeedFileTemplate(targetDir, orm, singularPascal, kebabName, primaryKey, fields, includeStatus = true) {
|
|
738
747
|
try {
|
|
739
748
|
const singularCamel = toSingularCamel(kebabName);
|
|
740
749
|
const dummyObjFields = fields.map((f) => {
|
|
@@ -743,6 +752,8 @@ async function generateSeedFileTemplate(targetDir, orm, singularPascal, kebabNam
|
|
|
743
752
|
return ` ${f.name}: ${valStr},`;
|
|
744
753
|
}).join('\n');
|
|
745
754
|
|
|
755
|
+
const statusProp = includeStatus ? '\n status: \'ACTIVE\',' : '';
|
|
756
|
+
|
|
746
757
|
if (orm === 'prisma') {
|
|
747
758
|
const seedsDir = path.join(targetDir, 'prisma', 'seeds');
|
|
748
759
|
await fs.ensureDir(seedsDir);
|
|
@@ -754,8 +765,7 @@ export async function seed${singularPascal}(prisma: PrismaClient) {
|
|
|
754
765
|
console.log('🌱 Seeding ${singularPascal}...');
|
|
755
766
|
await (prisma as any).${singularCamel}.create({
|
|
756
767
|
data: {
|
|
757
|
-
${dummyObjFields}
|
|
758
|
-
status: 'ACTIVE',
|
|
768
|
+
${dummyObjFields}${statusProp}
|
|
759
769
|
},
|
|
760
770
|
});
|
|
761
771
|
}
|
|
@@ -1163,13 +1173,13 @@ async function generateModule(providedModuleName, targetDir = process.cwd(), spe
|
|
|
1163
1173
|
|
|
1164
1174
|
// 1. Dynamic ORM Schema Synchronization
|
|
1165
1175
|
if (orm === 'prisma') {
|
|
1166
|
-
await syncPrismaSchema(targetDir, singularPascal, kebabName, primaryKey, options.fields, options.relations);
|
|
1176
|
+
await syncPrismaSchema(targetDir, singularPascal, kebabName, primaryKey, options.fields, options.relations, options.includeStatus);
|
|
1167
1177
|
} else if (orm === 'typeorm') {
|
|
1168
|
-
await syncTypeOrmSchema(moduleDir, singularPascal, kebabName, primaryKey, options.fields, options.relations);
|
|
1178
|
+
await syncTypeOrmSchema(moduleDir, singularPascal, kebabName, primaryKey, options.fields, options.relations, options.includeStatus);
|
|
1169
1179
|
} else if (orm === 'mongoose') {
|
|
1170
|
-
await syncMongooseSchema(moduleDir, singularPascal, kebabName, primaryKey, options.fields, options.relations);
|
|
1180
|
+
await syncMongooseSchema(moduleDir, singularPascal, kebabName, primaryKey, options.fields, options.relations, options.includeStatus);
|
|
1171
1181
|
} else if (orm === 'drizzle') {
|
|
1172
|
-
await syncDrizzleSchema(moduleDir, singularPascal, kebabName, primaryKey, options.fields, options.relations);
|
|
1182
|
+
await syncDrizzleSchema(moduleDir, singularPascal, kebabName, primaryKey, options.fields, options.relations, options.includeStatus);
|
|
1173
1183
|
}
|
|
1174
1184
|
|
|
1175
1185
|
// 2. Generate DTOs
|
|
@@ -1242,6 +1252,10 @@ export class ${updateDtoName} extends PartialType(${createDtoName}) {}
|
|
|
1242
1252
|
return ` ${swaggerDecorator}\n ${f.name}${f.isOptional ? '?' : ''}: ${details.tsType};`;
|
|
1243
1253
|
}).join('\n\n');
|
|
1244
1254
|
|
|
1255
|
+
const statusDtoField = options.includeStatus !== false
|
|
1256
|
+
? ` @ApiProperty({ example: 'ACTIVE' })\n status: string;\n\n`
|
|
1257
|
+
: '';
|
|
1258
|
+
|
|
1245
1259
|
await fs.writeFile(
|
|
1246
1260
|
path.join(dtoDir, `${kebabName}.dto.ts`),
|
|
1247
1261
|
`import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
@@ -1252,10 +1266,7 @@ export class ${responseDtoName} {
|
|
|
1252
1266
|
|
|
1253
1267
|
${responseFieldsText}
|
|
1254
1268
|
|
|
1255
|
-
@ApiProperty({ example: '
|
|
1256
|
-
status: string;
|
|
1257
|
-
|
|
1258
|
-
@ApiProperty({ example: '2025-01-01T00:00:00.000Z' })
|
|
1269
|
+
${statusDtoField} @ApiProperty({ example: '2025-01-01T00:00:00.000Z' })
|
|
1259
1270
|
createdAt: Date;
|
|
1260
1271
|
|
|
1261
1272
|
@ApiProperty({ example: '2025-01-01T00:00:00.000Z' })
|
|
@@ -1365,7 +1376,7 @@ ${ops.create ? `
|
|
|
1365
1376
|
const isAutoRegistered = await registerInAppModule(targetDir, pascalName, kebabName);
|
|
1366
1377
|
|
|
1367
1378
|
// 7. Auto-Generate Starter Seed File Template
|
|
1368
|
-
await generateSeedFileTemplate(targetDir, orm, singularPascal, kebabName, primaryKey, options.fields);
|
|
1379
|
+
await generateSeedFileTemplate(targetDir, orm, singularPascal, kebabName, primaryKey, options.fields, options.includeStatus);
|
|
1369
1380
|
|
|
1370
1381
|
console.log(chalk.green(`\n✅ Module "${kebabName}" successfully generated in ${path.relative(process.cwd(), moduleDir)}`));
|
|
1371
1382
|
console.log(chalk.gray(` Detected ORM: ${orm}`));
|
|
@@ -1392,10 +1403,134 @@ export class AppModule {}
|
|
|
1392
1403
|
}
|
|
1393
1404
|
}
|
|
1394
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
|
+
|
|
1395
1529
|
module.exports = {
|
|
1396
1530
|
generateModule,
|
|
1397
1531
|
promptForModuleOptions,
|
|
1398
1532
|
detectOrm,
|
|
1399
1533
|
registerInAppModule,
|
|
1400
1534
|
ensureBaseArchitecture,
|
|
1535
|
+
regenerateModuleComponents,
|
|
1401
1536
|
};
|
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
|
|