speedrun-cli 2.6.7 → 2.6.9

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 CHANGED
@@ -5,8 +5,64 @@ 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.6.9] - 2026-08-21
9
+
10
+ ### Fixed
11
+ - **Products module no longer shipped by default** — `src/modules/products/` was incorrectly included in the `base-crud` shared template and therefore copied into every project where `--base-crud` was enabled, even before the user ran `speedrun-cli generate`. The products example files have been moved exclusively into the ORM-specific overlay templates (`base-crud-{orm}`), which are only written during the guided CRUD setup step. The `base-crud` shared layer now only contains the abstract classes (`src/common/base/`) and `CRUD_README.md`.
12
+ - **Seed runs twice on Prisma** — removed the `"prisma": { "seed": "ts-node prisma/seed.ts" }` field from `templates/orm/prisma/package.json`. This field caused Prisma to automatically invoke the seed at the end of every `prisma migrate dev`, while `postSetup.js` also called `npm run prisma:seed` explicitly — resulting in the seed being executed twice and credentials being printed double.
13
+
14
+ ---
15
+
16
+ ## [2.6.8] - 2026-08-21
17
+
18
+ ### Added
19
+ - **Guided CRUD setup step** — `? Do you want to generate your first CRUD module now?` is now part of the main setup flow, positioned after Database (Schema/Migration → Seed) and before Dev server start
20
+ - **`promptCrudGeneration()`** in `postSetup.js` — reusable function that calls `generateModule` with the selected ORM token, and auto-registers the generated module in `src/app.module.ts`
21
+ - **Manual instructions** now include a CRUD generation step (`speedrun-cli generate <name>`) when interactive setup is skipped
22
+
23
+ ### Changed
24
+ - Setup prompt message updated: `"Would you like to complete the setup now? (JWT secrets, database, CRUD)"` to accurately reflect full scope
25
+ - CRUD generation is now **always offered** during guided setup (previously only shown when `--base-crud` flag was set, and appeared _after_ the dev server prompt)
26
+ - Guided setup flow order: **JWT → Database → CRUD → Dev server**
27
+
28
+ ### Fixed
29
+ - **Duplicate CRUD prompt** — removed orphaned CRUD prompt block from `bin/cli.js`; single source of truth is now `postSetup.js`
30
+ - **`registerInAppModule` false positive** — overly broad `includes(${Name}Module)` check replaced with regex word-boundary `\b${Name}Module\b` to prevent partial-name collisions
31
+ - **Drizzle inject token mismatch** — `@Inject('DB_CONNECTION')` corrected to `@Inject('DRIZZLE')` to match the exported token in `database.module.ts`
32
+ - **TypeORM `@InjectRepository(Object)`** — added inline comment guiding users to replace `Object` with their actual entity class
33
+ - Ctrl+C during dev server no longer silently drops the CRUD prompt (prompt now runs _before_ the dev server)
34
+
35
+ ---
36
+
37
+ ## [2.6.0] - 2026-08-18
38
+
39
+ ### Added
40
+ - **Base CRUD Architecture** (`--base-crud` flag) — generates abstract `BaseService<T>` and `BaseController<T>` in `src/common/base/` with full Swagger integration
41
+ - **`BaseService<T>`** — generic CRUD abstraction with `create`, `findAll` (paginated), `findOne`, `update`, `remove` (soft-delete) and automatic `NotFoundException` throwing
42
+ - **`BaseController<T>`** — generic REST controller wiring `BaseService` methods to NestJS route decorators with full `@nestjs/swagger` decorators
43
+ - **Swagger DTO helpers** — `ApiResponseDto<T>`, `PaginatedResponseDto<T>`, `ApiResponseSchema()`, `PaginatedResponseSchema()` utility functions
44
+ - **ORM-specific ProductModule examples** — concrete `ProductsService` + `ProductsModule` templates for all four ORMs (`base-crud-{prisma,typeorm,drizzle,mongoose}`)
45
+ - **`ProductEntity` alias pattern** — each ORM service exports `export type ProductEntity = <OrmType>` so the shared controller imports from a single uniform name
46
+ - **`CRUD_README.md`** — full guide and cheatsheet copied into generated projects when `--base-crud` is enabled
47
+ - **`speedrun-cli generate [module-name]`** (`g` alias) — interactive CRUD module generator:
48
+ - Full CRUD or Custom Selection (checkbox) for individual operations
49
+ - ORM auto-detection from `package.json` dependencies
50
+ - Generates `service`, `controller`, `module`, and `dto/` files
51
+ - Auto-registers generated module in `src/app.module.ts`
52
+
53
+ ### Changed
54
+ - Generator step 6 split into **6a (shared base-crud)** + **6b (ORM-specific overlay)** for correct template composition
55
+ - `printSuccessHeader` now displays Base CRUD status in success output
56
+
57
+ ### Fixed
58
+ - Prisma template import paths corrected (`../../database/` → `../../prisma/`) to resolve `TS2307` errors
59
+ - Removed direct `@prisma/client` model imports in templates; replaced with local interface stubs to decouple from user schema (`TS2305` fix)
60
+
61
+ ---
62
+
8
63
  ## [2.0.8] - 2025-12-05
