Source: combineAPIDocs.js

// 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;