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 });
    }
};



api.get(`/:app(${process.env.apps})/docs`, (req, res) =>
    openAPIMiddleware(`${process.cwd()}/src/openapi/${req.params.app}`, [app2serverURL(req.params.app)])(req, res)
);

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


const myRedoc= (specUrl)=>(req,res)=>
{
    redoc({
        title: `API Docs ${specUrl}`,
        specUrl,
        nonce: '',
        redocOptions: {
            theme: {
                colors: {
                    primary: {
                        main: '#6EC5AB'
                    }
                },
                typography: {
                    fontFamily: '"museo-sans", "Helvetica Neue", Helvetica, Arial, sans-serif',
                    fontSize: '15px',
                    lineHeight: '1.5',
                    code: {
                        color: '#87E8C7',
                        backgroundColor: '#4D4D4E'
                    }
                },
                menu: {
                    backgroundColor: '#ffffff'
                }
            }
        }
    })(req,res)
}

api.get(`/:app(${process.env.apps})`, (req, res) => {
   const specUrl = `/docu/${req.params.app}/docs`;
   myRedoc(specUrl)(req, res);
});



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'})
    }
   
});

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;