UNPKG

@nestjsvn/swagger-sse

Version:

OpenAPI documentation and interactive Swagger UI for NestJS Server-Sent Events endpoints

819 lines (726 loc) 21.4 kB
# Basic Usage Examples This document provides practical examples of using `@nestjsvn/swagger-sse` in real-world scenarios. ## Table of Contents 1. [Simple Event Stream](#simple-event-stream) 2. [Multi-Event Type Stream](#multi-event-type-stream) 3. [Authenticated Streams](#authenticated-streams) 4. [Error Handling](#error-handling) 5. [Query Parameter Filtering](#query-parameter-filtering) 6. [Real-time Chat Application](#real-time-chat-application) 7. [System Monitoring Dashboard](#system-monitoring-dashboard) 8. [E-commerce Order Tracking](#e-commerce-order-tracking) ## Simple Event Stream Basic SSE endpoint that emits periodic events: ```typescript // dto/notification.dto.ts export class NotificationDto { id: string; title: string; message: string; type: 'info' | 'warning' | 'error' | 'success'; timestamp: Date; } // controllers/notifications.controller.ts import { Controller, Sse } from '@nestjs/common'; import { Observable, interval, map } from 'rxjs'; import { ApiSse } from '@nestjsvn/swagger-sse'; import { NotificationDto } from '../dto/notification.dto'; @Controller('notifications') export class NotificationsController { @Sse('stream') @ApiSse({ summary: 'Notification stream', description: 'Real-time notifications for the application', events: { 'notification': NotificationDto, }, }) streamNotifications(): Observable<MessageEvent> { return interval(3000).pipe( map((i) => ({ data: JSON.stringify({ id: `notif-${i}`, title: `Notification ${i}`, message: `This is notification number ${i}`, type: i % 4 === 0 ? 'error' : i % 3 === 0 ? 'warning' : i % 2 === 0 ? 'success' : 'info', timestamp: new Date(), }), type: 'notification', } as MessageEvent)) ); } } ``` ## Multi-Event Type Stream Stream that emits different types of events: ```typescript // dto/user-events.dto.ts export class UserCreatedDto { userId: string; username: string; email: string; createdAt: Date; } export class UserUpdatedDto { userId: string; username: string; email: string; updatedAt: Date; changes: string[]; } export class UserDeletedDto { userId: string; deletedAt: Date; reason?: string; } // controllers/user-events.controller.ts import { Controller, Sse } from '@nestjs/common'; import { Observable, merge, interval, map, switchMap, of } from 'rxjs'; import { ApiSse } from '@nestjsvn/swagger-sse'; @Controller('user-events') export class UserEventsController { @Sse('stream') @ApiSse({ summary: 'User lifecycle events', description: 'Stream of user creation, update, and deletion events', events: { 'user-created': UserCreatedDto, 'user-updated': UserUpdatedDto, 'user-deleted': UserDeletedDto, }, }) streamUserEvents(): Observable<MessageEvent> { // Simulate different event types const createEvents = interval(5000).pipe( map((i) => ({ data: JSON.stringify({ userId: `user-${i}`, username: `user${i}`, email: `user${i}@example.com`, createdAt: new Date(), }), type: 'user-created', } as MessageEvent)) ); const updateEvents = interval(7000).pipe( map((i) => ({ data: JSON.stringify({ userId: `user-${i}`, username: `updated-user${i}`, email: `updated-user${i}@example.com`, updatedAt: new Date(), changes: ['username', 'email'], }), type: 'user-updated', } as MessageEvent)) ); const deleteEvents = interval(15000).pipe( map((i) => ({ data: JSON.stringify({ userId: `user-${i}`, deletedAt: new Date(), reason: 'Account deactivated', }), type: 'user-deleted', } as MessageEvent)) ); return merge(createEvents, updateEvents, deleteEvents); } } ``` ## Authenticated Streams SSE endpoint that requires authentication: ```typescript // guards/jwt-auth.guard.ts import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; @Injectable() export class JwtAuthGuard implements CanActivate { constructor(private jwtService: JwtService) {} canActivate(context: ExecutionContext): boolean { const request = context.switchToHttp().getRequest(); const token = this.extractTokenFromHeader(request); if (!token) { return false; } try { const payload = this.jwtService.verify(token); request.user = payload; return true; } catch { return false; } } private extractTokenFromHeader(request: any): string | undefined { const [type, token] = request.headers.authorization?.split(' ') ?? []; return type === 'Bearer' ? token : undefined; } } // dto/private-event.dto.ts export class PrivateEventDto { eventId: string; userId: string; eventType: string; data: any; timestamp: Date; } // controllers/private-events.controller.ts import { Controller, Sse, UseGuards, Request } from '@nestjs/common'; import { Observable, interval, map, filter } from 'rxjs'; import { ApiSse } from '@nestjsvn/swagger-sse'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { JwtAuthGuard } from '../guards/jwt-auth.guard'; import { PrivateEventDto } from '../dto/private-event.dto'; @Controller('private-events') @ApiTags('Private Events') export class PrivateEventsController { @Sse('user-specific') @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiSse({ summary: 'User-specific event stream', description: 'Private events stream for authenticated users', security: [{ bearer: [] }], events: { 'private-event': PrivateEventDto, }, }) streamPrivateEvents(@Request() req): Observable<MessageEvent> { const userId = req.user.sub; return interval(2000).pipe( map((i) => ({ data: JSON.stringify({ eventId: `event-${userId}-${i}`, userId: userId, eventType: 'user-activity', data: { action: 'page-view', page: `/dashboard/${i}` }, timestamp: new Date(), }), type: 'private-event', } as MessageEvent)) ); } } ``` ## Error Handling Stream with comprehensive error handling: ```typescript // dto/error-event.dto.ts export class ErrorEventDto { errorId: string; message: string; code: number; timestamp: Date; context?: any; } export class DataEventDto { id: string; data: any; timestamp: Date; } // services/data.service.ts import { Injectable } from '@nestjs/common'; import { Observable, throwError, of, interval } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; @Injectable() export class DataService { getData(): Observable<any> { return interval(1000).pipe( switchMap((i) => { // Simulate random errors if (i > 0 && i % 10 === 0) { return throwError(() => new Error(`Simulated error at interval ${i}`)); } return of({ value: i, timestamp: new Date() }); }) ); } } // controllers/data-stream.controller.ts import { Controller, Sse } from '@nestjs/common'; import { Observable, of } from 'rxjs'; import { map, catchError, startWith } from 'rxjs/operators'; import { ApiSse } from '@nestjsvn/swagger-sse'; import { DataService } from '../services/data.service'; @Controller('data-stream') export class DataStreamController { constructor(private dataService: DataService) {} @Sse('with-errors') @ApiSse({ summary: 'Data stream with error handling', description: 'Stream that demonstrates error handling patterns', events: { 'data-event': DataEventDto, 'error-event': ErrorEventDto, }, }) streamWithErrorHandling(): Observable<MessageEvent> { return this.dataService.getData().pipe( map((data) => ({ data: JSON.stringify({ id: `data-${Date.now()}`, data: data, timestamp: new Date(), }), type: 'data-event', } as MessageEvent)), catchError((error) => { console.error('Stream error:', error); return of({ data: JSON.stringify({ errorId: `error-${Date.now()}`, message: error.message, code: 500, timestamp: new Date(), context: { source: 'data-service' }, }), type: 'error-event', } as MessageEvent); }), startWith({ data: JSON.stringify({ id: 'stream-start', data: { message: 'Stream started' }, timestamp: new Date(), }), type: 'data-event', } as MessageEvent) ); } } ``` ## Query Parameter Filtering Stream that supports filtering via query parameters: ```typescript // controllers/filtered-events.controller.ts import { Controller, Sse, Query } from '@nestjs/common'; import { Observable, interval, map, filter } from 'rxjs'; import { ApiSse } from '@nestjsvn/swagger-sse'; import { ApiQuery } from '@nestjs/swagger'; @Controller('filtered-events') export class FilteredEventsController { @Sse('stream') @ApiQuery({ name: 'category', required: false, description: 'Filter events by category' }) @ApiQuery({ name: 'priority', required: false, description: 'Filter events by priority level' }) @ApiSse({ summary: 'Filtered event stream', description: 'Stream events filtered by query parameters', events: { 'filtered-event': { id: 'string', category: 'string', priority: 'number', message: 'string', timestamp: 'Date', }, }, }) streamFilteredEvents( @Query('category') category?: string, @Query('priority') priority?: string, ): Observable<MessageEvent> { const priorityNum = priority ? parseInt(priority, 10) : undefined; return interval(1000).pipe( map((i) => { const eventCategory = ['system', 'user', 'security', 'performance'][i % 4]; const eventPriority = (i % 5) + 1; return { id: `event-${i}`, category: eventCategory, priority: eventPriority, message: `Event ${i} in ${eventCategory} with priority ${eventPriority}`, timestamp: new Date(), }; }), filter((event) => { if (category && event.category !== category) return false; if (priorityNum && event.priority !== priorityNum) return false; return true; }), map((event) => ({ data: JSON.stringify(event), type: 'filtered-event', } as MessageEvent)) ); } } ``` ## Real-time Chat Application Complete chat application with SSE: ```typescript // dto/chat.dto.ts export class ChatMessageDto { messageId: string; roomId: string; userId: string; username: string; message: string; timestamp: Date; } export class UserJoinedDto { userId: string; username: string; roomId: string; timestamp: Date; } export class UserLeftDto { userId: string; username: string; roomId: string; timestamp: Date; } // services/chat.service.ts import { Injectable } from '@nestjs/common'; import { Subject, Observable } from 'rxjs'; import { filter, map } from 'rxjs/operators'; @Injectable() export class ChatService { private eventSubject = new Subject<any>(); sendMessage(roomId: string, userId: string, username: string, message: string) { this.eventSubject.next({ type: 'chat-message', data: { messageId: `msg-${Date.now()}`, roomId, userId, username, message, timestamp: new Date(), }, }); } userJoined(roomId: string, userId: string, username: string) { this.eventSubject.next({ type: 'user-joined', data: { userId, username, roomId, timestamp: new Date(), }, }); } userLeft(roomId: string, userId: string, username: string) { this.eventSubject.next({ type: 'user-left', data: { userId, username, roomId, timestamp: new Date(), }, }); } getEventsForRoom(roomId: string): Observable<MessageEvent> { return this.eventSubject.pipe( filter((event) => event.data.roomId === roomId), map((event) => ({ data: JSON.stringify(event.data), type: event.type, } as MessageEvent)) ); } } // controllers/chat.controller.ts import { Controller, Sse, Query, Post, Body } from '@nestjs/common'; import { Observable } from 'rxjs'; import { ApiSse } from '@nestjsvn/swagger-sse'; import { ApiQuery, ApiBody, ApiTags } from '@nestjs/swagger'; import { ChatService } from '../services/chat.service'; @Controller('chat') @ApiTags('Chat') export class ChatController { constructor(private chatService: ChatService) {} @Sse('room/:roomId') @ApiQuery({ name: 'roomId', description: 'Chat room ID' }) @ApiSse({ summary: 'Chat room event stream', description: 'Real-time chat events for a specific room', events: { 'chat-message': ChatMessageDto, 'user-joined': UserJoinedDto, 'user-left': UserLeftDto, }, }) streamChatEvents(@Query('roomId') roomId: string): Observable<MessageEvent> { return this.chatService.getEventsForRoom(roomId); } @Post('send-message') @ApiBody({ schema: { type: 'object', properties: { roomId: { type: 'string' }, userId: { type: 'string' }, username: { type: 'string' }, message: { type: 'string' }, }, }, }) sendMessage(@Body() body: any) { this.chatService.sendMessage(body.roomId, body.userId, body.username, body.message); return { success: true }; } @Post('join-room') @ApiBody({ schema: { type: 'object', properties: { roomId: { type: 'string' }, userId: { type: 'string' }, username: { type: 'string' }, }, }, }) joinRoom(@Body() body: any) { this.chatService.userJoined(body.roomId, body.userId, body.username); return { success: true }; } } ``` ## System Monitoring Dashboard Real-time system metrics streaming: ```typescript // dto/metrics.dto.ts export class CpuMetricDto { timestamp: Date; usage: number; cores: number; } export class MemoryMetricDto { timestamp: Date; used: number; total: number; percentage: number; } export class NetworkMetricDto { timestamp: Date; bytesIn: number; bytesOut: number; packetsIn: number; packetsOut: number; } // services/metrics.service.ts import { Injectable } from '@nestjs/common'; import { Observable, interval } from 'rxjs'; import { map } from 'rxjs/operators'; import * as os from 'os'; @Injectable() export class MetricsService { getCpuMetrics(): Observable<CpuMetricDto> { return interval(1000).pipe( map(() => ({ timestamp: new Date(), usage: Math.random() * 100, cores: os.cpus().length, })) ); } getMemoryMetrics(): Observable<MemoryMetricDto> { return interval(2000).pipe( map(() => { const total = os.totalmem(); const free = os.freemem(); const used = total - free; return { timestamp: new Date(), used: used, total: total, percentage: (used / total) * 100, }; }) ); } getNetworkMetrics(): Observable<NetworkMetricDto> { return interval(3000).pipe( map(() => ({ timestamp: new Date(), bytesIn: Math.floor(Math.random() * 1000000), bytesOut: Math.floor(Math.random() * 1000000), packetsIn: Math.floor(Math.random() * 10000), packetsOut: Math.floor(Math.random() * 10000), })) ); } } // controllers/metrics.controller.ts import { Controller, Sse } from '@nestjs/common'; import { Observable, merge } from 'rxjs'; import { map } from 'rxjs/operators'; import { ApiSse } from '@nestjsvn/swagger-sse'; import { ApiTags } from '@nestjs/swagger'; import { MetricsService } from '../services/metrics.service'; @Controller('metrics') @ApiTags('System Metrics') export class MetricsController { constructor(private metricsService: MetricsService) {} @Sse('system') @ApiSse({ summary: 'System metrics stream', description: 'Real-time system performance metrics', events: { 'cpu-metric': CpuMetricDto, 'memory-metric': MemoryMetricDto, 'network-metric': NetworkMetricDto, }, }) streamSystemMetrics(): Observable<MessageEvent> { const cpuEvents = this.metricsService.getCpuMetrics().pipe( map((data) => ({ data: JSON.stringify(data), type: 'cpu-metric', } as MessageEvent)) ); const memoryEvents = this.metricsService.getMemoryMetrics().pipe( map((data) => ({ data: JSON.stringify(data), type: 'memory-metric', } as MessageEvent)) ); const networkEvents = this.metricsService.getNetworkMetrics().pipe( map((data) => ({ data: JSON.stringify(data), type: 'network-metric', } as MessageEvent)) ); return merge(cpuEvents, memoryEvents, networkEvents); } } ``` ## E-commerce Order Tracking Order status updates via SSE: ```typescript // dto/order-events.dto.ts export class OrderCreatedDto { orderId: string; customerId: string; items: Array<{ productId: string; quantity: number; price: number }>; total: number; createdAt: Date; } export class OrderStatusUpdatedDto { orderId: string; customerId: string; status: 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled'; updatedAt: Date; estimatedDelivery?: Date; } export class PaymentProcessedDto { orderId: string; customerId: string; amount: number; paymentMethod: string; processedAt: Date; } // services/order.service.ts import { Injectable } from '@nestjs/common'; import { Subject, Observable } from 'rxjs'; import { filter, map } from 'rxjs/operators'; @Injectable() export class OrderService { private orderEvents = new Subject<any>(); createOrder(customerId: string, items: any[], total: number) { const orderId = `order-${Date.now()}`; this.orderEvents.next({ type: 'order-created', data: { orderId, customerId, items, total, createdAt: new Date(), }, }); return orderId; } updateOrderStatus(orderId: string, customerId: string, status: string) { this.orderEvents.next({ type: 'order-status-updated', data: { orderId, customerId, status, updatedAt: new Date(), estimatedDelivery: status === 'shipped' ? new Date(Date.now() + 3 * 24 * 60 * 60 * 1000) : undefined, }, }); } processPayment(orderId: string, customerId: string, amount: number, paymentMethod: string) { this.orderEvents.next({ type: 'payment-processed', data: { orderId, customerId, amount, paymentMethod, processedAt: new Date(), }, }); } getOrderEventsForCustomer(customerId: string): Observable<MessageEvent> { return this.orderEvents.pipe( filter((event) => event.data.customerId === customerId), map((event) => ({ data: JSON.stringify(event.data), type: event.type, } as MessageEvent)) ); } } // controllers/orders.controller.ts import { Controller, Sse, Query, Post, Body } from '@nestjs/common'; import { Observable } from 'rxjs'; import { ApiSse } from '@nestjsvn/swagger-sse'; import { ApiQuery, ApiBody, ApiTags } from '@nestjs/swagger'; import { OrderService } from '../services/order.service'; @Controller('orders') @ApiTags('Orders') export class OrdersController { constructor(private orderService: OrderService) {} @Sse('customer/:customerId') @ApiQuery({ name: 'customerId', description: 'Customer ID to track orders for' }) @ApiSse({ summary: 'Customer order tracking', description: 'Real-time order updates for a specific customer', events: { 'order-created': OrderCreatedDto, 'order-status-updated': OrderStatusUpdatedDto, 'payment-processed': PaymentProcessedDto, }, }) streamOrderEvents(@Query('customerId') customerId: string): Observable<MessageEvent> { return this.orderService.getOrderEventsForCustomer(customerId); } @Post('create') @ApiBody({ schema: { type: 'object', properties: { customerId: { type: 'string' }, items: { type: 'array', items: { type: 'object' } }, total: { type: 'number' }, }, }, }) createOrder(@Body() body: any) { const orderId = this.orderService.createOrder(body.customerId, body.items, body.total); return { orderId }; } @Post('update-status') @ApiBody({ schema: { type: 'object', properties: { orderId: { type: 'string' }, customerId: { type: 'string' }, status: { type: 'string' }, }, }, }) updateOrderStatus(@Body() body: any) { this.orderService.updateOrderStatus(body.orderId, body.customerId, body.status); return { success: true }; } } ``` These examples demonstrate various real-world use cases for `@nestjsvn/swagger-sse`, from simple notifications to complex multi-event systems. Each example includes proper TypeScript typing, error handling, and comprehensive Swagger documentation.