@nestjsvn/swagger-sse
Version:
OpenAPI documentation and interactive Swagger UI for NestJS Server-Sent Events endpoints
305 lines (304 loc) • 11.5 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.hasSseDecorator = hasSseDecorator;
exports.isDecoratorNamed = isDecoratorNamed;
exports.extractEventTypes = extractEventTypes;
exports.analyzeObservableType = analyzeObservableType;
exports.typeNameToEventName = typeNameToEventName;
exports.addApiSseDecorator = addApiSseDecorator;
exports.hasApiSseDecorator = hasApiSseDecorator;
const ts = __importStar(require("typescript"));
/**
* AST analysis utilities for the SSE transformer
*
* These utilities analyze TypeScript AST nodes and extract SSE-related information
* for automatic documentation generation.
*/
/**
* Check if a method declaration has an @Sse decorator
*
* @param node Method declaration node
* @returns True if the method has @Sse decorator
*/
function hasSseDecorator(node) {
const decorators = ts.getDecorators(node);
if (!decorators) {
return false;
}
return decorators.some((decorator) => isDecoratorNamed(decorator, 'Sse'));
}
/**
* Check if a decorator has a specific name
*
* @param decorator Decorator node
* @param name Expected decorator name
* @returns True if decorator matches the name
*/
function isDecoratorNamed(decorator, name) {
if (!ts.isCallExpression(decorator.expression) && !ts.isIdentifier(decorator.expression)) {
return false;
}
let decoratorName;
if (ts.isCallExpression(decorator.expression)) {
if (ts.isIdentifier(decorator.expression.expression)) {
decoratorName = decorator.expression.expression.text;
}
else {
return false;
}
}
else if (ts.isIdentifier(decorator.expression)) {
decoratorName = decorator.expression.text;
}
else {
return false;
}
return decoratorName === name;
}
/**
* Extract event types from Observable<MessageEvent<T>> return type
*
* @param node Method declaration node
* @returns Array of event type names
*/
function extractEventTypes(node) {
const returnType = node.type;
if (!returnType) {
return [];
}
if (ts.isTypeReferenceNode(returnType)) {
return analyzeObservableType(returnType);
}
return [];
}
/**
* Analyze Observable type to extract MessageEvent type arguments
*
* @param typeNode Type reference node
* @returns Array of event type names
*/
function analyzeObservableType(typeNode) {
const typeName = getTypeReferenceName(typeNode);
if (typeName !== 'Observable') {
return [];
}
if (!typeNode.typeArguments || typeNode.typeArguments.length === 0) {
return [];
}
const messageEventType = typeNode.typeArguments[0];
if (ts.isTypeReferenceNode(messageEventType)) {
const messageEventTypeName = getTypeReferenceName(messageEventType);
if (messageEventTypeName === 'MessageEvent') {
// Extract MessageEvent<T> type argument
if (messageEventType.typeArguments && messageEventType.typeArguments.length > 0) {
return extractTypesFromTypeArgument(messageEventType.typeArguments[0]);
}
}
}
return [];
}
/**
* Extract type names from a type argument (handles union types)
*
* @param typeArg Type argument node
* @returns Array of type names
*/
function extractTypesFromTypeArgument(typeArg) {
if (ts.isUnionTypeNode(typeArg)) {
// Handle union types like MessageEvent<UserDto | MessageDto>
return typeArg.types.flatMap(type => extractTypesFromTypeArgument(type));
}
if (ts.isTypeReferenceNode(typeArg)) {
const typeName = getTypeReferenceName(typeArg);
return typeName ? [typeName] : [];
}
if (ts.isTypeLiteralNode(typeArg)) {
// Handle inline object types - extract from type property if present
const typeProperty = typeArg.members.find(member => ts.isPropertySignature(member) &&
member.name &&
ts.isIdentifier(member.name) &&
member.name.text === 'type');
if (typeProperty?.type && ts.isLiteralTypeNode(typeProperty.type)) {
if (ts.isStringLiteral(typeProperty.type.literal)) {
return [typeProperty.type.literal.text];
}
}
}
return [];
}
/**
* Get the name of a type reference
*
* @param typeRef Type reference node
* @returns Type name or null
*/
function getTypeReferenceName(typeRef) {
if (ts.isIdentifier(typeRef.typeName)) {
return typeRef.typeName.text;
}
if (ts.isQualifiedName(typeRef.typeName)) {
return typeRef.typeName.right.text;
}
return null;
}
/**
* Convert type name to event name using specified convention
*
* @param typeName TypeScript type name
* @param convention Naming convention to use
* @returns Event name string
*/
function typeNameToEventName(typeName, convention = 'kebab-case') {
// Remove common suffixes like 'Dto', 'Event', etc.
const cleanTypeName = typeName.replace(/(Dto|Event|Data|Payload)$/, '');
switch (convention) {
case 'kebab-case':
return cleanTypeName
.replace(/([A-Z])/g, '-$1')
.toLowerCase()
.replace(/^-/, '');
case 'camelCase':
return cleanTypeName.charAt(0).toLowerCase() + cleanTypeName.slice(1);
case 'PascalCase':
return cleanTypeName;
default:
return cleanTypeName;
}
}
/**
* Add @ApiSse decorator to a method declaration
*
* @param node Method declaration node
* @param context Transformation context
* @param eventTypes Array of event type names
* @param options Plugin options
* @returns Modified method declaration
*/
function addApiSseDecorator(node, context, eventTypes = [], options = {}) {
const factory = context.factory;
// Generate events object from extracted types
const eventsObject = factory.createObjectLiteralExpression(eventTypes.map(typeName => {
const eventName = typeNameToEventName(typeName, options.eventNamingConvention);
return factory.createPropertyAssignment(factory.createStringLiteral(eventName), factory.createIdentifier(typeName));
}));
// Create @ApiSse decorator options
const apiSseOptions = [
factory.createPropertyAssignment(factory.createIdentifier('events'), eventsObject),
];
// Add JSDoc-derived metadata if introspection is enabled
if (options.introspectComments) {
const jsDocMetadata = extractJsDocMetadata(node);
if (jsDocMetadata.summary) {
apiSseOptions.push(factory.createPropertyAssignment(factory.createIdentifier('summary'), factory.createStringLiteral(jsDocMetadata.summary)));
}
if (jsDocMetadata.description) {
apiSseOptions.push(factory.createPropertyAssignment(factory.createIdentifier('description'), factory.createStringLiteral(jsDocMetadata.description)));
}
}
// Auto-generate operation ID if enabled
if (options.autoGenerateOperationId && node.name && ts.isIdentifier(node.name)) {
const operationId = `sse${node.name.text.charAt(0).toUpperCase()}${node.name.text.slice(1)}`;
apiSseOptions.push(factory.createPropertyAssignment(factory.createIdentifier('operationId'), factory.createStringLiteral(operationId)));
}
// Create @ApiSse decorator
const apiSseDecorator = factory.createDecorator(factory.createCallExpression(factory.createIdentifier('ApiSse'), undefined, [
factory.createObjectLiteralExpression(apiSseOptions, true),
]));
const existingDecorators = ts.getDecorators(node) ?? [];
const newDecorators = [...existingDecorators, apiSseDecorator];
// Create updated method declaration with TypeScript 5.0+ compatibility
try {
// For TypeScript 5.0+, decorators are included in modifiers
const allModifiers = [...newDecorators, ...(ts.getModifiers(node) ?? [])];
return factory.createMethodDeclaration(allModifiers, // All modifiers including decorators
node.asteriskToken, node.name, node.questionToken, node.typeParameters, node.parameters, node.type, node.body);
}
catch (error) {
console.warn('TypeScript method declaration transformation failed:', error);
// Return original node if transformation fails - this ensures the plugin doesn't break
return node;
}
}
/**
* Extract metadata from JSDoc comments
*
* @param node Method declaration node
* @returns Extracted metadata
*/
function extractJsDocMetadata(node) {
const jsDocTags = ts.getJSDocTags(node);
const metadata = {};
// Look for @summary tag
const summaryTag = jsDocTags.find(tag => tag.tagName.text === 'summary');
if (summaryTag?.comment) {
metadata.summary =
typeof summaryTag.comment === 'string'
? summaryTag.comment
: summaryTag.comment.map(part => part.text).join('');
}
// Look for @description tag or main comment
const descriptionTag = jsDocTags.find(tag => tag.tagName.text === 'description');
if (descriptionTag?.comment) {
metadata.description =
typeof descriptionTag.comment === 'string'
? descriptionTag.comment
: descriptionTag.comment.map(part => part.text).join('');
}
else {
// Use main JSDoc comment as description if no @description tag
const jsDoc = ts.getJSDocCommentsAndTags(node)[0];
if (jsDoc && ts.isJSDoc(jsDoc) && jsDoc.comment) {
metadata.description =
typeof jsDoc.comment === 'string'
? jsDoc.comment
: jsDoc.comment.map(part => part.text).join('');
}
}
return metadata;
}
/**
* Check if a method already has @ApiSse decorator
*
* @param node Method declaration node
* @returns True if method already has @ApiSse decorator
*/
function hasApiSseDecorator(node) {
const decorators = ts.getDecorators(node);
if (!decorators) {
return false;
}
return decorators.some((decorator) => isDecoratorNamed(decorator, 'ApiSse'));
}