@malamute/ai-rules 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +270 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
- package/configs/_shared/.claude/rules/conventions/git.md +265 -0
- package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
- package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
- package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/.claude/rules/devops/docker.md +275 -0
- package/configs/_shared/.claude/rules/devops/nx.md +194 -0
- package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
- package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
- package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
- package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
- package/configs/_shared/.claude/rules/quality/logging.md +45 -0
- package/configs/_shared/.claude/rules/quality/observability.md +240 -0
- package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
- package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/.claude/skills/dev/api-endpoint/SKILL.md +126 -0
- package/configs/_shared/.claude/{commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
- package/configs/_shared/.claude/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
- package/configs/angular/.claude/rules/core/resource.md +285 -0
- package/configs/angular/.claude/rules/core/signals.md +323 -0
- package/configs/angular/.claude/rules/http.md +338 -0
- package/configs/angular/.claude/rules/routing.md +291 -0
- package/configs/angular/.claude/rules/ssr.md +312 -0
- package/configs/angular/.claude/rules/state/signal-store.md +408 -0
- package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
- package/configs/angular/.claude/rules/testing.md +7 -7
- package/configs/angular/.claude/rules/ui/aria.md +422 -0
- package/configs/angular/.claude/rules/ui/forms.md +424 -0
- package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/.claude/settings.json +1 -0
- package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
- package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/dotnet/.claude/rules/background-services.md +552 -0
- package/configs/dotnet/.claude/rules/configuration.md +426 -0
- package/configs/dotnet/.claude/rules/ddd.md +447 -0
- package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
- package/configs/dotnet/.claude/rules/mediatr.md +320 -0
- package/configs/dotnet/.claude/rules/middleware.md +489 -0
- package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
- package/configs/dotnet/.claude/rules/validation.md +388 -0
- package/configs/dotnet/.claude/settings.json +21 -3
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
- package/configs/fastapi/.claude/rules/dependencies.md +170 -0
- package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
- package/configs/fastapi/.claude/rules/lifespan.md +274 -0
- package/configs/fastapi/.claude/rules/middleware.md +229 -0
- package/configs/fastapi/.claude/rules/pydantic.md +433 -0
- package/configs/fastapi/.claude/rules/responses.md +251 -0
- package/configs/fastapi/.claude/rules/routers.md +202 -0
- package/configs/fastapi/.claude/rules/security.md +222 -0
- package/configs/fastapi/.claude/rules/testing.md +251 -0
- package/configs/fastapi/.claude/rules/websockets.md +298 -0
- package/configs/fastapi/.claude/settings.json +33 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/flask/.claude/rules/blueprints.md +208 -0
- package/configs/flask/.claude/rules/cli.md +285 -0
- package/configs/flask/.claude/rules/configuration.md +281 -0
- package/configs/flask/.claude/rules/context.md +238 -0
- package/configs/flask/.claude/rules/error-handlers.md +278 -0
- package/configs/flask/.claude/rules/extensions.md +278 -0
- package/configs/flask/.claude/rules/flask.md +171 -0
- package/configs/flask/.claude/rules/marshmallow.md +206 -0
- package/configs/flask/.claude/rules/security.md +267 -0
- package/configs/flask/.claude/rules/testing.md +284 -0
- package/configs/flask/.claude/settings.json +33 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
- package/configs/nestjs/.claude/rules/filters.md +376 -0
- package/configs/nestjs/.claude/rules/interceptors.md +317 -0
- package/configs/nestjs/.claude/rules/middleware.md +321 -0
- package/configs/nestjs/.claude/rules/modules.md +26 -0
- package/configs/nestjs/.claude/rules/pipes.md +351 -0
- package/configs/nestjs/.claude/rules/websockets.md +451 -0
- package/configs/nestjs/.claude/settings.json +16 -2
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nextjs/.claude/rules/api-routes.md +358 -0
- package/configs/nextjs/.claude/rules/authentication.md +355 -0
- package/configs/nextjs/.claude/rules/components.md +52 -0
- package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
- package/configs/nextjs/.claude/rules/database.md +400 -0
- package/configs/nextjs/.claude/rules/middleware.md +303 -0
- package/configs/nextjs/.claude/rules/routing.md +324 -0
- package/configs/nextjs/.claude/rules/seo.md +350 -0
- package/configs/nextjs/.claude/rules/server-actions.md +353 -0
- package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
- package/configs/nextjs/.claude/settings.json +5 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/package.json +23 -9
- package/src/cli.js +220 -0
- package/src/config.js +29 -0
- package/src/index.js +13 -0
- package/src/installer.js +361 -0
- package/src/merge.js +116 -0
- package/src/tech-config.json +29 -0
- package/src/utils.js +96 -0
- package/configs/python/.claude/rules/flask.md +0 -332
- package/configs/python/.claude/settings.json +0 -18
- package/configs/python/CLAUDE.md +0 -273
- package/src/install.js +0 -315
- /package/configs/_shared/.claude/rules/{accessibility.md → domain/frontend/accessibility.md} +0 -0
- /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
- /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{review → git/review}/SKILL.md +0 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.gateway.ts"
|
|
4
|
+
- "**/gateways/**/*.ts"
|
|
5
|
+
- "**/websocket/**/*.ts"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# NestJS WebSockets
|
|
9
|
+
|
|
10
|
+
## Gateway Setup
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
// gateways/events.gateway.ts
|
|
14
|
+
import {
|
|
15
|
+
WebSocketGateway,
|
|
16
|
+
WebSocketServer,
|
|
17
|
+
SubscribeMessage,
|
|
18
|
+
MessageBody,
|
|
19
|
+
ConnectedSocket,
|
|
20
|
+
OnGatewayConnection,
|
|
21
|
+
OnGatewayDisconnect,
|
|
22
|
+
OnGatewayInit,
|
|
23
|
+
} from '@nestjs/websockets';
|
|
24
|
+
import { Server, Socket } from 'socket.io';
|
|
25
|
+
import { Logger } from '@nestjs/common';
|
|
26
|
+
|
|
27
|
+
@WebSocketGateway({
|
|
28
|
+
cors: {
|
|
29
|
+
origin: process.env.ALLOWED_ORIGINS?.split(',') || '*',
|
|
30
|
+
credentials: true,
|
|
31
|
+
},
|
|
32
|
+
namespace: '/events',
|
|
33
|
+
})
|
|
34
|
+
export class EventsGateway
|
|
35
|
+
implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
|
|
36
|
+
{
|
|
37
|
+
private readonly logger = new Logger(EventsGateway.name);
|
|
38
|
+
|
|
39
|
+
@WebSocketServer()
|
|
40
|
+
server: Server;
|
|
41
|
+
|
|
42
|
+
afterInit(server: Server) {
|
|
43
|
+
this.logger.log('WebSocket Gateway initialized');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
handleConnection(client: Socket) {
|
|
47
|
+
this.logger.log(`Client connected: ${client.id}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
handleDisconnect(client: Socket) {
|
|
51
|
+
this.logger.log(`Client disconnected: ${client.id}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@SubscribeMessage('message')
|
|
55
|
+
handleMessage(
|
|
56
|
+
@MessageBody() data: { text: string },
|
|
57
|
+
@ConnectedSocket() client: Socket,
|
|
58
|
+
) {
|
|
59
|
+
this.logger.log(`Message from ${client.id}: ${data.text}`);
|
|
60
|
+
return { event: 'message', data: { received: true } };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Authentication
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// gateways/auth.gateway.ts
|
|
69
|
+
import { WebSocketGateway, OnGatewayConnection } from '@nestjs/websockets';
|
|
70
|
+
import { Socket } from 'socket.io';
|
|
71
|
+
import { JwtService } from '@nestjs/jwt';
|
|
72
|
+
import { UnauthorizedException } from '@nestjs/common';
|
|
73
|
+
|
|
74
|
+
@WebSocketGateway()
|
|
75
|
+
export class AuthenticatedGateway implements OnGatewayConnection {
|
|
76
|
+
constructor(private readonly jwtService: JwtService) {}
|
|
77
|
+
|
|
78
|
+
async handleConnection(client: Socket) {
|
|
79
|
+
try {
|
|
80
|
+
const token = this.extractToken(client);
|
|
81
|
+
|
|
82
|
+
if (!token) {
|
|
83
|
+
throw new UnauthorizedException('No token provided');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const payload = await this.jwtService.verifyAsync(token);
|
|
87
|
+
client.data.user = payload;
|
|
88
|
+
|
|
89
|
+
// Join user-specific room
|
|
90
|
+
client.join(`user:${payload.sub}`);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
client.emit('error', { message: 'Authentication failed' });
|
|
93
|
+
client.disconnect();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private extractToken(client: Socket): string | null {
|
|
98
|
+
// From auth header
|
|
99
|
+
const authHeader = client.handshake.headers.authorization;
|
|
100
|
+
if (authHeader?.startsWith('Bearer ')) {
|
|
101
|
+
return authHeader.split(' ')[1];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// From query param
|
|
105
|
+
return client.handshake.query.token as string | null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Rooms and Broadcasting
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// gateways/chat.gateway.ts
|
|
114
|
+
import {
|
|
115
|
+
WebSocketGateway,
|
|
116
|
+
WebSocketServer,
|
|
117
|
+
SubscribeMessage,
|
|
118
|
+
MessageBody,
|
|
119
|
+
ConnectedSocket,
|
|
120
|
+
} from '@nestjs/websockets';
|
|
121
|
+
import { Server, Socket } from 'socket.io';
|
|
122
|
+
|
|
123
|
+
@WebSocketGateway({ namespace: '/chat' })
|
|
124
|
+
export class ChatGateway {
|
|
125
|
+
@WebSocketServer()
|
|
126
|
+
server: Server;
|
|
127
|
+
|
|
128
|
+
@SubscribeMessage('joinRoom')
|
|
129
|
+
handleJoinRoom(
|
|
130
|
+
@MessageBody() roomId: string,
|
|
131
|
+
@ConnectedSocket() client: Socket,
|
|
132
|
+
) {
|
|
133
|
+
client.join(`room:${roomId}`);
|
|
134
|
+
|
|
135
|
+
// Notify room
|
|
136
|
+
this.server.to(`room:${roomId}`).emit('userJoined', {
|
|
137
|
+
userId: client.data.user.sub,
|
|
138
|
+
roomId,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return { joined: roomId };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@SubscribeMessage('leaveRoom')
|
|
145
|
+
handleLeaveRoom(
|
|
146
|
+
@MessageBody() roomId: string,
|
|
147
|
+
@ConnectedSocket() client: Socket,
|
|
148
|
+
) {
|
|
149
|
+
client.leave(`room:${roomId}`);
|
|
150
|
+
|
|
151
|
+
this.server.to(`room:${roomId}`).emit('userLeft', {
|
|
152
|
+
userId: client.data.user.sub,
|
|
153
|
+
roomId,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
return { left: roomId };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@SubscribeMessage('sendMessage')
|
|
160
|
+
handleSendMessage(
|
|
161
|
+
@MessageBody() data: { roomId: string; message: string },
|
|
162
|
+
@ConnectedSocket() client: Socket,
|
|
163
|
+
) {
|
|
164
|
+
const payload = {
|
|
165
|
+
userId: client.data.user.sub,
|
|
166
|
+
message: data.message,
|
|
167
|
+
timestamp: new Date().toISOString(),
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// Broadcast to room except sender
|
|
171
|
+
client.to(`room:${data.roomId}`).emit('newMessage', payload);
|
|
172
|
+
|
|
173
|
+
return { sent: true };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Broadcast from service
|
|
177
|
+
broadcastToRoom(roomId: string, event: string, data: unknown) {
|
|
178
|
+
this.server.to(`room:${roomId}`).emit(event, data);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Send to specific user
|
|
182
|
+
sendToUser(userId: string, event: string, data: unknown) {
|
|
183
|
+
this.server.to(`user:${userId}`).emit(event, data);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Validation with DTOs
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
// dto/message.dto.ts
|
|
192
|
+
import { IsString, IsNotEmpty, MaxLength, IsUUID } from 'class-validator';
|
|
193
|
+
|
|
194
|
+
export class SendMessageDto {
|
|
195
|
+
@IsUUID()
|
|
196
|
+
roomId: string;
|
|
197
|
+
|
|
198
|
+
@IsString()
|
|
199
|
+
@IsNotEmpty()
|
|
200
|
+
@MaxLength(1000)
|
|
201
|
+
message: string;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// gateways/validated.gateway.ts
|
|
205
|
+
import { UsePipes, ValidationPipe } from '@nestjs/common';
|
|
206
|
+
|
|
207
|
+
@WebSocketGateway()
|
|
208
|
+
@UsePipes(new ValidationPipe({ transform: true }))
|
|
209
|
+
export class ValidatedGateway {
|
|
210
|
+
@SubscribeMessage('sendMessage')
|
|
211
|
+
handleMessage(@MessageBody() data: SendMessageDto) {
|
|
212
|
+
// data is validated
|
|
213
|
+
return { success: true };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Guards for WebSockets
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
// guards/ws-auth.guard.ts
|
|
222
|
+
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
|
223
|
+
import { WsException } from '@nestjs/websockets';
|
|
224
|
+
import { Socket } from 'socket.io';
|
|
225
|
+
|
|
226
|
+
@Injectable()
|
|
227
|
+
export class WsAuthGuard implements CanActivate {
|
|
228
|
+
canActivate(context: ExecutionContext): boolean {
|
|
229
|
+
const client = context.switchToWs().getClient<Socket>();
|
|
230
|
+
|
|
231
|
+
if (!client.data.user) {
|
|
232
|
+
throw new WsException('Unauthorized');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// guards/ws-role.guard.ts
|
|
240
|
+
@Injectable()
|
|
241
|
+
export class WsRoleGuard implements CanActivate {
|
|
242
|
+
constructor(private readonly requiredRole: string) {}
|
|
243
|
+
|
|
244
|
+
canActivate(context: ExecutionContext): boolean {
|
|
245
|
+
const client = context.switchToWs().getClient<Socket>();
|
|
246
|
+
const user = client.data.user;
|
|
247
|
+
|
|
248
|
+
if (user?.role !== this.requiredRole) {
|
|
249
|
+
throw new WsException('Forbidden');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Usage
|
|
257
|
+
@WebSocketGateway()
|
|
258
|
+
@UseGuards(WsAuthGuard)
|
|
259
|
+
export class ProtectedGateway {
|
|
260
|
+
@SubscribeMessage('adminAction')
|
|
261
|
+
@UseGuards(new WsRoleGuard('admin'))
|
|
262
|
+
handleAdminAction() {
|
|
263
|
+
return { success: true };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Exception Handling
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
// filters/ws-exception.filter.ts
|
|
272
|
+
import { Catch, ArgumentsHost } from '@nestjs/common';
|
|
273
|
+
import { BaseWsExceptionFilter, WsException } from '@nestjs/websockets';
|
|
274
|
+
import { Socket } from 'socket.io';
|
|
275
|
+
|
|
276
|
+
@Catch()
|
|
277
|
+
export class AllWsExceptionsFilter extends BaseWsExceptionFilter {
|
|
278
|
+
catch(exception: unknown, host: ArgumentsHost) {
|
|
279
|
+
const client = host.switchToWs().getClient<Socket>();
|
|
280
|
+
|
|
281
|
+
const error =
|
|
282
|
+
exception instanceof WsException
|
|
283
|
+
? exception.getError()
|
|
284
|
+
: { message: 'Internal error', code: 'INTERNAL_ERROR' };
|
|
285
|
+
|
|
286
|
+
client.emit('exception', {
|
|
287
|
+
status: 'error',
|
|
288
|
+
...error,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Apply globally in gateway
|
|
294
|
+
@WebSocketGateway()
|
|
295
|
+
@UseFilters(AllWsExceptionsFilter)
|
|
296
|
+
export class EventsGateway {}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Interceptors
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
// interceptors/ws-logging.interceptor.ts
|
|
303
|
+
import {
|
|
304
|
+
Injectable,
|
|
305
|
+
NestInterceptor,
|
|
306
|
+
ExecutionContext,
|
|
307
|
+
CallHandler,
|
|
308
|
+
Logger,
|
|
309
|
+
} from '@nestjs/common';
|
|
310
|
+
import { Observable } from 'rxjs';
|
|
311
|
+
import { tap } from 'rxjs/operators';
|
|
312
|
+
|
|
313
|
+
@Injectable()
|
|
314
|
+
export class WsLoggingInterceptor implements NestInterceptor {
|
|
315
|
+
private readonly logger = new Logger(WsLoggingInterceptor.name);
|
|
316
|
+
|
|
317
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
|
|
318
|
+
const client = context.switchToWs().getClient();
|
|
319
|
+
const data = context.switchToWs().getData();
|
|
320
|
+
const pattern = context.switchToWs().getPattern();
|
|
321
|
+
|
|
322
|
+
const start = Date.now();
|
|
323
|
+
|
|
324
|
+
return next.handle().pipe(
|
|
325
|
+
tap(() => {
|
|
326
|
+
const duration = Date.now() - start;
|
|
327
|
+
this.logger.log(
|
|
328
|
+
`WS ${pattern} - Client: ${client.id} - ${duration}ms`,
|
|
329
|
+
);
|
|
330
|
+
}),
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## Redis Adapter (Scaling)
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
// main.ts
|
|
340
|
+
import { IoAdapter } from '@nestjs/platform-socket.io';
|
|
341
|
+
import { createAdapter } from '@socket.io/redis-adapter';
|
|
342
|
+
import { createClient } from 'redis';
|
|
343
|
+
|
|
344
|
+
async function bootstrap() {
|
|
345
|
+
const app = await NestFactory.create(AppModule);
|
|
346
|
+
|
|
347
|
+
const pubClient = createClient({ url: process.env.REDIS_URL });
|
|
348
|
+
const subClient = pubClient.duplicate();
|
|
349
|
+
|
|
350
|
+
await Promise.all([pubClient.connect(), subClient.connect()]);
|
|
351
|
+
|
|
352
|
+
const redisAdapter = createAdapter(pubClient, subClient);
|
|
353
|
+
|
|
354
|
+
app.useWebSocketAdapter(new IoAdapter(app).createIOServer(3001, {
|
|
355
|
+
adapter: redisAdapter,
|
|
356
|
+
}));
|
|
357
|
+
|
|
358
|
+
await app.listen(3000);
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## Testing WebSockets
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
// gateways/events.gateway.spec.ts
|
|
366
|
+
import { Test } from '@nestjs/testing';
|
|
367
|
+
import { INestApplication } from '@nestjs/common';
|
|
368
|
+
import { io, Socket } from 'socket.io-client';
|
|
369
|
+
import { EventsGateway } from './events.gateway';
|
|
370
|
+
|
|
371
|
+
describe('EventsGateway', () => {
|
|
372
|
+
let app: INestApplication;
|
|
373
|
+
let client: Socket;
|
|
374
|
+
|
|
375
|
+
beforeAll(async () => {
|
|
376
|
+
const moduleRef = await Test.createTestingModule({
|
|
377
|
+
providers: [EventsGateway],
|
|
378
|
+
}).compile();
|
|
379
|
+
|
|
380
|
+
app = moduleRef.createNestApplication();
|
|
381
|
+
await app.listen(3001);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
beforeEach((done) => {
|
|
385
|
+
client = io('http://localhost:3001/events');
|
|
386
|
+
client.on('connect', done);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
afterEach(() => {
|
|
390
|
+
client.disconnect();
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
afterAll(async () => {
|
|
394
|
+
await app.close();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('should receive message response', (done) => {
|
|
398
|
+
client.emit('message', { text: 'hello' }, (response: unknown) => {
|
|
399
|
+
expect(response).toEqual({ event: 'message', data: { received: true } });
|
|
400
|
+
done();
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
## Anti-patterns
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
// BAD: Not handling disconnections
|
|
410
|
+
@WebSocketGateway()
|
|
411
|
+
export class LeakyGateway {
|
|
412
|
+
private connections = new Map<string, Socket>();
|
|
413
|
+
|
|
414
|
+
handleConnection(client: Socket) {
|
|
415
|
+
this.connections.set(client.id, client);
|
|
416
|
+
// Never cleaned up!
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// GOOD: Clean up on disconnect
|
|
421
|
+
handleDisconnect(client: Socket) {
|
|
422
|
+
this.connections.delete(client.id);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// BAD: Blocking event handlers
|
|
426
|
+
@SubscribeMessage('data')
|
|
427
|
+
async handleData(@MessageBody() data: unknown) {
|
|
428
|
+
await this.heavyOperation(data); // Blocks event loop
|
|
429
|
+
return { done: true };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// GOOD: Offload to queue
|
|
433
|
+
@SubscribeMessage('data')
|
|
434
|
+
handleData(@MessageBody() data: unknown) {
|
|
435
|
+
this.queue.add('process', data);
|
|
436
|
+
return { queued: true };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// BAD: Not validating input
|
|
440
|
+
@SubscribeMessage('action')
|
|
441
|
+
handleAction(@MessageBody() data: any) {
|
|
442
|
+
return this.service.doSomething(data.id); // Unsafe!
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// GOOD: Use DTOs with validation
|
|
446
|
+
@UsePipes(ValidationPipe)
|
|
447
|
+
@SubscribeMessage('action')
|
|
448
|
+
handleAction(@MessageBody() data: ActionDto) {
|
|
449
|
+
return this.service.doSomething(data.id);
|
|
450
|
+
}
|
|
451
|
+
```
|
|
@@ -2,14 +2,28 @@
|
|
|
2
2
|
"permissions": {
|
|
3
3
|
"allow": [
|
|
4
4
|
"Bash(npm run start:dev)",
|
|
5
|
+
"Bash(npm run start:debug)",
|
|
5
6
|
"Bash(npm run build)",
|
|
6
7
|
"Bash(npm run test*)",
|
|
7
8
|
"Bash(npm run lint*)",
|
|
8
9
|
"Bash(npm run format*)",
|
|
10
|
+
"Bash(npm install *)",
|
|
11
|
+
"Bash(npm ci)",
|
|
9
12
|
"Bash(npx prisma *)",
|
|
10
13
|
"Bash(npx typeorm *)",
|
|
11
|
-
"Bash(
|
|
14
|
+
"Bash(npx nest *)",
|
|
15
|
+
"Read",
|
|
16
|
+
"Edit",
|
|
17
|
+
"Write"
|
|
12
18
|
],
|
|
13
|
-
"deny": [
|
|
19
|
+
"deny": [
|
|
20
|
+
"Bash(rm -rf *)",
|
|
21
|
+
"Read(.env)",
|
|
22
|
+
"Read(.env.*)",
|
|
23
|
+
"Read(**/secrets/**)"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"env": {
|
|
27
|
+
"NODE_ENV": "development"
|
|
14
28
|
}
|
|
15
29
|
}
|