@su-record/vibe 2.7.14 → 2.7.15
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/.env.example +37 -37
- package/CLAUDE.md +134 -126
- package/LICENSE +21 -21
- package/README.md +449 -449
- package/agents/architect-low.md +41 -41
- package/agents/architect-medium.md +59 -59
- package/agents/architect.md +80 -80
- package/agents/build-error-resolver.md +115 -115
- package/agents/compounder.md +261 -261
- package/agents/diagrammer.md +178 -178
- package/agents/docs/api-documenter.md +99 -99
- package/agents/docs/changelog-writer.md +93 -93
- package/agents/e2e-tester.md +294 -294
- package/agents/explorer-low.md +42 -42
- package/agents/explorer-medium.md +59 -59
- package/agents/explorer.md +48 -48
- package/agents/implementer-low.md +43 -43
- package/agents/implementer-medium.md +52 -52
- package/agents/implementer.md +54 -54
- package/agents/junior-mentor.md +141 -141
- package/agents/planning/requirements-analyst.md +84 -84
- package/agents/planning/ux-advisor.md +83 -83
- package/agents/qa/acceptance-tester.md +86 -86
- package/agents/qa/edge-case-finder.md +93 -93
- package/agents/refactor-cleaner.md +143 -143
- package/agents/research/best-practices-agent.md +199 -199
- package/agents/research/codebase-patterns-agent.md +157 -157
- package/agents/research/framework-docs-agent.md +188 -188
- package/agents/research/security-advisory-agent.md +213 -213
- package/agents/review/architecture-reviewer.md +107 -107
- package/agents/review/complexity-reviewer.md +116 -116
- package/agents/review/data-integrity-reviewer.md +88 -88
- package/agents/review/git-history-reviewer.md +103 -103
- package/agents/review/performance-reviewer.md +86 -86
- package/agents/review/python-reviewer.md +150 -150
- package/agents/review/rails-reviewer.md +139 -139
- package/agents/review/react-reviewer.md +144 -144
- package/agents/review/security-reviewer.md +80 -80
- package/agents/review/simplicity-reviewer.md +140 -140
- package/agents/review/test-coverage-reviewer.md +116 -116
- package/agents/review/typescript-reviewer.md +127 -127
- package/agents/searcher.md +54 -54
- package/agents/simplifier.md +120 -120
- package/agents/tester.md +49 -49
- package/agents/ui/ui-a11y-auditor.md +93 -93
- package/agents/ui/ui-antipattern-detector.md +94 -94
- package/agents/ui/ui-dataviz-advisor.md +69 -69
- package/agents/ui/ui-design-system-gen.md +57 -57
- package/agents/ui/ui-industry-analyzer.md +49 -49
- package/agents/ui/ui-layout-architect.md +65 -65
- package/agents/ui/ui-stack-implementer.md +68 -68
- package/agents/ui/ux-compliance-reviewer.md +81 -81
- package/agents/ui-previewer.md +258 -258
- package/commands/vibe.analyze.md +11 -13
- package/commands/vibe.review.md +43 -1
- package/commands/vibe.run.md +2124 -2078
- package/commands/vibe.spec.md +9 -4
- package/commands/vibe.spec.review.md +569 -565
- package/commands/vibe.utils.md +413 -413
- package/commands/vibe.verify.md +33 -8
- package/dist/cli/collaborator.js +52 -52
- package/dist/cli/commands/evolution.js +12 -12
- package/dist/cli/commands/info.js +54 -54
- package/dist/cli/commands/init.js +5 -5
- package/dist/cli/commands/remove.js +14 -14
- package/dist/cli/commands/sentinel.js +27 -27
- package/dist/cli/commands/skills.js +5 -5
- package/dist/cli/commands/slack.js +10 -10
- package/dist/cli/commands/telegram.js +12 -12
- package/dist/cli/detect.js +32 -32
- package/dist/cli/index.js +51 -51
- package/dist/cli/llm/claude-commands.js +16 -16
- package/dist/cli/llm/config.js +18 -18
- package/dist/cli/llm/gemini-commands.js +16 -16
- package/dist/cli/llm/gpt-commands.js +19 -19
- package/dist/cli/llm/help.js +21 -21
- package/dist/cli/postinstall/cursor-agents.js +32 -32
- package/dist/cli/postinstall/cursor-rules.js +83 -83
- package/dist/cli/postinstall/cursor-skills.js +743 -743
- package/dist/cli/setup/Provisioner.js +42 -42
- package/dist/infra/lib/DeepInit.js +24 -24
- package/dist/infra/lib/IterationTracker.js +11 -11
- package/dist/infra/lib/PythonParser.js +108 -108
- package/dist/infra/lib/ReviewRace.js +96 -96
- package/dist/infra/lib/SkillFrontmatter.js +28 -28
- package/dist/infra/lib/SkillQualityGate.js +9 -9
- package/dist/infra/lib/SkillRepository.js +159 -159
- package/dist/infra/lib/UltraQA.js +99 -99
- package/dist/infra/lib/autonomy/AuditStore.js +41 -41
- package/dist/infra/lib/autonomy/ConfirmationStore.js +30 -30
- package/dist/infra/lib/autonomy/EventOutbox.js +38 -38
- package/dist/infra/lib/autonomy/PolicyEngine.js +18 -18
- package/dist/infra/lib/autonomy/SecuritySentinel.js +1 -1
- package/dist/infra/lib/autonomy/SuggestionStore.js +33 -33
- package/dist/infra/lib/embedding/VectorStore.js +22 -22
- package/dist/infra/lib/evolution/AgentAnalyzer.js +10 -10
- package/dist/infra/lib/evolution/DescriptionOptimizer.js +21 -21
- package/dist/infra/lib/evolution/GenerationRegistry.js +36 -36
- package/dist/infra/lib/evolution/InsightStore.js +90 -90
- package/dist/infra/lib/evolution/RollbackManager.js +5 -5
- package/dist/infra/lib/evolution/SkillBenchmark.js +23 -23
- package/dist/infra/lib/evolution/SkillEvalRunner.js +50 -50
- package/dist/infra/lib/evolution/SkillGapDetector.js +10 -10
- package/dist/infra/lib/evolution/UsageTracker.js +28 -28
- package/dist/infra/lib/gemini/orchestration.js +5 -5
- package/dist/infra/lib/gpt/orchestration.js +4 -4
- package/dist/infra/lib/memory/KnowledgeGraph.js +4 -4
- package/dist/infra/lib/memory/MemorySearch.js +57 -57
- package/dist/infra/lib/memory/MemoryStorage.js +181 -181
- package/dist/infra/lib/memory/ObservationStore.js +28 -28
- package/dist/infra/lib/memory/ReflectionStore.js +30 -30
- package/dist/infra/lib/memory/SessionRAGRetriever.js +7 -7
- package/dist/infra/lib/memory/SessionRAGStore.js +225 -225
- package/dist/infra/lib/memory/SessionSummarizer.js +9 -9
- package/dist/infra/orchestrator/AgentManager.js +12 -12
- package/dist/infra/orchestrator/AgentRegistry.js +65 -65
- package/dist/infra/orchestrator/MultiLlmResearch.js +8 -8
- package/dist/infra/orchestrator/SwarmOrchestrator.test.js +16 -16
- package/dist/infra/orchestrator/parallelResearch.js +24 -24
- package/dist/tools/convention/analyzeComplexity.test.js +115 -115
- package/dist/tools/convention/validateCodeQuality.test.js +104 -104
- package/dist/tools/memory/createMemoryTimeline.js +10 -10
- package/dist/tools/memory/getMemoryGraph.js +12 -12
- package/dist/tools/memory/getSessionContext.js +9 -9
- package/dist/tools/memory/linkMemories.js +14 -14
- package/dist/tools/memory/listMemories.js +4 -4
- package/dist/tools/memory/recallMemory.js +4 -4
- package/dist/tools/memory/saveMemory.js +4 -4
- package/dist/tools/memory/searchMemoriesAdvanced.js +23 -23
- package/dist/tools/semantic/analyzeDependencyGraph.js +12 -12
- package/dist/tools/semantic/astGrep.test.js +6 -6
- package/dist/tools/spec/prdParser.test.js +171 -171
- package/dist/tools/spec/specGenerator.js +169 -169
- package/dist/tools/spec/traceabilityMatrix.js +64 -64
- package/dist/tools/spec/traceabilityMatrix.test.js +28 -28
- package/hooks/gemini-hooks.json +73 -73
- package/hooks/hooks.json +137 -137
- package/hooks/scripts/code-check.js +77 -70
- package/hooks/scripts/context-save.js +212 -212
- package/hooks/scripts/hud-status.js +291 -291
- package/hooks/scripts/keyword-detector.js +214 -214
- package/hooks/scripts/llm-orchestrate.js +475 -475
- package/hooks/scripts/post-edit.js +32 -32
- package/hooks/scripts/pre-tool-guard.js +125 -125
- package/hooks/scripts/prompt-dispatcher.js +185 -185
- package/hooks/scripts/sentinel-guard.js +104 -104
- package/hooks/scripts/session-start.js +106 -106
- package/hooks/scripts/stop-notify.js +209 -209
- package/hooks/scripts/utils.js +100 -100
- package/languages/csharp-unity.md +515 -515
- package/languages/gdscript-godot.md +470 -470
- package/languages/ruby-rails.md +489 -489
- package/languages/typescript-angular.md +433 -433
- package/languages/typescript-astro.md +416 -416
- package/languages/typescript-electron.md +406 -406
- package/languages/typescript-nestjs.md +524 -524
- package/languages/typescript-svelte.md +407 -407
- package/languages/typescript-tauri.md +365 -365
- package/package.json +121 -121
- package/skills/agents-md/SKILL.md +120 -120
- package/skills/arch-guard/SKILL.md +180 -180
- package/skills/brand-assets/SKILL.md +146 -146
- package/skills/capability-loop/SKILL.md +167 -167
- package/skills/characterization-test/SKILL.md +206 -206
- package/skills/commerce-patterns/SKILL.md +59 -59
- package/skills/commit-push-pr/SKILL.md +75 -75
- package/skills/context7-usage/SKILL.md +105 -105
- package/skills/core-capabilities/SKILL.md +48 -48
- package/skills/e2e-commerce/SKILL.md +57 -57
- package/skills/exec-plan/SKILL.md +147 -147
- package/skills/frontend-design/SKILL.md +73 -73
- package/skills/git-worktree/SKILL.md +72 -72
- package/skills/handoff/SKILL.md +109 -109
- package/skills/parallel-research/SKILL.md +87 -87
- package/skills/priority-todos/SKILL.md +63 -63
- package/skills/seo-checklist/SKILL.md +57 -57
- package/skills/techdebt/SKILL.md +122 -122
- package/skills/tool-fallback/SKILL.md +103 -103
- package/skills/typescript-advanced-types/SKILL.md +66 -66
- package/skills/ui-ux-pro-max/SKILL.md +206 -206
- package/skills/vercel-react-best-practices/SKILL.md +59 -59
- package/skills/video-production/SKILL.md +51 -51
- package/vibe/config.json +29 -29
- package/vibe/constitution.md +227 -227
- package/vibe/rules/principles/communication-guide.md +98 -98
- package/vibe/rules/principles/development-philosophy.md +52 -52
- package/vibe/rules/principles/quick-start.md +102 -102
- package/vibe/rules/quality/bdd-contract-testing.md +393 -393
- package/vibe/rules/quality/checklist.md +276 -276
- package/vibe/rules/quality/performance.md +236 -236
- package/vibe/rules/quality/testing-strategy.md +440 -440
- package/vibe/rules/standards/anti-patterns.md +541 -541
- package/vibe/rules/standards/code-structure.md +291 -291
- package/vibe/rules/standards/complexity-metrics.md +313 -313
- package/vibe/rules/standards/git-workflow.md +237 -237
- package/vibe/rules/standards/naming-conventions.md +198 -198
- package/vibe/rules/standards/security.md +305 -305
- package/vibe/rules/writing/document-style.md +74 -74
- package/vibe/setup.sh +31 -31
- package/vibe/templates/constitution-template.md +252 -252
- package/vibe/templates/contract-backend-template.md +526 -526
- package/vibe/templates/contract-frontend-template.md +599 -599
- package/vibe/templates/feature-template.md +96 -96
- package/vibe/templates/spec-template.md +221 -221
- package/vibe/ui-ux-data/charts.csv +26 -26
- package/vibe/ui-ux-data/colors.csv +97 -97
- package/vibe/ui-ux-data/icons.csv +101 -101
- package/vibe/ui-ux-data/landing.csv +31 -31
- package/vibe/ui-ux-data/products.csv +96 -96
- package/vibe/ui-ux-data/react-performance.csv +45 -45
- package/vibe/ui-ux-data/stacks/astro.csv +54 -54
- package/vibe/ui-ux-data/stacks/flutter.csv +53 -53
- package/vibe/ui-ux-data/stacks/html-tailwind.csv +56 -56
- package/vibe/ui-ux-data/stacks/jetpack-compose.csv +53 -53
- package/vibe/ui-ux-data/stacks/nextjs.csv +53 -53
- package/vibe/ui-ux-data/stacks/nuxt-ui.csv +51 -51
- package/vibe/ui-ux-data/stacks/nuxtjs.csv +59 -59
- package/vibe/ui-ux-data/stacks/react-native.csv +52 -52
- package/vibe/ui-ux-data/stacks/react.csv +54 -54
- package/vibe/ui-ux-data/stacks/shadcn.csv +61 -61
- package/vibe/ui-ux-data/stacks/svelte.csv +54 -54
- package/vibe/ui-ux-data/stacks/swiftui.csv +51 -51
- package/vibe/ui-ux-data/stacks/vue.csv +50 -50
- package/vibe/ui-ux-data/styles.csv +68 -68
- package/vibe/ui-ux-data/typography.csv +57 -57
- package/vibe/ui-ux-data/ui-reasoning.csv +101 -101
- package/vibe/ui-ux-data/ux-guidelines.csv +99 -99
- package/vibe/ui-ux-data/version.json +31 -31
- package/vibe/ui-ux-data/web-interface.csv +31 -31
|
@@ -1,524 +1,524 @@
|
|
|
1
|
-
# 🐱 TypeScript + NestJS Quality Rules
|
|
2
|
-
|
|
3
|
-
## Core Principles (inherited from core)
|
|
4
|
-
|
|
5
|
-
```markdown
|
|
6
|
-
✅ Single Responsibility (SRP)
|
|
7
|
-
✅ Don't Repeat Yourself (DRY)
|
|
8
|
-
✅ Reusability
|
|
9
|
-
✅ Low Complexity
|
|
10
|
-
✅ Functions ≤ 30 lines
|
|
11
|
-
✅ Nesting ≤ 3 levels
|
|
12
|
-
✅ Cyclomatic complexity ≤ 10
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## NestJS Architecture
|
|
16
|
-
|
|
17
|
-
```
|
|
18
|
-
┌─────────────────────────────────────────────┐
|
|
19
|
-
│ Controller (HTTP Request Handling) │
|
|
20
|
-
│ - Routing, request validation, response │
|
|
21
|
-
├─────────────────────────────────────────────┤
|
|
22
|
-
│ Service (Business Logic) │
|
|
23
|
-
│ - Domain logic, data processing │
|
|
24
|
-
├─────────────────────────────────────────────┤
|
|
25
|
-
│ Repository (Data Access) │
|
|
26
|
-
│ - DB queries, ORM operations │
|
|
27
|
-
└─────────────────────────────────────────────┘
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## TypeScript/NestJS Patterns
|
|
31
|
-
|
|
32
|
-
### 1. Module Structure
|
|
33
|
-
|
|
34
|
-
```typescript
|
|
35
|
-
// ✅ Feature Module
|
|
36
|
-
@Module({
|
|
37
|
-
imports: [
|
|
38
|
-
TypeOrmModule.forFeature([User]),
|
|
39
|
-
CommonModule,
|
|
40
|
-
],
|
|
41
|
-
controllers: [UserController],
|
|
42
|
-
providers: [UserService, UserRepository],
|
|
43
|
-
exports: [UserService],
|
|
44
|
-
})
|
|
45
|
-
export class UserModule {}
|
|
46
|
-
|
|
47
|
-
// ✅ Module folder structure
|
|
48
|
-
// src/
|
|
49
|
-
// ├── user/
|
|
50
|
-
// │ ├── user.module.ts
|
|
51
|
-
// │ ├── user.controller.ts
|
|
52
|
-
// │ ├── user.service.ts
|
|
53
|
-
// │ ├── user.repository.ts
|
|
54
|
-
// │ ├── dto/
|
|
55
|
-
// │ │ ├── create-user.dto.ts
|
|
56
|
-
// │ │ └── update-user.dto.ts
|
|
57
|
-
// │ └── entities/
|
|
58
|
-
// │ └── user.entity.ts
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### 2. Controller Pattern
|
|
62
|
-
|
|
63
|
-
```typescript
|
|
64
|
-
// ✅ Controller
|
|
65
|
-
@Controller('users')
|
|
66
|
-
@ApiTags('users')
|
|
67
|
-
export class UserController {
|
|
68
|
-
constructor(private readonly userService: UserService) {}
|
|
69
|
-
|
|
70
|
-
@Get()
|
|
71
|
-
@ApiOperation({ summary: 'Get all users' })
|
|
72
|
-
@ApiResponse({ status: 200, type: [UserResponseDto] })
|
|
73
|
-
async findAll(@Query() query: FindUsersQueryDto): Promise<UserResponseDto[]> {
|
|
74
|
-
return this.userService.findAll(query);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
@Get(':id')
|
|
78
|
-
@ApiOperation({ summary: 'Get user by ID' })
|
|
79
|
-
@ApiResponse({ status: 200, type: UserResponseDto })
|
|
80
|
-
@ApiResponse({ status: 404, description: 'User not found' })
|
|
81
|
-
async findOne(@Param('id', ParseUUIDPipe) id: string): Promise<UserResponseDto> {
|
|
82
|
-
return this.userService.findOne(id);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
@Post()
|
|
86
|
-
@ApiOperation({ summary: 'Create user' })
|
|
87
|
-
@ApiResponse({ status: 201, type: UserResponseDto })
|
|
88
|
-
async create(@Body() createUserDto: CreateUserDto): Promise<UserResponseDto> {
|
|
89
|
-
return this.userService.create(createUserDto);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
@Patch(':id')
|
|
93
|
-
async update(
|
|
94
|
-
@Param('id', ParseUUIDPipe) id: string,
|
|
95
|
-
@Body() updateUserDto: UpdateUserDto,
|
|
96
|
-
): Promise<UserResponseDto> {
|
|
97
|
-
return this.userService.update(id, updateUserDto);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
@Delete(':id')
|
|
101
|
-
@HttpCode(HttpStatus.NO_CONTENT)
|
|
102
|
-
async remove(@Param('id', ParseUUIDPipe) id: string): Promise<void> {
|
|
103
|
-
await this.userService.remove(id);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
### 3. Service Pattern
|
|
109
|
-
|
|
110
|
-
```typescript
|
|
111
|
-
// ✅ Service
|
|
112
|
-
@Injectable()
|
|
113
|
-
export class UserService {
|
|
114
|
-
constructor(
|
|
115
|
-
private readonly userRepository: UserRepository,
|
|
116
|
-
private readonly eventEmitter: EventEmitter2,
|
|
117
|
-
) {}
|
|
118
|
-
|
|
119
|
-
async findAll(query: FindUsersQueryDto): Promise<User[]> {
|
|
120
|
-
return this.userRepository.findWithPagination(query);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async findOne(id: string): Promise<User> {
|
|
124
|
-
const user = await this.userRepository.findById(id);
|
|
125
|
-
if (!user) {
|
|
126
|
-
throw new NotFoundException(`User with ID ${id} not found`);
|
|
127
|
-
}
|
|
128
|
-
return user;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async create(dto: CreateUserDto): Promise<User> {
|
|
132
|
-
const existingUser = await this.userRepository.findByEmail(dto.email);
|
|
133
|
-
if (existingUser) {
|
|
134
|
-
throw new ConflictException('Email already exists');
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const user = await this.userRepository.create(dto);
|
|
138
|
-
this.eventEmitter.emit('user.created', new UserCreatedEvent(user));
|
|
139
|
-
return user;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async update(id: string, dto: UpdateUserDto): Promise<User> {
|
|
143
|
-
const user = await this.findOne(id);
|
|
144
|
-
return this.userRepository.update(user.id, dto);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async remove(id: string): Promise<void> {
|
|
148
|
-
const user = await this.findOne(id);
|
|
149
|
-
await this.userRepository.softDelete(user.id);
|
|
150
|
-
this.eventEmitter.emit('user.deleted', new UserDeletedEvent(user));
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
### 4. DTO + Validation
|
|
156
|
-
|
|
157
|
-
```typescript
|
|
158
|
-
// ✅ Request DTO
|
|
159
|
-
export class CreateUserDto {
|
|
160
|
-
@ApiProperty({ example: 'john@example.com' })
|
|
161
|
-
@IsEmail()
|
|
162
|
-
@IsNotEmpty()
|
|
163
|
-
email: string;
|
|
164
|
-
|
|
165
|
-
@ApiProperty({ example: 'John Doe' })
|
|
166
|
-
@IsString()
|
|
167
|
-
@MinLength(2)
|
|
168
|
-
@MaxLength(100)
|
|
169
|
-
name: string;
|
|
170
|
-
|
|
171
|
-
@ApiProperty({ example: 'password123' })
|
|
172
|
-
@IsString()
|
|
173
|
-
@MinLength(8)
|
|
174
|
-
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, {
|
|
175
|
-
message: 'Password must contain uppercase, lowercase, and number',
|
|
176
|
-
})
|
|
177
|
-
password: string;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// ✅ Response DTO (only Expose fields are visible)
|
|
181
|
-
@Exclude()
|
|
182
|
-
export class UserResponseDto {
|
|
183
|
-
@Expose()
|
|
184
|
-
@ApiProperty()
|
|
185
|
-
id: string;
|
|
186
|
-
|
|
187
|
-
@Expose()
|
|
188
|
-
@ApiProperty()
|
|
189
|
-
email: string;
|
|
190
|
-
|
|
191
|
-
@Expose()
|
|
192
|
-
@ApiProperty()
|
|
193
|
-
name: string;
|
|
194
|
-
|
|
195
|
-
@Expose()
|
|
196
|
-
@ApiProperty()
|
|
197
|
-
createdAt: Date;
|
|
198
|
-
|
|
199
|
-
// password is excluded
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// ✅ Query DTO
|
|
203
|
-
export class FindUsersQueryDto {
|
|
204
|
-
@ApiPropertyOptional()
|
|
205
|
-
@IsOptional()
|
|
206
|
-
@IsString()
|
|
207
|
-
search?: string;
|
|
208
|
-
|
|
209
|
-
@ApiPropertyOptional({ default: 1 })
|
|
210
|
-
@IsOptional()
|
|
211
|
-
@Type(() => Number)
|
|
212
|
-
@IsInt()
|
|
213
|
-
@Min(1)
|
|
214
|
-
page?: number = 1;
|
|
215
|
-
|
|
216
|
-
@ApiPropertyOptional({ default: 10 })
|
|
217
|
-
@IsOptional()
|
|
218
|
-
@Type(() => Number)
|
|
219
|
-
@IsInt()
|
|
220
|
-
@Min(1)
|
|
221
|
-
@Max(100)
|
|
222
|
-
limit?: number = 10;
|
|
223
|
-
}
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
### 5. Exception Handling
|
|
227
|
-
|
|
228
|
-
```typescript
|
|
229
|
-
// ✅ Custom Exception Filter
|
|
230
|
-
@Catch()
|
|
231
|
-
export class AllExceptionsFilter implements ExceptionFilter {
|
|
232
|
-
constructor(private readonly logger: Logger) {}
|
|
233
|
-
|
|
234
|
-
catch(exception: unknown, host: ArgumentsHost): void {
|
|
235
|
-
const ctx = host.switchToHttp();
|
|
236
|
-
const response = ctx.getResponse<Response>();
|
|
237
|
-
const request = ctx.getRequest<Request>();
|
|
238
|
-
|
|
239
|
-
const { status, message } = this.getErrorDetails(exception);
|
|
240
|
-
|
|
241
|
-
const errorResponse = {
|
|
242
|
-
statusCode: status,
|
|
243
|
-
timestamp: new Date().toISOString(),
|
|
244
|
-
path: request.url,
|
|
245
|
-
message,
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
this.logger.error(
|
|
249
|
-
`${request.method} ${request.url} - ${status}`,
|
|
250
|
-
exception instanceof Error ? exception.stack : undefined,
|
|
251
|
-
);
|
|
252
|
-
|
|
253
|
-
response.status(status).json(errorResponse);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
private getErrorDetails(exception: unknown): { status: number; message: string } {
|
|
257
|
-
if (exception instanceof HttpException) {
|
|
258
|
-
return {
|
|
259
|
-
status: exception.getStatus(),
|
|
260
|
-
message: exception.message,
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
return {
|
|
265
|
-
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
|
266
|
-
message: 'Internal server error',
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// ✅ Business Exception
|
|
272
|
-
export class UserNotFoundException extends NotFoundException {
|
|
273
|
-
constructor(userId: string) {
|
|
274
|
-
super(`User with ID ${userId} not found`);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
### 6. Guard & Interceptor
|
|
280
|
-
|
|
281
|
-
```typescript
|
|
282
|
-
// ✅ Auth Guard
|
|
283
|
-
@Injectable()
|
|
284
|
-
export class JwtAuthGuard extends AuthGuard('jwt') {
|
|
285
|
-
canActivate(context: ExecutionContext) {
|
|
286
|
-
return super.canActivate(context);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
handleRequest<T>(err: Error, user: T): T {
|
|
290
|
-
if (err || !user) {
|
|
291
|
-
throw err || new UnauthorizedException();
|
|
292
|
-
}
|
|
293
|
-
return user;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// ✅ Role Guard
|
|
298
|
-
@Injectable()
|
|
299
|
-
export class RolesGuard implements CanActivate {
|
|
300
|
-
constructor(private reflector: Reflector) {}
|
|
301
|
-
|
|
302
|
-
canActivate(context: ExecutionContext): boolean {
|
|
303
|
-
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(
|
|
304
|
-
ROLES_KEY,
|
|
305
|
-
[context.getHandler(), context.getClass()],
|
|
306
|
-
);
|
|
307
|
-
if (!requiredRoles) {
|
|
308
|
-
return true;
|
|
309
|
-
}
|
|
310
|
-
const { user } = context.switchToHttp().getRequest();
|
|
311
|
-
return requiredRoles.some((role) => user.roles?.includes(role));
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// ✅ Transform Interceptor
|
|
316
|
-
@Injectable()
|
|
317
|
-
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
|
|
318
|
-
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
|
|
319
|
-
return next.handle().pipe(
|
|
320
|
-
map((data) => ({
|
|
321
|
-
success: true,
|
|
322
|
-
data,
|
|
323
|
-
timestamp: new Date().toISOString(),
|
|
324
|
-
})),
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
### 7. Repository Pattern (TypeORM)
|
|
331
|
-
|
|
332
|
-
```typescript
|
|
333
|
-
// ✅ Custom Repository
|
|
334
|
-
@Injectable()
|
|
335
|
-
export class UserRepository {
|
|
336
|
-
constructor(
|
|
337
|
-
@InjectRepository(User)
|
|
338
|
-
private readonly repo: Repository<User>,
|
|
339
|
-
) {}
|
|
340
|
-
|
|
341
|
-
async findById(id: string): Promise<User | null> {
|
|
342
|
-
return this.repo.findOne({ where: { id } });
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
async findByEmail(email: string): Promise<User | null> {
|
|
346
|
-
return this.repo.findOne({ where: { email } });
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
async findWithPagination(query: FindUsersQueryDto): Promise<User[]> {
|
|
350
|
-
const { search, page, limit } = query;
|
|
351
|
-
const qb = this.repo.createQueryBuilder('user');
|
|
352
|
-
|
|
353
|
-
if (search) {
|
|
354
|
-
qb.where('user.name ILIKE :search OR user.email ILIKE :search', {
|
|
355
|
-
search: `%${search}%`,
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
return qb
|
|
360
|
-
.skip((page - 1) * limit)
|
|
361
|
-
.take(limit)
|
|
362
|
-
.orderBy('user.createdAt', 'DESC')
|
|
363
|
-
.getMany();
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
async create(dto: CreateUserDto): Promise<User> {
|
|
367
|
-
const user = this.repo.create(dto);
|
|
368
|
-
return this.repo.save(user);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
async update(id: string, dto: UpdateUserDto): Promise<User> {
|
|
372
|
-
await this.repo.update(id, dto);
|
|
373
|
-
return this.findById(id);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
async softDelete(id: string): Promise<void> {
|
|
377
|
-
await this.repo.softDelete(id);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
```
|
|
381
|
-
|
|
382
|
-
### 8. Configuration
|
|
383
|
-
|
|
384
|
-
```typescript
|
|
385
|
-
// ✅ Config Module
|
|
386
|
-
@Module({
|
|
387
|
-
imports: [
|
|
388
|
-
ConfigModule.forRoot({
|
|
389
|
-
isGlobal: true,
|
|
390
|
-
validationSchema: Joi.object({
|
|
391
|
-
NODE_ENV: Joi.string().valid('development', 'production', 'test').required(),
|
|
392
|
-
PORT: Joi.number().default(3000),
|
|
393
|
-
DATABASE_URL: Joi.string().required(),
|
|
394
|
-
JWT_SECRET: Joi.string().required(),
|
|
395
|
-
JWT_EXPIRES_IN: Joi.string().default('1d'),
|
|
396
|
-
}),
|
|
397
|
-
}),
|
|
398
|
-
],
|
|
399
|
-
})
|
|
400
|
-
export class AppConfigModule {}
|
|
401
|
-
|
|
402
|
-
// ✅ Typed Config Service
|
|
403
|
-
@Injectable()
|
|
404
|
-
export class AppConfigService {
|
|
405
|
-
constructor(private configService: ConfigService) {}
|
|
406
|
-
|
|
407
|
-
get port(): number {
|
|
408
|
-
return this.configService.get<number>('PORT', 3000);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
get databaseUrl(): string {
|
|
412
|
-
return this.configService.getOrThrow<string>('DATABASE_URL');
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
get jwtSecret(): string {
|
|
416
|
-
return this.configService.getOrThrow<string>('JWT_SECRET');
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
get isProduction(): boolean {
|
|
420
|
-
return this.configService.get('NODE_ENV') === 'production';
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
```
|
|
424
|
-
|
|
425
|
-
## Recommended Folder Structure
|
|
426
|
-
|
|
427
|
-
```
|
|
428
|
-
src/
|
|
429
|
-
├── main.ts
|
|
430
|
-
├── app.module.ts
|
|
431
|
-
├── common/ # Shared module
|
|
432
|
-
│ ├── decorators/
|
|
433
|
-
│ ├── filters/
|
|
434
|
-
│ ├── guards/
|
|
435
|
-
│ ├── interceptors/
|
|
436
|
-
│ ├── pipes/
|
|
437
|
-
│ └── common.module.ts
|
|
438
|
-
├── config/ # Configuration
|
|
439
|
-
│ ├── app.config.ts
|
|
440
|
-
│ └── database.config.ts
|
|
441
|
-
├── user/ # Feature module
|
|
442
|
-
│ ├── dto/
|
|
443
|
-
│ ├── entities/
|
|
444
|
-
│ ├── user.controller.ts
|
|
445
|
-
│ ├── user.service.ts
|
|
446
|
-
│ ├── user.repository.ts
|
|
447
|
-
│ └── user.module.ts
|
|
448
|
-
└── auth/
|
|
449
|
-
├── strategies/
|
|
450
|
-
├── guards/
|
|
451
|
-
└── auth.module.ts
|
|
452
|
-
```
|
|
453
|
-
|
|
454
|
-
## Testing
|
|
455
|
-
|
|
456
|
-
```typescript
|
|
457
|
-
// ✅ Unit Test
|
|
458
|
-
describe('UserService', () => {
|
|
459
|
-
let service: UserService;
|
|
460
|
-
let repository: MockType<UserRepository>;
|
|
461
|
-
|
|
462
|
-
beforeEach(async () => {
|
|
463
|
-
const module = await Test.createTestingModule({
|
|
464
|
-
providers: [
|
|
465
|
-
UserService,
|
|
466
|
-
{
|
|
467
|
-
provide: UserRepository,
|
|
468
|
-
useFactory: () => ({
|
|
469
|
-
findById: jest.fn(),
|
|
470
|
-
create: jest.fn(),
|
|
471
|
-
}),
|
|
472
|
-
},
|
|
473
|
-
],
|
|
474
|
-
}).compile();
|
|
475
|
-
|
|
476
|
-
service = module.get(UserService);
|
|
477
|
-
repository = module.get(UserRepository);
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
it('should create a user', async () => {
|
|
481
|
-
const dto = { email: 'test@test.com', name: 'Test' };
|
|
482
|
-
repository.create.mockResolvedValue({ id: '1', ...dto });
|
|
483
|
-
|
|
484
|
-
const result = await service.create(dto);
|
|
485
|
-
expect(result.email).toBe(dto.email);
|
|
486
|
-
});
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
// ✅ E2E Test
|
|
490
|
-
describe('UserController (e2e)', () => {
|
|
491
|
-
let app: INestApplication;
|
|
492
|
-
|
|
493
|
-
beforeAll(async () => {
|
|
494
|
-
const module = await Test.createTestingModule({
|
|
495
|
-
imports: [AppModule],
|
|
496
|
-
}).compile();
|
|
497
|
-
|
|
498
|
-
app = module.createNestApplication();
|
|
499
|
-
await app.init();
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
it('/users (GET)', () => {
|
|
503
|
-
return request(app.getHttpServer())
|
|
504
|
-
.get('/users')
|
|
505
|
-
.expect(200)
|
|
506
|
-
.expect((res) => {
|
|
507
|
-
expect(Array.isArray(res.body)).toBe(true);
|
|
508
|
-
});
|
|
509
|
-
});
|
|
510
|
-
});
|
|
511
|
-
```
|
|
512
|
-
|
|
513
|
-
## Checklist
|
|
514
|
-
|
|
515
|
-
- [ ] Separate request/response types with DTOs
|
|
516
|
-
- [ ] Validate with class-validator
|
|
517
|
-
- [ ] Serialize with class-transformer
|
|
518
|
-
- [ ] Use custom Exceptions
|
|
519
|
-
- [ ] Apply Repository pattern
|
|
520
|
-
- [ ] Handle auth/authorization with Guards
|
|
521
|
-
- [ ] Transform responses with Interceptors
|
|
522
|
-
- [ ] Validate environment variables with ConfigModule
|
|
523
|
-
- [ ] Document API with Swagger
|
|
524
|
-
- [ ] Write Unit + E2E tests
|
|
1
|
+
# 🐱 TypeScript + NestJS Quality Rules
|
|
2
|
+
|
|
3
|
+
## Core Principles (inherited from core)
|
|
4
|
+
|
|
5
|
+
```markdown
|
|
6
|
+
✅ Single Responsibility (SRP)
|
|
7
|
+
✅ Don't Repeat Yourself (DRY)
|
|
8
|
+
✅ Reusability
|
|
9
|
+
✅ Low Complexity
|
|
10
|
+
✅ Functions ≤ 30 lines
|
|
11
|
+
✅ Nesting ≤ 3 levels
|
|
12
|
+
✅ Cyclomatic complexity ≤ 10
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## NestJS Architecture
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
┌─────────────────────────────────────────────┐
|
|
19
|
+
│ Controller (HTTP Request Handling) │
|
|
20
|
+
│ - Routing, request validation, response │
|
|
21
|
+
├─────────────────────────────────────────────┤
|
|
22
|
+
│ Service (Business Logic) │
|
|
23
|
+
│ - Domain logic, data processing │
|
|
24
|
+
├─────────────────────────────────────────────┤
|
|
25
|
+
│ Repository (Data Access) │
|
|
26
|
+
│ - DB queries, ORM operations │
|
|
27
|
+
└─────────────────────────────────────────────┘
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## TypeScript/NestJS Patterns
|
|
31
|
+
|
|
32
|
+
### 1. Module Structure
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
// ✅ Feature Module
|
|
36
|
+
@Module({
|
|
37
|
+
imports: [
|
|
38
|
+
TypeOrmModule.forFeature([User]),
|
|
39
|
+
CommonModule,
|
|
40
|
+
],
|
|
41
|
+
controllers: [UserController],
|
|
42
|
+
providers: [UserService, UserRepository],
|
|
43
|
+
exports: [UserService],
|
|
44
|
+
})
|
|
45
|
+
export class UserModule {}
|
|
46
|
+
|
|
47
|
+
// ✅ Module folder structure
|
|
48
|
+
// src/
|
|
49
|
+
// ├── user/
|
|
50
|
+
// │ ├── user.module.ts
|
|
51
|
+
// │ ├── user.controller.ts
|
|
52
|
+
// │ ├── user.service.ts
|
|
53
|
+
// │ ├── user.repository.ts
|
|
54
|
+
// │ ├── dto/
|
|
55
|
+
// │ │ ├── create-user.dto.ts
|
|
56
|
+
// │ │ └── update-user.dto.ts
|
|
57
|
+
// │ └── entities/
|
|
58
|
+
// │ └── user.entity.ts
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 2. Controller Pattern
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// ✅ Controller
|
|
65
|
+
@Controller('users')
|
|
66
|
+
@ApiTags('users')
|
|
67
|
+
export class UserController {
|
|
68
|
+
constructor(private readonly userService: UserService) {}
|
|
69
|
+
|
|
70
|
+
@Get()
|
|
71
|
+
@ApiOperation({ summary: 'Get all users' })
|
|
72
|
+
@ApiResponse({ status: 200, type: [UserResponseDto] })
|
|
73
|
+
async findAll(@Query() query: FindUsersQueryDto): Promise<UserResponseDto[]> {
|
|
74
|
+
return this.userService.findAll(query);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@Get(':id')
|
|
78
|
+
@ApiOperation({ summary: 'Get user by ID' })
|
|
79
|
+
@ApiResponse({ status: 200, type: UserResponseDto })
|
|
80
|
+
@ApiResponse({ status: 404, description: 'User not found' })
|
|
81
|
+
async findOne(@Param('id', ParseUUIDPipe) id: string): Promise<UserResponseDto> {
|
|
82
|
+
return this.userService.findOne(id);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@Post()
|
|
86
|
+
@ApiOperation({ summary: 'Create user' })
|
|
87
|
+
@ApiResponse({ status: 201, type: UserResponseDto })
|
|
88
|
+
async create(@Body() createUserDto: CreateUserDto): Promise<UserResponseDto> {
|
|
89
|
+
return this.userService.create(createUserDto);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@Patch(':id')
|
|
93
|
+
async update(
|
|
94
|
+
@Param('id', ParseUUIDPipe) id: string,
|
|
95
|
+
@Body() updateUserDto: UpdateUserDto,
|
|
96
|
+
): Promise<UserResponseDto> {
|
|
97
|
+
return this.userService.update(id, updateUserDto);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@Delete(':id')
|
|
101
|
+
@HttpCode(HttpStatus.NO_CONTENT)
|
|
102
|
+
async remove(@Param('id', ParseUUIDPipe) id: string): Promise<void> {
|
|
103
|
+
await this.userService.remove(id);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 3. Service Pattern
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
// ✅ Service
|
|
112
|
+
@Injectable()
|
|
113
|
+
export class UserService {
|
|
114
|
+
constructor(
|
|
115
|
+
private readonly userRepository: UserRepository,
|
|
116
|
+
private readonly eventEmitter: EventEmitter2,
|
|
117
|
+
) {}
|
|
118
|
+
|
|
119
|
+
async findAll(query: FindUsersQueryDto): Promise<User[]> {
|
|
120
|
+
return this.userRepository.findWithPagination(query);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async findOne(id: string): Promise<User> {
|
|
124
|
+
const user = await this.userRepository.findById(id);
|
|
125
|
+
if (!user) {
|
|
126
|
+
throw new NotFoundException(`User with ID ${id} not found`);
|
|
127
|
+
}
|
|
128
|
+
return user;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async create(dto: CreateUserDto): Promise<User> {
|
|
132
|
+
const existingUser = await this.userRepository.findByEmail(dto.email);
|
|
133
|
+
if (existingUser) {
|
|
134
|
+
throw new ConflictException('Email already exists');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const user = await this.userRepository.create(dto);
|
|
138
|
+
this.eventEmitter.emit('user.created', new UserCreatedEvent(user));
|
|
139
|
+
return user;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async update(id: string, dto: UpdateUserDto): Promise<User> {
|
|
143
|
+
const user = await this.findOne(id);
|
|
144
|
+
return this.userRepository.update(user.id, dto);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async remove(id: string): Promise<void> {
|
|
148
|
+
const user = await this.findOne(id);
|
|
149
|
+
await this.userRepository.softDelete(user.id);
|
|
150
|
+
this.eventEmitter.emit('user.deleted', new UserDeletedEvent(user));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### 4. DTO + Validation
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
// ✅ Request DTO
|
|
159
|
+
export class CreateUserDto {
|
|
160
|
+
@ApiProperty({ example: 'john@example.com' })
|
|
161
|
+
@IsEmail()
|
|
162
|
+
@IsNotEmpty()
|
|
163
|
+
email: string;
|
|
164
|
+
|
|
165
|
+
@ApiProperty({ example: 'John Doe' })
|
|
166
|
+
@IsString()
|
|
167
|
+
@MinLength(2)
|
|
168
|
+
@MaxLength(100)
|
|
169
|
+
name: string;
|
|
170
|
+
|
|
171
|
+
@ApiProperty({ example: 'password123' })
|
|
172
|
+
@IsString()
|
|
173
|
+
@MinLength(8)
|
|
174
|
+
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, {
|
|
175
|
+
message: 'Password must contain uppercase, lowercase, and number',
|
|
176
|
+
})
|
|
177
|
+
password: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ✅ Response DTO (only Expose fields are visible)
|
|
181
|
+
@Exclude()
|
|
182
|
+
export class UserResponseDto {
|
|
183
|
+
@Expose()
|
|
184
|
+
@ApiProperty()
|
|
185
|
+
id: string;
|
|
186
|
+
|
|
187
|
+
@Expose()
|
|
188
|
+
@ApiProperty()
|
|
189
|
+
email: string;
|
|
190
|
+
|
|
191
|
+
@Expose()
|
|
192
|
+
@ApiProperty()
|
|
193
|
+
name: string;
|
|
194
|
+
|
|
195
|
+
@Expose()
|
|
196
|
+
@ApiProperty()
|
|
197
|
+
createdAt: Date;
|
|
198
|
+
|
|
199
|
+
// password is excluded
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ✅ Query DTO
|
|
203
|
+
export class FindUsersQueryDto {
|
|
204
|
+
@ApiPropertyOptional()
|
|
205
|
+
@IsOptional()
|
|
206
|
+
@IsString()
|
|
207
|
+
search?: string;
|
|
208
|
+
|
|
209
|
+
@ApiPropertyOptional({ default: 1 })
|
|
210
|
+
@IsOptional()
|
|
211
|
+
@Type(() => Number)
|
|
212
|
+
@IsInt()
|
|
213
|
+
@Min(1)
|
|
214
|
+
page?: number = 1;
|
|
215
|
+
|
|
216
|
+
@ApiPropertyOptional({ default: 10 })
|
|
217
|
+
@IsOptional()
|
|
218
|
+
@Type(() => Number)
|
|
219
|
+
@IsInt()
|
|
220
|
+
@Min(1)
|
|
221
|
+
@Max(100)
|
|
222
|
+
limit?: number = 10;
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### 5. Exception Handling
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
// ✅ Custom Exception Filter
|
|
230
|
+
@Catch()
|
|
231
|
+
export class AllExceptionsFilter implements ExceptionFilter {
|
|
232
|
+
constructor(private readonly logger: Logger) {}
|
|
233
|
+
|
|
234
|
+
catch(exception: unknown, host: ArgumentsHost): void {
|
|
235
|
+
const ctx = host.switchToHttp();
|
|
236
|
+
const response = ctx.getResponse<Response>();
|
|
237
|
+
const request = ctx.getRequest<Request>();
|
|
238
|
+
|
|
239
|
+
const { status, message } = this.getErrorDetails(exception);
|
|
240
|
+
|
|
241
|
+
const errorResponse = {
|
|
242
|
+
statusCode: status,
|
|
243
|
+
timestamp: new Date().toISOString(),
|
|
244
|
+
path: request.url,
|
|
245
|
+
message,
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
this.logger.error(
|
|
249
|
+
`${request.method} ${request.url} - ${status}`,
|
|
250
|
+
exception instanceof Error ? exception.stack : undefined,
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
response.status(status).json(errorResponse);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private getErrorDetails(exception: unknown): { status: number; message: string } {
|
|
257
|
+
if (exception instanceof HttpException) {
|
|
258
|
+
return {
|
|
259
|
+
status: exception.getStatus(),
|
|
260
|
+
message: exception.message,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
|
266
|
+
message: 'Internal server error',
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ✅ Business Exception
|
|
272
|
+
export class UserNotFoundException extends NotFoundException {
|
|
273
|
+
constructor(userId: string) {
|
|
274
|
+
super(`User with ID ${userId} not found`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### 6. Guard & Interceptor
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
// ✅ Auth Guard
|
|
283
|
+
@Injectable()
|
|
284
|
+
export class JwtAuthGuard extends AuthGuard('jwt') {
|
|
285
|
+
canActivate(context: ExecutionContext) {
|
|
286
|
+
return super.canActivate(context);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
handleRequest<T>(err: Error, user: T): T {
|
|
290
|
+
if (err || !user) {
|
|
291
|
+
throw err || new UnauthorizedException();
|
|
292
|
+
}
|
|
293
|
+
return user;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ✅ Role Guard
|
|
298
|
+
@Injectable()
|
|
299
|
+
export class RolesGuard implements CanActivate {
|
|
300
|
+
constructor(private reflector: Reflector) {}
|
|
301
|
+
|
|
302
|
+
canActivate(context: ExecutionContext): boolean {
|
|
303
|
+
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(
|
|
304
|
+
ROLES_KEY,
|
|
305
|
+
[context.getHandler(), context.getClass()],
|
|
306
|
+
);
|
|
307
|
+
if (!requiredRoles) {
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
const { user } = context.switchToHttp().getRequest();
|
|
311
|
+
return requiredRoles.some((role) => user.roles?.includes(role));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ✅ Transform Interceptor
|
|
316
|
+
@Injectable()
|
|
317
|
+
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
|
|
318
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
|
|
319
|
+
return next.handle().pipe(
|
|
320
|
+
map((data) => ({
|
|
321
|
+
success: true,
|
|
322
|
+
data,
|
|
323
|
+
timestamp: new Date().toISOString(),
|
|
324
|
+
})),
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### 7. Repository Pattern (TypeORM)
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
// ✅ Custom Repository
|
|
334
|
+
@Injectable()
|
|
335
|
+
export class UserRepository {
|
|
336
|
+
constructor(
|
|
337
|
+
@InjectRepository(User)
|
|
338
|
+
private readonly repo: Repository<User>,
|
|
339
|
+
) {}
|
|
340
|
+
|
|
341
|
+
async findById(id: string): Promise<User | null> {
|
|
342
|
+
return this.repo.findOne({ where: { id } });
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
346
|
+
return this.repo.findOne({ where: { email } });
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async findWithPagination(query: FindUsersQueryDto): Promise<User[]> {
|
|
350
|
+
const { search, page, limit } = query;
|
|
351
|
+
const qb = this.repo.createQueryBuilder('user');
|
|
352
|
+
|
|
353
|
+
if (search) {
|
|
354
|
+
qb.where('user.name ILIKE :search OR user.email ILIKE :search', {
|
|
355
|
+
search: `%${search}%`,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return qb
|
|
360
|
+
.skip((page - 1) * limit)
|
|
361
|
+
.take(limit)
|
|
362
|
+
.orderBy('user.createdAt', 'DESC')
|
|
363
|
+
.getMany();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async create(dto: CreateUserDto): Promise<User> {
|
|
367
|
+
const user = this.repo.create(dto);
|
|
368
|
+
return this.repo.save(user);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async update(id: string, dto: UpdateUserDto): Promise<User> {
|
|
372
|
+
await this.repo.update(id, dto);
|
|
373
|
+
return this.findById(id);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async softDelete(id: string): Promise<void> {
|
|
377
|
+
await this.repo.softDelete(id);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### 8. Configuration
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
// ✅ Config Module
|
|
386
|
+
@Module({
|
|
387
|
+
imports: [
|
|
388
|
+
ConfigModule.forRoot({
|
|
389
|
+
isGlobal: true,
|
|
390
|
+
validationSchema: Joi.object({
|
|
391
|
+
NODE_ENV: Joi.string().valid('development', 'production', 'test').required(),
|
|
392
|
+
PORT: Joi.number().default(3000),
|
|
393
|
+
DATABASE_URL: Joi.string().required(),
|
|
394
|
+
JWT_SECRET: Joi.string().required(),
|
|
395
|
+
JWT_EXPIRES_IN: Joi.string().default('1d'),
|
|
396
|
+
}),
|
|
397
|
+
}),
|
|
398
|
+
],
|
|
399
|
+
})
|
|
400
|
+
export class AppConfigModule {}
|
|
401
|
+
|
|
402
|
+
// ✅ Typed Config Service
|
|
403
|
+
@Injectable()
|
|
404
|
+
export class AppConfigService {
|
|
405
|
+
constructor(private configService: ConfigService) {}
|
|
406
|
+
|
|
407
|
+
get port(): number {
|
|
408
|
+
return this.configService.get<number>('PORT', 3000);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
get databaseUrl(): string {
|
|
412
|
+
return this.configService.getOrThrow<string>('DATABASE_URL');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
get jwtSecret(): string {
|
|
416
|
+
return this.configService.getOrThrow<string>('JWT_SECRET');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
get isProduction(): boolean {
|
|
420
|
+
return this.configService.get('NODE_ENV') === 'production';
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
## Recommended Folder Structure
|
|
426
|
+
|
|
427
|
+
```
|
|
428
|
+
src/
|
|
429
|
+
├── main.ts
|
|
430
|
+
├── app.module.ts
|
|
431
|
+
├── common/ # Shared module
|
|
432
|
+
│ ├── decorators/
|
|
433
|
+
│ ├── filters/
|
|
434
|
+
│ ├── guards/
|
|
435
|
+
│ ├── interceptors/
|
|
436
|
+
│ ├── pipes/
|
|
437
|
+
│ └── common.module.ts
|
|
438
|
+
├── config/ # Configuration
|
|
439
|
+
│ ├── app.config.ts
|
|
440
|
+
│ └── database.config.ts
|
|
441
|
+
├── user/ # Feature module
|
|
442
|
+
│ ├── dto/
|
|
443
|
+
│ ├── entities/
|
|
444
|
+
│ ├── user.controller.ts
|
|
445
|
+
│ ├── user.service.ts
|
|
446
|
+
│ ├── user.repository.ts
|
|
447
|
+
│ └── user.module.ts
|
|
448
|
+
└── auth/
|
|
449
|
+
├── strategies/
|
|
450
|
+
├── guards/
|
|
451
|
+
└── auth.module.ts
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
## Testing
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
// ✅ Unit Test
|
|
458
|
+
describe('UserService', () => {
|
|
459
|
+
let service: UserService;
|
|
460
|
+
let repository: MockType<UserRepository>;
|
|
461
|
+
|
|
462
|
+
beforeEach(async () => {
|
|
463
|
+
const module = await Test.createTestingModule({
|
|
464
|
+
providers: [
|
|
465
|
+
UserService,
|
|
466
|
+
{
|
|
467
|
+
provide: UserRepository,
|
|
468
|
+
useFactory: () => ({
|
|
469
|
+
findById: jest.fn(),
|
|
470
|
+
create: jest.fn(),
|
|
471
|
+
}),
|
|
472
|
+
},
|
|
473
|
+
],
|
|
474
|
+
}).compile();
|
|
475
|
+
|
|
476
|
+
service = module.get(UserService);
|
|
477
|
+
repository = module.get(UserRepository);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('should create a user', async () => {
|
|
481
|
+
const dto = { email: 'test@test.com', name: 'Test' };
|
|
482
|
+
repository.create.mockResolvedValue({ id: '1', ...dto });
|
|
483
|
+
|
|
484
|
+
const result = await service.create(dto);
|
|
485
|
+
expect(result.email).toBe(dto.email);
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// ✅ E2E Test
|
|
490
|
+
describe('UserController (e2e)', () => {
|
|
491
|
+
let app: INestApplication;
|
|
492
|
+
|
|
493
|
+
beforeAll(async () => {
|
|
494
|
+
const module = await Test.createTestingModule({
|
|
495
|
+
imports: [AppModule],
|
|
496
|
+
}).compile();
|
|
497
|
+
|
|
498
|
+
app = module.createNestApplication();
|
|
499
|
+
await app.init();
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it('/users (GET)', () => {
|
|
503
|
+
return request(app.getHttpServer())
|
|
504
|
+
.get('/users')
|
|
505
|
+
.expect(200)
|
|
506
|
+
.expect((res) => {
|
|
507
|
+
expect(Array.isArray(res.body)).toBe(true);
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
## Checklist
|
|
514
|
+
|
|
515
|
+
- [ ] Separate request/response types with DTOs
|
|
516
|
+
- [ ] Validate with class-validator
|
|
517
|
+
- [ ] Serialize with class-transformer
|
|
518
|
+
- [ ] Use custom Exceptions
|
|
519
|
+
- [ ] Apply Repository pattern
|
|
520
|
+
- [ ] Handle auth/authorization with Guards
|
|
521
|
+
- [ ] Transform responses with Interceptors
|
|
522
|
+
- [ ] Validate environment variables with ConfigModule
|
|
523
|
+
- [ ] Document API with Swagger
|
|
524
|
+
- [ ] Write Unit + E2E tests
|