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.
- package/CHANGELOG.md +96 -0
- package/LICENSE +21 -0
- package/README.md +620 -0
- package/bin/cli.js +224 -0
- package/index.js +12 -0
- package/package.json +74 -0
- package/src/constants.js +65 -0
- package/src/generator.js +271 -0
- package/src/index.js +13 -0
- package/src/moduleGenerator.js +586 -0
- package/src/postSetup.js +365 -0
- package/src/prompts.js +189 -0
- package/src/utils.js +112 -0
- package/templates/README.md +81 -0
- package/templates/base/eslint.config.mjs +34 -0
- package/templates/base/gitignore +78 -0
- package/templates/base/nest-cli.json +8 -0
- package/templates/base/src/common/constants/cookie.config.ts +20 -0
- package/templates/base/src/common/decorators/get-user.decorator.ts +13 -0
- package/templates/base/src/common/decorators/public.decorator.ts +4 -0
- package/templates/base/src/common/decorators/roles.decorator.ts +4 -0
- package/templates/base/src/common/dtos/pagination.dto.ts +29 -0
- package/templates/base/src/common/filters/http-exception.filter.ts +108 -0
- package/templates/base/src/common/guards/auth.guard.ts +51 -0
- package/templates/base/src/common/guards/refresh-token.guard.ts +39 -0
- package/templates/base/src/common/guards/roles.guard.ts +45 -0
- package/templates/base/src/common/interceptors/response.interceptor.ts +55 -0
- package/templates/base/src/common/interfaces/api-response.interface.ts +26 -0
- package/templates/base/src/common/middleware/correlation-id.middleware.ts +20 -0
- package/templates/base/src/common/validators/password.validator.ts +37 -0
- package/templates/base/src/config/config.module.ts +14 -0
- package/templates/base/src/config/logger.config.ts +109 -0
- package/templates/base/src/main.ts +84 -0
- package/templates/base/src/modules/auth/auth.controller.ts +133 -0
- package/templates/base/src/modules/auth/dtos/login.dto.ts +11 -0
- package/templates/base/src/modules/auth/dtos/signup.dto.ts +8 -0
- package/templates/base/src/modules/health/health.controller.ts +20 -0
- package/templates/base/src/modules/health/health.module.ts +7 -0
- package/templates/base/src/modules/users/dtos/update-profile.dto.ts +12 -0
- package/templates/base/src/modules/users/dtos/update-user.dto.ts +13 -0
- package/templates/base/src/modules/users/users.controller.ts +65 -0
- package/templates/base/test/app.e2e-spec.ts +24 -0
- package/templates/base/test/jest-e2e.json +9 -0
- package/templates/base/tsconfig.build.json +4 -0
- package/templates/base/tsconfig.json +24 -0
- package/templates/base-crud/CRUD_README.md +385 -0
- package/templates/base-crud/src/common/base/base.controller.ts +321 -0
- package/templates/base-crud/src/common/base/base.service.ts +192 -0
- package/templates/base-crud/src/common/base/index.ts +20 -0
- package/templates/base-crud/src/common/base/swagger/api-response.dto.ts +82 -0
- package/templates/base-crud/src/common/base/swagger/paginated.dto.ts +102 -0
- package/templates/base-crud/src/modules/products/dto/create-product.dto.ts +44 -0
- package/templates/base-crud/src/modules/products/dto/product.dto.ts +38 -0
- package/templates/base-crud/src/modules/products/dto/update-product.dto.ts +12 -0
- package/templates/base-crud/src/modules/products/products.controller.ts +94 -0
- package/templates/base-crud-drizzle/src/modules/products/products.controller.ts +79 -0
- package/templates/base-crud-drizzle/src/modules/products/products.module.ts +24 -0
- package/templates/base-crud-drizzle/src/modules/products/products.service.ts +140 -0
- package/templates/base-crud-drizzle/src/modules/products/schema/products.schema.ts +39 -0
- package/templates/base-crud-mongoose/src/modules/products/products.controller.ts +79 -0
- package/templates/base-crud-mongoose/src/modules/products/products.module.ts +26 -0
- package/templates/base-crud-mongoose/src/modules/products/products.service.ts +133 -0
- package/templates/base-crud-mongoose/src/modules/products/schemas/product.schema.ts +67 -0
- package/templates/base-crud-prisma/src/modules/products/products.module.ts +12 -0
- package/templates/base-crud-prisma/src/modules/products/products.service.ts +100 -0
- package/templates/base-crud-typeorm/src/modules/products/entities/product.entity.ts +58 -0
- package/templates/base-crud-typeorm/src/modules/products/products.controller.ts +79 -0
- package/templates/base-crud-typeorm/src/modules/products/products.module.ts +25 -0
- package/templates/base-crud-typeorm/src/modules/products/products.service.ts +102 -0
- package/templates/database/mongodb/.env.example +22 -0
- package/templates/database/mysql/.env.example +24 -0
- package/templates/database/mysql/drizzle.config.ts +13 -0
- package/templates/database/mysql/package.json +5 -0
- package/templates/database/mysql/prisma/schema.prisma +55 -0
- package/templates/database/mysql/src/database/drizzle.ts +13 -0
- package/templates/database/mysql/src/database/schema.ts +58 -0
- package/templates/database/postgres/.env.example +24 -0
- package/templates/database/postgres/drizzle.config.ts +13 -0
- package/templates/database/postgres/package.json +8 -0
- package/templates/database/postgres/prisma/schema.prisma +55 -0
- package/templates/database/sqlite/.env.example +24 -0
- package/templates/database/sqlite/drizzle.config.ts +13 -0
- package/templates/database/sqlite/package.json +8 -0
- package/templates/database/sqlite/prisma/schema.prisma +48 -0
- package/templates/database/sqlite/src/database/drizzle.ts +11 -0
- package/templates/database/sqlite/src/database/schema.ts +52 -0
- package/templates/orm/drizzle/drizzle.config.ts +13 -0
- package/templates/orm/drizzle/package.json +90 -0
- package/templates/orm/drizzle/src/app.module.ts +73 -0
- package/templates/orm/drizzle/src/config/env.validation.ts +55 -0
- package/templates/orm/drizzle/src/database/database.module.ts +25 -0
- package/templates/orm/drizzle/src/database/drizzle.ts +13 -0
- package/templates/orm/drizzle/src/database/schema.ts +60 -0
- package/templates/orm/drizzle/src/database/seed.ts +87 -0
- package/templates/orm/drizzle/src/modules/auth/auth.module.ts +12 -0
- package/templates/orm/drizzle/src/modules/auth/auth.service.ts +298 -0
- package/templates/orm/drizzle/src/modules/health/health.controller.ts +34 -0
- package/templates/orm/drizzle/src/modules/health/health.module.ts +7 -0
- package/templates/orm/drizzle/src/modules/users/users.module.ts +10 -0
- package/templates/orm/drizzle/src/modules/users/users.service.ts +152 -0
- package/templates/orm/mongoose/.env.example +22 -0
- package/templates/orm/mongoose/package.json +86 -0
- package/templates/orm/mongoose/src/app.module.ts +73 -0
- package/templates/orm/mongoose/src/config/env.validation.ts +55 -0
- package/templates/orm/mongoose/src/database/database.module.ts +19 -0
- package/templates/orm/mongoose/src/database/seed.ts +107 -0
- package/templates/orm/mongoose/src/modules/auth/auth.module.ts +21 -0
- package/templates/orm/mongoose/src/modules/auth/auth.service.ts +272 -0
- package/templates/orm/mongoose/src/modules/auth/dtos/login.dto.ts +10 -0
- package/templates/orm/mongoose/src/modules/auth/dtos/signup.dto.ts +8 -0
- package/templates/orm/mongoose/src/modules/health/health.controller.ts +26 -0
- package/templates/orm/mongoose/src/modules/health/health.module.ts +7 -0
- package/templates/orm/mongoose/src/modules/users/dtos/update-profile.dto.ts +24 -0
- package/templates/orm/mongoose/src/modules/users/dtos/update-user.dto.ts +13 -0
- package/templates/orm/mongoose/src/modules/users/users.module.ts +19 -0
- package/templates/orm/mongoose/src/modules/users/users.service.ts +179 -0
- package/templates/orm/mongoose/src/schemas/index.ts +2 -0
- package/templates/orm/mongoose/src/schemas/refresh-token.schema.ts +55 -0
- package/templates/orm/mongoose/src/schemas/user.schema.ts +53 -0
- package/templates/orm/prisma/package.json +102 -0
- package/templates/orm/prisma/prisma/schema.prisma +60 -0
- package/templates/orm/prisma/prisma/seed.ts +69 -0
- package/templates/orm/prisma/src/app.module.ts +55 -0
- package/templates/orm/prisma/src/config/env.validation.ts +56 -0
- package/templates/orm/prisma/src/modules/auth/auth.module.ts +17 -0
- package/templates/orm/prisma/src/modules/auth/auth.service.ts +308 -0
- package/templates/orm/prisma/src/modules/health/health.controller.ts +26 -0
- package/templates/orm/prisma/src/modules/health/health.module.ts +10 -0
- package/templates/orm/prisma/src/modules/users/users.module.ts +11 -0
- package/templates/orm/prisma/src/modules/users/users.service.ts +105 -0
- package/templates/orm/prisma/src/prisma/prisma.module.ts +9 -0
- package/templates/orm/prisma/src/prisma/prisma.service.ts +16 -0
- package/templates/orm/typeorm/package.json +100 -0
- package/templates/orm/typeorm/src/app.module.ts +55 -0
- package/templates/orm/typeorm/src/config/env.validation.ts +58 -0
- package/templates/orm/typeorm/src/database/data-source.ts +19 -0
- package/templates/orm/typeorm/src/database/database.module.ts +27 -0
- package/templates/orm/typeorm/src/database/seed.ts +76 -0
- package/templates/orm/typeorm/src/entities/index.ts +2 -0
- package/templates/orm/typeorm/src/entities/refresh-token.entity.ts +45 -0
- package/templates/orm/typeorm/src/entities/user.entity.ts +51 -0
- package/templates/orm/typeorm/src/modules/auth/auth.module.ts +19 -0
- package/templates/orm/typeorm/src/modules/auth/auth.service.ts +286 -0
- package/templates/orm/typeorm/src/modules/health/health.controller.ts +24 -0
- package/templates/orm/typeorm/src/modules/health/health.module.ts +9 -0
- package/templates/orm/typeorm/src/modules/users/users.module.ts +13 -0
- package/templates/orm/typeorm/src/modules/users/users.service.ts +108 -0
- package/templates/swagger/src/common/dtos/pagination.dto.ts +43 -0
- package/templates/swagger/src/main.ts +116 -0
- package/templates/swagger/src/modules/auth/auth.controller.ts +235 -0
- package/templates/swagger/src/modules/auth/dtos/login.dto.ts +20 -0
- package/templates/swagger/src/modules/auth/dtos/signup.dto.ts +14 -0
- package/templates/swagger/src/modules/users/dtos/update-profile.dto.ts +19 -0
- package/templates/swagger/src/modules/users/dtos/update-user.dto.ts +23 -0
- package/templates/swagger/src/modules/users/users.controller.ts +214 -0
- package/templates/swagger-mongoose/src/modules/auth/dtos/login.dto.ts +20 -0
- package/templates/swagger-mongoose/src/modules/auth/dtos/signup.dto.ts +14 -0
- package/templates/swagger-mongoose/src/modules/users/dtos/update-profile.dto.ts +40 -0
- 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
|
+
}
|