9
64
 
65
+
10
66
  ### Fixed
11
67
  - Fixed template copy failure when CLI is installed globally or via npx (node_modules path check issue)
12
68
  - Fixed .gitignore not being included in generated projects (renamed to gitignore for npm compatibility)
@@ -91,6 +147,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
91
147
 
92
148
  | Version | Date | Description |
93
149
  |---------|------|-------------|
150
+ | 2.6.8 | 2026-08-21 | CRUD generation integrated into guided setup flow + bug fixes |
151
+ | 2.6.0 | 2026-08-18 | Base CRUD Architecture, ORM-specific templates, `generate` command |
152
+ | 2.0.8 | 2025-12-05 | Template copy & Prisma migration prompt fixes |
94
153
  | 2.0.0 | 2025-12-04 | Multi-ORM and multi-database support |
95
154
  | 1.1.0 | 2025-11-17 | Interactive mode and post-setup automation |
96
155
  | 1.0.0 | 2025-11-16 | Initial release with Prisma + PostgreSQL |
package/bin/cli.js CHANGED
@@ -189,21 +189,6 @@ program
189
189
  console.log(chalk.cyan('🏗️ Base CRUD: src/common/base/ — see CRUD_README.md'));
190
190
  }
191
191
  console.log(chalk.magenta('\nHappy coding! 🎉\n'));
192
-
193
- // Post-setup module generation hook
194
- if (projectOptions.baseCrud) {
195
- const inquirer = require('inquirer');
196
- const { generateNow } = await inquirer.prompt([{
197
- type: 'confirm',
198
- name: 'generateNow',
199
- message: 'Do you want to generate your first CRUD module now?',
200
- default: true
201
- }]);
202
-
203
- if (generateNow) {
204
- await generateModule(undefined, targetDir, projectOptions.orm);
205
- }
206
- }
207
192
  }
208
193
 
209
194
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "speedrun-cli",
3
- "version": "2.6.7",
3
+ "version": "2.6.9",
4
4
  "description": "CLI tool to scaffold a production-ready NestJS authentication system with JWT, refresh tokens, and RBAC",
