Files
labFusion/services/api-docs/server.js
GSRN 8abc2fd55a
Some checks failed
Integration Tests / integration-tests (push) Failing after 19s
Integration Tests / performance-tests (push) Has been skipped
API Docs (Node.js Express) / test (20) (push) Successful in 1m29s
API Docs (Node.js Express) / build (push) Successful in 31s
fix: Clean up whitespace and improve code formatting in API Docs server
### Summary of Changes
- Removed unnecessary whitespace in the `generateUnifiedSpec` function to enhance code readability.
- Standardized formatting in the `operationsSorter` function for consistency.

### Expected Results
- Improved code clarity and maintainability, making it easier for developers to read and understand the API Docs server code.
2025-09-18 12:05:47 +02:00

403 lines
11 KiB
JavaScript

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
// Swagger JSDoc configuration
const swaggerOptions = {
definition: {
openapi: '3.0.0',
info: {
title: 'LabFusion API Docs Service',
version: '1.0.0',
description: 'API documentation aggregation service for LabFusion microservices'
},
servers: [
{
url: `http://localhost:${PORT}`,
description: 'API Docs Service'
}
]
},
apis: ['./server.js'] // Path to the API files
}
const swaggerSpec = swaggerJsdoc(swaggerOptions)
// 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:8001',
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,
rejectUnauthorized: false
})
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:8001',
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)
// Collect original tags before modifying them
const subCategories = new Set()
if (spec.paths) {
for (const [path, methods] of Object.entries(spec.paths)) {
for (const [method, operation] of Object.entries(methods)) {
if (operation.tags) {
operation.tags.forEach(tag => {
subCategories.add(tag)
})
}
}
}
}
// Merge paths with service prefix
if (spec.paths) {
for (const [path, methods] of Object.entries(spec.paths)) {
const prefixedPath = `/${serviceKey}${path}`
const updatedMethods = {}
for (const [method, operation] of Object.entries(methods)) {
// Use only the main service name as the primary tag
// Store original category in metadata for internal organization
const originalTags = operation.tags || ['General']
const category = originalTags[0] || 'General'
updatedMethods[method] = {
...operation,
tags: [service.name], // Only main service tag for top-level grouping
summary: `[${category}] ${operation.summary || `${method.toUpperCase()} ${path}`}`,
'x-service': serviceKey,
'x-service-url': service.url,
'x-original-tags': originalTags,
'x-category': category
}
}
unifiedSpec.paths[prefixedPath] = updatedMethods
}
}
// 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',
'x-service-key': serviceKey,
'x-categories': Array.from(subCategories) // Store available categories for reference
})
}
return unifiedSpec
}
// Routes
/**
* @swagger
* /health:
* get:
* summary: Health check endpoint
* description: Returns the health status of the API Docs service
* tags: [Health]
* responses:
* 200:
* description: Service is healthy
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* example: healthy
* timestamp:
* type: string
* format: date-time
* example: "2024-01-01T00:00:00.000Z"
*/
app.get('/health', (req, res) => {
res.json({ status: 'healthy', timestamp: new Date().toISOString() })
})
/**
* @swagger
* /services:
* get:
* summary: Get service status
* description: Returns the health status of all LabFusion services
* tags: [Services]
* responses:
* 200:
* description: Service status information
* content:
* application/json:
* schema:
* type: object
* additionalProperties:
* type: object
* properties:
* name:
* type: string
* url:
* type: string
* status:
* type: string
* enum: [healthy, unhealthy, planned]
* responseTime:
* type: string
* error:
* type: string
*/
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)
})
/**
* @swagger
* /openapi.json:
* get:
* summary: Get unified OpenAPI specification
* description: Returns the unified OpenAPI specification for all LabFusion services
* tags: [Documentation]
* responses:
* 200:
* description: Unified OpenAPI specification
* content:
* application/json:
* schema:
* type: object
* 500:
* description: Failed to generate OpenAPI spec
* content:
* application/json:
* schema:
* type: object
* properties:
* error:
* type: string
* details:
* type: string
*/
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 })
}
})
// API Docs service documentation endpoint
app.get('/api-docs.json', (req, res) => {
res.json(swaggerSpec)
})
// 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,
operationsSorter: function (a, b) {
// Sort by summary (which includes category tags)
const summaryA = a.get('summary') || ''
const summaryB = b.get('summary') || ''
return summaryA.localeCompare(summaryB)
},
tagsSorter: 'alpha'
},
customCss: `
.swagger-ui .topbar { display: none; }
.swagger-ui .info { margin: 20px 0; }
.swagger-ui .info .title { color: #1890ff; }
/* Style service tags */
.swagger-ui .opblock-tag {
margin: 20px 0 10px 0;
padding: 10px 0;
border-bottom: 2px solid #1890ff;
}
/* Style operation blocks */
.swagger-ui .opblock {
margin: 10px 0;
border-radius: 4px;
}
/* Style operation summaries with category badges */
.swagger-ui .opblock-summary-description {
font-weight: 500;
}
/* Add some spacing between operations */
.swagger-ui .opblock-tag-section .opblock {
margin-bottom: 15px;
}
`,
customSiteTitle: 'LabFusion API Documentation'
}))
// Export app for testing
module.exports = app
// Start server only if not in test environment
if (process.env.NODE_ENV !== 'test') {
app.listen(PORT, () => {
console.log(`LabFusion API Docs server running on port ${PORT}`)
console.log(`Access the documentation at: http://localhost:${PORT}`)
})
}