@cap-js/openapi
Version:
CAP tool for OpenAPI
242 lines (209 loc) • 7.61 kB
JavaScript
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
}