UNPKG

@nestjsvn/swagger-sse

Version:

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

305 lines (304 loc) 11.5 kB
"use strict"; 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')); }