Some checks failed
### 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.
403 lines
11 KiB
JavaScript
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}`)
|
|
})
|
|
}
|