omgkit 2.1.1 → 2.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/package.json +1 -1
- package/plugin/skills/SKILL_STANDARDS.md +743 -0
- package/plugin/skills/databases/mongodb/SKILL.md +797 -28
- package/plugin/skills/databases/prisma/SKILL.md +776 -30
- package/plugin/skills/databases/redis/SKILL.md +885 -25
- package/plugin/skills/devops/aws/SKILL.md +686 -28
- package/plugin/skills/devops/github-actions/SKILL.md +684 -29
- package/plugin/skills/devops/kubernetes/SKILL.md +621 -24
- package/plugin/skills/frameworks/django/SKILL.md +920 -20
- package/plugin/skills/frameworks/express/SKILL.md +1361 -35
- package/plugin/skills/frameworks/fastapi/SKILL.md +1260 -33
- package/plugin/skills/frameworks/laravel/SKILL.md +1244 -31
- package/plugin/skills/frameworks/nestjs/SKILL.md +1005 -26
- package/plugin/skills/frameworks/rails/SKILL.md +594 -28
- package/plugin/skills/frameworks/spring/SKILL.md +528 -35
- package/plugin/skills/frameworks/vue/SKILL.md +1296 -27
- package/plugin/skills/frontend/accessibility/SKILL.md +1108 -34
- package/plugin/skills/frontend/frontend-design/SKILL.md +1304 -26
- package/plugin/skills/frontend/responsive/SKILL.md +847 -21
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +976 -38
- package/plugin/skills/frontend/tailwindcss/SKILL.md +831 -35
- package/plugin/skills/frontend/threejs/SKILL.md +1298 -29
- package/plugin/skills/languages/javascript/SKILL.md +935 -31
- package/plugin/skills/methodology/brainstorming/SKILL.md +597 -23
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +832 -34
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +665 -31
- package/plugin/skills/methodology/executing-plans/SKILL.md +556 -24
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +595 -25
- package/plugin/skills/methodology/problem-solving/SKILL.md +429 -61
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +536 -24
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +632 -21
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +641 -30
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +262 -3
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +571 -32
- package/plugin/skills/methodology/test-driven-development/SKILL.md +779 -24
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +691 -29
- package/plugin/skills/methodology/token-optimization/SKILL.md +598 -29
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +543 -22
- package/plugin/skills/methodology/writing-plans/SKILL.md +590 -18
- package/plugin/skills/omega/omega-architecture/SKILL.md +838 -39
- package/plugin/skills/omega/omega-coding/SKILL.md +636 -39
- package/plugin/skills/omega/omega-sprint/SKILL.md +855 -48
- package/plugin/skills/omega/omega-testing/SKILL.md +940 -41
- package/plugin/skills/omega/omega-thinking/SKILL.md +703 -50
- package/plugin/skills/security/better-auth/SKILL.md +1065 -28
- package/plugin/skills/security/oauth/SKILL.md +968 -31
- package/plugin/skills/security/owasp/SKILL.md +894 -33
- package/plugin/skills/testing/playwright/SKILL.md +764 -38
- package/plugin/skills/testing/pytest/SKILL.md +873 -36
- package/plugin/skills/testing/vitest/SKILL.md +980 -35
|
@@ -1,67 +1,1046 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: nestjs
|
|
3
|
-
description: NestJS development
|
|
3
|
+
description: Enterprise NestJS development with TypeScript, dependency injection, and microservices patterns
|
|
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
|
|
4
14
|
---
|
|
5
15
|
|
|
6
|
-
# NestJS
|
|
16
|
+
# NestJS
|
|
7
17
|
|
|
8
|
-
|
|
18
|
+
Enterprise-grade **NestJS development** following industry best practices. This skill covers modular architecture, dependency injection, TypeORM integration, authentication with Passport, testing patterns, and microservices configurations used by top engineering teams.
|
|
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
|
|
9
35
|
|
|
10
|
-
### Module
|
|
11
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
|
+
|
|
12
52
|
@Module({
|
|
13
|
-
imports: [
|
|
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])],
|
|
14
106
|
controllers: [UsersController],
|
|
15
|
-
providers: [UsersService],
|
|
107
|
+
providers: [UsersService, UsersRepository],
|
|
16
108
|
exports: [UsersService],
|
|
17
109
|
})
|
|
18
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
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// src/users/entities/user.entity.ts
|
|
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;
|
|
185
|
+
|
|
186
|
+
@CreateDateColumn()
|
|
187
|
+
createdAt: Date;
|
|
188
|
+
|
|
189
|
+
@UpdateDateColumn()
|
|
190
|
+
updatedAt: Date;
|
|
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);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
// src/users/users.repository.ts
|
|
215
|
+
import { Injectable } from '@nestjs/common';
|
|
216
|
+
import { DataSource, Repository } from 'typeorm';
|
|
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
|
+
}
|
|
271
|
+
}
|
|
19
272
|
```
|
|
20
273
|
|
|
21
|
-
###
|
|
274
|
+
### 3. DTOs and Validation
|
|
275
|
+
|
|
22
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;
|
|
409
|
+
|
|
410
|
+
@ApiProperty()
|
|
411
|
+
hasMore: boolean;
|
|
412
|
+
|
|
413
|
+
static create<T>(
|
|
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
|
+
```
|
|
431
|
+
|
|
432
|
+
### 4. Controllers
|
|
433
|
+
|
|
434
|
+
```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')
|
|
23
468
|
@Controller('users')
|
|
469
|
+
@UseGuards(JwtAuthGuard, RolesGuard)
|
|
470
|
+
@ApiBearerAuth()
|
|
24
471
|
export class UsersController {
|
|
25
|
-
constructor(private usersService: UsersService) {}
|
|
472
|
+
constructor(private readonly usersService: UsersService) {}
|
|
26
473
|
|
|
27
474
|
@Get()
|
|
28
|
-
|
|
29
|
-
|
|
475
|
+
@Roles(UserRole.ADMIN)
|
|
476
|
+
@ApiOperation({ summary: 'List all users' })
|
|
477
|
+
@ApiResponse({ status: 200, type: PaginatedResponseDto })
|
|
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);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
@Get(':id')
|
|
504
|
+
@Roles(UserRole.ADMIN)
|
|
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> {
|
|
510
|
+
return this.usersService.findOne(id);
|
|
30
511
|
}
|
|
31
512
|
|
|
32
513
|
@Post()
|
|
33
|
-
|
|
34
|
-
|
|
514
|
+
@Roles(UserRole.ADMIN)
|
|
515
|
+
@ApiOperation({ summary: 'Create new user' })
|
|
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);
|
|
35
539
|
}
|
|
36
540
|
}
|
|
37
541
|
```
|
|
38
542
|
|
|
39
|
-
###
|
|
543
|
+
### 5. Services
|
|
544
|
+
|
|
40
545
|
```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
|
+
|
|
41
566
|
@Injectable()
|
|
42
567
|
export class UsersService {
|
|
43
|
-
constructor(private
|
|
568
|
+
constructor(private readonly usersRepository: UsersRepository) {}
|
|
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);
|
|
44
617
|
|
|
45
|
-
|
|
46
|
-
|
|
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 } });
|
|
625
|
+
|
|
626
|
+
if (!user) {
|
|
627
|
+
throw new NotFoundException(`User with ID ${id} not found`);
|
|
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,
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
async remove(id: string): Promise<void> {
|
|
646
|
+
const user = await this.usersRepository.findOne({ where: { id } });
|
|
647
|
+
|
|
648
|
+
if (!user) {
|
|
649
|
+
throw new NotFoundException(`User with ID ${id} not found`);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
await this.usersRepository.softRemove(user);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
async validateUser(email: string, password: string): Promise<User | null> {
|
|
656
|
+
const user = await this.usersRepository.findByEmail(email);
|
|
657
|
+
|
|
658
|
+
if (!user || !user.isActive) {
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const isValid = await user.validatePassword(password);
|
|
663
|
+
return isValid ? user : null;
|
|
47
664
|
}
|
|
48
665
|
}
|
|
49
666
|
```
|
|
50
667
|
|
|
51
|
-
###
|
|
668
|
+
### 6. Authentication
|
|
669
|
+
|
|
52
670
|
```typescript
|
|
53
|
-
|
|
54
|
-
|
|
671
|
+
// src/auth/auth.module.ts
|
|
672
|
+
import { Module } from '@nestjs/common';
|
|
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;
|
|
55
711
|
email: string;
|
|
712
|
+
role: string;
|
|
713
|
+
}
|
|
56
714
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
+
|
|
740
|
+
// src/auth/guards/roles.guard.ts
|
|
741
|
+
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
|
742
|
+
import { Reflector } from '@nestjs/core';
|
|
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
|
+
);
|
|
755
|
+
|
|
756
|
+
if (!requiredRoles) {
|
|
757
|
+
return true;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const { user } = context.switchToHttp().getRequest();
|
|
761
|
+
return requiredRoles.includes(user.role);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
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
|
+
```
|
|
784
|
+
|
|
785
|
+
### 7. Testing
|
|
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();
|
|
906
|
+
|
|
907
|
+
controller = module.get<UsersController>(UsersController);
|
|
908
|
+
service = module.get(UsersService);
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
it('should be defined', () => {
|
|
912
|
+
expect(controller).toBeDefined();
|
|
913
|
+
});
|
|
914
|
+
});
|
|
915
|
+
|
|
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
|
|
972
|
+
|
|
973
|
+
```typescript
|
|
974
|
+
// src/app.module.ts (Microservice)
|
|
975
|
+
import { Module } from '@nestjs/common';
|
|
976
|
+
import { ClientsModule, Transport } from '@nestjs/microservices';
|
|
977
|
+
|
|
978
|
+
@Module({
|
|
979
|
+
imports: [
|
|
980
|
+
ClientsModule.register([
|
|
981
|
+
{
|
|
982
|
+
name: 'NOTIFICATION_SERVICE',
|
|
983
|
+
transport: Transport.RMQ,
|
|
984
|
+
options: {
|
|
985
|
+
urls: [process.env.RABBITMQ_URL],
|
|
986
|
+
queue: 'notifications_queue',
|
|
987
|
+
queueOptions: { durable: false },
|
|
988
|
+
},
|
|
989
|
+
},
|
|
990
|
+
]),
|
|
991
|
+
],
|
|
992
|
+
})
|
|
993
|
+
export class AppModule {}
|
|
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
|
+
}
|
|
60
1009
|
}
|
|
61
1010
|
```
|
|
62
1011
|
|
|
63
1012
|
## Best Practices
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
- Use
|
|
1013
|
+
|
|
1014
|
+
### Do's
|
|
1015
|
+
|
|
1016
|
+
- Use modules to organize features
|
|
1017
|
+
- Use dependency injection throughout
|
|
1018
|
+
- Use DTOs for validation and transformation
|
|
1019
|
+
- Use interceptors for cross-cutting concerns
|
|
1020
|
+
- Use guards for authentication/authorization
|
|
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
|
|
1026
|
+
|
|
1027
|
+
### Don'ts
|
|
1028
|
+
|
|
1029
|
+
- Don't put business logic in controllers
|
|
1030
|
+
- Don't skip input validation
|
|
1031
|
+
- Don't expose internal errors
|
|
1032
|
+
- Don't use any type unnecessarily
|
|
1033
|
+
- Don't skip authentication guards
|
|
1034
|
+
- Don't ignore database transactions
|
|
1035
|
+
- Don't hardcode configuration
|
|
1036
|
+
- Don't forget error handling
|
|
1037
|
+
- Don't skip testing
|
|
1038
|
+
- Don't use circular dependencies
|
|
1039
|
+
|
|
1040
|
+
## References
|
|
1041
|
+
|
|
1042
|
+
- [NestJS Documentation](https://docs.nestjs.com/)
|
|
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)
|