UNPKG

@nestjsvn/swagger-sse

Version:

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

189 lines (188 loc) 6.53 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.generateSseSchema = generateSseSchema; exports.generateDiscriminatorMapping = generateDiscriminatorMapping; exports.getSchemaPath = getSchemaPath; exports.getEventSchemaName = getEventSchemaName; exports.validateEventDtos = validateEventDtos; exports.registerSseSchemas = registerSseSchemas; const swagger_1 = require("@nestjs/swagger"); /** * Generate OpenAPI schema for SSE endpoint * * Creates a schema using oneOf/discriminator pattern to represent * different event types that can be streamed from the endpoint. * * @param options ApiSse decorator options * @returns OpenAPI schema object * * @example * ```typescript * const schema = generateSseSchema({ * events: { * 'user-created': UserCreatedDto, * 'user-updated': UserUpdatedDto * } * }); * // Returns oneOf schema with discriminator * ``` */ function generateSseSchema(options) { if (!options.events || Object.keys(options.events).length === 0) { // Simple string schema for basic SSE without typed events return { type: 'string', description: 'Server-Sent Events stream', example: 'data: Hello World\n\n', }; } const eventSchemas = []; for (const [eventName, eventType] of Object.entries(options.events)) { eventSchemas.push({ type: 'object', properties: { type: { type: 'string', const: eventName, description: `Event type: ${eventName}`, }, data: { $ref: getSchemaPath(eventType), }, id: { type: 'string', description: 'Optional event ID', example: 'event-123', }, retry: { type: 'number', description: 'Optional retry time in milliseconds', example: 3000, }, }, required: ['type', 'data'], additionalProperties: false, }); } return { type: 'object', description: 'Server-Sent Events stream with structured event data', oneOf: eventSchemas, discriminator: { propertyName: 'type', mapping: generateDiscriminatorMapping(options.events), }, example: { type: Object.keys(options.events)[0], data: {}, id: 'event-123', }, }; } /** * Generate discriminator mapping for event types * * @param events Event type mapping * @returns Discriminator mapping object */ function generateDiscriminatorMapping(events) { const mapping = {}; Object.entries(events).forEach(([eventName, dtoClass]) => { // Create a reference to the event schema mapping[eventName] = `#/components/schemas/${getEventSchemaName(eventName, dtoClass)}`; }); return mapping; } /** * Get schema path for a given type * * @param type Type constructor or string * @returns Schema reference path */ function getSchemaPath(type) { if (Array.isArray(type)) { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument return (0, swagger_1.getSchemaPath)(type[0]); } return (0, swagger_1.getSchemaPath)(type); } /** * Generate a unique schema name for an event type * * @param eventName Name of the event * @param dtoClass DTO class constructor * @returns Unique schema name */ function getEventSchemaName(eventName, _dtoClass) { // Convert event name to PascalCase and append Event suffix const pascalEventName = eventName .split('-') .map(part => part.charAt(0).toUpperCase() + part.slice(1)) .join(''); return `${pascalEventName}Event`; } /** * Validate that all event DTOs are properly decorated with @nestjs/swagger decorators * * @param events Event name to DTO class mapping * @throws Error if any DTO is not properly decorated */ function validateEventDtos(events) { Object.entries(events).forEach(([eventName, dtoClass]) => { if (!dtoClass || typeof dtoClass !== 'function') { throw new Error(`Invalid DTO class for event '${eventName}': must be a class constructor`); } // Check if the class has swagger metadata (indicates it's decorated) const hasSwaggerMetadata = Reflect.hasMetadata('swagger/apiModelPropertiesArray', // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unnecessary-type-assertion Object.getPrototypeOf(dtoClass.prototype)); if (!hasSwaggerMetadata) { console.warn(`Warning: DTO class '${dtoClass.name}' for event '${eventName}' may not be properly decorated with @nestjs/swagger decorators. Consider adding @ApiProperty decorators to ensure proper schema generation.`); } }); } /** * Register SSE event schemas in OpenAPI document * * @param document OpenAPI document * @param options ApiSse options */ function registerSseSchemas(document, options) { if (!document.components) { document.components = {}; } if (!document.components.schemas) { document.components.schemas = {}; } // Validate event DTOs validateEventDtos(options.events); // Register individual event schemas Object.entries(options.events).forEach(([eventName, dtoClass]) => { const schemaName = getEventSchemaName(eventName, dtoClass); // The actual DTO schema will be registered by @nestjs/swagger automatically // We just need to ensure our event wrapper schema is available document.components.schemas[schemaName] = { type: 'object', properties: { type: { type: 'string', enum: [eventName], description: `Event type: ${eventName}`, }, data: { $ref: getSchemaPath(dtoClass), }, id: { type: 'string', description: 'Optional event ID', }, retry: { type: 'number', description: 'Optional retry time in milliseconds', }, }, required: ['type', 'data'], additionalProperties: false, }; }); }