5
5
  "keywords": [
6
6
  "nestjs",
@@ -141,7 +141,7 @@ class TypeOrm${pascalName}Repository implements IBaseRepository<${pascalName}Ent
141
141
  export class ${pascalName}Service extends BaseService<${pascalName}Entity, ${createDtoName}, ${updateDtoName}> {
142
142
  private readonly repository: TypeOrm${pascalName}Repository;
143
143
 
144
- constructor(@InjectRepository(Object) private readonly repo: Repository<${pascalName}Entity>) {
144
+ constructor(@InjectRepository(Object /* Replace with your Entity class, e.g. ${pascalName}Entity */) private readonly repo: Repository<${pascalName}Entity>) {
145
145
  super();
146
146
  this.repository = new TypeOrm${pascalName}Repository(this.repo);
147
147
  }
@@ -259,7 +259,7 @@ class Drizzle${pascalName}Repository implements IBaseRepository<${pascalName}Ent
259
259
  export class ${pascalName}Service extends BaseService<${pascalName}Entity, ${createDtoName}, ${updateDtoName}> {
260
260
  private readonly repository: Drizzle${pascalName}Repository;
261
261
 
262
- constructor(@Inject('DB_CONNECTION') private readonly db: any) {
262
+ constructor(@Inject('DRIZZLE') private readonly db: any) {
263
263
  super();
264
264
  this.repository = new Drizzle${pascalName}Repository(this.db);
265
265
  }
@@ -397,8 +397,8 @@ async function registerInAppModule(targetDir, pascalName, kebabName) {
397
397
  let content = await fs.readFile(appModulePath, 'utf8');
398
398
  const moduleImport = `import { ${pascalName}Module } from './modules/${kebabName}/${kebabName}.module';`;
399
399
 
400
- // Cegah duplikasi import
401
- if (content.includes(moduleImport) || content.includes(`${pascalName}Module`)) {
400
+ // Prevent duplicate registration — use exact module name match
401
+ if (content.includes(moduleImport) || new RegExp(`\\b${pascalName}Module\\b`).test(content)) {
402
402
  return true;
403
403
  }
404
404
 
package/src/postSetup.js CHANGED
@@ -10,6 +10,12 @@ const chalk = require('chalk');
10
10
  const inquirer = require('inquirer');
11
11
  const { ORM_OPTIONS, DATABASE_OPTIONS } = require('./constants');
12
12
  const { generateJWTSecret, getRunPrefix } = require('./utils');
13
+ // Lazy-loaded to avoid circular deps: moduleGenerator requires constants/utils
14
+ let _generateModule;
15
+ function getGenerateModule() {
16
+ if (!_generateModule) _generateModule = require('./moduleGenerator').generateModule;
17
+ return _generateModule;
18
+ }
13
19
 
14
20
  /**
15
21
  * Handles interactive post-setup configuration
@@ -30,7 +36,7 @@ async function handlePostSetup(targetDir, appName, options) {
30
36
  const { continueSetup } = await inquirer.prompt([{
31
37
  type: 'confirm',
32
38
  name: 'continueSetup',
33
- message: 'Would you like to complete the setup now? (JWT secrets, database, etc.)',
39
+ message: 'Would you like to complete the setup now? (JWT secrets, database, CRUD)',
34
40
  default: true,
35
41
  }]);
36
42
 
@@ -38,13 +44,16 @@ async function handlePostSetup(targetDir, appName, options) {
38
44
  return false;
39
45
  }
40
46
 
41
- // Configure JWT secrets and database URL
47
+ // Step 1: Configure JWT secrets and database URL
42
48
  await configureEnvironment(targetDir, database);
43
49
 
44
- // ORM-specific database setup
50
+ // Step 2: ORM-specific database setup (Schema/Migration → Seed)
45
51
  await setupDatabase(targetDir, orm, packageManager);
46
52
 
47
- // Optionally start dev server
53
+ // Step 3: CRUD module generation (always offered when setup is accepted)
54
+ await promptCrudGeneration(targetDir, orm);
55
+
56
+ // Step 4: Optionally start dev server
48
57
  await promptDevServer(targetDir, packageManager);
49
58
 
50
59
  return true;
@@ -272,6 +281,30 @@ function printCredentials() {
272
281
  console.log(chalk.white(' Password: Admin@123\n'));
273
282
  }
274
283
 
284
+ /**
285
+ * Prompts user to generate their first CRUD module
286
+ */
287
+ async function promptCrudGeneration(targetDir, orm) {
288
+ const { generateNow } = await inquirer.prompt([{
289
+ type: 'confirm',
290
+ name: 'generateNow',
291
+ message: 'Do you want to generate your first CRUD module now?',
292
+ default: true,
293
+ }]);
294
+
295
+ if (!generateNow) {
296
+ console.log(chalk.gray('\n Skipping CRUD generation. Run `speedrun-cli generate <name>` anytime.\n'));
297
+ return;
298
+ }
299
+
300
+ try {
301
+ await getGenerateModule()(undefined, targetDir, orm);
302
+ } catch (err) {
303
+ console.warn(chalk.yellow(`\n ⚠️ CRUD generation failed: ${err.message}`));
304
+ console.warn(chalk.gray(' Run `speedrun-cli generate <name>` manually inside your project.\n'));
305
+ }
306
+ }
307
+
275
308
  /**
276
309
  * Prompts user to start dev server
277
310
  */
@@ -339,6 +372,10 @@ function printManualInstructions(appName, options) {
339
372
  commands.forEach((cmd) => console.log(chalk.gray(` npm run ${cmd}`)));
340
373
  }
341
374
 
375
+ console.log(chalk.cyan('\n # Generate your first CRUD module:'));
376
+ console.log(chalk.gray(' speedrun-cli generate <module-name>'));
377
+ console.log(chalk.gray(' # e.g. speedrun-cli generate orders'));
378
+
342
379
  console.log(chalk.cyan('\n # Start development server:'));
343
380
  console.log(chalk.gray(' npm run start:dev'));
344
381
 
@@ -27,9 +27,6 @@
27
27
  "prisma:studio": "prisma studio",
28
28
  "prisma:reset": "prisma migrate reset --force"
29
29
  },
30
- "prisma": {
31
- "seed": "ts-node prisma/seed.ts"
32
- },
33
30
  "dependencies": {
34
31
  "@nestjs/common": "^11.0.1",
35
32
  "@nestjs/config": "^4.0.2",
@@ -1,44 +0,0 @@
1
- import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2
- import {
3
- IsString, IsNotEmpty, IsNumber, IsPositive, IsOptional,
4
- IsEnum, MinLength, MaxLength, Min, IsInt, Matches,
5
- } from 'class-validator';
6
- import { Type } from 'class-transformer';
7
-
8
- export enum ProductStatus {
9
- ACTIVE = 'ACTIVE',
10
- INACTIVE = 'INACTIVE',
11
- OUT_OF_STOCK = 'OUT_OF_STOCK',
12
- DISCONTINUED = 'DISCONTINUED',
13
- }
14
-
15
- export class CreateProductDto {
16
- @ApiProperty({ description: 'Product name', example: 'Wireless Mechanical Keyboard', minLength: 3, maxLength: 150 })
17
- @IsString() @IsNotEmpty() @MinLength(3) @MaxLength(150)
18
- name: string;
19
-
20
- @ApiPropertyOptional({ description: 'Product description', example: 'Compact TKL layout with RGB', maxLength: 1000 })
21
- @IsOptional() @IsString() @MaxLength(1000)
22
- description?: string;
23
-
24
- @ApiProperty({ description: 'Unique SKU (uppercase, numbers, hyphens)', example: 'KB-WL-MEC-001', pattern: '^[A-Z0-9-]+$' })
25
- @IsString() @IsNotEmpty()
26
- @Matches(/^[A-Z0-9-]+$/, { message: 'sku must contain only uppercase letters, numbers, and hyphens' })
27
- sku: string;
28
-
29
- @ApiProperty({ description: 'Price in smallest currency unit (cents)', example: 149999, minimum: 0 })
30
- @Type(() => Number) @IsNumber() @IsPositive()
31
- price: number;
32
-
33
- @ApiProperty({ description: 'Available stock quantity', example: 250, minimum: 0 })
34
- @Type(() => Number) @IsInt() @Min(0)
35
- stock: number;
36
-
37
- @ApiPropertyOptional({ description: 'Product category', example: 'Peripherals', maxLength: 100 })
38
- @IsOptional() @IsString() @MaxLength(100)
39
- category?: string;
40
-
41
- @ApiPropertyOptional({ enum: ProductStatus, default: ProductStatus.ACTIVE, example: ProductStatus.ACTIVE })
42
- @IsOptional() @IsEnum(ProductStatus)
43
- status?: ProductStatus = ProductStatus.ACTIVE;
44
- }
@@ -1,38 +0,0 @@
1
- import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2
- import { ProductStatus } from './create-product.dto';
3
-
4
- /** Product response DTO — shape returned from the API, used by Swagger @ApiExtraModels */
5
- export class ProductDto {
6
- @ApiProperty({ description: 'Product UUID v4', example: '123e4567-e89b-12d3-a456-426614174000', format: 'uuid' })
7
- id: string;
8
-
9
- @ApiProperty({ example: 'Wireless Mechanical Keyboard' })
10
- name: string;
11
-
12
- @ApiPropertyOptional({ example: 'Compact TKL layout with RGB' })
13
- description?: string;
14
-
15
- @ApiProperty({ example: 'KB-WL-MEC-001' })
16
- sku: string;
17
-
18
- @ApiProperty({ description: 'Price in cents', example: 149999 })
19
- price: number;
20
-
21
- @ApiProperty({ example: 250 })
22
- stock: number;
23
-
24
- @ApiPropertyOptional({ example: 'Peripherals' })
25
- category?: string;
26
-
27
- @ApiProperty({ enum: ProductStatus, example: ProductStatus.ACTIVE })
28
- status: ProductStatus;
29
-
30
- @ApiProperty({ example: '2025-01-01T00:00:00.000Z' })
31
- createdAt: Date;
32
-
33
- @ApiProperty({ example: '2025-01-15T08:30:00.000Z' })
34
- updatedAt: Date;
35
-
36
- @ApiPropertyOptional({ nullable: true, example: null })
37
- deletedAt?: Date | null;
38
- }
@@ -1,12 +0,0 @@
1
- import { PartialType } from '@nestjs/swagger';
2
- import { CreateProductDto } from './create-product.dto';
3
-
4
- /**
5
- * Update Product DTO
6
- *
7
- * Uses PartialType from @nestjs/swagger (NOT @nestjs/mapped-types) to:
8
- * 1. Make all fields optional (PATCH semantics)
9
- * 2. Preserve @ApiProperty decorators for Swagger schema rendering
10
- * 3. Keep all class-validator rules active on provided fields
11
- */
12
- export class UpdateProductDto extends PartialType(CreateProductDto) {}
@@ -1,94 +0,0 @@
1
- import {
2
- Controller,
3
- Get,
4
- Param,
5
- Query,
6
- ParseUUIDPipe,
7
- HttpStatus,
8
- Type,
9
- } from '@nestjs/common';
10
- import {
11
- ApiTags,
12
- ApiOperation,
13
- ApiResponse,
14
- ApiBearerAuth,
15
- ApiExtraModels,
16
- ApiParam,
17
- } from '@nestjs/swagger';
18
- import { BaseController } from '../../common/base/base.controller';
19
- import {
20
- ApiResponseDto,
21
- ApiResponseSchema,
22
- PaginatedResponseDto,
23
- PaginatedResponseSchema,
24
- PaginationQueryDto,
25
- } from '../../common/base';
26
- import { ProductsService, ProductEntity } from './products.service';
27
- import { CreateProductDto } from './dto/create-product.dto';
28
- import { UpdateProductDto } from './dto/update-product.dto';
29
- import { ProductDto } from './dto/product.dto';
30
-
31
- // ─────────────────────────────────────────────────────────────
32
- // Products Controller
33
- //
34
- // Extends BaseController which provides (via ORM-specific service):
35
- // POST /products → create()
36
- // GET /products → findAll()
37
- // GET /products/:id → findOne()
38
- // PUT /products/:id → update()
39
- // DELETE /products/:id → remove()
40
- //
41
- // @ApiExtraModels registers DTOs so Swagger renders
42
- // generic ApiResponseDto<ProductDto> and PaginatedResponseDto<ProductDto>.
43
- // ─────────────────────────────────────────────────────────────
44
- @ApiTags('Products')
45
- @ApiBearerAuth('bearer')
46
- @ApiExtraModels(ApiResponseDto, PaginatedResponseDto, ProductDto)
47
- @Controller('products')
48
- export class ProductsController extends BaseController<
49
- ProductEntity,
50
- CreateProductDto,
51
- UpdateProductDto
52
- > {
53
- constructor(private readonly productsService: ProductsService) {
54
- super(productsService);
55
- }
56
-
57
- protected getDtoClass(): Type<ProductEntity> {
58
- return ProductDto as unknown as Type<ProductEntity>;
59
- }
60
-
61
- // ── Overrides to inject Swagger response schemas ────────────
62
-
63
- @Get()
64
- @ApiOperation({ summary: 'Get all products (paginated)' })
65
- @ApiResponse({ status: HttpStatus.OK, description: 'Paginated products', schema: PaginatedResponseSchema(ProductDto) })
66
- @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Unauthorized' })
67
- override async findAll(@Query() pagination: PaginationQueryDto): Promise<PaginatedResponseDto<ProductEntity>> {
68
- return super.findAll(pagination);
69
- }
70
-
71
- @Get('by-sku/:sku')
72
- @ApiOperation({ summary: 'Get a product by SKU' })
73
- @ApiParam({ name: 'sku', example: 'KB-WL-MEC-001', description: 'Unique Stock Keeping Unit' })
74
- @ApiResponse({ status: HttpStatus.OK, description: 'Product found by SKU', schema: ApiResponseSchema(ProductDto) })
75
- @ApiResponse({ status: HttpStatus.NOT_FOUND, description: 'Product not found' })
76
- @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Unauthorized' })
77
- async findBySku(@Param('sku') sku: string): Promise<ApiResponseDto<ProductEntity>> {
78
- const data = await this.productsService.findBySku(sku);
79
- return { success: true, data, meta: { correlationId: '', timestamp: new Date().toISOString() } };
80
- }
81
-
82
- @Get(':id')
83
- @ApiOperation({ summary: 'Get a product by UUID' })
84
- @ApiParam({ name: 'id', format: 'uuid', example: '123e4567-e89b-12d3-a456-426614174000' })
85
- @ApiResponse({ status: HttpStatus.OK, description: 'Product found', schema: ApiResponseSchema(ProductDto) })
86
- @ApiResponse({ status: HttpStatus.NOT_FOUND, description: 'Product not found' })
87
- @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Invalid UUID format' })
88
- @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Unauthorized' })
89
- override async findOne(
90
- @Param('id', new ParseUUIDPipe({ version: '4', errorHttpStatusCode: HttpStatus.BAD_REQUEST })) id: string,
91
- ): Promise<ApiResponseDto<ProductEntity>> {
92
- return super.findOne(id);
93
- }
94
- }