Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions __tests__/unit/metaData.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,15 +201,6 @@ describe("metaData", () => {
}
});

test("getMetadata should handle invalid URL format", async () => {
const url = "/invalid/url/format";
try {
await getMetadata(url);
} catch (error) {
expect(error.message).toContain("Invalid URL format");
}
});

test("getMetadata should handle missing service name in URL", async () => {
const url = "/ord/v1/sap.test.cdsrc.sample:apiResource::v1/Service.oas3.json";
openapi.mockImplementation(() => "Mock content");
Expand Down
168 changes: 75 additions & 93 deletions lib/metaData.js
Original file line number Diff line number Diff line change
@@ -1,108 +1,90 @@
const path = require("path");
const cds = require("@sap/cds/lib");
const assert = require("node:assert");
const cdsc = require("@sap/cds-compiler/lib/main");
const { compile: openapi } = require("@cap-js/openapi");
const { compile: asyncapi } = require("@cap-js/asyncapi");
const { COMPILER_TYPES, OPENAPI_SERVERS_ANNOTATION } = require("./constants");

const Logger = require("./logger");
const { interopCSN } = require("./interopCsn.js");
const cdsc = require("@sap/cds-compiler/lib/main");
const { COMPILER_TYPES, OPENAPI_SERVERS_ANNOTATION } = require("./constants");

/**
* Read @OpenAPI.servers annotation from service definition
* @param {object} csn - The CSN model
* @param {string} serviceName - The service name
* @returns {string|undefined} - JSON string of servers array or undefined
*/
const _getServersFromAnnotation = (csn, serviceName) => {
const servers = csn?.definitions?.[serviceName]?.[OPENAPI_SERVERS_ANNOTATION];
const isValidServers = Array.isArray(servers) && servers.length > 0;
return isValidServers ? JSON.stringify(servers) : undefined;
};
function extractServiceName(url) {
return path.basename(url, ".json").split(".").slice(0, -1).join(".");
}

function extractCompilerType(url) {
return path.basename(url, ".json").split(".").pop();
}

const compilers = Object.freeze({
[COMPILER_TYPES.csn]: async function (csn, options) {
return {
contentType: "application/json",
response: interopCSN(
cdsc.for.effective(csn, { beta: { effectiveCsn: true }, effectiveServiceName: options.service }),
),
};
},
[COMPILER_TYPES.mcp]: async function (csn, options) {
return {
contentType: "application/json",
response: await cds.compile(csn).to["mcp"]({ ...options, ...(cds.env.ord?.compileOptions?.mcp || {}) }),
};
},
[COMPILER_TYPES.oas3]: async function (csn, options) {
// Check for service-level @OpenAPI.servers annotation
const servers = csn?.definitions?.[options.service]?.[OPENAPI_SERVERS_ANNOTATION];
const openapiOptions = { ...options, ...(cds.env?.ord?.compileOptions?.openapi || {}) };

// Service-level annotation takes precedence over global config
if (Array.isArray(servers) && servers.length) {
openapiOptions["openapi:servers"] = JSON.stringify(servers);
}

return {
contentType: "application/json",
response: openapi(csn, openapiOptions),
};
},
[COMPILER_TYPES.edmx]: async function (csn, options) {
return {
contentType: "application/xml",
response: await cds
.compile(csn)
.to["edmx"]({ ...options, ...(cds.env?.ord?.compileOptions?.edmx || {}) }),
};
},
[COMPILER_TYPES.graphql]: async function (csn, options) {
const { generateSchema4 } = require("@cap-js/graphql/lib/schema");
const { printSchema, lexicographicSortSchema } = require("graphql");
const srv = new cds.ApplicationService(options.service, cds.linked(csn));

return {
contentType: "text/plain",
response: printSchema(lexicographicSortSchema(generateSchema4({ [options.service]: srv }))),
};
},
[COMPILER_TYPES.asyncapi2]: async function (csn, options) {
return {
contentType: "application/json",
response: asyncapi(csn, { ...options, ...(cds.env?.ord?.compileOptions?.asyncapi || {}) }),
};
},
});

const getMetadata = async (url, model = null) => {
const parts = url
?.split("/")
.pop()
.replace(/\.json$/, "")
.split(".");
const compilerType = parts.pop();
const serviceName = parts.join(".");
const serviceName = extractServiceName(url);
const compilerType = extractCompilerType(url);
const csn = model || cds.services[serviceName]?.model;
const compileOptions = cds.env["ord"]?.compileOptions || {};

let responseFile;
const options = { service: serviceName, as: "str", messages: [] };
switch (compilerType) {
case COMPILER_TYPES.oas3:
try {
// Check for service-level @OpenAPI.servers annotation
const serversFromAnnotation = _getServersFromAnnotation(csn, serviceName);
const openapiOptions = { ...options, ...(compileOptions?.openapi || {}) };

// Service-level annotation takes precedence over global config
if (serversFromAnnotation) {
openapiOptions["openapi:servers"] = serversFromAnnotation;
}
assert(Object.hasOwn(compilers, compilerType), `Unsupported format: ${compilerType}`);

responseFile = openapi(csn, openapiOptions);
} catch (error) {
Logger.error(`OpenApi error for service ${serviceName} - ${error.message}`);
throw error;
}
break;
case COMPILER_TYPES.asyncapi2:
try {
responseFile = asyncapi(csn, { ...options, ...(compileOptions?.asyncapi || {}) });
} catch (error) {
Logger.error(`AsyncApi error for service ${serviceName} - ${error.message}`);
throw error;
}
break;
case COMPILER_TYPES.csn:
try {
const opt_eff = { beta: { effectiveCsn: true }, effectiveServiceName: serviceName };
let effCsn = cdsc.for.effective(csn, opt_eff);
responseFile = interopCSN(effCsn);
} catch (error) {
Logger.error(`Csn error for service ${serviceName} - ${error.message}`);
throw error;
}
break;
case COMPILER_TYPES.edmx:
try {
responseFile = await cds.compile(csn).to["edmx"]({ ...options, ...(compileOptions?.edmx || {}) });
} catch (error) {
Logger.error(`Edmx error for service ${serviceName} - ${error.message}`);
throw error;
}
break;
case COMPILER_TYPES.mcp:
try {
responseFile = await cds.compile(csn).to["mcp"]({ ...options, ...(compileOptions?.mcp || {}) });
} catch (error) {
Logger.error("MCP error:", error.message);
throw error;
}
break;
case COMPILER_TYPES.graphql:
try {
const { generateSchema4 } = require("@cap-js/graphql/lib/schema");
const { lexicographicSortSchema, printSchema } = require("graphql");
const linked = cds.linked(csn);
const srv = new cds.ApplicationService(serviceName, linked);
let schema = generateSchema4({ [serviceName]: srv });
schema = lexicographicSortSchema(schema);
responseFile = printSchema(schema);
} catch (error) {
Logger.error("GraphQL SDL error:", error.message);
throw error;
}
break;
}
return {
contentType:
compilerType === "graphql" ? "text/plain" : `application/${compilerType === "edmx" ? "xml" : "json"}`,
response: responseFile,
};
return await compilers[compilerType](csn, options).catch((error) => {
Logger.error(`Compilation failed for service ${serviceName} (compiler: ${compilerType}) - ${error.message}`);
throw error;
});
};

module.exports = getMetadata;