UNPKG

@diagramers/api

Version:

Diagramers API - A comprehensive Node.js API template with TypeScript, Firebase Functions, and Socket.io

368 lines (326 loc) 11.1 kB
import * as express from 'express'; import { Application, Request, Response, NextFunction } from 'express'; import * as cors from 'cors'; import { createServer } from 'http'; import { Server as SocketIOServer } from 'socket.io'; import { logger } from '../logging'; import { PluginManager } from '../../plugins/base/manager'; import { Result } from '../../shared/types/result'; import { ResponseCode } from '../../shared/constants/enums'; import * as swaggerUi from 'swagger-ui-express'; // eslint-disable-next-line @typescript-eslint/no-var-requires const swaggerJSDoc = require('swagger-jsdoc'); export interface ServerManagerConfig { port: number; host: string; cors: boolean; compression: boolean; security: boolean; socketio?: { enabled: boolean; cors?: { origin: string | string[]; methods: string[]; }; }; } export interface SocketEvent { event: string; handler: (socket: any, data: any) => void; description?: string; } export class ServerManager { private app: Application; private config: ServerManagerConfig; private pluginManager: PluginManager; private server: any = null; private io: SocketIOServer | null = null; private socketEvents: Map<string, SocketEvent> = new Map(); constructor(config: ServerManagerConfig, pluginManager: PluginManager) { this.config = config; this.pluginManager = pluginManager; this.app = express(); this.setupMiddleware(); this.setupSwagger(); this.setupSocketIO(); } private setupMiddleware(): void { // Security middleware - simplified for now if (this.config.security) { this.app.use((req: Request, res: Response, next: NextFunction) => { res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('X-Frame-Options', 'DENY'); res.setHeader('X-XSS-Protection', '1; mode=block'); next(); }); } // CORS middleware if (this.config.cors) { this.app.use(cors()); } // Compression middleware - simplified for now if (this.config.compression) { this.app.use((req: Request, res: Response, next: NextFunction) => { // Basic compression logic would go here next(); }); } // Body parsing middleware this.app.use(express.json({ limit: '10mb' })); this.app.use(express.urlencoded({ extended: true, limit: '10mb' })); // Request logging this.app.use((req: Request, res: Response, next: NextFunction) => { logger.info(`${req.method} ${req.path}`, { ip: req.ip, userAgent: req.get('User-Agent') }); next(); }); // Health check endpoint this.app.get('/health', (req: Request, res: Response) => { const result = Result.success({ status: 'ok', timestamp: new Date().toISOString(), uptime: process.uptime() }); res.json(result); }); } private setupSwagger(): void { const swaggerOptions = { definition: { openapi: '3.0.0', info: { title: 'Diagramers API', version: '1.0.0', description: 'API documentation for Diagramers', }, }, apis: ['./src/modules/**/*.ts'], // Scan all module files for JSDoc comments }; const swaggerSpec = swaggerJSDoc(swaggerOptions); // Serve Swagger UI static files this.app.use('/api-docs', express.static(require('swagger-ui-dist').absolutePath())); // Serve the Swagger UI HTML this.app.get('/api-docs', (req: Request, res: Response) => { const html = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="description" content="SwaggerUI" /> <title>SwaggerUI</title> <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui.css" /> </head> <body> <div id="swagger-ui"></div> <script src="https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui-bundle.js" crossorigin></script> <script src="https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui-standalone-preset.js" crossorigin></script> <script> window.onload = () => { const ui = SwaggerUIBundle({ url: '/api-docs/swagger.json', dom_id: '#swagger-ui', deepLinking: true, presets: [ SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset ], plugins: [ SwaggerUIBundle.plugins.DownloadUrl ], layout: "StandaloneLayout" }); }; </script> </body> </html> `; res.send(html); }); // Serve the OpenAPI spec as JSON this.app.get('/api-docs/swagger.json', (req: Request, res: Response) => { res.setHeader('Content-Type', 'application/json'); res.send(swaggerSpec); }); } private setupSocketIO(): void { if (this.config.socketio?.enabled) { this.server = createServer(this.app); this.io = new SocketIOServer(this.server, { cors: this.config.socketio.cors || { origin: "*", methods: ["GET", "POST"] } }); // Setup Socket.IO event handlers this.setupSocketEvents(); logger.info('[ServerManager] Socket.IO enabled'); } } private setupSocketEvents(): void { if (!this.io) return; // Connection event this.io.on('connection', (socket) => { logger.info(`[Socket.IO] Client connected: ${socket.id}`); // Register all custom events for (const [eventName, eventHandler] of this.socketEvents) { socket.on(eventName, (data) => { try { eventHandler.handler(socket, data); } catch (error) { logger.error(`[Socket.IO] Error handling event ${eventName}:`, error); socket.emit('error', { message: 'Internal server error' }); } }); } // Disconnect event socket.on('disconnect', () => { logger.info(`[Socket.IO] Client disconnected: ${socket.id}`); }); // Error event socket.on('error', (error) => { logger.error(`[Socket.IO] Socket error for ${socket.id}:`, error); }); }); } /** * Register a Socket.IO event handler */ registerSocketEvent(event: SocketEvent): void { this.socketEvents.set(event.event, event); logger.info(`[Socket.IO] Registered event: ${event.event} - ${event.description || 'No description'}`); } /** * Emit an event to all connected clients */ emitToAll(event: string, data: any): void { if (this.io) { this.io.emit(event, data); logger.debug(`[Socket.IO] Emitted to all: ${event}`); } } /** * Emit an event to a specific room */ emitToRoom(room: string, event: string, data: any): void { if (this.io) { this.io.to(room).emit(event, data); logger.debug(`[Socket.IO] Emitted to room ${room}: ${event}`); } } /** * Get Socket.IO server instance */ getSocketIO(): SocketIOServer | null { return this.io; } async initialize(): Promise<void> { try { logger.info('[ServerManager] Initializing server...'); // Start plugins await this.pluginManager.startPlugins(); // Register plugin routes this.registerPluginRoutes(); // Error handling middleware this.setupErrorHandling(); logger.info('[ServerManager] Server initialized successfully'); } catch (error) { logger.error('[ServerManager] Failed to initialize server:', error); throw error; } } private registerPluginRoutes(): void { // Register user routes // eslint-disable-next-line @typescript-eslint/no-var-requires const userRoutes = require('../../modules/users/routes/user.routes').default; this.app.use('/api', userRoutes); // Plugin routes will be registered through a different mechanism // For now, we'll add a basic API info endpoint this.app.get('/api/plugins', (req: Request, res: Response) => { const plugins = this.pluginManager.getPlugins(); const pluginInfo = plugins.map(plugin => { const metadata = plugin.getMetadata(); return { name: metadata.name, version: metadata.version, description: metadata.description, enabled: plugin.isEnabled() }; }); const result = Result.success({ plugins: pluginInfo }); res.json(result); }); // Socket.IO info endpoint if (this.config.socketio?.enabled) { this.app.get('/api/socket-info', (req: Request, res: Response) => { const events = Array.from(this.socketEvents.values()).map(event => ({ event: event.event, description: event.description })); const result = Result.success({ enabled: true, events: events }); res.json(result); }); } } private setupErrorHandling(): void { // 404 handler this.app.use((req: Request, res: Response) => { const result = Result.notFound(`Route ${req.method} ${req.path} not found`); res.status(404).json(result); }); // Global error handler this.app.use((error: any, req: Request, res: Response, next: NextFunction) => { logger.error('[ServerManager] Unhandled error:', error); const result = Result.error('Internal Server Error'); res.status(500).json(result); }); } async start(): Promise<void> { return new Promise((resolve, reject) => { try { const serverToUse = this.server || this.app; const port = this.config.port; const host = this.config.host; serverToUse.listen(port, host, () => { logger.info(`[ServerManager] Server started on ${host}:${port}`); if (this.config.socketio?.enabled) { logger.info(`[ServerManager] Socket.IO available at ws://${host}:${port}`); } resolve(); }); serverToUse.on('error', (error: any) => { logger.error('[ServerManager] Server error:', error); reject(error); }); } catch (error) { reject(error); } }); } async stop(): Promise<void> { return new Promise((resolve, reject) => { const serverToUse = this.server || this.app; if (serverToUse) { serverToUse.close((error: any) => { if (error) { logger.error('[ServerManager] Error stopping server:', error); reject(error); } else { logger.info('[ServerManager] Server stopped'); resolve(); } }); } else { resolve(); } }); } getApp(): Application { return this.app; } }