Add API Documentation Service and enhance existing services with OpenAPI support

This commit is contained in:
glenn schrooyen
2025-09-11 22:24:56 +02:00
parent 21e4972ab1
commit 63b4bb487d
14 changed files with 800 additions and 69 deletions

View File

@@ -0,0 +1,18 @@
FROM node:18-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm install --only=production
# Copy source code
COPY . .
# Expose port
EXPOSE 8083
# Start the application
CMD ["npm", "start"]

View File

@@ -0,0 +1,18 @@
FROM node:18-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy source code
COPY . .
# Expose port
EXPOSE 8083
# Start the application in development mode
CMD ["npm", "run", "dev"]

View File

@@ -0,0 +1,30 @@
# API Documentation Service
A unified API documentation service that aggregates OpenAPI specifications from all LabFusion services.
## Purpose
- Provide a single entry point for all API documentation
- Aggregate OpenAPI specs from all active services
- Display unified Swagger UI for the entire LabFusion ecosystem
- Monitor service health and availability
## Technology Stack
- **Language**: Node.js
- **Port**: 8083
- **Dependencies**: Express, Swagger UI, Axios
## Features
- **Unified Documentation**: Single Swagger UI for all services
- **Service Health Monitoring**: Real-time status of all services
- **Dynamic Spec Generation**: Automatically fetches and merges OpenAPI specs
- **Service Prefixing**: Each service's endpoints are prefixed for clarity
- **Fallback Handling**: Graceful handling of unavailable services
## API Endpoints
- `GET /` - Swagger UI interface
- `GET /openapi.json` - Unified OpenAPI specification
- `GET /services` - Service health status
- `GET /health` - Documentation service health
## Development Status
**Complete** - Ready for use

View File

@@ -0,0 +1,30 @@
{
"name": "labfusion-api-docs",
"version": "1.0.0",
"description": "Unified API documentation service for LabFusion",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"express": "^4.18.2",
"swagger-ui-express": "^5.0.0",
"swagger-jsdoc": "^6.2.8",
"axios": "^1.6.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1"
},
"devDependencies": {
"nodemon": "^3.0.2"
},
"keywords": [
"api",
"documentation",
"swagger",
"openapi",
"labfusion"
],
"author": "LabFusion Team",
"license": "MIT"
}

228
services/api-docs/server.js Normal file
View File

