speedrun-cli 2.6.1

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.
Files changed (159) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/LICENSE +21 -0
  3. package/README.md +620 -0
  4. package/bin/cli.js +224 -0
  5. package/index.js +12 -0
  6. package/package.json +74 -0
  7. package/src/constants.js +65 -0
  8. package/src/generator.js +271 -0
  9. package/src/index.js +13 -0
  10. package/src/moduleGenerator.js +586 -0
  11. package/src/postSetup.js +365 -0
  12. package/src/prompts.js +189 -0
  13. package/src/utils.js +112 -0
  14. package/templates/README.md +81 -0
  15. package/templates/base/eslint.config.mjs +34 -0
  16. package/templates/base/gitignore +78 -0
  17. package/templates/base/nest-cli.json +8 -0
  18. package/templates/base/src/common/constants/cookie.config.ts +20 -0
  19. package/templates/base/src/common/decorators/get-user.decorator.ts +13 -0
  20. package/templates/base/src/common/decorators/public.decorator.ts +4 -0
  21. package/templates/base/src/common/decorators/roles.decorator.ts +4 -0
  22. package/templates/base/src/common/dtos/pagination.dto.ts +29 -0
  23. package/templates/base/src/common/filters/http-exception.filter.ts +108 -0
  24. package/templates/base/src/common/guards/auth.guard.ts +51 -0
  25. package/templates/base/src/common/guards/refresh-token.guard.ts +39 -0
  26. package/templates/base/src/common/guards/roles.guard.ts +45 -0
  27. package/templates/base/src/common/interceptors/response.interceptor.ts +55 -0
  28. package/templates/base/src/common/interfaces/api-response.interface.ts +26 -0
  29. package/templates/base/src/common/middleware/correlation-id.middleware.ts +20 -0
  30. package/templates/base/src/common/validators/password.validator.ts +37 -0
  31. package/templates/base/src/config/config.module.ts +14 -0
  32. package/templates/base/src/config/logger.config.ts +109 -0
  33. package/templates/base/src/main.ts +84 -0
  34. package/templates/base/src/modules/auth/auth.controller.ts +133 -0
  35. package/templates/base/src/modules/auth/dtos/login.dto.ts +11 -0
  36. package/templates/base/src/modules/auth/dtos/signup.dto.ts +8 -0
  37. package/templates/base/src/modules/health/health.controller.ts +20 -0
  38. package/templates/base/src/modules/health/health.module.ts +7 -0
  39. package/templates/base/src/modules/users/dtos/update-profile.dto.ts +12 -0
  40. package/templates/base/src/modules/users/dtos/update-user.dto.ts +13 -0
  41. package/templates/base/src/modules/users/users.controller.ts +65 -0
  42. package/templates/base/test/app.e2e-spec.ts +24 -0
  43. package/templates/base/test/jest-e2e.json +9 -0
  44. package/templates/base/tsconfig.build.json +4 -0
  45. package/templates/base/tsconfig.json +24 -0
  46. package/templates/base-crud/CRUD_README.md +385 -0
  47. package/templates/base-crud/src/common/base/base.controller.ts +321 -0
  48. package/templates/base-crud/src/common/base/base.service.ts +192 -0
  49. package/templates/base-crud/src/common/base/index.ts +20 -0
  50. package/templates/base-crud/src/common/base/swagger/api-response.dto.ts +82 -0
  51. package/templates/base-crud/src/common/base/swagger/paginated.dto.ts +102 -0
  52. package/templates/base-crud/src/modules/products/dto/create-product.dto.ts +44 -0
  53. package/templates/base-crud/src/modules/products/dto/product.dto.ts +38 -0
  54. package/templates/base-crud/src/modules/products/dto/update-product.dto.ts +12 -0
  55. package/templates/base-crud/src/modules/products/products.controller.ts +94 -0
  56. package/templates/base-crud-drizzle/src/modules/products/products.controller.ts +79 -0
  57. package/templates/base-crud-drizzle/src/modules/products/products.module.ts +24 -0
  58. package/templates/base-crud-drizzle/src/modules/products/products.service.ts +140 -0
  59. package/templates/base-crud-drizzle/src/modules/products/schema/products.schema.ts +39 -0
  60. package/templates/base-crud-mongoose/src/modules/products/products.controller.ts +79 -0
  61. package/templates/base-crud-mongoose/src/modules/products/products.module.ts +26 -0
  62. package/templates/base-crud-mongoose/src/modules/products/products.service.ts +133 -0
  63. package/templates/base-crud-mongoose/src/modules/products/schemas/product.schema.ts +67 -0
  64. package/templates/base-crud-prisma/src/modules/products/products.module.ts +12 -0
  65. package/templates/base-crud-prisma/src/modules/products/products.service.ts +100 -0
  66. package/templates/base-crud-typeorm/src/modules/products/entities/product.entity.ts +58 -0
  67. package/templates/base-crud-typeorm/src/modules/products/products.controller.ts +79 -0
  68. package/templates/base-crud-typeorm/src/modules/products/products.module.ts +25 -0
  69. package/templates/base-crud-typeorm/src/modules/products/products.service.ts +102 -0
  70. package/templates/database/mongodb/.env.example +22 -0
  71. package/templates/database/mysql/.env.example +24 -0
  72. package/templates/database/mysql/drizzle.config.ts +13 -0
  73. package/templates/database/mysql/package.json +5 -0
  74. package/templates/database/mysql/prisma/schema.prisma +55 -0
  75. package/templates/database/mysql/src/database/drizzle.ts +13 -0
  76. package/templates/database/mysql/src/database/schema.ts +58 -0
  77. package/templates/database/postgres/.env.example +24 -0
  78. package/templates/database/postgres/drizzle.config.ts +13 -0
  79. package/templates/database/postgres/package.json +8 -0
  80. package/templates/database/postgres/prisma/schema.prisma +55 -0
  81. package/templates/database/sqlite/.env.example +24 -0
  82. package/templates/database/sqlite/drizzle.config.ts +13 -0
  83. package/templates/database/sqlite/package.json +8 -0
  84. package/templates/database/sqlite/prisma/schema.prisma +48 -0
  85. package/templates/database/sqlite/src/database/drizzle.ts +11 -0
  86. package/templates/database/sqlite/src/database/schema.ts +52 -0
  87. package/templates/orm/drizzle/drizzle.config.ts +13 -0
  88. package/templates/orm/drizzle/package.json +90 -0
  89. package/templates/orm/drizzle/src/app.module.ts +73 -0
  90. package/templates/orm/drizzle/src/config/env.validation.ts +55 -0
  91. package/templates/orm/drizzle/src/database/database.module.ts +25 -0
  92. package/templates/orm/drizzle/src/database/drizzle.ts +13 -0
  93. package/templates/orm/drizzle/src/database/schema.ts +60 -0
  94. package/templates/orm/drizzle/src/database/seed.ts +87 -0
  95. package/templates/orm/drizzle/src/modules/auth/auth.module.ts +12 -0
  96. package/templates/orm/drizzle/src/modules/auth/auth.service.ts +298 -0
  97. package/templates/orm/drizzle/src/modules/health/health.controller.ts +34 -0
  98. package/templates/orm/drizzle/src/modules/health/health.module.ts +7 -0
  99. package/templates/orm/drizzle/src/modules/users/users.module.ts +10 -0
  100. package/templates/orm/drizzle/src/modules/users/users.service.ts +152 -0
  101. package/templates/orm/mongoose/.env.example +22 -0
  102. package/templates/orm/mongoose/package.json +86 -0
  103. package/templates/orm/mongoose/src/app.module.ts +73 -0
  104. package/templates/orm/mongoose/src/config/env.validation.ts +55 -0
  105. package/templates/orm/mongoose/src/database/database.module.ts +19 -0
  106. package/templates/orm/mongoose/src/database/seed.ts +107 -0
  107. package/templates/orm/mongoose/src/modules/auth/auth.module.ts +21 -0
  108. package/templates/orm/mongoose/src/modules/auth/auth.service.ts +272 -0
  109. package/templates/orm/mongoose/src/modules/auth/dtos/login.dto.ts +10 -0
  110. package/templates/orm/mongoose/src/modules/auth/dtos/signup.dto.ts +8 -0
  111. package/templates/orm/mongoose/src/modules/health/health.controller.ts +26 -0
  112. package/templates/orm/mongoose/src/modules/health/health.module.ts +7 -0
  113. package/templates/orm/mongoose/src/modules/users/dtos/update-profile.dto.ts +24 -0
  114. package/templates/orm/mongoose/src/modules/users/dtos/update-user.dto.ts +13 -0
  115. package/templates/orm/mongoose/src/modules/users/users.module.ts +19 -0
  116. package/templates/orm/mongoose/src/modules/users/users.service.ts +179 -0
  117. package/templates/orm/mongoose/src/schemas/index.ts +2 -0
  118. package/templates/orm/mongoose/src/schemas/refresh-token.schema.ts +55 -0
  119. package/templates/orm/mongoose/src/schemas/user.schema.ts +53 -0
  120. package/templates/orm/prisma/package.json +102 -0
  121. package/templates/orm/prisma/prisma/schema.prisma +60 -0
  122. package/templates/orm/prisma/prisma/seed.ts +69 -0
  123. package/templates/orm/prisma/src/app.module.ts +55 -0
  124. package/templates/orm/prisma/src/config/env.validation.ts +56 -0
  125. package/templates/orm/prisma/src/modules/auth/auth.module.ts +17 -0
  126. package/templates/orm/prisma/src/modules/auth/auth.service.ts +308 -0
  127. package/templates/orm/prisma/src/modules/health/health.controller.ts +26 -0
  128. package/templates/orm/prisma/src/modules/health/health.module.ts +10 -0
  129. package/templates/orm/prisma/src/modules/users/users.module.ts +11 -0
  130. package/templates/orm/prisma/src/modules/users/users.service.ts +105 -0
  131. package/templates/orm/prisma/src/prisma/prisma.module.ts +9 -0
  132. package/templates/orm/prisma/src/prisma/prisma.service.ts +16 -0
  133. package/templates/orm/typeorm/package.json +100 -0
  134. package/templates/orm/typeorm/src/app.module.ts +55 -0
  135. package/templates/orm/typeorm/src/config/env.validation.ts +58 -0
  136. package/templates/orm/typeorm/src/database/data-source.ts +19 -0
  137. package/templates/orm/typeorm/src/database/database.module.ts +27 -0
  138. package/templates/orm/typeorm/src/database/seed.ts +76 -0
  139. package/templates/orm/typeorm/src/entities/index.ts +2 -0
  140. package/templates/orm/typeorm/src/entities/refresh-token.entity.ts +45 -0
  141. package/templates/orm/typeorm/src/entities/user.entity.ts +51 -0
  142. package/templates/orm/typeorm/src/modules/auth/auth.module.ts +19 -0
  143. package/templates/orm/typeorm/src/modules/auth/auth.service.ts +286 -0
  144. package/templates/orm/typeorm/src/modules/health/health.controller.ts +24 -0
  145. package/templates/orm/typeorm/src/modules/health/health.module.ts +9 -0
  146. package/templates/orm/typeorm/src/modules/users/users.module.ts +13 -0
  147. package/templates/orm/typeorm/src/modules/users/users.service.ts +108 -0
  148. package/templates/swagger/src/common/dtos/pagination.dto.ts +43 -0
  149. package/templates/swagger/src/main.ts +116 -0
  150. package/templates/swagger/src/modules/auth/auth.controller.ts +235 -0
  151. package/templates/swagger/src/modules/auth/dtos/login.dto.ts +20 -0
  152. package/templates/swagger/src/modules/auth/dtos/signup.dto.ts +14 -0
  153. package/templates/swagger/src/modules/users/dtos/update-profile.dto.ts +19 -0
  154. package/templates/swagger/src/modules/users/dtos/update-user.dto.ts +23 -0
  155. package/templates/swagger/src/modules/users/users.controller.ts +214 -0
  156. package/templates/swagger-mongoose/src/modules/auth/dtos/login.dto.ts +20 -0
  157. package/templates/swagger-mongoose/src/modules/auth/dtos/signup.dto.ts +14 -0
  158. package/templates/swagger-mongoose/src/modules/users/dtos/update-profile.dto.ts +40 -0
  159. package/templates/swagger-mongoose/src/modules/users/dtos/update-user.dto.ts +23 -0
