UNPKG

@cap-js/openapi

Version:

CAP tool for OpenAPI

242 lines (209 loc) 7.61 kB
const csdl2openapi = require('./csdl2openapi') const cds = require('@sap/cds'); const fs = require('fs'); const DEBUG = cds.debug('openapi'); const supportedProtocols = ["rest", "odata", "odata-v4"]; const events = /** @type {const} */({ /** * Called before OpenAPI conversion is started. * Can be used to modify the CSN or options before conversion. * Callback should not be async to avoid race conditions! * @event * @property {{ csn: object, options: object }} parameter */ before: 'compile.to.openapi', /** * Called after OpenAPI conversion is done. * Can be used to modify the resulting OpenAPI document before * it is written to file. * Callback should not be async to avoid race conditions! * @event * @property {{ csn: object, options: object, result: object }} parameter */ after: 'after:compile.to.openapi', }); function compileToOpenAPI(csn, options = {}) { cds.emit(events.before, { csn, options }); const result = processor(csn, options); cds.emit(events.after, { csn, options, result }); return result; } function processor(csn, options = {}) { const edmOptions = { odataOpenapiHints: true, // hint to cds-compiler edm4OpenAPI: true, // downgrades certain OData errors to warnings in cds-compiler to: 'openapi' // hint to cds.compile.to.edm (usually set by CLI, but also do this in programmatic usages) , ...options } // must not be part of function* otherwise thrown errors are swallowed const csdl = cds.compile.to.edm(csn, edmOptions); let openApiDocs = {}; if (csdl[Symbol.iterator]) { // generator function means multiple services openApiDocs = _getOpenApiForMultipleServices(csdl, csn, options); return _iterate(openApiDocs); } const openApiOptions = toOpenApiOptions(csdl, csn, options); const serviceName = csdl.$EntityContainer.replace(/\.[^.]+$/, ""); openApiDocs = _getOpenApi(csdl, openApiOptions,serviceName); return Object.keys(openApiDocs).length === 1 ? openApiDocs[serviceName] : _iterate(openApiDocs); } function _getOpenApiForMultipleServices(csdl, csn, options) { let openApiDocs = {}; for (let [content, metadata] of csdl) { if (typeof content === "string") { content = JSON.parse(content); } const openApiOptions = toOpenApiOptions(content, csn, options); const openApiDocsForService = _getOpenApi(content, openApiOptions, metadata.file); openApiDocs = { ...openApiDocs, ...openApiDocsForService }; } return openApiDocs; } function* _iterate(openApiDocs) { for (const key in openApiDocs) { if (key != "") { yield [openApiDocs[key], { file: key }]; } else { yield [openApiDocs[key]]; } } } function _getOpenApi(csdl, options, serviceName = "") { const openApiDocs = {}; let filename; const protocols = Object.keys(options.url); protocols.forEach((protocol) => { const sOptions = { ...options}; const url = options.url[protocol]; if (protocol == "rest" && !options.odataVersion) { options.odataVersion = "4.01"; } sOptions.url = url; const openapi = csdl2openapi.csdl2openapi(csdl, sOptions); if (protocols.length > 1) { filename = `${serviceName}.${protocol}`; } else { filename = serviceName; } openApiDocs[filename] = openapi; }); return openApiDocs; } function toOpenApiOptions(csdl, csn, options = {}) { const callerOptions = {}; for (const key in options) { if (/^openapi:(.*)/.test(key) && RegExp.$1) { callerOptions[RegExp.$1] = options[key]; } else if (key === "odata-version") { callerOptions.odataVersion = options[key]; } } const envOptions = cds.env.openapi instanceof Object && !Array.isArray(cds.env.openapi) ? cds.env.openapi : {}; const fileOptions = _readConfigFile(callerOptions["config-file"]); const result = { ...envOptions, ...fileOptions, ...callerOptions }; delete result["config-file"]; const protocols = _getProtocols(csdl, csn, result.odataVersion); if (result.url) { const servicePaths = _servicePath(csdl, csn, protocols); const keys = Object.keys(servicePaths); const urls = {}; keys.forEach((protocol) => { urls[protocol] = result.url.replace( /\/*\$\{service-path\}/g, servicePaths[protocol] ); }); result.url = urls; } else { // no 'url' option set: infer URL from service path result.url = _servicePath(csdl, csn, protocols); // /catalog } return result; } function _getProtocols(csdl, csn, odataVersion) { if (csdl.$EntityContainer) { const serviceName = csdl.$EntityContainer.replace(/\.[^.]+$/, ""); const service = csn.definitions[serviceName]; const protocols = []; if(odataVersion === "4.01"){ protocols.push("rest"); } else if(odataVersion === "4.0"){ protocols.push("odata"); } else if (!service["@protocol"]) { protocols.push("rest"); //taking rest as default in case no relevant protocol is there } else if (service["@protocol"] === "none") { // if @protocol is 'none' then throw an error throw new Error( `Service "${serviceName}" is annotated with @protocol:'none' which is not supported in openAPI generation.` ); } else if (supportedProtocols.includes(service["@protocol"])) { protocols.push(service["@protocol"]); } else if (Array.isArray(service["@protocol"])) { service["@protocol"].forEach((protocol) => { if(typeof protocol === "string"){ if (supportedProtocols.includes(protocol)) { protocols.push(protocol); } else { DEBUG?.(`"${protocol}" protocol is not supported`); } } else if (typeof protocol === "object" && !Array.isArray(protocol) && protocol !== null) { if(supportedProtocols.includes(protocol.kind)) { protocols.push(protocol.kind); } else { DEBUG?.(`"${protocol.kind}" protocol is not supported`); } } else { DEBUG?.(`incorrect ${protocol} type`) } }); } return protocols; } } function _servicePath(csdl, csn, protocols) { if (csdl.$EntityContainer) { const serviceName = csdl.$EntityContainer.replace(/\.[^.]+$/, ""); const service = csn.definitions[serviceName]; const paths = {}; let path; if (Array.isArray(protocols)) { protocols.forEach((protocol) => { service["@protocol"] = protocol; path = cds.service.path4?.(service) || cds.serve.path4(service); paths[protocol] = path; }); } return paths; } return {} } function _readConfigFile(configFilePath) { if (!configFilePath) return {}; if (!fs.existsSync(configFilePath)) { throw new Error(`Unable to find openapi config file ${configFilePath}`); } let fileContent; try { fileContent = JSON.parse(fs.readFileSync(configFilePath, 'utf-8')); } catch (err) { throw new Error(`Unable to parse OpenAPI config ${configFilePath}`, { cause: err }); } const result = {}; for (const key of Object.keys(fileContent)) { const normalizedKey = key === "odata-version" ? "odataVersion" : key; const value = fileContent[key]; result[normalizedKey] = typeof value === 'object' && value !== null ? JSON.stringify(value) : value; } return result; } // we're attaching the events to the main function so they become automatically exposed through the cds facade: // cds.compile.to.openapi(...) // cds.compile.to.openapi.events.before compileToOpenAPI.events = events module.exports = { compileToOpenAPI }