@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
text/typescript
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;
}
}