@@ -0,0 +1,228 @@
const express = require('express');
const swaggerUi = require('swagger-ui-express');
const swaggerJsdoc = require('swagger-jsdoc');
const axios = require('axios');
const cors = require('cors');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 8083;
// Middleware
app.use(cors());
app.use(express.json());
// Service configurations
const SERVICES = {
'api-gateway': {
name: 'API Gateway',
url: process.env.API_GATEWAY_URL || 'http://localhost:8080',
openapiPath: '/v3/api-docs',
description: 'Core API gateway for authentication, dashboards, and data management'
},
'service-adapters': {
name: 'Service Adapters',
url: process.env.SERVICE_ADAPTERS_URL || 'http://localhost:8000',
openapiPath: '/openapi.json',
description: 'Integration adapters for Home Assistant, Frigate, Immich, and other services'
},
'metrics-collector': {
name: 'Metrics Collector',
url: process.env.METRICS_COLLECTOR_URL || 'http://localhost:8081',
openapiPath: '/openapi.json',
description: 'System metrics collection and monitoring service',
status: 'planned'
},
'notification-service': {
name: 'Notification Service',
url: process.env.NOTIFICATION_SERVICE_URL || 'http://localhost:8082',
openapiPath: '/openapi.json',
description: 'Notification and alert management service',
status: 'planned'
}
};
// Fetch OpenAPI spec from a service
async function fetchServiceSpec(serviceKey, service) {
try {
if (service.status === 'planned') {
return {
openapi: '3.0.0',
info: {
title: service.name,
description: service.description,
version: '1.0.0'
},
paths: {},
components: {},
tags: [{
name: service.name.toLowerCase().replace(/\s+/g, '-'),
description: service.description
}]
};
}
const response = await axios.get(`${service.url}${service.openapiPath}`, {
timeout: 5000
});
return response.data;
} catch (error) {
console.warn(`Failed to fetch spec from ${service.name}:`, error.message);
return {
openapi: '3.0.0',
info: {
title: service.name,
description: service.description,
version: '1.0.0'
},
paths: {},
components: {},
tags: [{
name: service.name.toLowerCase().replace(/\s+/g, '-'),
description: service.description
}],
x-service-status: 'unavailable'
};
}
}
// Generate unified OpenAPI spec
async function generateUnifiedSpec() {
const unifiedSpec = {
openapi: '3.0.0',
info: {
title: 'LabFusion API',
description: 'Unified API documentation for all LabFusion services',
version: '1.0.0',
contact: {
name: 'LabFusion Team',
url: 'https://github.com/labfusion/labfusion'
}
},
servers: [
{
url: 'http://localhost:8080',
description: 'API Gateway (Production)'
},
{
url: 'http://localhost:8000',
description: 'Service Adapters (Production)'
},
{
url: 'http://localhost:8081',
description: 'Metrics Collector (Production)'
},
{
url: 'http://localhost:8082',
description: 'Notification Service (Production)'
}
],
paths: {},
components: {
schemas: {},
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT'
}
}
},
tags: []
};
// Fetch specs from all services
for (const [serviceKey, service] of Object.entries(SERVICES)) {
const spec = await fetchServiceSpec(serviceKey, service);
// Merge paths with service prefix
if (spec.paths) {
for (const [path, methods] of Object.entries(spec.paths)) {
const prefixedPath = `/${serviceKey}${path}`;
unifiedSpec.paths[prefixedPath] = methods;
}
}
// Merge components
if (spec.components) {
if (spec.components.schemas) {
Object.assign(unifiedSpec.components.schemas, spec.components.schemas);
}
}
// Add service tag
unifiedSpec.tags.push({
name: service.name,
description: service.description,
'x-service-url': service.url,
'x-service-status': service.status || 'active'
});
}
return unifiedSpec;
}
// Routes
app.get('/health', (req, res) => {
res.json({ status: 'healthy', timestamp: new Date().toISOString() });
});
app.get('/services', async (req, res) => {
const serviceStatus = {};
for (const [serviceKey, service] of Object.entries(SERVICES)) {
try {
const response = await axios.get(`${service.url}/health`, { timeout: 2000 });
serviceStatus[serviceKey] = {
name: service.name,
url: service.url,
status: 'healthy',
responseTime: response.headers['x-response-time'] || 'unknown'
};
} catch (error) {
serviceStatus[serviceKey] = {
name: service.name,
url: service.url,
status: service.status || 'unhealthy',
error: error.message
};
}
}
res.json(serviceStatus);
});
// Dynamic OpenAPI spec endpoint
app.get('/openapi.json', async (req, res) => {
try {
const spec = await generateUnifiedSpec();
res.json(spec);
} catch (error) {
res.status(500).json({ error: 'Failed to generate OpenAPI spec', details: error.message });
}
});
// Swagger UI
app.use('/', swaggerUi.serve);
app.get('/', swaggerUi.setup(null, {
swaggerOptions: {
url: '/openapi.json',
deepLinking: true,
displayRequestDuration: true,
filter: true,
showExtensions: true,
showCommonExtensions: true
},
customCss: `
.swagger-ui .topbar { display: none; }
.swagger-ui .info { margin: 20px 0; }
.swagger-ui .info .title { color: #1890ff; }
`,
customSiteTitle: 'LabFusion API Documentation'
}));
// Start server
app.listen(PORT, () => {
console.log(`LabFusion API Docs server running on port ${PORT}`);
console.log(`Access the documentation at: http://localhost:${PORT}`);
});