omgkit 2.2.0 → 2.3.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/package.json +1 -1
- package/plugin/skills/databases/mongodb/SKILL.md +60 -776
- package/plugin/skills/databases/prisma/SKILL.md +53 -744
- package/plugin/skills/databases/redis/SKILL.md +53 -860
- package/plugin/skills/devops/aws/SKILL.md +68 -672
- package/plugin/skills/devops/github-actions/SKILL.md +54 -657
- package/plugin/skills/devops/kubernetes/SKILL.md +67 -602
- package/plugin/skills/devops/performance-profiling/SKILL.md +59 -863
- package/plugin/skills/frameworks/django/SKILL.md +87 -853
- package/plugin/skills/frameworks/express/SKILL.md +95 -1301
- package/plugin/skills/frameworks/fastapi/SKILL.md +90 -1198
- package/plugin/skills/frameworks/laravel/SKILL.md +87 -1187
- package/plugin/skills/frameworks/nestjs/SKILL.md +106 -973
- package/plugin/skills/frameworks/react/SKILL.md +94 -962
- package/plugin/skills/frameworks/vue/SKILL.md +95 -1242
- package/plugin/skills/frontend/accessibility/SKILL.md +91 -1056
- package/plugin/skills/frontend/frontend-design/SKILL.md +69 -1262
- package/plugin/skills/frontend/responsive/SKILL.md +76 -799
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +73 -921
- package/plugin/skills/frontend/tailwindcss/SKILL.md +60 -788
- package/plugin/skills/frontend/threejs/SKILL.md +72 -1266
- package/plugin/skills/languages/javascript/SKILL.md +106 -849
- package/plugin/skills/methodology/brainstorming/SKILL.md +70 -576
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +79 -831
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +81 -654
- package/plugin/skills/methodology/executing-plans/SKILL.md +86 -529
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +95 -586
- package/plugin/skills/methodology/problem-solving/SKILL.md +67 -681
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +70 -533
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +70 -610
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +70 -646
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +70 -478
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +66 -559
- package/plugin/skills/methodology/test-driven-development/SKILL.md +91 -752
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +78 -687
- package/plugin/skills/methodology/token-optimization/SKILL.md +72 -602
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +108 -529
- package/plugin/skills/methodology/writing-plans/SKILL.md +79 -566
- package/plugin/skills/omega/omega-architecture/SKILL.md +91 -752
- package/plugin/skills/omega/omega-coding/SKILL.md +161 -552
- package/plugin/skills/omega/omega-sprint/SKILL.md +132 -777
- package/plugin/skills/omega/omega-testing/SKILL.md +157 -845
- package/plugin/skills/omega/omega-thinking/SKILL.md +165 -606
- package/plugin/skills/security/better-auth/SKILL.md +46 -1034
- package/plugin/skills/security/oauth/SKILL.md +80 -934
- package/plugin/skills/security/owasp/SKILL.md +78 -862
- package/plugin/skills/testing/playwright/SKILL.md +77 -700
- package/plugin/skills/testing/pytest/SKILL.md +73 -811
- package/plugin/skills/testing/vitest/SKILL.md +60 -920
- package/plugin/skills/tools/document-processing/SKILL.md +111 -838
- package/plugin/skills/tools/image-processing/SKILL.md +126 -659
- package/plugin/skills/tools/mcp-development/SKILL.md +85 -758
- package/plugin/skills/tools/media-processing/SKILL.md +118 -735
- package/plugin/stdrules/SKILL_STANDARDS.md +490 -0
- package/plugin/skills/SKILL_STANDARDS.md +0 -743
|
@@ -1,1046 +1,179 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: nestjs
|
|
3
|
-
description:
|
|
4
|
-
category: frameworks
|
|
5
|
-
triggers:
|
|
6
|
-
- nestjs
|
|
7
|
-
- nest.js
|
|
8
|
-
- nest
|
|
9
|
-
- node typescript
|
|
10
|
-
- typescript backend
|
|
11
|
-
- nodejs framework
|
|
12
|
-
- nestjs api
|
|
13
|
-
- nestjs microservices
|
|
2
|
+
name: building-nestjs-apis
|
|
3
|
+
description: Builds enterprise NestJS applications with TypeScript, dependency injection, TypeORM, and microservices patterns. Use when creating scalable Node.js backends, REST/GraphQL APIs, or microservices.
|
|
14
4
|
---
|
|
15
5
|
|
|
16
6
|
# NestJS
|
|
17
7
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
## Purpose
|
|
21
|
-
|
|
22
|
-
Build scalable Node.js applications with confidence:
|
|
23
|
-
|
|
24
|
-
- Design modular, maintainable architectures
|
|
25
|
-
- Implement robust dependency injection
|
|
26
|
-
- Handle authentication and authorization
|
|
27
|
-
- Validate requests with class-validator
|
|
28
|
-
- Write comprehensive tests with Jest
|
|
29
|
-
- Deploy production-ready applications
|
|
30
|
-
- Build microservices and message queues
|
|
31
|
-
|
|
32
|
-
## Features
|
|
33
|
-
|
|
34
|
-
### 1. Module Architecture
|
|
35
|
-
|
|
36
|
-
```typescript
|
|
37
|
-
// src/app.module.ts
|
|
38
|
-
import { Module, MiddlewareConsumer, NestModule } from '@nestjs/common';
|
|
39
|
-
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
40
|
-
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
41
|
-
import { ThrottlerModule } from '@nestjs/throttler';
|
|
42
|
-
import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
|
|
43
|
-
|
|
44
|
-
import { AuthModule } from './auth/auth.module';
|
|
45
|
-
import { UsersModule } from './users/users.module';
|
|
46
|
-
import { OrganizationsModule } from './organizations/organizations.module';
|
|
47
|
-
import { ProjectsModule } from './projects/projects.module';
|
|
48
|
-
import { LoggingInterceptor } from './common/interceptors/logging.interceptor';
|
|
49
|
-
import { RequestIdMiddleware } from './common/middleware/request-id.middleware';
|
|
50
|
-
import configuration from './config/configuration';
|
|
51
|
-
|
|
52
|
-
@Module({
|
|
53
|
-
imports: [
|
|
54
|
-
ConfigModule.forRoot({
|
|
55
|
-
isGlobal: true,
|
|
56
|
-
load: [configuration],
|
|
57
|
-
}),
|
|
58
|
-
TypeOrmModule.forRootAsync({
|
|
59
|
-
imports: [ConfigModule],
|
|
60
|
-
inject: [ConfigService],
|
|
61
|
-
useFactory: (config: ConfigService) => ({
|
|
62
|
-
type: 'postgres',
|
|
63
|
-
host: config.get('database.host'),
|
|
64
|
-
port: config.get('database.port'),
|
|
65
|
-
username: config.get('database.username'),
|
|
66
|
-
password: config.get('database.password'),
|
|
67
|
-
database: config.get('database.name'),
|
|
68
|
-
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
|
69
|
-
synchronize: config.get('database.synchronize'),
|
|
70
|
-
logging: config.get('database.logging'),
|
|
71
|
-
}),
|
|
72
|
-
}),
|
|
73
|
-
ThrottlerModule.forRoot([{
|
|
74
|
-
ttl: 60000,
|
|
75
|
-
limit: 100,
|
|
76
|
-
}]),
|
|
77
|
-
AuthModule,
|
|
78
|
-
UsersModule,
|
|
79
|
-
OrganizationsModule,
|
|
80
|
-
ProjectsModule,
|
|
81
|
-
],
|
|
82
|
-
providers: [
|
|
83
|
-
{
|
|
84
|
-
provide: APP_INTERCEPTOR,
|
|
85
|
-
useClass: LoggingInterceptor,
|
|
86
|
-
},
|
|
87
|
-
],
|
|
88
|
-
})
|
|
89
|
-
export class AppModule implements NestModule {
|
|
90
|
-
configure(consumer: MiddlewareConsumer) {
|
|
91
|
-
consumer.apply(RequestIdMiddleware).forRoutes('*');
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// src/users/users.module.ts
|
|
97
|
-
import { Module } from '@nestjs/common';
|
|
98
|
-
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
99
|
-
import { UsersController } from './users.controller';
|
|
100
|
-
import { UsersService } from './users.service';
|
|
101
|
-
import { User } from './entities/user.entity';
|
|
102
|
-
import { UsersRepository } from './users.repository';
|
|
103
|
-
|
|
104
|
-
@Module({
|
|
105
|
-
imports: [TypeOrmModule.forFeature([User])],
|
|
106
|
-
controllers: [UsersController],
|
|
107
|
-
providers: [UsersService, UsersRepository],
|
|
108
|
-
exports: [UsersService],
|
|
109
|
-
})
|
|
110
|
-
export class UsersModule {}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// src/config/configuration.ts
|
|
114
|
-
export default () => ({
|
|
115
|
-
port: parseInt(process.env.PORT, 10) || 3000,
|
|
116
|
-
database: {
|
|
117
|
-
host: process.env.DB_HOST || 'localhost',
|
|
118
|
-
port: parseInt(process.env.DB_PORT, 10) || 5432,
|
|
119
|
-
username: process.env.DB_USERNAME || 'postgres',
|
|
120
|
-
password: process.env.DB_PASSWORD,
|
|
121
|
-
name: process.env.DB_NAME || 'app',
|
|
122
|
-
synchronize: process.env.NODE_ENV !== 'production',
|
|
123
|
-
logging: process.env.NODE_ENV === 'development',
|
|
124
|
-
},
|
|
125
|
-
jwt: {
|
|
126
|
-
secret: process.env.JWT_SECRET,
|
|
127
|
-
expiresIn: process.env.JWT_EXPIRES_IN || '1h',
|
|
128
|
-
},
|
|
129
|
-
redis: {
|
|
130
|
-
host: process.env.REDIS_HOST || 'localhost',
|
|
131
|
-
port: parseInt(process.env.REDIS_PORT, 10) || 6379,
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
### 2. Entities and Repositories
|
|
8
|
+
## Quick Start
|
|
137
9
|
|
|
138
10
|
```typescript
|
|
139
|
-
|
|
140
|
-
import {
|
|
141
|
-
Entity,
|
|
142
|
-
PrimaryGeneratedColumn,
|
|
143
|
-
Column,
|
|
144
|
-
CreateDateColumn,
|
|
145
|
-
UpdateDateColumn,
|
|
146
|
-
DeleteDateColumn,
|
|
147
|
-
ManyToMany,
|
|
148
|
-
OneToMany,
|
|
149
|
-
BeforeInsert,
|
|
150
|
-
} from 'typeorm';
|
|
151
|
-
import * as bcrypt from 'bcrypt';
|
|
152
|
-
import { Organization } from '../../organizations/entities/organization.entity';
|
|
153
|
-
import { Exclude } from 'class-transformer';
|
|
154
|
-
|
|
155
|
-
export enum UserRole {
|
|
156
|
-
ADMIN = 'admin',
|
|
157
|
-
USER = 'user',
|
|
158
|
-
GUEST = 'guest',
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
@Entity('users')
|
|
162
|
-
export class User {
|
|
163
|
-
@PrimaryGeneratedColumn('uuid')
|
|
164
|
-
id: string;
|
|
165
|
-
|
|
166
|
-
@Column({ unique: true })
|
|
167
|
-
email: string;
|
|
168
|
-
|
|
169
|
-
@Column()
|
|
170
|
-
name: string;
|
|
171
|
-
|
|
172
|
-
@Column()
|
|
173
|
-
@Exclude()
|
|
174
|
-
password: string;
|
|
175
|
-
|
|
176
|
-
@Column({
|
|
177
|
-
type: 'enum',
|
|
178
|
-
enum: UserRole,
|
|
179
|
-
default: UserRole.USER,
|
|
180
|
-
})
|
|
181
|
-
role: UserRole;
|
|
182
|
-
|
|
183
|
-
@Column({ default: true })
|
|
184
|
-
isActive: boolean;
|
|
11
|
+
import { Controller, Get, Module, NestFactory } from '@nestjs/common';
|
|
185
12
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
@DeleteDateColumn()
|
|
193
|
-
deletedAt?: Date;
|
|
194
|
-
|
|
195
|
-
@ManyToMany(() => Organization, (org) => org.members)
|
|
196
|
-
organizations: Organization[];
|
|
197
|
-
|
|
198
|
-
@OneToMany(() => Organization, (org) => org.owner)
|
|
199
|
-
ownedOrganizations: Organization[];
|
|
200
|
-
|
|
201
|
-
@BeforeInsert()
|
|
202
|
-
async hashPassword() {
|
|
203
|
-
if (this.password) {
|
|
204
|
-
this.password = await bcrypt.hash(this.password, 10);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
async validatePassword(password: string): Promise<boolean> {
|
|
209
|
-
return bcrypt.compare(password, this.password);
|
|
13
|
+
@Controller('health')
|
|
14
|
+
class HealthController {
|
|
15
|
+
@Get()
|
|
16
|
+
check() {
|
|
17
|
+
return { status: 'ok' };
|
|
210
18
|
}
|
|
211
19
|
}
|
|
212
20
|
|
|
21
|
+
@Module({ controllers: [HealthController] })
|
|
22
|
+
class AppModule {}
|
|
213
23
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
import { User, UserRole } from './entities/user.entity';
|
|
218
|
-
|
|
219
|
-
interface FindAllOptions {
|
|
220
|
-
search?: string;
|
|
221
|
-
role?: UserRole;
|
|
222
|
-
isActive?: boolean;
|
|
223
|
-
skip?: number;
|
|
224
|
-
take?: number;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
@Injectable()
|
|
228
|
-
export class UsersRepository extends Repository<User> {
|
|
229
|
-
constructor(private dataSource: DataSource) {
|
|
230
|
-
super(User, dataSource.createEntityManager());
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
async findByEmail(email: string): Promise<User | null> {
|
|
234
|
-
return this.findOne({
|
|
235
|
-
where: { email },
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
async findAllWithCount(options: FindAllOptions): Promise<[User[], number]> {
|
|
240
|
-
const query = this.createQueryBuilder('user')
|
|
241
|
-
.where('user.deletedAt IS NULL');
|
|
242
|
-
|
|
243
|
-
if (options.search) {
|
|
244
|
-
query.andWhere(
|
|
245
|
-
'(user.name ILIKE :search OR user.email ILIKE :search)',
|
|
246
|
-
{ search: `%${options.search}%` },
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (options.role) {
|
|
251
|
-
query.andWhere('user.role = :role', { role: options.role });
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
if (options.isActive !== undefined) {
|
|
255
|
-
query.andWhere('user.isActive = :isActive', { isActive: options.isActive });
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
return query
|
|
259
|
-
.orderBy('user.createdAt', 'DESC')
|
|
260
|
-
.skip(options.skip || 0)
|
|
261
|
-
.take(options.take || 20)
|
|
262
|
-
.getManyAndCount();
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
async findWithOrganizations(id: string): Promise<User | null> {
|
|
266
|
-
return this.findOne({
|
|
267
|
-
where: { id },
|
|
268
|
-
relations: ['organizations'],
|
|
269
|
-
});
|
|
270
|
-
}
|
|
24
|
+
async function bootstrap() {
|
|
25
|
+
const app = await NestFactory.create(AppModule);
|
|
26
|
+
await app.listen(3000);
|
|
271
27
|
}
|
|
28
|
+
bootstrap();
|
|
272
29
|
```
|
|
273
30
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
```typescript
|
|
277
|
-
// src/users/dto/create-user.dto.ts
|
|
278
|
-
import {
|
|
279
|
-
IsEmail,
|
|
280
|
-
IsString,
|
|
281
|
-
IsEnum,
|
|
282
|
-
IsOptional,
|
|
283
|
-
MinLength,
|
|
284
|
-
MaxLength,
|
|
285
|
-
Matches,
|
|
286
|
-
} from 'class-validator';
|
|
287
|
-
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
288
|
-
import { UserRole } from '../entities/user.entity';
|
|
289
|
-
|
|
290
|
-
export class CreateUserDto {
|
|
291
|
-
@ApiProperty({ example: 'user@example.com' })
|
|
292
|
-
@IsEmail()
|
|
293
|
-
email: string;
|
|
294
|
-
|
|
295
|
-
@ApiProperty({ example: 'John Doe' })
|
|
296
|
-
@IsString()
|
|
297
|
-
@MinLength(2)
|
|
298
|
-
@MaxLength(100)
|
|
299
|
-
name: string;
|
|
300
|
-
|
|
301
|
-
@ApiProperty({ example: 'SecurePass123!' })
|
|
302
|
-
@IsString()
|
|
303
|
-
@MinLength(8)
|
|
304
|
-
@MaxLength(128)
|
|
305
|
-
@Matches(
|
|
306
|
-
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/,
|
|
307
|
-
{ message: 'Password must contain uppercase, lowercase, number and special character' },
|
|
308
|
-
)
|
|
309
|
-
password: string;
|
|
310
|
-
|
|
311
|
-
@ApiPropertyOptional({ enum: UserRole })
|
|
312
|
-
@IsOptional()
|
|
313
|
-
@IsEnum(UserRole)
|
|
314
|
-
role?: UserRole;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
// src/users/dto/update-user.dto.ts
|
|
319
|
-
import { PartialType, OmitType } from '@nestjs/swagger';
|
|
320
|
-
import { CreateUserDto } from './create-user.dto';
|
|
321
|
-
import { IsBoolean, IsOptional } from 'class-validator';
|
|
322
|
-
|
|
323
|
-
export class UpdateUserDto extends PartialType(
|
|
324
|
-
OmitType(CreateUserDto, ['password'] as const),
|
|
325
|
-
) {
|
|
326
|
-
@IsOptional()
|
|
327
|
-
@IsBoolean()
|
|
328
|
-
isActive?: boolean;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
// src/users/dto/user-response.dto.ts
|
|
333
|
-
import { ApiProperty } from '@nestjs/swagger';
|
|
334
|
-
import { Expose, Type } from 'class-transformer';
|
|
335
|
-
import { UserRole } from '../entities/user.entity';
|
|
336
|
-
|
|
337
|
-
export class UserResponseDto {
|
|
338
|
-
@ApiProperty()
|
|
339
|
-
@Expose()
|
|
340
|
-
id: string;
|
|
341
|
-
|
|
342
|
-
@ApiProperty()
|
|
343
|
-
@Expose()
|
|
344
|
-
email: string;
|
|
345
|
-
|
|
346
|
-
@ApiProperty()
|
|
347
|
-
@Expose()
|
|
348
|
-
name: string;
|
|
349
|
-
|
|
350
|
-
@ApiProperty({ enum: UserRole })
|
|
351
|
-
@Expose()
|
|
352
|
-
role: UserRole;
|
|
353
|
-
|
|
354
|
-
@ApiProperty()
|
|
355
|
-
@Expose()
|
|
356
|
-
isActive: boolean;
|
|
357
|
-
|
|
358
|
-
@ApiProperty()
|
|
359
|
-
@Expose()
|
|
360
|
-
createdAt: Date;
|
|
361
|
-
|
|
362
|
-
@ApiProperty()
|
|
363
|
-
@Expose()
|
|
364
|
-
updatedAt: Date;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
// src/common/dto/pagination.dto.ts
|
|
369
|
-
import { ApiPropertyOptional } from '@nestjs/swagger';
|
|
370
|
-
import { IsOptional, IsInt, Min, Max } from 'class-validator';
|
|
371
|
-
import { Type } from 'class-transformer';
|
|
372
|
-
|
|
373
|
-
export class PaginationDto {
|
|
374
|
-
@ApiPropertyOptional({ default: 1 })
|
|
375
|
-
@IsOptional()
|
|
376
|
-
@Type(() => Number)
|
|
377
|
-
@IsInt()
|
|
378
|
-
@Min(1)
|
|
379
|
-
page?: number = 1;
|
|
380
|
-
|
|
381
|
-
@ApiPropertyOptional({ default: 20 })
|
|
382
|
-
@IsOptional()
|
|
383
|
-
@Type(() => Number)
|
|
384
|
-
@IsInt()
|
|
385
|
-
@Min(1)
|
|
386
|
-
@Max(100)
|
|
387
|
-
limit?: number = 20;
|
|
388
|
-
|
|
389
|
-
get skip(): number {
|
|
390
|
-
return (this.page - 1) * this.limit;
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
export class PaginatedResponseDto<T> {
|
|
395
|
-
@ApiProperty()
|
|
396
|
-
data: T[];
|
|
397
|
-
|
|
398
|
-
@ApiProperty()
|
|
399
|
-
total: number;
|
|
400
|
-
|
|
401
|
-
@ApiProperty()
|
|
402
|
-
page: number;
|
|
403
|
-
|
|
404
|
-
@ApiProperty()
|
|
405
|
-
limit: number;
|
|
406
|
-
|
|
407
|
-
@ApiProperty()
|
|
408
|
-
totalPages: number;
|
|
31
|
+
## Features
|
|
409
32
|
|
|
410
|
-
|
|
411
|
-
|
|
33
|
+
| Feature | Description | Guide |
|
|
34
|
+
|---------|-------------|-------|
|
|
35
|
+
| Modules | Dependency injection, providers | [MODULES.md](MODULES.md) |
|
|
36
|
+
| Controllers | Routes, validation, guards | [CONTROLLERS.md](CONTROLLERS.md) |
|
|
37
|
+
| Services | Business logic, repositories | [SERVICES.md](SERVICES.md) |
|
|
38
|
+
| Database | TypeORM, Prisma integration | [DATABASE.md](DATABASE.md) |
|
|
39
|
+
| Auth | Passport, JWT, guards | [AUTH.md](AUTH.md) |
|
|
40
|
+
| Testing | Unit, e2e with Jest | [TESTING.md](TESTING.md) |
|
|
412
41
|
|
|
413
|
-
|
|
414
|
-
data: T[],
|
|
415
|
-
total: number,
|
|
416
|
-
page: number,
|
|
417
|
-
limit: number,
|
|
418
|
-
): PaginatedResponseDto<T> {
|
|
419
|
-
const totalPages = Math.ceil(total / limit);
|
|
420
|
-
return {
|
|
421
|
-
data,
|
|
422
|
-
total,
|
|
423
|
-
page,
|
|
424
|
-
limit,
|
|
425
|
-
totalPages,
|
|
426
|
-
hasMore: page < totalPages,
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
```
|
|
42
|
+
## Common Patterns
|
|
431
43
|
|
|
432
|
-
###
|
|
44
|
+
### Controller with Validation
|
|
433
45
|
|
|
434
46
|
```typescript
|
|
435
|
-
// src/users/users.controller.ts
|
|
436
|
-
import {
|
|
437
|
-
Controller,
|
|
438
|
-
Get,
|
|
439
|
-
Post,
|
|
440
|
-
Patch,
|
|
441
|
-
Delete,
|
|
442
|
-
Body,
|
|
443
|
-
Param,
|
|
444
|
-
Query,
|
|
445
|
-
UseGuards,
|
|
446
|
-
ParseUUIDPipe,
|
|
447
|
-
HttpCode,
|
|
448
|
-
HttpStatus,
|
|
449
|
-
} from '@nestjs/common';
|
|
450
|
-
import {
|
|
451
|
-
ApiTags,
|
|
452
|
-
ApiOperation,
|
|
453
|
-
ApiResponse,
|
|
454
|
-
ApiBearerAuth,
|
|
455
|
-
} from '@nestjs/swagger';
|
|
456
|
-
import { UsersService } from './users.service';
|
|
457
|
-
import { CreateUserDto } from './dto/create-user.dto';
|
|
458
|
-
import { UpdateUserDto } from './dto/update-user.dto';
|
|
459
|
-
import { UserResponseDto } from './dto/user-response.dto';
|
|
460
|
-
import { PaginationDto, PaginatedResponseDto } from '../common/dto/pagination.dto';
|
|
461
|
-
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
|
462
|
-
import { RolesGuard } from '../auth/guards/roles.guard';
|
|
463
|
-
import { Roles } from '../auth/decorators/roles.decorator';
|
|
464
|
-
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
|
465
|
-
import { User, UserRole } from './entities/user.entity';
|
|
466
|
-
|
|
467
|
-
@ApiTags('users')
|
|
468
47
|
@Controller('users')
|
|
469
|
-
@UseGuards(JwtAuthGuard
|
|
470
|
-
@ApiBearerAuth()
|
|
48
|
+
@UseGuards(JwtAuthGuard)
|
|
471
49
|
export class UsersController {
|
|
472
50
|
constructor(private readonly usersService: UsersService) {}
|
|
473
51
|
|
|
474
52
|
@Get()
|
|
475
|
-
@Roles(
|
|
476
|
-
@
|
|
477
|
-
|
|
478
|
-
async findAll(
|
|
479
|
-
@Query() pagination: PaginationDto,
|
|
480
|
-
@Query('search') search?: string,
|
|
481
|
-
@Query('role') role?: UserRole,
|
|
482
|
-
): Promise<PaginatedResponseDto<UserResponseDto>> {
|
|
483
|
-
return this.usersService.findAll(pagination, { search, role });
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
@Get('me')
|
|
487
|
-
@ApiOperation({ summary: 'Get current user profile' })
|
|
488
|
-
@ApiResponse({ status: 200, type: UserResponseDto })
|
|
489
|
-
async getProfile(@CurrentUser() user: User): Promise<UserResponseDto> {
|
|
490
|
-
return this.usersService.findOne(user.id);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
@Patch('me')
|
|
494
|
-
@ApiOperation({ summary: 'Update current user profile' })
|
|
495
|
-
@ApiResponse({ status: 200, type: UserResponseDto })
|
|
496
|
-
async updateProfile(
|
|
497
|
-
@CurrentUser() user: User,
|
|
498
|
-
@Body() updateUserDto: UpdateUserDto,
|
|
499
|
-
): Promise<UserResponseDto> {
|
|
500
|
-
return this.usersService.update(user.id, updateUserDto);
|
|
53
|
+
@Roles('admin')
|
|
54
|
+
findAll(@Query() query: PaginationDto) {
|
|
55
|
+
return this.usersService.findAll(query);
|
|
501
56
|
}
|
|
502
57
|
|
|
503
58
|
@Get(':id')
|
|
504
|
-
@
|
|
505
|
-
@ApiOperation({ summary: 'Get user by ID' })
|
|
506
|
-
@ApiResponse({ status: 200, type: UserResponseDto })
|
|
507
|
-
async findOne(
|
|
508
|
-
@Param('id', ParseUUIDPipe) id: string,
|
|
509
|
-
): Promise<UserResponseDto> {
|
|
59
|
+
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
|
510
60
|
return this.usersService.findOne(id);
|
|
511
61
|
}
|
|
512
62
|
|
|
513
63
|
@Post()
|
|
514
|
-
@
|
|
515
|
-
|
|
516
|
-
@ApiResponse({ status: 201, type: UserResponseDto })
|
|
517
|
-
async create(@Body() createUserDto: CreateUserDto): Promise<UserResponseDto> {
|
|
518
|
-
return this.usersService.create(createUserDto);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
@Patch(':id')
|
|
522
|
-
@Roles(UserRole.ADMIN)
|
|
523
|
-
@ApiOperation({ summary: 'Update user' })
|
|
524
|
-
@ApiResponse({ status: 200, type: UserResponseDto })
|
|
525
|
-
async update(
|
|
526
|
-
@Param('id', ParseUUIDPipe) id: string,
|
|
527
|
-
@Body() updateUserDto: UpdateUserDto,
|
|
528
|
-
): Promise<UserResponseDto> {
|
|
529
|
-
return this.usersService.update(id, updateUserDto);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
@Delete(':id')
|
|
533
|
-
@Roles(UserRole.ADMIN)
|
|
534
|
-
@HttpCode(HttpStatus.NO_CONTENT)
|
|
535
|
-
@ApiOperation({ summary: 'Delete user' })
|
|
536
|
-
@ApiResponse({ status: 204 })
|
|
537
|
-
async remove(@Param('id', ParseUUIDPipe) id: string): Promise<void> {
|
|
538
|
-
await this.usersService.remove(id);
|
|
64
|
+
create(@Body() dto: CreateUserDto) {
|
|
65
|
+
return this.usersService.create(dto);
|
|
539
66
|
}
|
|
540
67
|
}
|
|
541
68
|
```
|
|
542
69
|
|
|
543
|
-
###
|
|
70
|
+
### Service with Repository
|
|
544
71
|
|
|
545
72
|
```typescript
|
|
546
|
-
// src/users/users.service.ts
|
|
547
|
-
import {
|
|
548
|
-
Injectable,
|
|
549
|
-
NotFoundException,
|
|
550
|
-
ConflictException,
|
|
551
|
-
} from '@nestjs/common';
|
|
552
|
-
import { plainToInstance } from 'class-transformer';
|
|
553
|
-
import { UsersRepository } from './users.repository';
|
|
554
|
-
import { CreateUserDto } from './dto/create-user.dto';
|
|
555
|
-
import { UpdateUserDto } from './dto/update-user.dto';
|
|
556
|
-
import { UserResponseDto } from './dto/user-response.dto';
|
|
557
|
-
import { PaginationDto, PaginatedResponseDto } from '../common/dto/pagination.dto';
|
|
558
|
-
import { User, UserRole } from './entities/user.entity';
|
|
559
|
-
|
|
560
|
-
interface FindAllFilters {
|
|
561
|
-
search?: string;
|
|
562
|
-
role?: UserRole;
|
|
563
|
-
isActive?: boolean;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
73
|
@Injectable()
|
|
567
74
|
export class UsersService {
|
|
568
|
-
constructor(private readonly
|
|
569
|
-
|
|
570
|
-
async findAll(
|
|
571
|
-
pagination: PaginationDto,
|
|
572
|
-
filters: FindAllFilters = {},
|
|
573
|
-
): Promise<PaginatedResponseDto<UserResponseDto>> {
|
|
574
|
-
const [users, total] = await this.usersRepository.findAllWithCount({
|
|
575
|
-
...filters,
|
|
576
|
-
skip: pagination.skip,
|
|
577
|
-
take: pagination.limit,
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
const data = users.map((user) =>
|
|
581
|
-
plainToInstance(UserResponseDto, user, { excludeExtraneousValues: true }),
|
|
582
|
-
);
|
|
583
|
-
|
|
584
|
-
return PaginatedResponseDto.create(
|
|
585
|
-
data,
|
|
586
|
-
total,
|
|
587
|
-
pagination.page,
|
|
588
|
-
pagination.limit,
|
|
589
|
-
);
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
async findOne(id: string): Promise<UserResponseDto> {
|
|
593
|
-
const user = await this.usersRepository.findOne({ where: { id } });
|
|
594
|
-
|
|
595
|
-
if (!user) {
|
|
596
|
-
throw new NotFoundException(`User with ID ${id} not found`);
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
return plainToInstance(UserResponseDto, user, {
|
|
600
|
-
excludeExtraneousValues: true,
|
|
601
|
-
});
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
async findByEmail(email: string): Promise<User | null> {
|
|
605
|
-
return this.usersRepository.findByEmail(email);
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
async create(createUserDto: CreateUserDto): Promise<UserResponseDto> {
|
|
609
|
-
const existing = await this.usersRepository.findByEmail(createUserDto.email);
|
|
610
|
-
|
|
611
|
-
if (existing) {
|
|
612
|
-
throw new ConflictException('Email already in use');
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
const user = this.usersRepository.create(createUserDto);
|
|
616
|
-
await this.usersRepository.save(user);
|
|
617
|
-
|
|
618
|
-
return plainToInstance(UserResponseDto, user, {
|
|
619
|
-
excludeExtraneousValues: true,
|
|
620
|
-
});
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
async update(id: string, updateUserDto: UpdateUserDto): Promise<UserResponseDto> {
|
|
624
|
-
const user = await this.usersRepository.findOne({ where: { id } });
|
|
75
|
+
constructor(private readonly usersRepo: UsersRepository) {}
|
|
625
76
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
if (updateUserDto.email && updateUserDto.email !== user.email) {
|
|
631
|
-
const existing = await this.usersRepository.findByEmail(updateUserDto.email);
|
|
632
|
-
if (existing) {
|
|
633
|
-
throw new ConflictException('Email already in use');
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
Object.assign(user, updateUserDto);
|
|
638
|
-
await this.usersRepository.save(user);
|
|
639
|
-
|
|
640
|
-
return plainToInstance(UserResponseDto, user, {
|
|
641
|
-
excludeExtraneousValues: true,
|
|
77
|
+
async findAll(query: PaginationDto) {
|
|
78
|
+
const [users, total] = await this.usersRepo.findAllWithCount({
|
|
79
|
+
skip: query.skip,
|
|
80
|
+
take: query.limit,
|
|
642
81
|
});
|
|
82
|
+
return { data: users, total, page: query.page };
|
|
643
83
|
}
|
|
644
84
|
|
|
645
|
-
async
|
|
646
|
-
const user = await this.
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
throw new NotFoundException(`User with ID ${id} not found`);
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
await this.usersRepository.softRemove(user);
|
|
85
|
+
async findOne(id: string) {
|
|
86
|
+
const user = await this.usersRepo.findOne({ where: { id } });
|
|
87
|
+
if (!user) throw new NotFoundException('User not found');
|
|
88
|
+
return user;
|
|
653
89
|
}
|
|
654
90
|
|
|
655
|
-
async
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
return null;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
const isValid = await user.validatePassword(password);
|
|
663
|
-
return isValid ? user : null;
|
|
91
|
+
async create(dto: CreateUserDto) {
|
|
92
|
+
const exists = await this.usersRepo.findByEmail(dto.email);
|
|
93
|
+
if (exists) throw new ConflictException('Email in use');
|
|
94
|
+
return this.usersRepo.save(this.usersRepo.create(dto));
|
|
664
95
|
}
|
|
665
96
|
}
|
|
666
97
|
```
|
|
667
98
|
|
|
668
|
-
###
|
|
99
|
+
### DTO with Validation
|
|
669
100
|
|
|
670
101
|
```typescript
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
import { JwtModule } from '@nestjs/jwt';
|
|
674
|
-
import { PassportModule } from '@nestjs/passport';
|
|
675
|
-
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
676
|
-
import { AuthController } from './auth.controller';
|
|
677
|
-
import { AuthService } from './auth.service';
|
|
678
|
-
import { JwtStrategy } from './strategies/jwt.strategy';
|
|
679
|
-
import { LocalStrategy } from './strategies/local.strategy';
|
|
680
|
-
import { UsersModule } from '../users/users.module';
|
|
681
|
-
|
|
682
|
-
@Module({
|
|
683
|
-
imports: [
|
|
684
|
-
UsersModule,
|
|
685
|
-
PassportModule,
|
|
686
|
-
JwtModule.registerAsync({
|
|
687
|
-
imports: [ConfigModule],
|
|
688
|
-
inject: [ConfigService],
|
|
689
|
-
useFactory: (config: ConfigService) => ({
|
|
690
|
-
secret: config.get('jwt.secret'),
|
|
691
|
-
signOptions: { expiresIn: config.get('jwt.expiresIn') },
|
|
692
|
-
}),
|
|
693
|
-
}),
|
|
694
|
-
],
|
|
695
|
-
controllers: [AuthController],
|
|
696
|
-
providers: [AuthService, JwtStrategy, LocalStrategy],
|
|
697
|
-
exports: [AuthService],
|
|
698
|
-
})
|
|
699
|
-
export class AuthModule {}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
// src/auth/strategies/jwt.strategy.ts
|
|
703
|
-
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
|
704
|
-
import { PassportStrategy } from '@nestjs/passport';
|
|
705
|
-
import { ExtractJwt, Strategy } from 'passport-jwt';
|
|
706
|
-
import { ConfigService } from '@nestjs/config';
|
|
707
|
-
import { UsersService } from '../../users/users.service';
|
|
708
|
-
|
|
709
|
-
interface JwtPayload {
|
|
710
|
-
sub: string;
|
|
102
|
+
export class CreateUserDto {
|
|
103
|
+
@IsEmail()
|
|
711
104
|
email: string;
|
|
712
|
-
role: string;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
@Injectable()
|
|
716
|
-
export class JwtStrategy extends PassportStrategy(Strategy) {
|
|
717
|
-
constructor(
|
|
718
|
-
private readonly configService: ConfigService,
|
|
719
|
-
private readonly usersService: UsersService,
|
|
720
|
-
) {
|
|
721
|
-
super({
|
|
722
|
-
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
|
723
|
-
ignoreExpiration: false,
|
|
724
|
-
secretOrKey: configService.get('jwt.secret'),
|
|
725
|
-
});
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
async validate(payload: JwtPayload) {
|
|
729
|
-
const user = await this.usersService.findByEmail(payload.email);
|
|
730
|
-
|
|
731
|
-
if (!user || !user.isActive) {
|
|
732
|
-
throw new UnauthorizedException();
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
return user;
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
|
|
739
105
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
import { UserRole } from '../../users/entities/user.entity';
|
|
744
|
-
import { ROLES_KEY } from '../decorators/roles.decorator';
|
|
745
|
-
|
|
746
|
-
@Injectable()
|
|
747
|
-
export class RolesGuard implements CanActivate {
|
|
748
|
-
constructor(private reflector: Reflector) {}
|
|
749
|
-
|
|
750
|
-
canActivate(context: ExecutionContext): boolean {
|
|
751
|
-
const requiredRoles = this.reflector.getAllAndOverride<UserRole[]>(
|
|
752
|
-
ROLES_KEY,
|
|
753
|
-
[context.getHandler(), context.getClass()],
|
|
754
|
-
);
|
|
106
|
+
@IsString()
|
|
107
|
+
@MinLength(2)
|
|
108
|
+
name: string;
|
|
755
109
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
110
|
+
@IsString()
|
|
111
|
+
@MinLength(8)
|
|
112
|
+
@Matches(/^(?=.*[A-Z])(?=.*\d)/, {
|
|
113
|
+
message: 'Password must contain uppercase and number',
|
|
114
|
+
})
|
|
115
|
+
password: string;
|
|
759
116
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
117
|
+
@IsOptional()
|
|
118
|
+
@IsEnum(UserRole)
|
|
119
|
+
role?: UserRole;
|
|
763
120
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
// src/auth/decorators/roles.decorator.ts
|
|
767
|
-
import { SetMetadata } from '@nestjs/common';
|
|
768
|
-
import { UserRole } from '../../users/entities/user.entity';
|
|
769
|
-
|
|
770
|
-
export const ROLES_KEY = 'roles';
|
|
771
|
-
export const Roles = (...roles: UserRole[]) => SetMetadata(ROLES_KEY, roles);
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
// src/auth/decorators/current-user.decorator.ts
|
|
775
|
-
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
|
776
|
-
|
|
777
|
-
export const CurrentUser = createParamDecorator(
|
|
778
|
-
(data: unknown, ctx: ExecutionContext) => {
|
|
779
|
-
const request = ctx.switchToHttp().getRequest();
|
|
780
|
-
return request.user;
|
|
781
|
-
},
|
|
782
|
-
);
|
|
783
121
|
```
|
|
784
122
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
```typescript
|
|
788
|
-
// src/users/users.service.spec.ts
|
|
789
|
-
import { Test, TestingModule } from '@nestjs/testing';
|
|
790
|
-
import { NotFoundException, ConflictException } from '@nestjs/common';
|
|
791
|
-
import { UsersService } from './users.service';
|
|
792
|
-
import { UsersRepository } from './users.repository';
|
|
793
|
-
import { User, UserRole } from './entities/user.entity';
|
|
794
|
-
|
|
795
|
-
describe('UsersService', () => {
|
|
796
|
-
let service: UsersService;
|
|
797
|
-
let repository: jest.Mocked<UsersRepository>;
|
|
798
|
-
|
|
799
|
-
const mockUser: Partial<User> = {
|
|
800
|
-
id: 'test-id',
|
|
801
|
-
email: 'test@example.com',
|
|
802
|
-
name: 'Test User',
|
|
803
|
-
role: UserRole.USER,
|
|
804
|
-
isActive: true,
|
|
805
|
-
createdAt: new Date(),
|
|
806
|
-
updatedAt: new Date(),
|
|
807
|
-
};
|
|
808
|
-
|
|
809
|
-
beforeEach(async () => {
|
|
810
|
-
const module: TestingModule = await Test.createTestingModule({
|
|
811
|
-
providers: [
|
|
812
|
-
UsersService,
|
|
813
|
-
{
|
|
814
|
-
provide: UsersRepository,
|
|
815
|
-
useValue: {
|
|
816
|
-
findOne: jest.fn(),
|
|
817
|
-
findByEmail: jest.fn(),
|
|
818
|
-
findAllWithCount: jest.fn(),
|
|
819
|
-
create: jest.fn(),
|
|
820
|
-
save: jest.fn(),
|
|
821
|
-
softRemove: jest.fn(),
|
|
822
|
-
},
|
|
823
|
-
},
|
|
824
|
-
],
|
|
825
|
-
}).compile();
|
|
826
|
-
|
|
827
|
-
service = module.get<UsersService>(UsersService);
|
|
828
|
-
repository = module.get(UsersRepository);
|
|
829
|
-
});
|
|
830
|
-
|
|
831
|
-
describe('findOne', () => {
|
|
832
|
-
it('should return a user', async () => {
|
|
833
|
-
repository.findOne.mockResolvedValue(mockUser as User);
|
|
834
|
-
|
|
835
|
-
const result = await service.findOne('test-id');
|
|
836
|
-
|
|
837
|
-
expect(result.email).toBe(mockUser.email);
|
|
838
|
-
expect(repository.findOne).toHaveBeenCalledWith({
|
|
839
|
-
where: { id: 'test-id' },
|
|
840
|
-
});
|
|
841
|
-
});
|
|
842
|
-
|
|
843
|
-
it('should throw NotFoundException if user not found', async () => {
|
|
844
|
-
repository.findOne.mockResolvedValue(null);
|
|
845
|
-
|
|
846
|
-
await expect(service.findOne('test-id')).rejects.toThrow(NotFoundException);
|
|
847
|
-
});
|
|
848
|
-
});
|
|
849
|
-
|
|
850
|
-
describe('create', () => {
|
|
851
|
-
it('should create a new user', async () => {
|
|
852
|
-
repository.findByEmail.mockResolvedValue(null);
|
|
853
|
-
repository.create.mockReturnValue(mockUser as User);
|
|
854
|
-
repository.save.mockResolvedValue(mockUser as User);
|
|
855
|
-
|
|
856
|
-
const result = await service.create({
|
|
857
|
-
email: 'test@example.com',
|
|
858
|
-
name: 'Test User',
|
|
859
|
-
password: 'Password123!',
|
|
860
|
-
});
|
|
861
|
-
|
|
862
|
-
expect(result.email).toBe(mockUser.email);
|
|
863
|
-
expect(repository.save).toHaveBeenCalled();
|
|
864
|
-
});
|
|
865
|
-
|
|
866
|
-
it('should throw ConflictException for duplicate email', async () => {
|
|
867
|
-
repository.findByEmail.mockResolvedValue(mockUser as User);
|
|
868
|
-
|
|
869
|
-
await expect(
|
|
870
|
-
service.create({
|
|
871
|
-
email: 'test@example.com',
|
|
872
|
-
name: 'Test',
|
|
873
|
-
password: 'Password123!',
|
|
874
|
-
}),
|
|
875
|
-
).rejects.toThrow(ConflictException);
|
|
876
|
-
});
|
|
877
|
-
});
|
|
878
|
-
});
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
// src/users/users.controller.spec.ts
|
|
882
|
-
import { Test, TestingModule } from '@nestjs/testing';
|
|
883
|
-
import { UsersController } from './users.controller';
|
|
884
|
-
import { UsersService } from './users.service';
|
|
885
|
-
|
|
886
|
-
describe('UsersController', () => {
|
|
887
|
-
let controller: UsersController;
|
|
888
|
-
let service: jest.Mocked<UsersService>;
|
|
889
|
-
|
|
890
|
-
beforeEach(async () => {
|
|
891
|
-
const module: TestingModule = await Test.createTestingModule({
|
|
892
|
-
controllers: [UsersController],
|
|
893
|
-
providers: [
|
|
894
|
-
{
|
|
895
|
-
provide: UsersService,
|
|
896
|
-
useValue: {
|
|
897
|
-
findAll: jest.fn(),
|
|
898
|
-
findOne: jest.fn(),
|
|
899
|
-
create: jest.fn(),
|
|
900
|
-
update: jest.fn(),
|
|
901
|
-
remove: jest.fn(),
|
|
902
|
-
},
|
|
903
|
-
},
|
|
904
|
-
],
|
|
905
|
-
}).compile();
|
|
123
|
+
## Workflows
|
|
906
124
|
|
|
907
|
-
|
|
908
|
-
service = module.get(UsersService);
|
|
909
|
-
});
|
|
125
|
+
### API Development
|
|
910
126
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
127
|
+
1. Create module with `nest g module [name]`
|
|
128
|
+
2. Create controller and service
|
|
129
|
+
3. Define DTOs with class-validator
|
|
130
|
+
4. Add guards for auth/roles
|
|
131
|
+
5. Write unit and e2e tests
|
|
915
132
|
|
|
916
|
-
|
|
917
|
-
// test/users.e2e-spec.ts
|
|
918
|
-
import { Test, TestingModule } from '@nestjs/testing';
|
|
919
|
-
import { INestApplication, ValidationPipe } from '@nestjs/common';
|
|
920
|
-
import * as request from 'supertest';
|
|
921
|
-
import { AppModule } from '../src/app.module';
|
|
922
|
-
|
|
923
|
-
describe('Users (e2e)', () => {
|
|
924
|
-
let app: INestApplication;
|
|
925
|
-
let authToken: string;
|
|
926
|
-
|
|
927
|
-
beforeAll(async () => {
|
|
928
|
-
const moduleFixture: TestingModule = await Test.createTestingModule({
|
|
929
|
-
imports: [AppModule],
|
|
930
|
-
}).compile();
|
|
931
|
-
|
|
932
|
-
app = moduleFixture.createNestApplication();
|
|
933
|
-
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
|
|
934
|
-
await app.init();
|
|
935
|
-
|
|
936
|
-
// Get auth token
|
|
937
|
-
const loginResponse = await request(app.getHttpServer())
|
|
938
|
-
.post('/auth/login')
|
|
939
|
-
.send({ email: 'admin@test.com', password: 'Admin123!' });
|
|
940
|
-
|
|
941
|
-
authToken = loginResponse.body.access_token;
|
|
942
|
-
});
|
|
943
|
-
|
|
944
|
-
afterAll(async () => {
|
|
945
|
-
await app.close();
|
|
946
|
-
});
|
|
947
|
-
|
|
948
|
-
describe('/users (GET)', () => {
|
|
949
|
-
it('should return paginated users', () => {
|
|
950
|
-
return request(app.getHttpServer())
|
|
951
|
-
.get('/users')
|
|
952
|
-
.set('Authorization', `Bearer ${authToken}`)
|
|
953
|
-
.expect(200)
|
|
954
|
-
.expect((res) => {
|
|
955
|
-
expect(res.body).toHaveProperty('data');
|
|
956
|
-
expect(res.body).toHaveProperty('pagination');
|
|
957
|
-
});
|
|
958
|
-
});
|
|
959
|
-
|
|
960
|
-
it('should return 401 without auth token', () => {
|
|
961
|
-
return request(app.getHttpServer())
|
|
962
|
-
.get('/users')
|
|
963
|
-
.expect(401);
|
|
964
|
-
});
|
|
965
|
-
});
|
|
966
|
-
});
|
|
967
|
-
```
|
|
968
|
-
|
|
969
|
-
## Use Cases
|
|
970
|
-
|
|
971
|
-
### Microservices with Message Queue
|
|
133
|
+
### Module Structure
|
|
972
134
|
|
|
973
135
|
```typescript
|
|
974
|
-
// src/app.module.ts (Microservice)
|
|
975
|
-
import { Module } from '@nestjs/common';
|
|
976
|
-
import { ClientsModule, Transport } from '@nestjs/microservices';
|
|
977
|
-
|
|
978
136
|
@Module({
|
|
979
|
-
imports: [
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
transport: Transport.RMQ,
|
|
984
|
-
options: {
|
|
985
|
-
urls: [process.env.RABBITMQ_URL],
|
|
986
|
-
queue: 'notifications_queue',
|
|
987
|
-
queueOptions: { durable: false },
|
|
988
|
-
},
|
|
989
|
-
},
|
|
990
|
-
]),
|
|
991
|
-
],
|
|
137
|
+
imports: [TypeOrmModule.forFeature([User])],
|
|
138
|
+
controllers: [UsersController],
|
|
139
|
+
providers: [UsersService, UsersRepository],
|
|
140
|
+
exports: [UsersService],
|
|
992
141
|
})
|
|
993
|
-
export class
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
// src/notifications/notifications.service.ts
|
|
997
|
-
import { Injectable, Inject } from '@nestjs/common';
|
|
998
|
-
import { ClientProxy } from '@nestjs/microservices';
|
|
999
|
-
|
|
1000
|
-
@Injectable()
|
|
1001
|
-
export class NotificationsService {
|
|
1002
|
-
constructor(
|
|
1003
|
-
@Inject('NOTIFICATION_SERVICE') private client: ClientProxy,
|
|
1004
|
-
) {}
|
|
1005
|
-
|
|
1006
|
-
async sendWelcomeEmail(userId: string, email: string) {
|
|
1007
|
-
return this.client.emit('user_welcome', { userId, email });
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
142
|
+
export class UsersModule {}
|
|
1010
143
|
```
|
|
1011
144
|
|
|
1012
145
|
## Best Practices
|
|
1013
146
|
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
- Use custom decorators for cleaner code
|
|
1022
|
-
- Write unit and e2e tests
|
|
1023
|
-
- Use class-transformer for responses
|
|
1024
|
-
- Use environment configuration
|
|
1025
|
-
- Document APIs with Swagger
|
|
147
|
+
| Do | Avoid |
|
|
148
|
+
|----|-------|
|
|
149
|
+
| Use dependency injection | Direct instantiation |
|
|
150
|
+
| Validate with DTOs | Trusting input |
|
|
151
|
+
| Use guards for auth | Auth logic in controllers |
|
|
152
|
+
| Use interceptors for cross-cutting | Duplicating logging/transform |
|
|
153
|
+
| Write unit + e2e tests | Skipping test coverage |
|
|
1026
154
|
|
|
1027
|
-
|
|
155
|
+
## Project Structure
|
|
1028
156
|
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
157
|
+
```
|
|
158
|
+
src/
|
|
159
|
+
├── main.ts
|
|
160
|
+
├── app.module.ts
|
|
161
|
+
├── common/
|
|
162
|
+
│ ├── decorators/
|
|
163
|
+
│ ├── guards/
|
|
164
|
+
│ ├── interceptors/
|
|
165
|
+
│ └── pipes/
|
|
166
|
+
├── config/
|
|
167
|
+
├── users/
|
|
168
|
+
│ ├── users.module.ts
|
|
169
|
+
│ ├── users.controller.ts
|
|
170
|
+
│ ├── users.service.ts
|
|
171
|
+
│ ├── dto/
|
|
172
|
+
│ └── entities/
|
|
173
|
+
└── auth/
|
|
174
|
+
├── auth.module.ts
|
|
175
|
+
├── strategies/
|
|
176
|
+
└── guards/
|
|
177
|
+
```
|
|
1041
178
|
|
|
1042
|
-
|
|
1043
|
-
- [NestJS Best Practices](https://github.com/nestjs/awesome-nestjs)
|
|
1044
|
-
- [TypeORM Documentation](https://typeorm.io/)
|
|
1045
|
-
- [Passport.js Documentation](http://www.passportjs.org/)
|
|
1046
|
-
- [class-validator](https://github.com/typestack/class-validator)
|
|
179
|
+
For detailed examples and patterns, see reference files above.
|