@nestjsvn/swagger-sse
Version:
OpenAPI documentation and interactive Swagger UI for NestJS Server-Sent Events endpoints
189 lines (188 loc) • 6.53 kB
JavaScript
;
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,
};
});
}