// Middleware to serve a combined OpenAPI document
import fs from 'fs';
import path from 'path';
import yaml from 'yaml';
import express from 'express';
import redoc from 'redoc-express';
import { json } from 'stream/consumers';
const api = express.Router(['caseSensitive']);
/**
* Combine OpenAPI documents by resolving $ref references
* @param {string} baseDir - Base directory for resolving references
* @returns {object} - Combined OpenAPI document
*/
const app2serverURL=(api,path=null)=>{
const app2url={
db:`https://kpe20.${process.env.API_BASE}/api/kpe20`,
admin: `https://kpe20.${process.env.API_BASE}/api/kpe20`,
events: `https://kpe20.${process.env.API_BASE}/api/event`,
locations: `https://kpe20.${process.env.API_BASE}/api/location`,
trainings: `https://trainings.${process.env.API_BASE}/api/ltrainings`
}
if(path)
return { url: `${app2url[api]}${path}`}
else
return { url: `${app2url[api]}`}
}
const path2filename=(path)=>
{
if(path[0]==='/')
path=path.slice(1)
const elements=path.split('/')
return elements.join('.')+`.paths.yaml`
}
const combineOpenAPIDocuments = (baseDir, doc) => {
const selectRef=(data,refs)=>
{
// recursivly resolve a ref array
const cref =refs.shift()
if(data[cref])
{
if(refs.length>0)
{
return selectRef(data[cref],refs)
}
else
{
return data[cref]
}
}
else return null
}
// document which will be included, if the path definition does not exist
const emptyPath= {
description: 'Not yet documented, file not found'
};
const mainDoc = yaml.parse(fs.readFileSync(`${baseDir}/${doc}`, 'utf8'));
const resolveReferences = (obj, parentKey = '', filename, recursion=0) => {
if (Array.isArray(obj)) {
return obj.map(item => resolveReferences(item, parentKey, filename,recursion));
} else if (typeof obj === 'object' && obj !== null) {
const result = {};
for (const [key,value] of Object.entries(obj)) {
if (key === '$ref') {
const [refFilename,hashRef ]= obj[key].split('#')
if(refFilename!=='')
{
const refPath = path.resolve(baseDir, refFilename);
if (fs.existsSync(refPath)) {
const referencedDoc = yaml.parse(fs.readFileSync(refPath, 'utf8'));
if (parentKey === 'paths' && referencedDoc.paths) {
// Merge paths with prefix from the parent key
for (const [subKey, value] of Object.entries(referencedDoc.paths)) {
const prefixedKey = `${parentKey}${subKey}`;
result[prefixedKey] = resolveReferences(value, 'paths',refFilename,0);
}
} else {
if(refFilename===filename)
++ recursion
if(recursion <3)
{
const resolved= resolveReferences(referencedDoc, key,refFilename, recursion)
const values=Object.values(resolved)
if( !hashRef || hashRef==='')
{
// we are de-referencing the included object, if it only contains one key
if(values.length===1)
Object.assign(result,values[0]);
else
Object.assign(result,resolved)
}
else
{
// we ae de-referencing the supplied key
Object.assign(result,selectRef(resolved, hashRef.split('/')))
}
}
else
Object.assign(result,null)
}
} else {
if (parentKey === 'paths') {
const prefixedKey = `${parentKey}${subKey}`;
result[prefixedKey] = emptyPath
} else {
Object.assign(result,emptyPath) }
}
}
else
{
result[key]=value
}
} else {
result[key] = resolveReferences(obj[key], key, filename, recursion);
}
}
return result;
}
return obj;
};
// Merge all references and return the final document
const combinedDoc = { ...mainDoc };
for (const [key, value] of Object.entries(mainDoc)) {
combinedDoc[key]=resolveReferences(value,key,doc,0)
}
return combinedDoc;
};
/**
* Middleware to serve combined OpenAPI documentation
* @param {string} baseDir - Base directory for resolving references
*/
const openAPIMiddleware = (baseDir,doc='index.yaml',servers=null) => (req, res) => {
try {
const combinedDoc = combineOpenAPIDocuments(baseDir, doc);
// add replace servers
if(servers)
{
combinedDoc.servers=servers
}
// Optional: Save the combined document to a file for debugging or direct access
const outputPath = path.join(baseDir, 'combined_openapi.yaml');
fs.writeFileSync(outputPath, yaml.stringify(combinedDoc), 'utf8');
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify(combinedDoc, null, 2));
} catch (err) {
console.log('Error combining OpenAPI documents:', err);
res.status(500).send({ error: 'Failed to combine OpenAPI documents', details: err.message });
}
};
/**
* @route GET /:app/docs
* @group API Documentation
* @param {string} app.path.required - Application name
* @returns {object} Combined OpenAPI documentation
* @description Serves combined OpenAPI documentation for a specific application
*/
api.get(`/:app(${process.env.apps})/docs`, (req, res) =>
openAPIMiddleware(`${process.cwd()}/src/openapi/${req.params.app}`, [app2serverURL(req.params.app)])(req, res)
);
/**
* @route GET /:app/:path/docs
* @group API Documentation
* @param {string} app.path.required - Application name
* @param {string} path.path.required - API path
* @returns {object} Combined OpenAPI documentation for specific path
* @description Serves combined OpenAPI documentation for a specific application and path
*/
api.get(`/:app(${process.env.apps})/:path/docs`, (req, res) =>
openAPIMiddleware(`${process.cwd()}/src/openapi/${req.params.app}`,path2filename(req.params.path) , [app2serverURL(req.params.app,req.params.path)] )(req, res)
);
/**
* @route GET /:app
* @group API Documentation UI
* @param {string} app.path.required - Application name
* @returns {html} ReDoc HTML interface
* @description Displays interactive API documentation using ReDoc
*/
api.get(`/:app(${process.env.apps})`, (req, res) => {
const specUrl = `/docu/${req.params.app}/docs`;
myRedoc(specUrl)(req, res);
});
/**
* @route GET /:app/index
* @group API Documentation
* @param {string} app.path.required - Application name
* @returns {object} API index information
* @description Returns index information for an application's API
*/
api.get(`/:app(${process.env.apps})/index`, (req,res)=>{
const refPath=`${process.cwd()}/src/openapi/${req.params.app}/index.yaml`
if (fs.existsSync(refPath)) {
const mainDoc = yaml.parse(fs.readFileSync(refPath, 'utf8'));
mainDoc.servers=[app2serverURL[req.params.app]]
res.json({success: true, result: mainDoc})
}
else
{
res.json({success: false, message: 'this endpoint is not yet documented'})
}
});
/**
* @route GET /:app/:api
* @group API Documentation UI
* @param {string} app.path.required - Application name
* @param {string} api.path.required - API name
* @returns {html} ReDoc HTML interface
* @description Displays interactive API documentation for a specific API endpoint
*/
api.get(`/:app(${process.env.apps})/:api`, (req,res)=>{
const specUrl = `/docu/${req.params.app}/${req.params.api}/docs`;
myRedoc(specUrl)(req,res)
});
export default api;