memory-journal-mcp 7.0.1 → 7.2.0
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/README.md +75 -66
- package/dist/{chunk-6J4RPJ4I.js → chunk-GR4T3SRW.js} +146 -105
- package/dist/{chunk-ARLH46WS.js → chunk-IWKLHSPU.js} +89 -3
- package/dist/{chunk-2BJHLTYP.js → chunk-ORV7ZZOE.js} +1086 -86
- package/dist/cli.js +30 -4
- package/dist/github-integration-2TFMXHIJ.js +1 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +3 -3
- package/dist/{tools-FFFGXIKN.js → tools-CXR2FEB2.js} +2 -2
- package/package.json +2 -2
- package/skills/README.md +77 -0
- package/skills/autonomous-dev/SKILL.md +56 -0
- package/skills/bin/sync.js +50 -0
- package/skills/bun/SKILL.md +156 -0
- package/skills/github-commander/SKILL.md +1 -1
- package/skills/github-commander/workflows/code-quality-audit.md +7 -5
- package/skills/github-commander/workflows/issue-triage.md +13 -4
- package/skills/github-commander/workflows/milestone-sprint.md +9 -1
- package/skills/github-commander/workflows/perf-audit.md +2 -0
- package/skills/github-commander/workflows/pr-review.md +9 -3
- package/skills/github-commander/workflows/roadmap-kickoff.md +79 -0
- package/skills/github-commander/workflows/security-audit.md +3 -3
- package/skills/github-commander/workflows/update-deps.md +2 -2
- package/skills/gitlab/SKILL.md +115 -0
- package/skills/gitlab/package-lock.json +392 -0
- package/skills/gitlab/package.json +14 -0
- package/skills/gitlab/scripts/gitlab-client.ts +125 -0
- package/skills/gitlab/scripts/gitlab-helper.ts +80 -0
- package/skills/golang/SKILL.md +54 -0
- package/skills/mysql/SKILL.md +30 -0
- package/skills/package.json +48 -0
- package/skills/playwright-standard/SKILL.md +58 -0
- package/skills/playwright-standard/examples/fixtures.ts +66 -0
- package/skills/playwright-standard/examples/type-stubs.d.ts +10 -0
- package/skills/playwright-standard/references/advanced-scenarios.md +59 -0
- package/skills/playwright-standard/references/infrastructure.md +43 -0
- package/skills/postgres/SKILL.md +33 -0
- package/skills/react-best-practices/AGENTS.md +2883 -0
- package/skills/react-best-practices/README.md +127 -0
- package/skills/react-best-practices/SKILL.md +138 -0
- package/skills/react-best-practices/metadata.json +17 -0
- package/skills/react-best-practices/rules/_sections.md +46 -0
- package/skills/react-best-practices/rules/_template.md +28 -0
- package/skills/react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/skills/react-best-practices/rules/advanced-init-once.md +42 -0
- package/skills/react-best-practices/rules/advanced-use-latest.md +39 -0
- package/skills/react-best-practices/rules/async-api-routes.md +35 -0
- package/skills/react-best-practices/rules/async-defer-await.md +80 -0
- package/skills/react-best-practices/rules/async-dependencies.md +48 -0
- package/skills/react-best-practices/rules/async-parallel.md +24 -0
- package/skills/react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/skills/react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/skills/react-best-practices/rules/bundle-conditional.md +37 -0
- package/skills/react-best-practices/rules/bundle-defer-third-party.md +48 -0
- package/skills/react-best-practices/rules/bundle-dynamic-imports.md +34 -0
- package/skills/react-best-practices/rules/bundle-preload.md +44 -0
- package/skills/react-best-practices/rules/client-event-listeners.md +78 -0
- package/skills/react-best-practices/rules/client-localstorage-schema.md +74 -0
- package/skills/react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/skills/react-best-practices/rules/client-swr-dedup.md +56 -0
- package/skills/react-best-practices/rules/js-batch-dom-css.md +110 -0
- package/skills/react-best-practices/rules/js-cache-function-results.md +80 -0
- package/skills/react-best-practices/rules/js-cache-property-access.md +28 -0
- package/skills/react-best-practices/rules/js-cache-storage.md +68 -0
- package/skills/react-best-practices/rules/js-combine-iterations.md +32 -0
- package/skills/react-best-practices/rules/js-early-exit.md +50 -0
- package/skills/react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/skills/react-best-practices/rules/js-index-maps.md +37 -0
- package/skills/react-best-practices/rules/js-length-check-first.md +50 -0
- package/skills/react-best-practices/rules/js-min-max-loop.md +82 -0
- package/skills/react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/skills/react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/skills/react-best-practices/rules/rendering-activity.md +24 -0
- package/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +38 -0
- package/skills/react-best-practices/rules/rendering-conditional-render.md +32 -0
- package/skills/react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/skills/react-best-practices/rules/rendering-hoist-jsx.md +36 -0
- package/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +72 -0
- package/skills/react-best-practices/rules/rendering-hydration-suppress-warning.md +26 -0
- package/skills/react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/skills/react-best-practices/rules/rendering-usetransition-loading.md +75 -0
- package/skills/react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/skills/react-best-practices/rules/rerender-dependencies.md +45 -0
- package/skills/react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
- package/skills/react-best-practices/rules/rerender-derived-state.md +29 -0
- package/skills/react-best-practices/rules/rerender-functional-setstate.md +77 -0
- package/skills/react-best-practices/rules/rerender-lazy-state-init.md +56 -0
- package/skills/react-best-practices/rules/rerender-memo-with-default-value.md +36 -0
- package/skills/react-best-practices/rules/rerender-memo.md +44 -0
- package/skills/react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
- package/skills/react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
- package/skills/react-best-practices/rules/rerender-transitions.md +40 -0
- package/skills/react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
- package/skills/react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/skills/react-best-practices/rules/server-auth-actions.md +96 -0
- package/skills/react-best-practices/rules/server-cache-lru.md +41 -0
- package/skills/react-best-practices/rules/server-cache-react.md +76 -0
- package/skills/react-best-practices/rules/server-dedup-props.md +65 -0
- package/skills/react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/skills/react-best-practices/rules/server-serialization.md +38 -0
- package/skills/rust/SKILL.md +86 -0
- package/skills/shadcn-ui/SKILL.md +72 -0
- package/skills/skill-builder/SKILL.md +457 -0
- package/skills/skill-builder/checklist.md +65 -0
- package/skills/sqlite/SKILL.md +38 -0
- package/skills/typescript/SKILL.md +453 -0
- package/skills/typescript/assets/eslint-template.js +102 -0
- package/skills/typescript/assets/tsconfig-template.json +45 -0
- package/skills/typescript/references/enterprise-patterns.md +531 -0
- package/skills/typescript/references/generics.md +493 -0
- package/skills/typescript/references/nestjs-integration.md +579 -0
- package/skills/typescript/references/react-integration.md +616 -0
- package/skills/typescript/references/toolchain.md +547 -0
- package/skills/typescript/references/type-system.md +481 -0
- package/skills/vitest-standard/SKILL.md +82 -0
- package/skills/vitest-standard/examples/service-mock.ts +60 -0
- package/skills/vitest-standard/examples/tdd-calculator.ts +41 -0
- package/skills/vitest-standard/examples/type-stubs.d.ts +18 -0
- package/skills/vitest-standard/references/async-and-errors.md +58 -0
- package/skills/vitest-standard/references/coverage-and-config.md +53 -0
- package/skills/vitest-standard/references/mocking.md +61 -0
- package/skills/vitest-standard/references/tdd-patterns.md +60 -0
- package/dist/github-integration-PDRLXKGM.js +0 -1
- package/skills/github-commander/workflows/full-audit.md +0 -134
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
# NestJS Integration Reference
|
|
2
|
+
|
|
3
|
+
> **Load when:** User asks about NestJS with TypeScript, API development, DTOs, validation, authentication, or backend patterns.
|
|
4
|
+
|
|
5
|
+
Type-safe API development with NestJS 11+.
|
|
6
|
+
|
|
7
|
+
## Contents
|
|
8
|
+
|
|
9
|
+
- [Project Structure](#project-structure)
|
|
10
|
+
- [Controllers and Routes](#controllers-and-routes)
|
|
11
|
+
- [DTOs and Validation](#dtos-and-validation)
|
|
12
|
+
- [Services and Dependency Injection](#services-and-dependency-injection)
|
|
13
|
+
- [Authentication](#authentication)
|
|
14
|
+
- [Error Handling](#error-handling)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Project Structure
|
|
19
|
+
|
|
20
|
+
### Recommended Layout
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
src/
|
|
24
|
+
├── main.ts # Application entry point
|
|
25
|
+
├── app.module.ts # Root module
|
|
26
|
+
├── common/ # Shared utilities
|
|
27
|
+
│ ├── decorators/
|
|
28
|
+
│ ├── filters/
|
|
29
|
+
│ ├── guards/
|
|
30
|
+
│ ├── interceptors/
|
|
31
|
+
│ └── pipes/
|
|
32
|
+
├── config/ # Configuration
|
|
33
|
+
│ ├── config.module.ts
|
|
34
|
+
│ └── env.validation.ts
|
|
35
|
+
└── modules/
|
|
36
|
+
├── users/
|
|
37
|
+
│ ├── users.module.ts
|
|
38
|
+
│ ├── users.controller.ts
|
|
39
|
+
│ ├── users.service.ts
|
|
40
|
+
│ ├── users.repository.ts
|
|
41
|
+
│ ├── dto/
|
|
42
|
+
│ │ ├── create-user.dto.ts
|
|
43
|
+
│ │ └── update-user.dto.ts
|
|
44
|
+
│ ├── entities/
|
|
45
|
+
│ │ └── user.entity.ts
|
|
46
|
+
│ └── __tests__/
|
|
47
|
+
└── auth/
|
|
48
|
+
└── ...
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Module Configuration
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
// users.module.ts
|
|
55
|
+
import { Module } from '@nestjs/common'
|
|
56
|
+
import { UsersController } from './users.controller'
|
|
57
|
+
import { UsersService } from './users.service'
|
|
58
|
+
import { UsersRepository } from './users.repository'
|
|
59
|
+
|
|
60
|
+
@Module({
|
|
61
|
+
controllers: [UsersController],
|
|
62
|
+
providers: [UsersService, UsersRepository],
|
|
63
|
+
exports: [UsersService], // Export for use in other modules
|
|
64
|
+
})
|
|
65
|
+
export class UsersModule {}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Controllers and Routes
|
|
71
|
+
|
|
72
|
+
### Basic Controller
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import {
|
|
76
|
+
Controller,
|
|
77
|
+
Get,
|
|
78
|
+
Post,
|
|
79
|
+
Put,
|
|
80
|
+
Delete,
|
|
81
|
+
Param,
|
|
82
|
+
Body,
|
|
83
|
+
Query,
|
|
84
|
+
HttpCode,
|
|
85
|
+
HttpStatus,
|
|
86
|
+
} from '@nestjs/common'
|
|
87
|
+
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'
|
|
88
|
+
import { UsersService } from './users.service'
|
|
89
|
+
import { CreateUserDto, UpdateUserDto, UserResponseDto } from './dto'
|
|
90
|
+
import { PaginationDto } from '@/common/dto'
|
|
91
|
+
|
|
92
|
+
@ApiTags('users')
|
|
93
|
+
@Controller('users')
|
|
94
|
+
export class UsersController {
|
|
95
|
+
constructor(private readonly usersService: UsersService) {}
|
|
96
|
+
|
|
97
|
+
@Get()
|
|
98
|
+
@ApiOperation({ summary: 'Get all users' })
|
|
99
|
+
@ApiResponse({ status: 200, type: [UserResponseDto] })
|
|
100
|
+
async findAll(@Query() query: PaginationDto): Promise<UserResponseDto[]> {
|
|
101
|
+
return this.usersService.findAll(query)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@Get(':id')
|
|
105
|
+
@ApiOperation({ summary: 'Get user by ID' })
|
|
106
|
+
@ApiResponse({ status: 200, type: UserResponseDto })
|
|
107
|
+
@ApiResponse({ status: 404, description: 'User not found' })
|
|
108
|
+
async findOne(@Param('id') id: string): Promise<UserResponseDto> {
|
|
109
|
+
return this.usersService.findOne(id)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@Post()
|
|
113
|
+
@HttpCode(HttpStatus.CREATED)
|
|
114
|
+
@ApiOperation({ summary: 'Create new user' })
|
|
115
|
+
@ApiResponse({ status: 201, type: UserResponseDto })
|
|
116
|
+
async create(@Body() dto: CreateUserDto): Promise<UserResponseDto> {
|
|
117
|
+
return this.usersService.create(dto)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@Put(':id')
|
|
121
|
+
@ApiOperation({ summary: 'Update user' })
|
|
122
|
+
async update(@Param('id') id: string, @Body() dto: UpdateUserDto): Promise<UserResponseDto> {
|
|
123
|
+
return this.usersService.update(id, dto)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@Delete(':id')
|
|
127
|
+
@HttpCode(HttpStatus.NO_CONTENT)
|
|
128
|
+
@ApiOperation({ summary: 'Delete user' })
|
|
129
|
+
async remove(@Param('id') id: string): Promise<void> {
|
|
130
|
+
return this.usersService.remove(id)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## DTOs and Validation
|
|
138
|
+
|
|
139
|
+
### Class-Validator Approach
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// dto/create-user.dto.ts
|
|
143
|
+
import {
|
|
144
|
+
IsString,
|
|
145
|
+
IsEmail,
|
|
146
|
+
MinLength,
|
|
147
|
+
MaxLength,
|
|
148
|
+
IsOptional,
|
|
149
|
+
IsEnum,
|
|
150
|
+
ValidateNested,
|
|
151
|
+
IsArray,
|
|
152
|
+
} from 'class-validator'
|
|
153
|
+
import { Type } from 'class-transformer'
|
|
154
|
+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
|
|
155
|
+
|
|
156
|
+
export enum UserRole {
|
|
157
|
+
User = 'user',
|
|
158
|
+
Admin = 'admin',
|
|
159
|
+
Moderator = 'moderator',
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export class AddressDto {
|
|
163
|
+
@ApiProperty()
|
|
164
|
+
@IsString()
|
|
165
|
+
street: string
|
|
166
|
+
|
|
167
|
+
@ApiProperty()
|
|
168
|
+
@IsString()
|
|
169
|
+
city: string
|
|
170
|
+
|
|
171
|
+
@ApiProperty()
|
|
172
|
+
@IsString()
|
|
173
|
+
country: string
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export class CreateUserDto {
|
|
177
|
+
@ApiProperty({ example: 'john@example.com' })
|
|
178
|
+
@IsEmail()
|
|
179
|
+
email: string
|
|
180
|
+
|
|
181
|
+
@ApiProperty({ minLength: 2, maxLength: 50 })
|
|
182
|
+
@IsString()
|
|
183
|
+
@MinLength(2)
|
|
184
|
+
@MaxLength(50)
|
|
185
|
+
name: string
|
|
186
|
+
|
|
187
|
+
@ApiProperty({ minLength: 8 })
|
|
188
|
+
@IsString()
|
|
189
|
+
@MinLength(8)
|
|
190
|
+
password: string
|
|
191
|
+
|
|
192
|
+
@ApiPropertyOptional({ enum: UserRole, default: UserRole.User })
|
|
193
|
+
@IsOptional()
|
|
194
|
+
@IsEnum(UserRole)
|
|
195
|
+
role?: UserRole = UserRole.User
|
|
196
|
+
|
|
197
|
+
@ApiPropertyOptional({ type: AddressDto })
|
|
198
|
+
@IsOptional()
|
|
199
|
+
@ValidateNested()
|
|
200
|
+
@Type(() => AddressDto)
|
|
201
|
+
address?: AddressDto
|
|
202
|
+
|
|
203
|
+
@ApiPropertyOptional({ type: [String] })
|
|
204
|
+
@IsOptional()
|
|
205
|
+
@IsArray()
|
|
206
|
+
@IsString({ each: true })
|
|
207
|
+
tags?: string[]
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// dto/update-user.dto.ts
|
|
211
|
+
import { PartialType } from '@nestjs/swagger'
|
|
212
|
+
import { CreateUserDto } from './create-user.dto'
|
|
213
|
+
|
|
214
|
+
export class UpdateUserDto extends PartialType(CreateUserDto) {}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Zod-Based DTOs (Modern Approach)
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// dto/user.schema.ts
|
|
221
|
+
import { z } from 'zod'
|
|
222
|
+
import { createZodDto } from 'nestjs-zod'
|
|
223
|
+
|
|
224
|
+
// Define Zod schemas
|
|
225
|
+
export const UserRoleSchema = z.enum(['user', 'admin', 'moderator'])
|
|
226
|
+
|
|
227
|
+
export const AddressSchema = z.object({
|
|
228
|
+
street: z.string(),
|
|
229
|
+
city: z.string(),
|
|
230
|
+
country: z.string(),
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
export const CreateUserSchema = z.object({
|
|
234
|
+
email: z.string().email(),
|
|
235
|
+
name: z.string().min(2).max(50),
|
|
236
|
+
password: z.string().min(8),
|
|
237
|
+
role: UserRoleSchema.default('user').optional(),
|
|
238
|
+
address: AddressSchema.optional(),
|
|
239
|
+
tags: z.array(z.string()).optional(),
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
export const UpdateUserSchema = CreateUserSchema.partial()
|
|
243
|
+
|
|
244
|
+
// Create DTO classes from schemas
|
|
245
|
+
export class CreateUserDto extends createZodDto(CreateUserSchema) {}
|
|
246
|
+
export class UpdateUserDto extends createZodDto(UpdateUserSchema) {}
|
|
247
|
+
|
|
248
|
+
// Infer types
|
|
249
|
+
export type CreateUser = z.infer<typeof CreateUserSchema>
|
|
250
|
+
export type UpdateUser = z.infer<typeof UpdateUserSchema>
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Global Validation Pipe
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
// main.ts
|
|
257
|
+
import { ValidationPipe } from '@nestjs/common'
|
|
258
|
+
import { NestFactory } from '@nestjs/core'
|
|
259
|
+
import { AppModule } from './app.module'
|
|
260
|
+
|
|
261
|
+
async function bootstrap() {
|
|
262
|
+
const app = await NestFactory.create(AppModule)
|
|
263
|
+
|
|
264
|
+
app.useGlobalPipes(
|
|
265
|
+
new ValidationPipe({
|
|
266
|
+
whitelist: true, // Strip unknown properties
|
|
267
|
+
forbidNonWhitelisted: true, // Throw on unknown properties
|
|
268
|
+
transform: true, // Transform payloads to DTO classes
|
|
269
|
+
transformOptions: {
|
|
270
|
+
enableImplicitConversion: true,
|
|
271
|
+
},
|
|
272
|
+
})
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
await app.listen(3000)
|
|
276
|
+
}
|
|
277
|
+
bootstrap()
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Services and Dependency Injection
|
|
283
|
+
|
|
284
|
+
### Typed Service
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
// users.service.ts
|
|
288
|
+
import { Injectable, NotFoundException } from '@nestjs/common'
|
|
289
|
+
import { UsersRepository } from './users.repository'
|
|
290
|
+
import { CreateUserDto, UpdateUserDto, UserResponseDto } from './dto'
|
|
291
|
+
import { PaginationDto } from '@/common/dto'
|
|
292
|
+
|
|
293
|
+
@Injectable()
|
|
294
|
+
export class UsersService {
|
|
295
|
+
constructor(private readonly usersRepository: UsersRepository) {}
|
|
296
|
+
|
|
297
|
+
async findAll(query: PaginationDto): Promise<UserResponseDto[]> {
|
|
298
|
+
const users = await this.usersRepository.findAll(query)
|
|
299
|
+
return users.map(this.toResponseDto)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async findOne(id: string): Promise<UserResponseDto> {
|
|
303
|
+
const user = await this.usersRepository.findById(id)
|
|
304
|
+
if (!user) {
|
|
305
|
+
throw new NotFoundException(`User with ID ${id} not found`)
|
|
306
|
+
}
|
|
307
|
+
return this.toResponseDto(user)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async create(dto: CreateUserDto): Promise<UserResponseDto> {
|
|
311
|
+
const user = await this.usersRepository.create(dto)
|
|
312
|
+
return this.toResponseDto(user)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async update(id: string, dto: UpdateUserDto): Promise<UserResponseDto> {
|
|
316
|
+
const user = await this.usersRepository.update(id, dto)
|
|
317
|
+
if (!user) {
|
|
318
|
+
throw new NotFoundException(`User with ID ${id} not found`)
|
|
319
|
+
}
|
|
320
|
+
return this.toResponseDto(user)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async remove(id: string): Promise<void> {
|
|
324
|
+
const deleted = await this.usersRepository.delete(id)
|
|
325
|
+
if (!deleted) {
|
|
326
|
+
throw new NotFoundException(`User with ID ${id} not found`)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private toResponseDto(user: User): UserResponseDto {
|
|
331
|
+
return {
|
|
332
|
+
id: user.id,
|
|
333
|
+
email: user.email,
|
|
334
|
+
name: user.name,
|
|
335
|
+
role: user.role,
|
|
336
|
+
createdAt: user.createdAt,
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Repository Pattern
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
// users.repository.ts
|
|
346
|
+
import { Injectable } from '@nestjs/common'
|
|
347
|
+
import { PrismaService } from '@/prisma/prisma.service'
|
|
348
|
+
import { User, Prisma } from '@prisma/client'
|
|
349
|
+
import { PaginationDto } from '@/common/dto'
|
|
350
|
+
|
|
351
|
+
@Injectable()
|
|
352
|
+
export class UsersRepository {
|
|
353
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
354
|
+
|
|
355
|
+
async findAll(query: PaginationDto): Promise<User[]> {
|
|
356
|
+
return this.prisma.user.findMany({
|
|
357
|
+
skip: query.skip,
|
|
358
|
+
take: query.take,
|
|
359
|
+
orderBy: { createdAt: 'desc' },
|
|
360
|
+
})
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async findById(id: string): Promise<User | null> {
|
|
364
|
+
return this.prisma.user.findUnique({ where: { id } })
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
368
|
+
return this.prisma.user.findUnique({ where: { email } })
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async create(data: Prisma.UserCreateInput): Promise<User> {
|
|
372
|
+
return this.prisma.user.create({ data })
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async update(id: string, data: Prisma.UserUpdateInput): Promise<User | null> {
|
|
376
|
+
try {
|
|
377
|
+
return await this.prisma.user.update({ where: { id }, data })
|
|
378
|
+
} catch {
|
|
379
|
+
return null
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async delete(id: string): Promise<boolean> {
|
|
384
|
+
try {
|
|
385
|
+
await this.prisma.user.delete({ where: { id } })
|
|
386
|
+
return true
|
|
387
|
+
} catch {
|
|
388
|
+
return false
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## Authentication
|
|
397
|
+
|
|
398
|
+
### JWT Authentication
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
// auth/strategies/jwt.strategy.ts
|
|
402
|
+
import { Injectable, UnauthorizedException } from '@nestjs/common'
|
|
403
|
+
import { PassportStrategy } from '@nestjs/passport'
|
|
404
|
+
import { ExtractJwt, Strategy } from 'passport-jwt'
|
|
405
|
+
import { ConfigService } from '@nestjs/config'
|
|
406
|
+
import { UsersService } from '@/modules/users/users.service'
|
|
407
|
+
|
|
408
|
+
interface JwtPayload {
|
|
409
|
+
sub: string
|
|
410
|
+
email: string
|
|
411
|
+
role: string
|
|
412
|
+
iat: number
|
|
413
|
+
exp: number
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
@Injectable()
|
|
417
|
+
export class JwtStrategy extends PassportStrategy(Strategy) {
|
|
418
|
+
constructor(
|
|
419
|
+
configService: ConfigService,
|
|
420
|
+
private readonly usersService: UsersService
|
|
421
|
+
) {
|
|
422
|
+
super({
|
|
423
|
+
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
|
424
|
+
ignoreExpiration: false,
|
|
425
|
+
secretOrKey: configService.getOrThrow<string>('JWT_SECRET'),
|
|
426
|
+
})
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async validate(payload: JwtPayload) {
|
|
430
|
+
const user = await this.usersService.findOne(payload.sub)
|
|
431
|
+
if (!user) {
|
|
432
|
+
throw new UnauthorizedException()
|
|
433
|
+
}
|
|
434
|
+
return { id: payload.sub, email: payload.email, role: payload.role }
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### Role-Based Access Control
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
// common/decorators/roles.decorator.ts
|
|
443
|
+
import { SetMetadata } from '@nestjs/common'
|
|
444
|
+
|
|
445
|
+
export const ROLES_KEY = 'roles'
|
|
446
|
+
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles)
|
|
447
|
+
|
|
448
|
+
// common/guards/roles.guard.ts
|
|
449
|
+
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
|
|
450
|
+
import { Reflector } from '@nestjs/core'
|
|
451
|
+
import { ROLES_KEY } from '../decorators/roles.decorator'
|
|
452
|
+
|
|
453
|
+
@Injectable()
|
|
454
|
+
export class RolesGuard implements CanActivate {
|
|
455
|
+
constructor(private reflector: Reflector) {}
|
|
456
|
+
|
|
457
|
+
canActivate(context: ExecutionContext): boolean {
|
|
458
|
+
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
|
|
459
|
+
context.getHandler(),
|
|
460
|
+
context.getClass(),
|
|
461
|
+
])
|
|
462
|
+
|
|
463
|
+
if (!requiredRoles) {
|
|
464
|
+
return true
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const { user } = context.switchToHttp().getRequest()
|
|
468
|
+
return requiredRoles.includes(user.role)
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Usage in controller
|
|
473
|
+
@Controller('admin')
|
|
474
|
+
@UseGuards(JwtAuthGuard, RolesGuard)
|
|
475
|
+
export class AdminController {
|
|
476
|
+
@Get('users')
|
|
477
|
+
@Roles('admin')
|
|
478
|
+
findAllUsers() {
|
|
479
|
+
return this.adminService.findAllUsers()
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
@Delete('users/:id')
|
|
483
|
+
@Roles('admin', 'moderator')
|
|
484
|
+
removeUser(@Param('id') id: string) {
|
|
485
|
+
return this.adminService.removeUser(id)
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
## Error Handling
|
|
493
|
+
|
|
494
|
+
### Exception Filters
|
|
495
|
+
|
|
496
|
+
```typescript
|
|
497
|
+
// common/filters/http-exception.filter.ts
|
|
498
|
+
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common'
|
|
499
|
+
import { Response } from 'express'
|
|
500
|
+
|
|
501
|
+
interface ErrorResponse {
|
|
502
|
+
statusCode: number
|
|
503
|
+
message: string
|
|
504
|
+
error: string
|
|
505
|
+
timestamp: string
|
|
506
|
+
path: string
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
@Catch()
|
|
510
|
+
export class AllExceptionsFilter implements ExceptionFilter {
|
|
511
|
+
catch(exception: unknown, host: ArgumentsHost) {
|
|
512
|
+
const ctx = host.switchToHttp()
|
|
513
|
+
const response = ctx.getResponse<Response>()
|
|
514
|
+
const request = ctx.getRequest()
|
|
515
|
+
|
|
516
|
+
let status = HttpStatus.INTERNAL_SERVER_ERROR
|
|
517
|
+
let message = 'Internal server error'
|
|
518
|
+
let error = 'Internal Server Error'
|
|
519
|
+
|
|
520
|
+
if (exception instanceof HttpException) {
|
|
521
|
+
status = exception.getStatus()
|
|
522
|
+
const exceptionResponse = exception.getResponse()
|
|
523
|
+
|
|
524
|
+
if (typeof exceptionResponse === 'string') {
|
|
525
|
+
message = exceptionResponse
|
|
526
|
+
} else if (typeof exceptionResponse === 'object') {
|
|
527
|
+
const responseObj = exceptionResponse as Record<string, unknown>
|
|
528
|
+
message = (responseObj.message as string) || message
|
|
529
|
+
error = (responseObj.error as string) || exception.name
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const errorResponse: ErrorResponse = {
|
|
534
|
+
statusCode: status,
|
|
535
|
+
message,
|
|
536
|
+
error,
|
|
537
|
+
timestamp: new Date().toISOString(),
|
|
538
|
+
path: request.url,
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
response.status(status).json(errorResponse)
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// main.ts
|
|
546
|
+
app.useGlobalFilters(new AllExceptionsFilter())
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
### Custom Exceptions
|
|
550
|
+
|
|
551
|
+
```typescript
|
|
552
|
+
// common/exceptions/business.exception.ts
|
|
553
|
+
import { HttpException, HttpStatus } from '@nestjs/common'
|
|
554
|
+
|
|
555
|
+
export class BusinessException extends HttpException {
|
|
556
|
+
constructor(
|
|
557
|
+
message: string,
|
|
558
|
+
public readonly code: string,
|
|
559
|
+
status: HttpStatus = HttpStatus.BAD_REQUEST
|
|
560
|
+
) {
|
|
561
|
+
super({ message, code }, status)
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
export class InsufficientFundsException extends BusinessException {
|
|
566
|
+
constructor(required: number, available: number) {
|
|
567
|
+
super(`Insufficient funds: required ${required}, available ${available}`, 'INSUFFICIENT_FUNDS')
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export class DuplicateEmailException extends BusinessException {
|
|
572
|
+
constructor(email: string) {
|
|
573
|
+
super(`Email ${email} is already registered`, 'DUPLICATE_EMAIL')
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Usage
|
|
578
|
+
throw new InsufficientFundsException(100, 50)
|
|
579
|
+
```
|