@@ -0,0 +1,385 @@
1
+ # 🏗️ Base CRUD Architecture — Complete Guide
2
+
3
+ > **Generated by `create-nestjs-auth`** — Production-ready NestJS scaffolding CLI
4
+ >
5
+ > This file was added because you chose **"Enable Base CRUD Architecture"** during setup.
6
+
7
+ ---
8
+
9
+ ## 📋 Table of Contents
10
+
11
+ 1. [Architecture Overview](#architecture-overview)
12
+ 2. [File Structure](#file-structure)
13
+ 3. [Quick Start — Create a New CRUD Module](#quick-start)
14
+ 4. [Step-by-Step Checklist](#checklist)
15
+ 5. [DTO Snippets (Copy-Paste Ready)](#dto-snippets)
16
+ 6. [Service Snippet](#service-snippet)
17
+ 7. [Controller Snippet](#controller-snippet)
18
+ 8. [Module Snippet](#module-snippet)
19
+ 9. [Swagger UI Guide](#swagger-ui-guide)
20
+ 10. [Security Checklist](#security-checklist)
21
+ 11. [FAQ & Troubleshooting](#faq)
22
+
23
+ ---
24
+
25
+ ## Architecture Overview
26
+
27
+ ```
28
+ AbstractLayer (base/) ← You never touch this
29
+
30
+
31
+ ConcreteModule (modules/xyz/) ← You extend this
32
+
33
+
34
+ ORM / Database (Prisma / TypeORM / Drizzle / Mongoose)
35
+ ```
36
+
37
+ | File | Role |
38
+ |------|------|
39
+ | `base.service.ts` | Abstract CRUD logic — `create`, `findAll`, `findOne`, `update`, `remove` |
40
+ | `base.controller.ts` | Abstract HTTP routes — full Swagger + security decorators |
41
+ | `swagger/api-response.dto.ts` | Generic `ApiResponseDto<T>` wrapper for Swagger schema rendering |
42
+ | `swagger/paginated.dto.ts` | Generic `PaginatedResponseDto<T>` + `PaginationQueryDto` |
43
+
44
+ ### Why this pattern?
45
+
46
+ ✅ **DRY** — Write CRUD once, reuse across all modules
47
+ ✅ **Type-safe** — Full TypeScript generics, no `any`
48
+ ✅ **Secure by default** — `ParseUUIDPipe`, `ValidationPipe(whitelist: true)`
49
+ ✅ **Swagger-ready** — All endpoints documented automatically
50
+ ✅ **Testable** — Repository pattern allows easy mock injection
51
+
52
+ ---
53
+
54
+ ## File Structure
55
+
56
+ ```
57
+ src/
58
+ ├── common/
59
+ │ └── base/ ← Abstract layer (don't modify)
60
+ │ ├── index.ts ← Barrel export
61
+ │ ├── base.service.ts ← Abstract CRUD service
62
+ │ ├── base.controller.ts ← Abstract CRUD controller
63
+ │ └── swagger/
64
+ │ ├── api-response.dto.ts ← ApiResponseDto<T> wrapper
65
+ │ └── paginated.dto.ts ← PaginatedResponseDto<T> + PaginationQueryDto
66
+
67
+ └── modules/
68
+ └── products/ ← Concrete example (copy this!)
69
+ ├── dto/
70
+ │ ├── create-product.dto.ts ← POST body validation
71
+ │ ├── update-product.dto.ts ← PUT body (PartialType of create)
72
+ │ └── product.dto.ts ← Response shape for Swagger
73
+ ├── products.service.ts ← Extends BaseService
74
+ ├── products.controller.ts ← Extends BaseController
75
+ └── products.module.ts ← NestJS module wiring
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Quick Start
81
+
82
+ To create a new `Orders` CRUD module in **4 steps**:
83
+
84
+ ```bash
85
+ # 1. Create the directory structure
86
+ mkdir -p src/modules/orders/dto
87
+ ```
88
+
89
+ Then follow the checklist below. ⬇️
90
+
91
+ ---
92
+
93
+ ## Checklist
94
+
95
+ ### ✅ Step 1 — Create DTOs
96
+
97
+ **`src/modules/orders/dto/create-order.dto.ts`**
98
+ ```typescript
99
+ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
100
+ import { IsString, IsNotEmpty, IsNumber, IsPositive, IsOptional, IsUUID } from 'class-validator';
101
+ import { Type } from 'class-transformer';
102
+
103
+ export class CreateOrderDto {
104
+ @ApiProperty({ description: 'Customer UUID', example: '123e4567-e89b-12d3-a456-426614174000' })
105
+ @IsUUID('4', { message: 'customerId must be a valid UUID v4' })
106
+ @IsNotEmpty()
107
+ customerId: string;
108
+
109
+ @ApiProperty({ description: 'Total order amount in cents', example: 29999 })
110
+ @Type(() => Number)
111
+ @IsNumber()
112
+ @IsPositive()
113
+ total: number;
114
+
115
+ @ApiPropertyOptional({ description: 'Order notes', example: 'Leave at door' })
116
+ @IsOptional()
117
+ @IsString()
118
+ notes?: string;
119
+ }
120
+ ```
121
+
122
+ **`src/modules/orders/dto/update-order.dto.ts`**
123
+ ```typescript
124
+ // IMPORTANT: Use PartialType from @nestjs/swagger (NOT @nestjs/mapped-types!)
125
+ // @nestjs/swagger's PartialType preserves @ApiProperty decorators for Swagger
126
+ import { PartialType } from '@nestjs/swagger';
127
+ import { CreateOrderDto } from './create-order.dto';
128
+
129
+ export class UpdateOrderDto extends PartialType(CreateOrderDto) {}
130
+ ```
131
+
132
+ **`src/modules/orders/dto/order.dto.ts`** — Response shape
133
+ ```typescript
134
+ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
135
+
136
+ export class OrderDto {
137
+ @ApiProperty({ example: '123e4567-...', format: 'uuid' })
138
+ id: string;
139
+
140
+ @ApiProperty({ example: '123e4567-...' })
141
+ customerId: string;
142
+
143
+ @ApiProperty({ example: 29999 })
144
+ total: number;
145
+
146
+ @ApiPropertyOptional({ example: 'Leave at door' })
147
+ notes?: string;
148
+
149
+ @ApiProperty({ example: '2025-01-01T00:00:00.000Z' })
150
+ createdAt: Date;
151
+
152
+ @ApiProperty({ example: '2025-01-01T00:00:00.000Z' })
153
+ updatedAt: Date;
154
+
155
+ @ApiPropertyOptional({ nullable: true, example: null })
156
+ deletedAt?: Date | null;
157
+ }
158
+ ```
159
+
160
+ ---
161
+
162
+ ### ✅ Step 2 — Create Service
163
+
164
+ **`src/modules/orders/orders.service.ts`**
165
+ ```typescript
166
+ import { Injectable } from '@nestjs/common';
167
+ import { PrismaService } from '../../prisma/prisma.service';
168
+ import { BaseService, IBaseRepository, PaginationQueryDto } from '../../common/base';
169
+ import { CreateOrderDto } from './dto/create-order.dto';
170
+ import { UpdateOrderDto } from './dto/update-order.dto';
171
+ import { Order } from '@prisma/client';
172
+
173
+ // ── Repository ────────────────────────────────────────────────
174
+ class PrismaOrderRepository
175
+ implements IBaseRepository<Order, CreateOrderDto, UpdateOrderDto>
176
+ {
177
+ constructor(private readonly prisma: PrismaService) {}
178
+
179
+ async create(dto: CreateOrderDto): Promise<Order> {
180
+ return this.prisma.order.create({ data: dto });
181
+ }
182
+
183
+ async findAll(pagination: PaginationQueryDto): Promise<{ data: Order[]; total: number }> {
184
+ const { page = 1, limit = 10 } = pagination;
185
+ const skip = (page - 1) * limit;
186
+ const [data, total] = await this.prisma.$transaction([
187
+ this.prisma.order.findMany({ where: { deletedAt: null }, skip, take: limit, orderBy: { createdAt: 'desc' } }),
188
+ this.prisma.order.count({ where: { deletedAt: null } }),
189
+ ]);
190
+ return { data, total };
191
+ }
192
+
193
+ async findOne(id: string): Promise<Order | null> {
194
+ return this.prisma.order.findFirst({ where: { id, deletedAt: null } });
195
+ }
196
+
197
+ async update(id: string, dto: UpdateOrderDto): Promise<Order> {
198
+ return this.prisma.order.update({ where: { id }, data: dto });
199
+ }
200
+
201
+ async remove(id: string): Promise<Order> {
202
+ return this.prisma.order.update({ where: { id }, data: { deletedAt: new Date() } });
203
+ }
204
+ }
205
+
206
+ // ── Service ───────────────────────────────────────────────────
207
+ @Injectable()
208
+ export class OrdersService extends BaseService<Order, CreateOrderDto, UpdateOrderDto> {
209
+ private readonly repository: PrismaOrderRepository;
210
+
211
+ constructor(private readonly prisma: PrismaService) {
212
+ super();
213
+ this.repository = new PrismaOrderRepository(this.prisma);
214
+ }
215
+
216
+ protected getRepository(): IBaseRepository<Order, CreateOrderDto, UpdateOrderDto> {
217
+ return this.repository;
218
+ }
219
+
220
+ // Add domain methods here ↓
221
+ // async findByCustomer(customerId: string): Promise<Order[]> { ... }
222
+ }
223
+ ```
224
+
225
+ ---
226
+
227
+ ### ✅ Step 3 — Create Controller
228
+
229
+ **`src/modules/orders/orders.controller.ts`**
230
+ ```typescript
231
+ import { Controller, Query, Param, Get, ParseUUIDPipe, HttpStatus, Type } from '@nestjs/common';
232
+ import { ApiTags, ApiBearerAuth, ApiExtraModels, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';
233
+ import { BaseController } from '../../common/base/base.controller';
234
+ import {
235
+ ApiResponseDto, ApiResponseSchema,
236
+ PaginatedResponseDto, PaginatedResponseSchema,
237
+ PaginationQueryDto,
238
+ } from '../../common/base';
239
+ import { OrdersService } from './orders.service';
240
+ import { CreateOrderDto } from './dto/create-order.dto';
241
+ import { UpdateOrderDto } from './dto/update-order.dto';
242
+ import { OrderDto } from './dto/order.dto';
243
+ import { Order } from '@prisma/client';
244
+
245
+ @ApiTags('Orders')
246
+ @ApiBearerAuth('bearer')
247
+ @ApiExtraModels(ApiResponseDto, PaginatedResponseDto, OrderDto) // ← REQUIRED for generic Swagger schemas
248
+ @Controller('orders')
249
+ export class OrdersController extends BaseController<Order, CreateOrderDto, UpdateOrderDto> {
250
+ constructor(private readonly ordersService: OrdersService) {
251
+ super(ordersService);
252
+ }
253
+
254
+ protected getDtoClass(): Type<Order> {
255
+ return OrderDto as unknown as Type<Order>;
256
+ }
257
+
258
+ // Override to attach proper response schemas for Swagger
259
+ @Get()
260
+ @ApiOperation({ summary: 'Get all orders (paginated)' })
261
+ @ApiResponse({ status: 200, description: 'Paginated orders', schema: PaginatedResponseSchema(OrderDto) })
262
+ override async findAll(@Query() pagination: PaginationQueryDto): Promise<PaginatedResponseDto<Order>> {
263
+ return super.findAll(pagination);
264
+ }
265
+
266
+ @Get(':id')
267
+ @ApiOperation({ summary: 'Get order by ID' })
268
+ @ApiParam({ name: 'id', format: 'uuid' })
269
+ @ApiResponse({ status: 200, description: 'Order found', schema: ApiResponseSchema(OrderDto) })
270
+ @ApiResponse({ status: 404, description: 'Order not found' })
271
+ override async findOne(
272
+ @Param('id', new ParseUUIDPipe({ version: '4', errorHttpStatusCode: HttpStatus.BAD_REQUEST })) id: string,
273
+ ): Promise<ApiResponseDto<Order>> {
274
+ return super.findOne(id);
275
+ }
276
+ }
277
+ ```
278
+
279
+ ---
280
+
281
+ ### ✅ Step 4 — Create Module & Register
282
+
283
+ **`src/modules/orders/orders.module.ts`**
284
+ ```typescript
285
+ import { Module } from '@nestjs/common';
286
+ import { OrdersController } from './orders.controller';
287
+ import { OrdersService } from './orders.service';
288
+ import { PrismaModule } from '../../prisma/prisma.module';
289
+
290
+ @Module({
291
+ imports: [PrismaModule],
292
+ controllers: [OrdersController],
293
+ providers: [OrdersService],
294
+ exports: [OrdersService],
295
+ })
296
+ export class OrdersModule {}
297
+ ```
298
+
299
+ **Register in `src/app.module.ts`:**
300
+ ```typescript
301
+ import { OrdersModule } from './modules/orders/orders.module';
302
+
303
+ @Module({
304
+ imports: [
305
+ // ... existing modules
306
+ OrdersModule, // ← Add this
307
+ ],
308
+ })
309
+ export class AppModule {}
310
+ ```
311
+
312
+ ---
313
+
314
+ ## Swagger UI Guide
315
+
316
+ After running your NestJS app, open: **`http://localhost:8080/api/docs`**
317
+
318
+ ### Authenticate in Swagger:
319
+ 1. Click the **🔒 Authorize** button (top right)
320
+ 2. Enter your JWT: `Bearer <your-access-token>`
321
+ 3. Click **Authorize** → **Close**
322
+
323
+ ### Test an endpoint:
324
+ 1. Find the endpoint (e.g., `GET /products`)
325
+ 2. Click **Try it out**
326
+ 3. Fill in parameters
327
+ 4. Click **Execute**
328
+ 5. Inspect **Response body** and **Curl** command
329
+
330
+ ### View response schemas:
331
+ - Click **Schema** tab on any response to see the full `ApiResponseDto<T>` structure
332
+ - Generic schemas (e.g., `PaginatedResponseDto<ProductDto>`) require `@ApiExtraModels` in the controller
333
+
334
+ > **Tip**: The `@ApiExtraModels(ApiResponseDto, PaginatedResponseDto, YourDto)` decorator on the controller is **mandatory** for generic schemas to appear in Swagger UI.
335
+
336
+ ---
337
+
338
+ ## Security Checklist
339
+
340
+ | Security Control | How It's Implemented |
341
+ |---|---|
342
+ | **Mass Assignment Protection** | `ValidationPipe({ whitelist: true, forbidNonWhitelisted: true })` in `main.ts` |
343
+ | **URL Injection Prevention** | `@ParseUUIDPipe({ version: '4' })` on all `:id` params |
344
+ | **SQL/NoSQL Injection Prevention** | Parameterized queries via Prisma/TypeORM/Mongoose (no raw SQL) |
345
+ | **Type Coercion Attacks** | `@IsString()`, `@IsNumber()`, `@IsInt()` on all DTO fields |
346
+ | **Overflow/DoS Prevention** | `@MaxLength()`, `@Max(100)` on limit param |
347
+ | **Enum Injection** | `@IsEnum()` enforces strict set of allowed values |
348
+ | **Auth Protection** | `@ApiBearerAuth()` + `@UseGuards(JwtAuthGuard)` on all routes |
349
+ | **Structured Logging** | `Logger` in `BaseService` traces all CRUD operations by entity |
350
+ | **Soft Delete** | Records marked `deletedAt`, never physically removed |
351
+
352
+ ---
353
+
354
+ ## FAQ
355
+
356
+ **Q: Can I use TypeORM/Drizzle/Mongoose instead of Prisma?**
357
+ A: Yes! Implement `IBaseRepository<T, CreateDto, UpdateDto>` in your concrete class. The base layer is fully ORM-agnostic.
358
+
359
+ **Q: Why `PartialType` from `@nestjs/swagger` instead of `@nestjs/mapped-types`?**
360
+ A: `@nestjs/swagger`'s `PartialType` preserves `@ApiProperty` decorators, ensuring UpdateDto appears correctly in Swagger UI. `@nestjs/mapped-types` strips them.
361
+
362
+ **Q: How do I add authentication to individual routes?**
363
+ A: Override the method in your concrete controller and add `@UseGuards(JwtAuthGuard)`:
364
+ ```typescript
365
+ @UseGuards(JwtAuthGuard)
366
+ @Roles(Role.ADMIN)
367
+ override async create(@Body() dto: CreateOrderDto) {
368
+ return super.create(dto);
369
+ }
370
+ ```
371
+
372
+ **Q: How do I add filtering/search to `findAll`?**
373
+ A: Override `findAll()` in your concrete service and extend `PaginationQueryDto`:
374
+ ```typescript
375
+ export class OrderQueryDto extends PaginationQueryDto {
376
+ @ApiPropertyOptional() @IsOptional() @IsString() status?: string;
377
+ }
378
+ ```
379
+
380
+ **Q: Where is the example `ProductModule`?**
381
+ A: See `src/modules/products/` — it's a fully working concrete implementation of this pattern.
382
+
383
+ ---
384
+
385
+ *Generated by [`create-nestjs-auth`](https://github.com/masabinhok/create-nestjs-auth) — Production-ready NestJS authentication & architecture scaffolding.*
@@ -0,0 +1,321 @@
1
+ import {
2
+ Get,
3
+ Post,
4
+ Put,
5
+ Delete,
6
+ Body,
7
+ Param,
8
+ Query,
9
+ HttpCode,
10
+ HttpStatus,
11
+ ParseUUIDPipe,
12
+ UseGuards,
13
+ Type,
14
+ } from '@nestjs/common';
15
+ import {
16
+ ApiOperation,
17
+ ApiResponse,
18
+ ApiBearerAuth,
19
+ ApiParam,
20
+ ApiQuery,
21
+ ApiExtraModels,
22
+ } from '@nestjs/swagger';
23
+ import { BaseService } from './base.service';
24
+ import {
25
+ ApiResponseDto,
26
+ ApiResponseSchema,
27
+ ApiResponseArraySchema,
28
+ } from './swagger/api-response.dto';
29
+ import {
30
+ PaginationQueryDto,
31
+ PaginatedResponseDto,
32
+ PaginatedResponseSchema,
33
+ } from './swagger/paginated.dto';
34
+
35
+ // ─────────────────────────────────────────────────────────────
36
+ // Abstract Base Controller
37
+ //
38
+ // Generic parameters:
39
+ // T — Entity / Document type (e.g. Product)
40
+ // CreateDto — DTO for POST / (e.g. CreateProductDto)
41
+ // UpdateDto — DTO for PUT /:id (e.g. UpdateProductDto)
42
+ //
43
+ // How to extend:
44
+ //
45
+ // @ApiTags('products')
46
+ // @ApiBearerAuth('bearer')
47
+ // @ApiExtraModels(ApiResponseDto, PaginatedResponseDto, ProductDto)
48
+ // @Controller('products')
49
+ // export class ProductsController extends BaseController<Product, CreateProductDto, UpdateProductDto> {
50
+ // constructor(private readonly productsService: ProductsService) {
51
+ // super(productsService);
52
+ // }
53
+ // protected getDtoClass() { return ProductDto; }
54
+ // protected getCreateDtoClass() { return CreateProductDto; }
55
+ // }
56
+ // ─────────────────────────────────────────────────────────────
57
+ export abstract class BaseController<T, CreateDto, UpdateDto> {
58
+ constructor(protected readonly service: BaseService<T, CreateDto, UpdateDto>) {}
59
+
60
+ /**
61
+ * Returns the DTO class used as the Swagger response model.
62
+ * Must be implemented by each concrete controller.
63
+ */
64
+ protected abstract getDtoClass(): Type<T>;
65
+
66
+ // ─────────────────────────────────────────────────────────
67
+ // POST / — Create Resource
68
+ // ─────────────────────────────────────────────────────────
69
+ @Post()
70
+ @HttpCode(HttpStatus.CREATED)
71
+ @ApiBearerAuth('bearer')
72
+ @ApiOperation({
73
+ summary: 'Create a new resource',
74
+ description:
75
+ 'Creates a new resource entry. The request body is strictly validated via `class-validator`. ' +
76
+ 'Extra or unknown properties are rejected (whitelist enforcement).',
77
+ })
78
+ @ApiResponse({
79
+ status: HttpStatus.CREATED,
80
+ description: 'Resource created successfully',
81
+ })
82
+ @ApiResponse({
83
+ status: HttpStatus.BAD_REQUEST,
84
+ description: 'Validation error — invalid or missing fields in request body',
85
+ schema: {
86
+ example: {
87
+ success: false,
88
+ message: 'Validation failed (name should not be empty)',
89
+ meta: { timestamp: '2025-01-01T00:00:00.000Z' },
90
+ },
91
+ },
92
+ })
93
+ @ApiResponse({
94
+ status: HttpStatus.UNAUTHORIZED,
95
+ description: 'Unauthorized — valid Bearer JWT required',
96
+ })
97
+ @ApiResponse({
98
+ status: HttpStatus.CONFLICT,
99
+ description: 'Conflict — resource with given unique field already exists',
100
+ })
101
+ async create(@Body() createDto: CreateDto): Promise<ApiResponseDto<T>> {
102
+ const data = await this.service.create(createDto);
103
+ return {
104
+ success: true,
105
+ data,
106
+ meta: {
107
+ correlationId: '',
108
+ timestamp: new Date().toISOString(),
109
+ message: 'Resource created successfully',
110
+ },
111
+ };
112
+ }
113
+
114
+ // ─────────────────────────────────────────────────────────
115
+ // GET / — Find All (Paginated)
116
+ // ─────────────────────────────────────────────────────────
117
+ @Get()
118
+ @ApiBearerAuth('bearer')
119
+ @ApiOperation({
120
+ summary: 'Retrieve all resources (paginated)',
121
+ description:
122
+ 'Returns a paginated list of resources. ' +
123
+ 'Use `page` and `limit` query parameters for pagination control. ' +
124
+ 'Max `limit` is capped at 100 per page.',
125
+ })
126
+ @ApiQuery({ name: 'page', required: false, type: Number, example: 1, description: 'Page number (default: 1)' })
127
+ @ApiQuery({ name: 'limit', required: false, type: Number, example: 10, description: 'Items per page (default: 10, max: 100)' })
128
+ @ApiResponse({
129
+ status: HttpStatus.OK,
130
+ description: 'Paginated list of resources',
131
+ })
132
+ @ApiResponse({
133
+ status: HttpStatus.BAD_REQUEST,
134
+ description: 'Invalid pagination parameters',
135
+ })
136
+ @ApiResponse({
137
+ status: HttpStatus.UNAUTHORIZED,
138
+ description: 'Unauthorized — valid Bearer JWT required',
139
+ })
140
+ async findAll(@Query() pagination: PaginationQueryDto): Promise<PaginatedResponseDto<T>> {
141
+ return this.service.findAll(pagination);
142
+ }
143
+
144
+ // ─────────────────────────────────────────────────────────
145
+ // GET /:id — Find One
146
+ // ─────────────────────────────────────────────────────────
147
+ @Get(':id')
148
+ @ApiBearerAuth('bearer')
149
+ @ApiOperation({
150
+ summary: 'Retrieve a single resource by ID',
151
+ description:
152
+ 'Fetches a single resource by its UUID. ' +
153
+ 'Returns 404 if the resource does not exist.',
154
+ })
155
+ @ApiParam({
156
+ name: 'id',
157
+ description: 'Resource UUID (v4)',
158
+ example: '123e4567-e89b-12d3-a456-426614174000',
159
+ format: 'uuid',
160
+ })
161
+ @ApiResponse({
162
+ status: HttpStatus.OK,
163
+ description: 'Resource found and returned',
164
+ })
165
+ @ApiResponse({
166
+ status: HttpStatus.NOT_FOUND,
167
+ description: 'Resource not found — no record with the given ID exists',
168
+ schema: {
169
+ example: {
170
+ success: false,
171
+ message: 'Resource with id "123e4567-..." was not found',
172
+ meta: { timestamp: '2025-01-01T00:00:00.000Z' },
173
+ },
174
+ },
175
+ })
176
+ @ApiResponse({
177
+ status: HttpStatus.BAD_REQUEST,
178
+ description: 'Bad Request — the provided ID is not a valid UUID v4',
179
+ schema: {
180
+ example: {
181
+ success: false,
182
+ message: 'Validation failed (uuid is expected)',
183
+ meta: { timestamp: '2025-01-01T00:00:00.000Z' },
184
+ },
185
+ },
186
+ })
187
+ @ApiResponse({
188
+ status: HttpStatus.UNAUTHORIZED,
189
+ description: 'Unauthorized — valid Bearer JWT required',
190
+ })
191
+ async findOne(
192
+ @Param('id', new ParseUUIDPipe({ version: '4', errorHttpStatusCode: HttpStatus.BAD_REQUEST }))
193
+ id: string,
194
+ ): Promise<ApiResponseDto<T>> {
195
+ const data = await this.service.findOne(id);
196
+ return {
197
+ success: true,
198
+ data,
199
+ meta: {
200
+ correlationId: '',
201
+ timestamp: new Date().toISOString(),
202
+ },
203
+ };
204
+ }
205
+
206
+ // ─────────────────────────────────────────────────────────
207
+ // PUT /:id — Full Update
208
+ // ─────────────────────────────────────────────────────────
209
+ @Put(':id')
210
+ @ApiBearerAuth('bearer')
211
+ @ApiOperation({
212
+ summary: 'Update a resource (full or partial)',
213
+ description:
214
+ 'Updates an existing resource by its UUID. ' +
215
+ 'All provided fields are validated. Unknown fields are rejected. ' +
216
+ 'Returns 404 if the resource does not exist.',
217
+ })
218
+ @ApiParam({
219
+ name: 'id',
220
+ description: 'Resource UUID (v4)',
221
+ example: '123e4567-e89b-12d3-a456-426614174000',
222
+ format: 'uuid',
223
+ })
224
+ @ApiResponse({
225
+ status: HttpStatus.OK,
226
+ description: 'Resource updated successfully',
227
+ })
228
+ @ApiResponse({
229
+ status: HttpStatus.NOT_FOUND,
230
+ description: 'Resource not found',
231
+ schema: {
232
+ example: {
233
+ success: false,
234
+ message: 'Resource with id "..." was not found',
235
+ meta: { timestamp: '2025-01-01T00:00:00.000Z' },
236
+ },
237
+ },
238
+ })
239
+ @ApiResponse({
240
+ status: HttpStatus.BAD_REQUEST,
241
+ description: 'Bad Request — invalid UUID or validation error in request body',
242
+ })
243
+ @ApiResponse({
244
+ status: HttpStatus.UNAUTHORIZED,
245
+ description: 'Unauthorized — valid Bearer JWT required',
246
+ })
247
+ async update(
248
+ @Param('id', new ParseUUIDPipe({ version: '4', errorHttpStatusCode: HttpStatus.BAD_REQUEST }))
249
+ id: string,
250
+ @Body() updateDto: UpdateDto,
251
+ ): Promise<ApiResponseDto<T>> {
252
+ const data = await this.service.update(id, updateDto);
253
+ return {
254
+ success: true,
255
+ data,
256
+ meta: {
257
+ correlationId: '',
258
+ timestamp: new Date().toISOString(),
259
+ message: 'Resource updated successfully',
260
+ },
261
+ };
262
+ }
263
+
264
+ // ─────────────────────────────────────────────────────────
265
+ // DELETE /:id — Soft Delete
266
+ // ─────────────────────────────────────────────────────────
267
+ @Delete(':id')
268
+ @HttpCode(HttpStatus.OK)
269
+ @ApiBearerAuth('bearer')
270
+ @ApiOperation({
271
+ summary: 'Soft-delete a resource',
272
+ description:
273
+ 'Marks a resource as deleted by setting its `deletedAt` timestamp. ' +
274
+ 'The record is NOT permanently removed from the database. ' +
275
+ 'Returns 404 if the resource does not exist.',
276
+ })
277
+ @ApiParam({
278
+ name: 'id',
279
+ description: 'Resource UUID (v4)',
280
+ example: '123e4567-e89b-12d3-a456-426614174000',
281
+ format: 'uuid',
282
+ })
283
+ @ApiResponse({
284
+ status: HttpStatus.OK,
285
+ description: 'Resource soft-deleted successfully',
286
+ schema: {
287
+ example: {
288
+ success: true,
289
+ data: { id: '123e4567-...', deletedAt: '2025-01-01T00:00:00.000Z' },
290
+ meta: { timestamp: '2025-01-01T00:00:00.000Z', message: 'Resource deleted successfully' },
291
+ },
292
+ },
293
+ })
294
+ @ApiResponse({
295
+ status: HttpStatus.NOT_FOUND,
296
+ description: 'Resource not found',
297
+ })
298
+ @ApiResponse({
299
+ status: HttpStatus.BAD_REQUEST,
300
+ description: 'Bad Request — invalid UUID format',
301
+ })
302
+ @ApiResponse({
303
+ status: HttpStatus.UNAUTHORIZED,
304
+ description: 'Unauthorized — valid Bearer JWT required',
305
+ })
306
+ async remove(
307
+ @Param('id', new ParseUUIDPipe({ version: '4', errorHttpStatusCode: HttpStatus.BAD_REQUEST }))
308
+ id: string,
309
+ ): Promise<ApiResponseDto<T>> {
310
+ const data = await this.service.remove(id);
311
+ return {
312
+ success: true,
313
+ data,
314
+ meta: {
315
+ correlationId: '',
316
+ timestamp: new Date().toISOString(),
317
+ message: 'Resource deleted successfully',
318
+ },
319
+ };
320
+ }
321
+ }