@cap-js-community/odata-v2-adapter
Version:
OData V2 adapter for CDS
5,154 lines • 179 kB
JavaScript
"use strict";
// Suppress deprecation warning in Node 22 due to http-proxy using util._extend()
require("util")._extend = Object.assign;
// OData V2/V4 Delta: http://docs.oasis-open.org/odata/new-in-odata/v4.0/cn01/new-in-odata-v4.0-cn01.html
const os = require("os");
const fs = require("fs");
const fsPath = require("path");
const URL = require("url");
const { pipeline } = require("stream");
const express = require("express");
const expressFileUpload = require("express-fileupload");
const cds = require("@sap/cds");
const { promisify } = require("util");
const { createProxyMiddleware } = require("http-proxy-middleware");
const bodyParser = require("body-parser");
require("body-parser-xml")(bodyParser);
const xml2js = require("xml2js");
const xmlParser = new xml2js.Parser({
async: false,
tagNameProcessors: [xml2js.processors.stripPrefix],
});
const cacheSymbol = Symbol("cov2ap");
const CACHE_DIR = fs.realpathSync(os.tmpdir());
const SeverityMap = {
1: "success",
2: "info",
3: "warning",
4: "error",
};
// Support HANA's SYSUUID, which does not conform to real UUID formats
const UUIDLikeRegex = /guid'([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'/gi;
// https://www.w3.org/TR/xmlschema11-2/#nt-duDTFrag
const DurationRegex =
/^P(?:(\d)Y)?(?:(\d{1,2})M)?(?:(\d{1,2})D)?T(?:(\d{1,2})H)?(?:(\d{2})M)?(?:(\d{2}(?:\.\d+)?)S)?$/i;
// Unsupported Draft Filter
const UnsupportedDraftFilterRegex =
/\(IsActiveEntity eq true and (.*?)\) or \(IsActiveEntity eq false and \((.*?) or HasActiveEntity eq false\)\)/;
// https://cap.cloud.sap/docs/cds/types
const DataTypeMap = {
"cds.UUID": { v2: `guid'$1'`, v4: UUIDLikeRegex },
// "cds.Boolean" - no transformation
// "cds.UInt8" - no transformation
// "cds.Int16" - no transformation
// "cds.Int32" - no transformation
// "cds.Integer" - no transformation
"cds.Int64": { v2: `$1L`, v4: /([-]?[0-9]+?)L/gi },
"cds.Integer64": { v2: `$1L`, v4: /([-]?[0-9]+?)L/gi },
"cds.Decimal": { v2: `$1m`, v4: /([-]?[0-9]+?\.?[0-9]*)m/gi },
"cds.DecimalFloat": { v2: `$1f`, v4: /([-]?[0-9]+?\.?[0-9]*)f/gi },
"cds.Double": { v2: `$1d`, v4: /([-]?[0-9]+?\.?[0-9]*(?:E[+-]?[0-9]+?)?)d/gi },
"cds.Date": { v2: `datetime'$1'`, v4: /datetime'(.+?)'/gi },
"cds.Time": { v2: `time'$1'`, v4: /time'(.+?)'/gi },
"cds.DateTime": { v2: `datetimeoffset'$1'`, v4: /datetime(?:offset)?'(.+?)'/gi },
"cds.Timestamp": { v2: `datetimeoffset'$1'`, v4: /datetime(?:offset)?'(.+?)'/gi },
"cds.String": { v2: `'$1'`, v4: /(.*)/gis },
"cds.Binary": { v2: `binary'$1'`, v4: /X'(?:[0-9a-f][0-9a-f])+?'/gi },
"cds.LargeBinary": { v2: `binary'$1'`, v4: /X'(?:[0-9a-f][0-9a-f])+?'/gi },
"cds.LargeString": { v2: `'$1'`, v4: /(.*)/gis },
};
// https://www.odata.org/documentation/odata-version-2-0/overview/ (6. Primitive Data Types)
// https://cap.cloud.sap/docs/advanced/odata#type-mapping
const DataTypeOData = {
Binary: "cds.Binary",
Boolean: "cds.Boolean",
Byte: "cds.UInt8",
DateTime: "cds.DateTime",
Decimal: "cds.Decimal",
Double: "cds.Double",
Single: "cds.Double",
Guid: "cds.UUID",
Int16: "cds.Int16",
Int32: "cds.Integer",
Int64: "cds.Integer64",
SByte: "cds.Integer",
String: "cds.String",
Time: "cds.Time",
DateTimeOffset: "cds.Timestamp",
Date: "cds.Date",
TimeOfDay: "cds.Time",
_Decimal: "cds.DecimalFloat",
_Binary: "cds.LargeBinary",
_String: "cds.LargeString",
};
// https://cap.cloud.sap/docs/advanced/odata#type-mapping
const ODataType = {
"cds.UUID": "Edm.Guid",
"cds.Boolean": "Edm.Boolean",
"cds.UInt8": "Edm.Byte",
"cds.Int16": "Edm.Int16",
"cds.Int32": "Edm.Int32",
"cds.Integer": "Edm.Int32",
"cds.Int64": "Edm.Int64",
"cds.Integer64": "Edm.Int64",
"cds.Decimal": "Edm.Decimal",
"cds.DecimalFloat": "Edm.Decimal",
"cds.Double": "Edm.Double",
"cds.Date": "Edm.DateTime",
"cds.Time": "Edm.Time",
"cds.DateTime": "Edm.DateTime",
"cds.Timestamp": "Edm.DateTimeOffset",
"cds.String": "Edm.String",
"cds.Binary": "Edm.Binary",
"cds.LargeBinary": "Edm.Binary",
"cds.LargeString": "Edm.String",
};
const AggregationMap = {
SUM: "sum",
MIN: "min",
MAX: "max",
AVG: "average",
COUNT: "$count",
COUNT_DISTINCT: "countdistinct",
NONE: "none",
NOP: "nop",
};
const DefaultAggregation = AggregationMap.SUM;
const FilterFunctions = {
"substringof($,$)": "contains($2,$1)",
"gettotaloffsetminutes($)": "totaloffsetminutes($1)",
};
const FilterFunctionsCaseInsensitive = {
"substringof($,$)": "contains(tolower($2),tolower($1))",
"startswith($,$)": "startswith(tolower($1),tolower($2))",
"endswith($,$)": "endswith(tolower($1),tolower($2))",
};
const ProcessingDirection = {
Request: "req",
Response: "res",
};
const DefaultHost = "localhost";
const DefaultPort = 4004;
const DefaultTenant = "00000000-0000-0000-0000-000000000000";
const AggregationPrefix = "__AGGREGATION__";
const IEEE754Compatible = "IEEE754Compatible=true";
const MessageTargetTransient = "/#TRANSIENT#";
const MessageTargetTransientPrefix = `${MessageTargetTransient}/`;
function convertToNodeHeaders(webHeaders) {
return Array.from(webHeaders.entries()).reduce((result, [key, value]) => {
result[key] = value;
return result;
}, {});
}
/**
* The OData V2 adapter for CDS instantiates an Express router. The following options are available:
* @param {object} [options] OData V2 adapter for CDS options object.
* @param {string} [options.base] Base path under which the service is reachable. Default is ''.
* @param {string} [options.path] Path under which the service is reachable. Default is `'odata/v2'`. Default path is `'v2'` for CDS <7 or `middlewares` deactivated.
* @param {string|string[]|object} [options.model] CDS service model (path(s) or CSN). Default is 'all'.
* @param {number} [options.port] Target port which points to OData V4 backend port. Default is process.env.PORT or 4004.
* @param {string} [options.target] Target which points to OData V4 backend host:port. Use 'auto' to infer the target from server url after listening. Default is e.g. 'auto'.
* @param {string} [options.targetPath] Target path to which is redirected. Default is `'odata/v4'`. Default path is `''` for CDS <7 or `middlewares` deactivated.
* @param {object} [options.services] Service mapping object from url path name to service name. Default is {}.
* @param {boolean} [options.mtxRemote] CDS model is retrieved remotely via MTX endpoint for multitenant scenario (classic MTX only). Default is false.
* @param {string} [options.mtxEndpoint] Endpoint to retrieve MTX metadata when option 'mtxRemote' is active (classic MTX only). Default is '/mtx/v1'.
* @param {boolean} [options.ieee754Compatible] Edm.Decimal and Edm.Int64 are serialized IEEE754 compatible. Default is true.
* @param {number} [options.fileUploadSizeLimit] File upload file size limit (in bytes) for multipart/form-data requests. Default is 10485760 (10 MB).
* @param {boolean} [options.continueOnError] Indicates to OData V4 backend to continue on error. Default is false.
* @param {boolean} [options.isoTime] Use ISO 8601 format for type cds.Time (Edm.Time). Default is false.
* @param {boolean} [options.isoDate] Use ISO 8601 format for type cds.Date (Edm.DateTime). Default is false.
* @param {boolean} [options.isoDateTime] Use ISO 8601 format for type cds.DateTime (Edm.DateTimeOffset). Default is false.
* @param {boolean} [options.isoTimestamp] Use ISO 8601 format for type cds.Timestamp (Edm.DateTimeOffset). Default is false.
* @param {boolean} [options.isoDateTimeOffset] Use ISO 8601 format for type Edm.DateTimeOffset (cds.DateTime, cds.Timestamp). Default is false.
* @param {string} [options.bodyParserLimit] Request and response body parser size limit. Default is '100mb'.
* @param {boolean} [options.returnCollectionNested] Collection of entity type is returned nested into a results section. Default is true.
* @param {boolean} [options.returnComplexNested] Function import return structure of complex type (non collection) is nested using function import name. Default is true.
* @param {boolean} [options.returnPrimitiveNested] Function import return structure of primitive type (non collection) is nested using function import name. Default is true.
* @param {boolean} [options.returnPrimitivePlain] Function import return value of primitive type is rendered as plain JSON value. Default is true.
* @param {string} [options.messageTargetDefault] Specifies the message target default, if target is undefined. Default is '/#TRANSIENT#'.
* @param {boolean} [options.caseInsensitive] Transforms search functions i.e. substringof, startswith, endswith to case-insensitive variant. Default is false.
* @param {boolean} [options.propagateMessageToDetails] Propagates root error or message always to details section. Default is false.
* @param {boolean} [options.contentDisposition] Default content disposition for media streams (inline, attachment), if not available or calculated. Default is 'attachment'.
* @param {boolean} [options.calcContentDisposition] Calculate content disposition for media streams even if already available. Default is false.
* @param {boolean} [options.quoteSearch] Specifies if search expression is quoted automatically. Default is true.
* @param {boolean} [options.fixDraftRequests] Specifies if unsupported draft requests are converted to a working version. Default is false.
* @param {string} [options.changesetDeviationLogLevel] Log level of batch changeset content-id deviation logs (none, debug, info, warn, error). Default is 'info'.
* @param {string} [options.defaultFormat] Specifies the default entity response format (json, atom). Default is 'json'.
* @param {boolean} [options.processForwardedHeaders] Specifies if 'x-forwarded' headers are processed. Default is true.
* @param {boolean} [options.cacheDefinitions] Specifies if the definition elements are cached. Default is true.
* @param {string} [options.cacheMetadata] Specifies the caching and provisioning strategy of metadata (e.g. edmx) (memory, disk, stream). Default is 'memory'.
* @param {string} [options.registerOnListening] Routes are registered on CDS `listening` event instead of registering routes immediately. Default is true.
* @param {boolean} [options.excludeNonSelectedKeys] Excludes non-selected keys from entity response (OData V4 auto-includes keys). Default is 'false'.
* @returns {express.Router} OData V2 adapter for CDS Express Router
*/
function cov2ap(options = {}) {
if (cov2ap._singleton) {
return cov2ap._singleton;
}
const router = express.Router();
const optionWithFallback = (name, fallback) => {
if (options && Object.prototype.hasOwnProperty.call(options, name)) {
return options[name];
}
if (cds.env.cov2ap && Object.prototype.hasOwnProperty.call(cds.env.cov2ap, name)) {
return cds.env.cov2ap[name];
}
const scName = name.replace(/[A-Z]/g, (char) => `_${char.toLowerCase()}`);
if (cds.env.cov2ap && Object.prototype.hasOwnProperty.call(cds.env.cov2ap, scName)) {
return cds.env.cov2ap[scName];
}
const acName = name.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
if (cds.env.cov2ap && Object.prototype.hasOwnProperty.call(cds.env.cov2ap, acName)) {
return cds.env.cov2ap[acName]["undefined"];
}
return fallback;
};
let oDataV2Path = "v2";
let oDataV4Path = "";
const oDataProtocolPrefixActive = parseInt(cds.version, 10) >= 7 && cds.env.requires.middlewares;
if (oDataProtocolPrefixActive) {
oDataV2Path = "odata/v2";
oDataV4Path = "odata/v4";
if (cds.env.protocols) {
if (cds.env.protocols["odata-v2"]) {
oDataV2Path = cds.env.protocols["odata-v2"].path;
}
if (cds.env.protocols.odata) {
oDataV4Path = cds.env.protocols.odata.path;
} else if (cds.env.protocols["odata-v4"]) {
oDataV4Path = cds.env.protocols["odata-v4"].path;
}
}
}
const oDataV2RelativePath = oDataV2Path.replace(/^\//, "");
const oDataV4RelativePath = oDataV4Path.replace(/^\//, "");
const metadataCache = {};
const base = optionWithFallback("base", "");
const path = optionWithFallback("path", oDataV2RelativePath);
const sourcePath = `${base ? "/" + base : ""}/${path}`;
const targetPath = optionWithFallback("targetPath", oDataV4RelativePath);
const rewritePath = `${base ? "/" + base : ""}${targetPath ? "/" : ""}${targetPath}`;
let port = optionWithFallback("port", process.env.PORT || DefaultPort);
const defaultTarget = `http://${DefaultHost}:${port}`;
let target = optionWithFallback("target", "auto");
const services = optionWithFallback("services", {});
const mtxRemote = optionWithFallback("mtxRemote", false);
const mtxEndpoint = optionWithFallback("mtxEndpoint", "/mtx/v1");
const ieee754Compatible = optionWithFallback("ieee754Compatible", true);
const fileUploadSizeLimit = optionWithFallback("fileUploadSizeLimit", 10 * 1024 * 1024);
const continueOnError = optionWithFallback("continueOnError", false);
const isoTime = optionWithFallback("isoTime", false);
const isoDate = optionWithFallback("isoDate", false);
const isoDateTime = optionWithFallback("isoDateTime", false);
const isoTimestamp = optionWithFallback("isoTimestamp", false);
const isoDateTimeOffset = optionWithFallback("isoDateTimeOffset", false);
const bodyParserLimit = optionWithFallback("bodyParserLimit", "100mb");
const returnCollectionNested = optionWithFallback("returnCollectionNested", true);
const returnComplexNested = optionWithFallback("returnComplexNested", true);
const returnPrimitiveNested = optionWithFallback("returnPrimitiveNested", true);
const returnPrimitivePlain = optionWithFallback("returnPrimitivePlain", true);
const messageTargetDefault = optionWithFallback("messageTargetDefault", MessageTargetTransient);
const caseInsensitive = optionWithFallback("caseInsensitive", false);
const propagateMessageToDetails = optionWithFallback("propagateMessageToDetails", false);
const contentDisposition = optionWithFallback("contentDisposition", "attachment");
const calcContentDisposition = optionWithFallback("calcContentDisposition", false);
const quoteSearch = optionWithFallback("quoteSearch", true);
const fixDraftRequests = optionWithFallback("fixDraftRequests", false);
const changesetDeviationLogLevel = optionWithFallback("changesetDeviationLogLevel", "info");
const defaultFormat = optionWithFallback("defaultFormat", "json");
const processForwardedHeaders = optionWithFallback("processForwardedHeaders", true);
const cacheDefinitions = optionWithFallback("cacheDefinitions", true);
const cacheMetadata = optionWithFallback("cacheMetadata", "memory");
const registerOnListening = optionWithFallback("registerOnListening", true);
const excludeNonSelectedKeys = optionWithFallback("excludeNonSelectedKeys", false);
if (cds.env.protocols) {
cds.env.protocols["odata-v2"] = {
path: sourcePath,
impl: __filename,
};
}
if (caseInsensitive) {
Object.assign(FilterFunctions, FilterFunctionsCaseInsensitive);
}
const fileUpload = expressFileUpload({
abortOnLimit: true,
limits: {
files: 1,
fileSize: fileUploadSizeLimit,
},
});
let model = optionWithFallback("model", "all");
if (Array.isArray(model)) {
model = model.map((entry) => (entry === "all" ? "*" : entry));
} else {
model = model === "all" ? "*" : model;
}
model = cds.resolve(model);
async function clearMetadataCache(tenant) {
if (metadataCache[tenant]) {
const tenantCache = metadataCache[tenant];
delete metadataCache[tenant];
if (cacheDefinitions) {
const csn = await callCached(tenantCache, "csn");
if (csn) {
for (const name in csn.definitions) {
const definition = csn.definitions[name];
delete definition[cacheSymbol];
}
}
}
}
}
cds.on("serving", (service) => {
const isOData = isServedViaOData(service);
if (!isOData) {
return;
}
const odataV2Path = serviceODataV2Path(service);
const odataV4Path = serviceODataV4Path(service);
const protocolPath = `${sourcePath}${sourceServicePath(odataV4Path)}`;
let endpointPath = protocolPath;
// Protocol re-routing
if (odataV2Path && protocolPath !== odataV2Path) {
endpointPath = endpointPath.replace(protocolPath, odataV2Path);
router.all([`${odataV2Path}`, `${odataV2Path}/*`], (req, res, next) => {
req.url = req.url.replace(odataV2Path, protocolPath);
req.originalUrl = req.url;
req.endpointRewrite = (url) => {
return url.replace(protocolPath, odataV2Path);
};
next();
});
}
const provider = (entity, endpoint) => {
if (endpoint && !endpoint.kind.startsWith("odata")) {
return;
}
const href = `${endpointPath}/${entity || "$metadata"}`;
return { href, name: `${entity || "$metadata"} (V2)`, title: "OData V2" };
};
service.$linkProviders = service.$linkProviders || [];
service.$linkProviders.push(provider);
});
if (cds.mtx && cds.mtx.eventEmitter) {
cds.mtx.eventEmitter.on(cds.mtx.events.TENANT_UPDATED, async (tenant) => {
try {
await clearMetadataCache(tenant);
} catch (err) {
logError({ tenant }, "Cache", err);
}
});
}
cds.on("cds.xt.TENANT_UPDATED", async ({ tenant }) => {
try {
await clearMetadataCache(tenant);
} catch (err) {
logError({ tenant }, "Cache", err);
}
});
async function routeInitRequest(req, res, next) {
req.now = new Date();
req.contextId =
req.headers["x-correlation-id"] ||
req.headers["x-correlationid"] ||
req.headers["x-request-id"] ||
req.headers["x-vcap-request-id"] ||
cds.utils.uuid();
res.set("x-request-id", req.contextId);
res.set("x-correlation-id", req.contextId);
res.set("x-correlationid", req.contextId);
try {
// Authorization is validated within target request (-> simple decode)
const [authType, token] = (req.headers.authorization && req.headers.authorization.split(" ")) || [];
if (authType && token) {
let jwtBody;
switch (authType) {
case "Basic":
req.user = {
id: decodeBase64(token).split(":")[0],
};
if (req.user.id && cds.env.requires.auth && ["basic", "mocked"].includes(cds.env.requires.auth.kind)) {
const user = (cds.env.requires.auth.users || {})[req.user.id];
req.tenant = user && (user.tenant || (user.jwt && user.jwt.zid));
}
break;
case "Bearer":
jwtBody = decodeJwtTokenBody(token);
req.user = {
id: jwtBody.user_name || jwtBody.client_id,
};
req.tenant = jwtBody.zid;
if (!req.authInfo) {
req.authInfo = {
getSubdomain: () => {
return jwtBody.ext_attr && jwtBody.ext_attr.zdn;
},
};
}
break;
}
}
} catch (err) {
logError(req, "Authorization", err);
}
if (req.tenant) {
req.tenant = String(req.tenant);
}
if (["constructor", "prototype", "__proto__"].includes(req.tenant)) {
logWarn(req, "Authorization", "Invalid tenant", { tenant: req.tenant });
req.tenant = undefined;
}
next();
}
async function routeGetMetadata(req, res) {
let serviceValid = true;
const urlPath = targetUrl(req.originalUrl);
try {
const metadataUrl = URL.parse(urlPath, true);
let metadataPath = metadataUrl.pathname.substring(0, metadataUrl.pathname.length - 9);
const { csn } = await getMetadata(req);
req.csn = csn;
const service = serviceFromRequest(req);
if (service.absolute && metadataPath.startsWith(`/${targetPath}`)) {
metadataPath = metadataPath.substring(targetPath.length + 1);
}
const serviceUrl = target + metadataPath;
// Trace
traceRequest(req, "Request", req.method, urlPath, req.headers, req.body);
traceRequest(req, "ProxyRequest", req.method, metadataPath, req.headers, req.body);
const result = await Promise.all([
fetch(serviceUrl, {
method: "GET",
headers: {
...propagateHeaders(req),
accept: "application/json",
},
}),
(async () => {
if (service && service.name) {
serviceValid = service.valid;
const { edmx } = await getMetadata(req, service.name);
return edmx;
}
})(),
]);
const [metadataResponse, edmx] = result;
const metadataBody = await metadataResponse.text();
const headers = convertBasicHeaders(convertToNodeHeaders(metadataResponse.headers));
delete headers["content-encoding"];
let body;
if (metadataResponse.ok || metadataResponse.status === 304) {
headers["content-type"] = "application/xml";
if (cacheMetadata === "disk") {
body = await fs.promises.readFile(edmx, "utf8");
} else if (cacheMetadata === "stream") {
body = fs.createReadStream(edmx);
} else {
body = edmx;
}
} else {
body = metadataBody;
}
setContentLength(headers, body);
// Trace
traceResponse(
req,
"Proxy Response",
metadataResponse.status,
metadataResponse.statusMessage,
headers,
metadataBody,
);
respond(req, res, metadataResponse.status, headers, body);
} catch (err) {
if (serviceValid) {
// Error
if (err.statusCode === 400) {
logWarn(req, "MetadataRequest", err);
} else {
logError(req, "MetadataRequest", err);
}
// Trace
logWarn(req, "MetadataRequest", "Request with Error", {
method: req.method,
url: urlPath,
target,
});
if (err.statusCode) {
res.status(err.statusCode).send(err.message);
} else if (err.message === "fetch failed") {
res.status(400).send(err.message);
} else {
res.status(500).send("Internal Server Error");
}
} else {
res.status(404).send("Not Found");
}
}
}
async function routeBodyParser(req, res, next) {
const contentType = req.header("content-type");
if (!contentType) {
return next();
}
if (isApplicationJSON(contentType)) {
express.json({ limit: bodyParserLimit })(req, res, next);
} else if (isXML(contentType)) {
bodyParser.xml({
limit: bodyParserLimit,
xmlParseOptions: {
tagNameProcessors: [xml2js.processors.stripPrefix],
},
})(req, res, next);
} else if (isMultipartMixed(contentType)) {
express.text({ type: "multipart/mixed", limit: bodyParserLimit })(req, res, next);
} else {
req.checkUploadBinary = req.method === "POST";
next();
}
}
async function routeSetContext(req, res, next) {
try {
const { csn } = await getMetadata(req);
req.csn = csn;
} catch (err) {
// Error
logError(req, "Request", err);
res.status(500).send("Internal Server Error");
return;
}
try {
const service = serviceFromRequest(req);
req.base = base;
req.service = service.name;
req.servicePath = service.path;
req.serviceAbsolute = service.absolute;
req.context = {};
req.contexts = [];
req.contentId = {};
req.lookupContext = {};
next();
} catch (err) {
// Error
if (err.statusCode === 400) {
logWarn(req, "Request", err);
} else {
logError(req, "Request", err);
}
// Trace
logWarn(req, "Request", "Request with Error", {
method: req.method,
url: req.url,
target,
});
if (err.statusCode) {
res.status(err.statusCode).send(err.message);
} else {
res.status(500).send("Internal Server Error");
}
}
}
async function routeFileUpload(req, res, next) {
if (!req.checkUploadBinary) {
return next();
}
const urlPath = targetUrl(req.originalUrl);
const url = parseUrl(urlPath, req);
const definition = contextFromUrl(url, req);
if (!definition) {
return next();
}
convertUrl(url, req);
const elements = definitionElements(definition);
const mediaDataElementName =
findElementByAnnotation(elements, "@Core.MediaType") ||
findElementByType(elements, DataTypeOData._Binary, req) ||
findElementByType(elements, DataTypeOData.Binary, req);
if (!mediaDataElementName) {
return next();
}
const handleMediaEntity = async (contentType, filename, headers = {}) => {
try {
contentType = contentType || "application/octet-stream";
const body = {};
// Custom body
const caseInsensitiveElements = Object.keys(elements).reduce((result, name) => {
result[name.toLowerCase()] = elements[name];
return result;
}, {});
Object.keys(headers).forEach((name) => {
const element = caseInsensitiveElements[name.toLowerCase()];
if (element) {
const value = convertDataTypeToV4(headers[name], elementType(element, req), definition, headers);
body[element.name] = decodeHeaderValue(definition, element, element.name, value);
}
});
const mediaDataElement = elements[mediaDataElementName];
const mediaTypeElementName =
(mediaDataElement["@Core.MediaType"] && mediaDataElement["@Core.MediaType"]["="]) ||
findElementByAnnotation(elements, "@Core.IsMediaType");
if (mediaTypeElementName) {
body[mediaTypeElementName] = contentType;
}
const contentDispositionFilenameElementName =
findElementValueByAnnotation(elements, "@Core.ContentDisposition.Filename") ||
findElementValueByAnnotation(elements, "@Common.ContentDisposition.Filename");
if (contentDispositionFilenameElementName && filename) {
const element = elements[contentDispositionFilenameElementName];
body[contentDispositionFilenameElementName] = decodeHeaderValue(definition, element, element.name, filename);
}
const postUrl = target + url.pathname;
const postHeaders = propagateHeaders(req, {
...headers,
"content-type": "application/json",
});
delete postHeaders["transfer-encoding"];
// Trace
traceRequest(req, "ProxyRequest", "POST", postUrl, postHeaders, body);
const postBody = JSON.stringify(body);
postHeaders["content-length"] = postBody.length;
const response = await fetch(postUrl, {
method: "POST",
headers: postHeaders,
body: postBody,
});
const responseBody = await response.json();
const responseHeaders = convertToNodeHeaders(response.headers);
if (!response.ok) {
res
.status(response.status)
.set({
"content-type": "application/json",
})
.send(convertResponseError(responseBody, responseHeaders, definition, req));
return;
}
// Rewrite
req.method = "PUT";
req.url += `(${entityKey(responseBody, definition, elements, req)})/${mediaDataElementName}`;
req.originalUrl = req.url;
req.overwriteResponse = {
kind: "uploadBinary",
statusCode: response.status,
headers: responseHeaders,
body: responseBody,
};
// Trace
traceResponse(req, "ProxyResponse", response.status, response.statusText, responseHeaders, responseBody);
next();
} catch (err) {
// Error
logError(req, "FileUpload", err);
res.status(500).send("Internal Server Error");
}
};
const headers = req.headers;
if (isMultipartFormData(headers["content-type"])) {
fileUpload(req, res, async () => {
await handleMediaEntity(
req.body && req.body["content-type"],
req.body &&
(req.body["slug"] ||
req.body["filename"] ||
contentDispositionFilename(req.body) ||
contentDispositionFilename(headers) ||
req.body["name"]),
req.body,
);
});
} else {
await handleMediaEntity(
headers["content-type"],
headers["slug"] || headers["filename"] || contentDispositionFilename(headers) || headers["name"],
headers,
);
}
}
function routeBeforeRequest(req, res, next) {
if (typeof router.before === "function") {
router.before(req, res, next);
} else if (Array.isArray(router.before) && router.before.length > 0) {
const routes = router.before.slice(0);
function call() {
try {
const route = routes.shift();
if (!route) {
return next(null);
}
route(req, res, (err) => {
if (err) {
next(err);
} else {
call();
}
});
} catch (err) {
next(err);
}
}
call();
} else {
next();
}
}
function bindRoutes() {
const routeMiddleware = createProxyMiddleware({
target: `${target}${rewritePath}`,
changeOrigin: true,
selfHandleResponse: true,
on: {
error: convertProxyError,
proxyReq: convertProxyRequest,
proxyRes: convertProxyResponse,
},
logger: cds.log("cov2ap/hpm"),
});
router.use(`/${path}`, routeBeforeRequest);
router.use(`/${path}`, routeInitRequest);
router.get(`/${path}/*\\$metadata`, routeGetMetadata);
router.use(`/${path}`, routeBodyParser, routeSetContext, routeFileUpload, routeMiddleware);
}
if (registerOnListening) {
cds.on("listening", ({ server, url }) => {
if (target === "auto") {
target = url;
port = server.address().port;
}
bindRoutes();
});
} else {
if (target === "auto") {
target = defaultTarget;
}
bindRoutes();
}
function contentDispositionFilename(headers) {
const contentDispositionHeader = headers["content-disposition"] || headers["Content-Disposition"];
if (contentDispositionHeader) {
const filenameMatch = contentDispositionHeader.match(/^.*filename="(.*)"$/is);
return filenameMatch && filenameMatch.pop();
}
return null;
}
function decodeHeaderValue(entity, element, name, value) {
if (value === undefined || value === null || value === "" || typeof value !== "string") {
return value;
}
let decodes = [];
if (Array.isArray(element["@cov2ap.headerDecode"])) {
decodes = element["@cov2ap.headerDecode"];
} else if (typeof element["@cov2ap.headerDecode"] === "string") {
decodes = [element["@cov2ap.headerDecode"]];
}
if (decodes.length > 0) {
decodes.forEach((decode) => {
switch (decode.toLowerCase()) {
case "uri":
value = decodeURI(value);
break;
case "uricomponent":
value = decodeURIComponent(value);
break;
case "base64":
value = decodeBase64(value);
break;
}
});
}
return value;
}
function serviceFromRequest(req) {
const servicePathUrl = normalizeSlashes(req.params["0"] || req.url); // wildcard or non-wildcard
const servicePath = targetPath ? `/${targetPath}${servicePathUrl}` : servicePathUrl;
const service = {
name: "",
path: "",
valid: true,
absolute: false,
};
Object.assign(
service,
determineMostSelectiveService(
Object.keys(services)
.map((path) => {
if (servicePath.toLowerCase().startsWith(normalizeSlashes(path).toLowerCase())) {
return {
name: services[path],
path: stripSlashes(path),
};
}
})
.filter((entry) => !!entry),
),
);
if (!service.name) {
Object.assign(
service,
determineMostSelectiveService(
Object.keys(cds.services)
.map((service) => {
const path = serviceODataV4Path(cds.services[service]);
if (path) {
const absolute = !normalizeSlashes(path)
.toLowerCase()
.startsWith(normalizeSlashes(targetPath).toLowerCase());
if (
convertUrlAbsolutePath(absolute, servicePath)
.toLowerCase()
.startsWith(normalizeSlashes(path).toLowerCase())
) {
return {
name: service,
path: stripSlashes(path),
absolute,
};
}
}
})
.filter((entry) => !!entry),
),
);
}
if (!service.name) {
Object.assign(
service,
determineMostSelectiveService(
Object.keys(cds.services)
.map((service) => {
const path = serviceODataV4Path(cds.services[service]);
if (path) {
if (servicePathUrl.toLowerCase().startsWith(normalizeSlashes(path).toLowerCase())) {
return {
name: service,
path: stripSlashes(path),
absolute: true,
};
}
}
})
.filter((entry) => !!entry),
),
);
}
if (!service.name) {
Object.assign(
service,
determineMostSelectiveService(
Object.keys(cds.services)
.map((service) => {
const path = serviceODataV4Path(cds.services[service]);
if (path === "/") {
return {
name: service,
path: "",
};
}
})
.filter((entry) => !!entry),
),
);
}
if (!service.name || !req.csn.definitions[service.name] || req.csn.definitions[service.name].kind !== "service") {
logWarn(req, "Service", "Invalid service", {
name: service.name,
path: service.path,
});
service.valid = false;
}
if (service.name && req.csn.definitions[service.name] && !isServedViaOData(req.csn.definitions[service.name])) {
logWarn(req, "Service", "Invalid service protocol", {
name: service.name,
path: service.path,
});
const error = new Error("Invalid service protocol. Only OData services supported");
error.statusCode = 400;
throw error;
}
if (req.csn.definitions[service.name] && req.csn.definitions[service.name]["@cov2ap.ignore"]) {
const error = new Error("Service is not exposed as OData V2 protocol");
error.statusCode = 400;
throw error;
}
return {
name: service.name,
path: service.path,
valid: service.valid,
absolute: service.absolute,
};
}
function serviceODataV2Path(service) {
if (Array.isArray(service.endpoints)) {
const odataV2Endpoint = service.endpoints.find((endpoint) => ["odata-v2"].includes(endpoint.kind));
if (odataV2Endpoint) {
return odataV2Endpoint.path;
}
}
}
function serviceODataV4Path(service) {
if (Array.isArray(service.endpoints)) {
const odataV4Endpoint = service.endpoints.find((endpoint) => ["odata", "odata-v4"].includes(endpoint.kind));
if (odataV4Endpoint) {
return odataV4Endpoint.path;
}
}
return service.path;
}
function determineMostSelectiveService(services) {
services.sort((a, b) => {
return b.path.length - a.path.length;
});
if (services.length > 0) {
return services[0];
}
return null;
}
function isServedViaOData(service) {
let protocols = service["@protocol"];
if (protocols) {
protocols = !Array.isArray(protocols) ? [protocols] : protocols;
return protocols.some((protocol) => {
return (typeof protocol === "string" ? protocol : protocol.kind).startsWith("odata");
});
}
const protocolDirect = Object.keys(cds.env.protocols || {}).find((protocol) => service["@" + protocol]);
if (protocolDirect) {
return protocolDirect.startsWith("odata");
}
return true;
}
async function getMetadata(req, service) {
let metadata;
if (req.tenant) {
if (mtxRemote && mtxEndpoint) {
metadata = await getTenantMetadataRemote(req, service);
} else if (cds.mtx && cds.env.requires && cds.env.requires.multitenancy) {
metadata = await getTenantMetadataLocal(req, service);
} else if (cds.env.requires && cds.env.requires["cds.xt.ModelProviderService"]) {
metadata = await getTenantMetadataStreamlined(req, service);
}
}
if (!metadata) {
metadata = await getDefaultMetadata(req, service);
}
return metadata;
}
async function getTenantMetadataRemote(req, service) {
const mtxBasePath =
mtxEndpoint.startsWith("http://") || mtxEndpoint.startsWith("https://") ? mtxEndpoint : `${target}${mtxEndpoint}`;
return await prepareMetadata(
req.tenant,
async (tenant) => {
const response = await fetch(`${mtxBasePath}/metadata/csn/${tenant}`, {
method: "GET",
headers: propagateHeaders(req),
});
if (!response.ok) {
throw new Error(await response.text());
}
return response.json();
},
async (tenant, service, locale) => {
const response = await fetch(
`${mtxBasePath}/metadata/edmx/${tenant}?name=${service}&language=${locale}&odataVersion=v2`,
{
method: "GET",
headers: propagateHeaders(req),
},
);
if (!response.ok) {
throw new Error(await response.text());
}
return response.text();
},
service,
determineLocale(req),
);
}
async function getTenantMetadataLocal(req, service) {
metadataCache[req.tenant] = metadataCache[req.tenant] || {};
const isExtended = await callCached(metadataCache[req.tenant], "isExtended", () => {
return cds.mtx.isExtended(req.tenant);
});
if (isExtended) {
return await prepareMetadata(
req.tenant,
async (tenant) => {
return await cds.mtx.getCsn(tenant);
},
async (tenant, service, locale) => {
return await cds.mtx.getEdmx(tenant, service, locale, "v2");
},
service,
determineLocale(req),
);
}
}
async function getTenantMetadataStreamlined(req, service) {
metadataCache[req.tenant] = metadataCache[req.tenant] || {};
const { "cds.xt.ModelProviderService": mps } = cds.services;
if (mps) {
const isExtended = await callCached(metadataCache[req.tenant], "isExtended", () => {
return mps.isExtended({
tenant: req.tenant,
});
});
if (isExtended) {
return await prepareMetadata(
req.tenant,
async (tenant) => {
return await mps.getCsn({
tenant,
toggles: ensureArray(req.features),
for: "nodejs",
});
},
async (tenant, service, locale) => {
return await mps.getEdmx({
tenant,
toggles: ensureArray(req.features),
service,
locale,
flavor: "v2",
for: "nodejs",
});
},
service,
determineLocale(req),
);
}
}
}
async function getDefaultMetadata(req, service) {
return await prepareMetadata(
DefaultTenant,
async () => {
if (typeof model === "object" && !Array.isArray(model)) {
return model;
}
return await cds.load(model);
},
async () => {},
service,
determineLocale(req),
);
}
async function prepareMetadata(tenant, loadCsn, loadEdmx, service, locale) {
metadataCache[tenant] = metadataCache[tenant] || {};
const csn = await callCached(metadataCache[tenant], "csn", () => {
return prepareCSN(tenant, loadCsn);
});
if (!service) {
return { csn };
}
metadataCache[tenant].edmx = metadataCache[tenant].edmx || {};
metadataCache[tenant].edmx[service] = metadataCache[tenant].edmx[service] || {};
const edmx = await callCached(metadataCache[tenant].edmx[service], locale, async () => {
const edmx = await prepareEdmx(tenant, csn, loadEdmx, service, locale);
if (["disk", "stream"].includes(cacheMetadata)) {
const edmxFilename = fsPath.join(CACHE_DIR, `${tenant}$${service}$${locale}.edmx.xml`);
await fs.promises.writeFile(edmxFilename, edmx);
return edmxFilename;
}
return edmx;
});
return { csn, edmx };
}
async function prepareCSN(tenant, loadCsn) {
let csnRaw;
if (cds.server && cds.model && tenant === DefaultTenant) {
csnRaw = cds.model;
} else {
csnRaw = await loadCsn(tenant);
}
let csn;
if (cds.compile.for.nodejs) {
csn = cds.compile.for.nodejs(csnRaw);
} else {
csn = csnRaw.meta && csnRaw.meta.transformation === "odata" ? csnRaw : cds.linked(cds.compile.for.odata(csnRaw));
}
return csn;
}
async function prepareEdmx(tenant, csn, loadEdmx, service, locale) {
let edmx;
if (tenant !== DefaultTenant) {
edmx = await loadEdmx(tenant, service, locale);
}
if (!edmx) {
edmx = await edmxFromFile(tenant, service);
if (!edmx) {
edmx = await cds.compile.to.edmx(csn, {
service,
version: "v2",
});
}
edmx = cds.localize(csn, locale, edmx);
}
return edmx;
}
async function edmxFromFile(tenant, service) {
const filePath = cds.root + `/srv/odata/v2/${service}.xml`;
let exists;
try {
exists = !(await fs.promises.access(filePath, fs.constants.F_OK));
} catch (e) {
logDebug({ tenant }, "Metadata", `No metadata file found for service ${service} at ${filePath}`);
}
if (exists) {
return await fs.promises.readFile(filePath, "utf8");
}
}
async function callCached(cache, field, call) {
if (call && !cache[field]) {
cache[field] = call();
}
try {
return await cache[field];
} catch (err) {
delete cache[field];
throw err;
}
}
function localName(definition, req) {
const localName = isServiceName(definition.name, req) ? odataName(definition.name, req) : definition.name;
const nameSuffix =
definition.kind === "entity" &&
definition.params &&
req.context.parameters &&
req.context.parameters.kind === "Set"
? "Set"
: "";
return localName + nameSuffix;
}
function isServiceName(name, req) {
return name.startsWith(`${req.service}.`);
}
function odataName(name, req) {
return name.substring(`${req.service}.`.length).replace(/\./g, "_");
}
function qualifiedODataName(name, req) {
return `${req.service}.${odataName(name, req)}`;
}
function qualifiedName(name, req) {
const serviceNamespacePrefix = `${req.service}.`;
return (name.startsWith(serviceNamespacePrefix) ? "" : serviceNamespacePrefix) + name;
}
function qualifiedSubName(name, req) {
if (name.includes("_")) {
const parts = name.split("_");
const endPart = parts.pop();
name = `${parts.join("_")}.${endPart}`;
}
return qualifiedName(name, req);
}
function lookupDefinition(name, req) {
if (["$metadata"].includes(name) || name.startsWith("cds.") || name.startsWith("Edm.")) {
return;
}
const definitionName = qualifiedName(name, req);
const definitionSubName = qualifiedSubName(name, req);
const definition =
req.csn.definitions[definitionSubName] || req.csn.definitions[definitionName] || req.csn.definitions[name];
if (definition) {
return definition;
}
for (const name in req.csn.definitions) {
if (!isServiceName(name, req)) {
continue;
}
if (definitionName === qualifiedODataName(name, req)) {
return req.csn.definitions[name];
}
}
}
function lookupBoundDefinition(name, req) {
for (const definitionName in req.csn.definitions) {
const definition = req.csn.definitions[definitionName];
if (definition.actions) {
for (const actionName in definition.actions) {
if (name.endsWith(`_${actionName}`)) {
const entityName = name.substring(0, name.length - `_${actionName}`.length);
const entityDefinition = lookupDefinition(entityName, req);
if (entityDefinition === definition) {
const boundAction = definition.actions[actionName];
req.lookupContext.boundDefinition = definition;
req.lookupContext.operation = boundAction;
const returnDefinition = lookupReturnDefinition(boundAction.returns, req);
if (returnDefinition) {
req.lookupContext.returnDefinition = returnDefinition;
}
return boundAction;
}
}
}
}
}
}
function lookupParametersDefinition(name, req) {
const definitionTypeName = qualifiedName(name, req);
let definitionKind;
if (definitionTypeName.endsWith("Set")) {
definitionKind = "Set";
} else if (definitionTypeName.endsWith("Parameters")) {
definitionKind = "Parameters";
}
if (definitionKind) {
const definitionName = definitionTypeName.substring(0, definitionTypeName.length - definitionKind.length);
const definition = req.csn.definitions[definitionName] || req.csn.definitions[name];
if (definition && definition.kind === "entity" && definition.params) {
req.lookupContext.parameters = {
kind: definitionKind,
entity: localName(definition, req),
type: localName({ name: definitionTypeName }, req),
values: {},
keys: {},
count: false,
};
return definition;
}
}
}
function enhanceParametersDefinition(context, req) {
if (context && context.kind === "entity" && context.params) {
req.lookupContext.parameters = req.lookupContext.parameters || {
kind: "Parameters",
entity: localName(context, req),
type: localName(context, req),
values: {},
keys: {},
count: false,
};
}
}
function convertProxyError(err, req, res) {
logError(req, "Proxy", err);
if (!req && !res) {
throw err;
}
if (res.writeHead && !res.headersSent) {
if (/HPE_INVALID/.test(err.code)) {
res.writeHead(502);
} else {
switch (err.code) {
case "ECONNRESET":
case "ENOTFOUND":
case "ECONNREFUSED":
case "ETIMEDOUT":
res.writeHead(504);
break;
default:
res.writeHead(500);
}
}
}
if (!res.writableEnded) {
res.end("Unexpected error occurred while processing request");
}
}
/**
* Convert Proxy Request (V2 -> V4)
* @param proxyReq Proxy Request
* @param req Request
* @param res Response
*/
async function convertProxyRequest(proxyReq, req, res) {
try {
// Trace
traceRequest(req, "Request", req.method, req.originalUrl, req.headers, req.body);
const headers = propagateHeaders(req);
let body = req.body;
let contentType = req.header("content-type");
if (isMultipartMixed(contentType)) {
// Multipart
req.contentIdOrder = [];
if (req.method === "HEAD") {
body = "";
} else {
body = processMultipartMixed(
req,
body,
contentType,
({ method, url }) => {
method = convertMethod(method);
url = convertUrlAndSetContext(url, req, method);
return { method, url };
},
({ contentType, body, headers, url, contentId }) => {
if (contentId) {
req.contentId[`$${contentId}`] = req.context.url;
}
delete headers["odata-version"];
delete headers["Odata-Version"];
delete headers.dataserviceversion;
delete headers.DataServiceVersion;
delete headers.maxdataserviceversion;
delete headers.MaxDataServiceVersion;
if (isResponseFormatXML(req.context, headers)) {
req.context.serviceResponseAsXML = true;
}
if (headers.accept && !headers.accept.includes("application/json")) {
headers.accept = "application/json," + headers.accept;
}
if (isXML(contentType)) {
req.context.serviceRequestAsXML = true;
body = convertRequestBodyFromXML(body, req);
contentType = "application/json";
headers["content-type"] = contentType;
}
if (headers["sap-messages"]) {
req.context.messages = headers["sap-messages"];
}
if (isApplicationJSON(contentType)) {
if (ieee754Compatible) {
contentType = enrichApplicationJSON(contentType);
headers["content-type"] = contentType;
}
body = convertRequestBody(body, headers, url, req);
}
if (body !== undefined) {
for (const name in headers) {
if (name.toLowerCase() === "content-length") {
headers[name] = Buffer.byteLength(body);
break;
}
}
}
return { body, headers };
},
req.contentIdOrder,
ProcessingDirection.Request,
);
}
proxyReq.path = convertUrlAbsolutePath(req.serviceAbsolute, proxyReq.path);
headers.accept = "multipart/mixed,application/json";
proxyReq.setHeader("accept", headers.accept);
} else {
// Single
proxyReq.method = convertMethod(proxyReq.method);
proxyReq.path = convertUrlAndSetContext(proxyReq.path, req, proxyReq.method);
if (
req.context.serviceRoot &&
(!headers.accept || headers.accept.includes("xml")) &&
req.query.$format !== "json"
) {
req.context.serviceRootAsXML = true;
headers.accept = "application/json";
proxyReq.setHeader("accept", headers.accept);
}
if (isResponseFormatXML(req.context, headers)) {
req.context.serviceResponseAsXML = true;
}
if (headers.accept && !headers.accept.includes("application/json")) {
headers.accept = "application/json," + headers.accept;
proxyReq.setHeader("accept", headers.accept);
}
if (headers["sap-messages"]) {
req.context.messages = headers["sap-messages"];
}
if (isXML(contentType)) {
req.context.serviceRequestAsXML = true;
body = convertRequestBodyFromXML(body, req);
contentType = "application/json";
}
if (isApplicationJSON(contentType)) {
if (ieee754Compatible) {
contentType = enrichApplicationJSON(contentType);
}
body = convertRequestBody(body, req.headers, proxyReq.path, req);
}
}
Object.entries(headers).forEach(([name, value]) => {
if (
[
"odata-version",
"Odata-Version",
"dataserviceversion",
"DataServiceVersion",
"maxdataserviceversion",
"MaxDataServiceVersion",
].includes(name)
) {
delete headers[name];
proxyReq.removeHeader(name);
}
});
if (continueOnError) {
headers["prefer"] = "odata.continue-on-error";
proxyReq.setHeader("prefer", "odata.continue-on-error");
}
headers["x-cds-odata-version"] = "v2";
proxyReq.setHeader("x-cds-odata-version", "v2");
if (req.body) {
delete req.body;
}
if (headers["x-http-method"]) {
proxyReq.method = headers["x-http-method"].toUpperCase();
}
proxyReq.method = convertMethod(proxyReq.method);
if (contentType) {
if (body !== undefined) {
// File Upload
if (req.files && Object.keys(req.files).length === 1) {
const file = req.files[Object.keys(req.files)[0]];
contentType = body["content-type"] || file.mimetype;
body = file.data;
}
proxyReq.setHeader("content-type", contentType);
body = normalizeBody(body);
proxyReq.setHeader("content-length", Buffer.byteLength(body));
proxyReq.write(body);
}
}
// Trace
traceRequest(req, "ProxyRequest", proxyReq.method, proxyReq.path, headers, body);
} catch (err) {
// Error
if (err.statusCode === 400) {
logWarn(req, "Request", err);
} else {
logError(req, "Request", err);
}
// Trace
logWarn(req, "Request", "Request with Error", {
method: req.method,
url: req.url,
target,
});
if (err.statusCode) {
res.status(err.statusCode).send(err.message);
} else {
res.status(500).send("Internal Server Error");
}
}
}
function convertMethod(method) {
return method === "MERGE" ? "PATCH" : method;
}
function isResponseFormatXML(context, headers) {
if (context.$format === "atom") {
return true;
} else if (!headers.accept && !context.$format && defaultFormat === "atom") {
return true;
} else if (
headers.accept &&
headers.accept.includes("xml") &&
(defaultFormat === "atom" || (!headers.accept.includes("json") && !headers.accept.includes("html"))) &&
context.$format !== "json"
) {
return true;
}
return false;
}
function convertUrlAndSetContext(urlPath, req, method) {
const url = parseUrl(urlPath, req);
const definition = lookupContextFromUrl(url, req);
enrichRequest(definition, url, urlPath, req, method);
convertUrl(url, req);
return URL.format(url);
}
function convertUrlAbsolutePath(absolute, path) {
if (absolute && path.startsWith(`/${targetPath}`)) {
return path.substring(targetPath.length + 1);
}
return path;
}
function parseUrl(urlPath, req) {
const url = URL.parse(urlPath, true);
url.pathname = (url.pathname && url.pathname.replace(/%27/g, "'")) || "";
url.pathname = convertUrlAbsolutePath(req.serviceAbsolute, url.pathname);
url.originalUrl = { ...url, query: { ...url.query } };
url.basePath = "";
url.servicePath = "";
url.contextPath = url.pathname;
if (req.base && url.contextPath.startsWith(`/${req.base}`)) {
url.basePath = `/${req.base}`;
url.contextPath = url.contextPath.substring(url.basePath.length);
}
if (url.contextPath.startsWith(`/${req.servicePath}`)) {
url.servicePath = `/${req.servicePath}`;
url.contextPath = url.contextPath.substring(url.servicePath.length);
}
if (url.contextPath.startsWith("/")) {
url.servicePath += "/";
url.contextPath = url.contextPath.substring(1);
}
url.originalUrl.servicePath = url.servicePath;
url.originalUrl.contextPath = url.contextPath;
// Normalize system and reserved query parameters (no array), others not (if array)
Object.keys(url.query || {}).forEach((name) => {
if (Array.isArray(url.query[name])) {
if (name.startsWith("$") || ["search", "SideEffectsQualifier"].includes(name)) {
url.query[name] = url.query[name][0];
}
}
});
return url;
}
function lookupContextFromUrl(url, req) {
req.lookupContext = {};
return contextFromUrl(url, req);
}
function contextFromUrl(url, req, context, suppressWarning) {
let stop = false;
return url.contextPath.split("/").reduce((context, part) => {
if (stop || !part) {
return context;
}
const keyStart = part.indexOf("(");
if (keyStart !== -1) {
part = part.substring(0, keyStart);
}
context = lookupContext(part, context, req, suppressWarning, url.contextPath);
if (!context) {
stop = true;
}
return context;
}, context);
}
function lookupContext(name, context, req, suppressWarning, path) {
if (!name) {
return context;
}
name = decodeURIComponent(name);
if (!context) {
if (name.startsWith("$") && req.contentId[name]) {
return contextFromUrl(req.contentId[name], req, undefined, suppressWarning);
} else {
context = lookupDefinition(name, req);
if (!context) {
context = lookupBoundDefinition(name, req);
}
if (!context) {
context = lookupParametersDefinition(name, req);
}
enhanceParametersDefinition(context, req);
if (!context) {
if (suppressWarning === "debug") {
logDebug(req, "Context", "Invalid definition", {
name,
path,
});
} else if (!suppressWarning) {
logWarn(req, "Context", "Invalid definition", {
name,
path,
});
}
}
if (context && (context.kind === "function" || context.kind === "action")) {
req.lookupContext.operation = context;
const returnDefinition = lookupReturnDefinition(context.returns, req);
if (returnDefinition) {
req.lookupContext.returnDefinition = returnDefinition;
}
}
return context;
}
} else {
if (name.startsWith("$")) {
return context;
}
if (context.kind === "function" || context.kind === "action") {
req.lookupContext.operation = context;
const returnDefinition = lookupReturnDefinition(context.returns, req);
if (returnDefinition) {
context = returnDefinition;
req.lookupContext.returnDefinition = context;
}
}
const element = definitionElements(context)[name];
if (element) {
const type = elementType(element, req);
if (type === "cds.Composition" || type === "cds.Association") {
// Navigation
return element._target;
} else {
// Element
return context;
}
}
if (context && context.kind === "entity" && context.params && ["Set", "Parameters"].includes(name)) {
return context;
}
if (suppressWarning === "debug") {
logDebug(req, "Context", "Invalid sub-definition", {
name,
path,
});
} else if (!suppressWarning) {
logWarn(req, "Context", "Invalid sub-definition", {
name,
path,
});
}
}
}
function enrichRequest(definition, url, urlPath, req, method) {
req.context = {
method,
url,
urlPath,
serviceRoot: url.contextPath.length === 0,
serviceRootAsXML: false,
serviceRequestAsXML: false,
serviceResponseAsXML: false,
definition: definition,
definitionElements: definitionElements(definition),
requestDefinition: definition,
serviceUri: "",
operation: null,
operationNested: false,
boundDefinition: null,
returnDefinition: null,
bodyParameters: {},
selects: [],
$entityValue: false,
$value: false,
$count: false,
$apply: null,
$format: null,
aggregationKey: false,
aggregationFilter: "",
parameters: null,
messages: null,
expandSiblingEntity: false,
...req.lookupContext,
};
req.contexts.push(req.context);
return req.context;
}
function convertUrl(url, req) {
// Order is important
convertFormat(url, req);
convertUrlLinks(url, req);
convertUrlDataTypes(url, req);
convertUrlCount(url, req);
convertDraft(url, req);
convertActionFunction(url, req);
convertFilter(url, req);
convertExpandSelect(url, req);
convertSearch(url, req);
convertAnalytics(url, req);
convertValue(url, req);
convertParameters(url, req);
delete url.search;
url.pathname = url.basePath + url.servicePath + url.contextPath;
}
function convertFormat(url, req) {
req.context.$format = url.query.$format;
if (url.query.$format === "atom") {
delete url.query.$format;
}
}
function convertUrlLinks(url, req) {
url.contextPath = url.contextPath.replace(/\/\$links\//gi, "/");
}
function convertUrlDataTypes(url, req) {
// Keys & Parameters
let context;
let stop = false;
url.contextPath = url.contextPath
.split("/")
.map((part) => {
if (stop || !part) {
return part;
}
let name = part;
let keyPart = "";
const keyStart = part.indexOf("(");
const keyEnd = part.lastIndexOf(")");
if (keyStart !== -1 && keyEnd === part.length - 1) {
name = part.substring(0, keyStart);
keyPart = part.substring(keyStart + 1, keyEnd);
}
context = lookupContext(name, context, req, false, url.contextPath);
if (!context) {
stop = true;
}
const contextElements = definitionElements(context);
const contextKeys = definitionKeys(context);
if (context && keyPart) {
const isAggregation = convertAggregationKey(keyPart, url, req);
if (isAggregation) {
return name;
}
const keys = decodeURIComponent(keyPart).split(",");
return (
name +
encodeURIComponent(
`(${keys
.map((key) => {
const [name, value] = key.split("=");
let type;
if (name && value) {
if (context.params && context.params[name]) {
type = context.params[name].type;
}
if (!type) {
type = elementType(contextElements[name], req);
}
return `${name}=${replaceConvertDataTypeToV4(value, type)}`;
} else if (name) {
const key = Object.keys(contextKeys).find((key) => {
return contextKeys[key].type !== "cds.Composition" && contextKeys[key].type !== "cds.Association";
});
type = key && elementType(contextElements[key], req);
return type && `${replaceConvertDataTypeToV4(name, type)}`;
}
return "";
})
.filter((part) => !!part)
.join(",")})`,
)
);
} else {
return part;
}
})
.join("/");
// Query
Object.keys(url.query).forEach((name) => {
if (name === "$filter") {
url.query[name] = convertUrlDataTypesForFilter(url.query[name], context, req);
} else if (!name.startsWith("$")) {
const contextElements = definitionElements(context);
if (contextElements[name]) {
const element = contextElements[name];
const type = elementType(element, req);
if (DataTypeMap[type]) {
url.query[name] = replaceConvertDataTypeToV4(url.query[name], type);
}
}
if (context && (context.kind === "function" || context.kind === "action")) {
if (context.params && context.params[name]) {
const element = context.params[name];
const type = elementType(element, req);
if (DataTypeMap[type]) {
url.query[name] = replaceConvertDataTypeToV4(url.query[name], type);
}
} else if (context.parent && context.parent.kind === "entity") {
const parentElements = definitionElements(context.parent);
if (parentElements[name]) {
const element = parentElements[name];
const type = elementType(element, req);
if (DataTypeMap[type]) {
url.query[name] = replaceConvertDataTypeToV4(url.query[name], type);
}
}
}
}
}
});
}
function buildQuoteParts(input) {
let quote = false;
let quoteEscape = false;
let quoteTypeStart = false;
let part = "";
const parts = [];
input.split("").forEach((char, index) => {
part += char;
if (char === "'") {
if (quote) {
if (quoteEscape) {
quoteEscape = false;
return;
}
const nextChar = input.substring(index + 1, index + 2);
if (nextChar === "'") {
quoteEscape = true;
return;
}
}
const typeStart = !!Object.keys(DataTypeMap)
.filter((type) => !["cds.String", "cds.LargeString"].includes(type))
.find((type) => {
const v2Pattern = DataTypeMap[type].v2;
return v2Pattern.includes("'") && part.endsWith(v2Pattern.split("'").shift() + "'");
});
if (!typeStart && !quoteTypeStart) {
if (part.length > 0) {
parts.push({
content: part,
quote: quote,
});
part = "";
}
quote = !quote;
}
quoteTypeStart = typeStart;
}
});
if (part.length > 0) {
parts.push({
content: part,
quote: quote,
});
}
return parts;
}
function convertAggregationKey(keyPart, url, req) {
const aggregationMatch =
keyPart.match(/^aggregation'(.*)'$/is) ||
keyPart.match(/^'aggregation'(.*)''$/is) ||
keyPart.match(/^ID__='aggregation'(.*)''$/is);
const aggregationKey = aggregationMatch && aggregationMatch.pop();
if (aggregationKey) {
// Aggregation Key
try {
const aggregation = JSON.parse(decodeURIKey(aggregationKey));
if (url.query["$select"]) {
url.query["$select"] += "," + (aggregation.value || []).join(",");
} else {
url.query["$select"] = (aggregation.value || []).join(",");
}
delete url.query["$filter"];
if (Object.keys(aggregation.key || {}).length > 0) {
url.query["$filter"] = Object.keys(aggregation.key || {})
.map((name) => {
return `${name} eq ${aggregation.key[name]}`;
})
.join(" and ");
}
if (aggregation.filter) {
if (!url.query["$filter"]) {
url.query["$filter"] = aggregation.filter;
} else {
url.query["$filter"] = `(${url.query["$filter"]}) and (${aggregation.filter})`;
}
req.context.aggregationFilter = aggregation.filter;
}
if (aggregation.search) {
url.query["search"] = aggregation.search;
req.context.aggregationSearch = aggregation.search;
}
req.context.aggregationKey = true;
} catch (err) {
// Error
logError(req, "AggregationKey", err);
}
return true;
}
return false;
}
function convertUrlDataTypesForFilter(filter, context, req) {
if (filter === null || filter === undefined) {
return filter;
}
return buildQuoteParts(filter)
.map((part) => {
if (!part.quote) {
convertUrlDataTypesForFilterElements(part, context, req);
}
return part.content;
})
.join("");
}
function convertUrlDataTypesForFilterElements(part, entity, req, path = "", depth = 0) {
const elements = definitionElements(entity);
for (const name of Object.keys(elements)) {
const namePath = (path ? `${path}/` : "") + name;
if (part.content.includes(namePath)) {
const element = elements[name];
const type = elementType(element, req);
if (type !== "cds.Composition" && type !== "cds.Association") {
if (DataTypeMap[type]) {
const v4Regex = new RegExp(
`(${namePath})(\\)?\\s+?(?:eq|ne|gt|ge|lt|le)\\s+?)${DataTypeMap[type].v4.source}`,
DataTypeMap[type].v4.flags,
);
if (v4Regex.test(part.content)) {
part.content = part.content.replace(v4Regex, (_, name, op, value) => {
return `${name}${op}${convertDataTypeToV4(value, type)}`;
});
}
}
} else if (depth < 3 && (!element.cardinality || element.cardinality.max !== "*")) {
convertUrlDataTypesForFilterElements(part, element._target, req, namePath, depth + 1);
}
}
}
}
function convertUrlCount(url, req) {
if (url.query["$inlinecount"]) {
url.query["$count"] = url.query["$inlinecount"] === "allpages";
req.context.$count = url.query["$count"];
delete url.query["$inlinecount"];
}
return url;
}
function convertDraft(url, req) {
if (
req.context &&
req.context.definition &&
req.context.definition.kind === "action" &&
req.context.definition.params &&
req.context.definition.params.SideEffectsQualifier
) {
url.query.SideEffectsQualifier = url.query.SideEffectsQualifier || "";
}
}
function convertActionFunction(url, req) {
const definition = req.context && (req.context.operation || req.context.definition);
if (!(definition && (definition.kind === "function" || definition.kind === "action"))) {
return;
}
const operationLocalName = localName(definition, req);
let reqContextPathSuffix = "";
if (url.contextPath.startsWith(operationLocalName)) {
reqContextPathSuffix = url.contextPath.substring(operationLocalName.length);
url.contextPath = url.contextPath.substring(0, operationLocalName.length);
}
let queryOptions = { ...url.query };
if (queryOptions.ID__) {
const ID__ = unquoteValue(queryOptions.ID__);
const aggregationMatch = ID__.match(/^aggregation'(.*)'$/is);
if (aggregationMatch) {
const aggregationKey = aggregationMatch && aggregationMatch.pop();
try {
const aggregation = JSON.parse(decodeURIKey(aggregationKey));
queryOptions = { ...queryOptions, ...(aggregation.key || {}) };
url.query.ID__ = ID__;
} catch (err) {
// Error
logError(req, "AggregationKey", err);
}
}
}
// Key Parameters
if (definition.parent && definition.parent.kind === "entity") {
url.contextPath = localName(definition.parent, req);
url.contextPath += `(${Object.keys(definitionKeys(definition.parent))
.reduce((result, name) => {
const parentElements = definitionElements(definition.parent);
const element = parentElements[name];
const type = elementType(element, req);
if (!(type === "cds.Composition" || type === "cds.Association")) {
const value = queryOptions[name];
result.push(`${name}=${quoteParameter(element, encodeURIComponent(value), req)}`);
delete url.query[name];
}
return result;
}, [])
.join(",")})`;
url.contextPath += `/${req.service}.${definition.name}`;
}
// Function Parameters
if (definition.kind === "function") {
url.contextPath += `(${Object.keys(queryOptions)
.reduce((result, name) => {
if (!name.startsWith("$")) {
const element = definition.params && definition.params[name];
if (element) {
let value = queryOptions[name];
if (Array.isArray(value)) {
value = value.map((entry) => {
return quoteParameter(element, encodeURIComponent(entry), req);
});
} else {
value = quoteParameter(element, encodeURIComponent(value), req);
if (element.items && element.items.type) {
value = [value];
}
}
if (Array.isArray(value)) {
result.push(`${name}=@${name}Col`);
value = value.map((entry) => {
return quoteParameter(element, entry, req, '"');
});
url.query[`@${name}Col`] = `[${value}]`;
} else {
result.push(`${name}=${value}`);
}
delete url.query[name];
}
}
return result;
}, [])
.join(",")})`;
}
url.contextPath += reqContextPathSuffix;
// Action Body
if (definition.kind === "action") {
Object.keys(queryOptions).forEach((name) => {
if (!name.startsWith("$")) {
const element = definition.params && definition.params[name];
if (element) {
let value = queryOptions[name];
if (Array.isArray(value)) {
value = value.map((entry) => {
return unescapeSingleQuote(element, unquoteParameter(element, entry, req), req);
});
} else {
value = unescapeSingleQuote(element, unquoteParameter(element, value, req), req);
if (element.items && element.items.type) {
value = [value];
}
}
req.context.bodyParameters[name] = value;
delete url.query[name];
}
}
});
}
}
function unescapeSingleQuote(element, value, req) {
if (
element &&
value &&
typeof value === "string" &&
["cds.String", "cds.LargeString"].includes(elementType(element, req))
) {
return value.replace(/''/g, "'");
}
return value;
}
function quoteParameter(element, value, req, quote = "'") {
if (element && ["cds.String", "cds.LargeString"].includes(elementType(element, req))) {
return `${quote}${unquoteParameter(element, value, req)}${quote}`;
}
return value;
}
function unquoteParameter(element, value, req) {
if (
element &&
value &&
typeof value === "string" &&
[
"cds.String",
"cds.LargeString",
"cds.UUID",
"cds.Binary",
"cds.LargeBinary",
"cds.Date",
"cds.Time",
"cds.DateTime",
"cds.Timestamp",
].includes(elementType(element, req))
) {
return value.replace(/^['](.*)[']$/s, "$1");
}
return value;
}
function unquoteValue(value) {
if (value && typeof value === "string" && value.match(/^['](.*)[']$/s)) {
return value.replace(/^['](.*)[']$/s, "$1").replace(/''/g, "'");
}
return value;
}
function stripSlashes(path) {
return path && path.replace(/^\/|\/$/g, "");
}
function normalizeSlashes(path) {
path = stripSlashes(path);
return path ? `/${path}/` : "/";
}
function convertExpandSelect(url, req) {
const definition = req.context && req.context.definition;
if (definition) {
const context = { select: {}, expand: {} };
if (url.query["$expand"]) {
let expands = url.query["$expand"].split(",");
if (definition.kind === "entity" && definition.params) {
expands = expands.filter((expand) => !["Set", "Parameters"].includes(expand));
}
expands.forEach((expand) => {
if (fixDraftRequests && expand === "SiblingEntity") {
req.context.expandSiblingEntity = true;
return;
}
let current = context.expand;
expand.split("/").forEach((part) => {
current[part] = current[part] || { select: {}, expand: {} };
current = current[part].expand;
});
});
}
if (url.query["$select"]) {
req.context.selects = url.query["$select"].split(",");
req.context.selects.forEach((select) => {
let current = context;
let currentDefinition = definition;
const parts = select.split("/");
parts.forEach((part, index) => {
if (!current) {
return;
}
if (part === "*") {
current.select[part] = true;
} else {
const element = definitionElements(currentDefinition)[part];
if (element) {
current.select[part] = true;
const type = elementType(element, req);
if (type === "cds.Composition" || type === "cds.Association") {
current = current.expand[part];
if (current && index === parts.length - 1) {
current.all = true;
}
currentDefinition = element._target;
}
}
}
});
});
if (Object.keys(context.select).length > 0) {
url.query["$select"] = Object.keys(context.select).join(",");
} else {
delete url.query["$select"];
}
}
if (url.query["$expand"]) {
const serializeExpand = (expand) => {
return Object.keys(expand || {})
.map((name) => {
let value = expand[name];
let result = name;
const selects = value.all ? [] : Object.keys(value.select);
const expands = Object.keys(value.expand);
if (selects.length > 0 || expands.length > 0) {
result += "(";
if (selects.length > 0) {
result += `$select=${selects.join(",")}`;
}
if (expands.length > 0) {
if (selects.length > 0) {
result += ";";
}
result += `$expand=${serializeExpand(value.expand)}`;
}
result += ")";
}
return result;
})
.join(",");
};
if (Object.keys(context.expand).length > 0) {
url.query["$expand"] = serializeExpand(context.expand);
} else {
delete url.query["$expand"];
}
}
}
}
function convertFilter(url, req) {
const _ = "§§";
let filter = url.query["$filter"];
if (filter) {
// Fix unsupported draft requests
if (fixDraftRequests) {
const match = filter.match(UnsupportedDraftFilterRegex);
if (match && match.length === 3 && match[1] === match[2]) {
filter = filter.replace(match[0], encodeReplaceValue(match[1]));
url.query["$filter"] = filter;
}
}
// Convert functions
let quote = false;
let lookBehind = "";
let bracket = 0;
let brackets = [];
let bracketMax = 0;
filter = filter
.split("")
.map((char) => {
if (char === "'") {
quote = !quote;
}
if (!quote) {
if (char === "(") {
bracket++;
const filterFunctionStart = !!Object.keys(FilterFunctions).find((name) => {
return lookBehind.endsWith(name.split("(").shift());
});
if (filterFunctionStart) {
brackets.push(bracket - 1);
bracketMax = Math.max(bracketMax, brackets.length);
return `${_}(${brackets.length}${_}`;
}
} else if (char === ")") {
bracket--;
const [mark] = brackets.slice(-1);
if (mark === bracket) {
brackets.pop();
return `${_})${brackets.length + 1}${_}`;
}
} else if (char === ",") {
if (brackets.length > 0) {
return `${_},${brackets.length}${_}`;
}
}
lookBehind += char;
} else {
lookBehind = "";
}
return char;
})
.join("");
if (bracketMax > 0) {
for (let i = 1; i <= bracketMax; i++) {
Object.keys(FilterFunctions).forEach((name) => {
const placeholders = (name.match(/[$]/g) || []).length;
const pattern = name
.replace(/([()])/g, `${_}\\$1${i}${_}`)
.replace(/([,])/g, `${_}$1${i}${_}`)
.replace(/[$]/g, "(.*?)");
filter = filter.replace(new RegExp(pattern, "gi"), (...args) => {
let result = FilterFunctions[name];
for (let j = 1; j <= placeholders; j++) {
if (args[j] !== undefined) {
result = result.replace(`$${j}`, encodeReplaceValue(args[j].trim()));
}
}
return result;
});
});
filter = filter.replace(new RegExp(`${_}([(),])${i}${_}`, "g"), "$1");
}
url.query["$filter"] = filter;
}
}
}
function convertSearch(url, req) {
if (url.query.search) {
let search = url.query.search;
if (quoteSearch || (!/^".*"$/s.test(search) && search.includes('"'))) {
search = `"${search.replace(/\\/g, "\\\\").replace(/"/g, `\\"`)}"`;
}
url.query["$search"] = search;
delete url.query.search;
}
}
function convertAnalytics(url, req) {
const definition = req.context && req.context.definition;
if (!(isAnalyticsEntity(definition) && url.query["$select"])) {
return;
}
const elements = req.context.definitionElements;
const keyDimensions = [];
for (const name of Object.keys(elements)) {
const element = elements[name];
if (isDimensionElement(element) && element.key) {
keyDimensions.push(element);
}
}
const measures = [];
const dimensions = [];
const selects = url.query["$select"].split(",");
const values = [];
selects.forEach((select) => {
const element = elements[select];
if (element) {
values.push(element);
}
});
selects.forEach((select) => {
const element = elements[select];
if (element) {
if (isMeasureElement(element)) {
measures.push(element);
} else {
// isDimensionElement(element)
dimensions.push(element);
}
}
});
if (
definition["@cov2ap.analytics.skipForKey"] &&
keyDimensions.every((keyDimension) => dimensions.includes(keyDimension))
) {
return;
}
if (dimensions.length > 0 || measures.length > 0) {
url.query["$apply"] = "";
if (dimensions.length) {
url.query["$apply"] = "groupby(";
url.query["$apply"] += `(${dimensions
.map((dimension) => {
return dimension.name;
})
.join(",")})`;
}
if (measures.length > 0) {
if (url.query["$apply"]) {
url.query["$apply"] += ",";
}
url.query["$apply"] += `aggregate(${measures
.map((measure) => {
const aggregation = aggregationDefault(measure);
const aggregationName = aggregation ? aggregation["#"] || aggregation : DefaultAggregation;
const aggregationFunction = aggregationName ? AggregationMap[aggregationName.toUpperCase()] : undefined;
if (!aggregationFunction) {
throw new Error(`Aggregation '${aggregationName}' is not supported`);
}
if ([AggregationMap.NONE, AggregationMap.NOP].includes(aggregationFunction)) {
return null;
}
if (aggregationFunction.startsWith("$")) {
return `${aggregationFunction} as ${AggregationPrefix}${measure.name}`;
} else {
return `${referenceElement(measure) || measure.name} with ${aggregationFunction} as ${AggregationPrefix}${
measure.name
}`;
}
})
.filter((aggregation) => !!aggregation)
.join(",")})`;
}
if (dimensions.length) {
url.query["$apply"] += ")";
}
const filter = url.query["$filter"];
if (filter) {
url.query["$apply"] = `filter(${filter})/` + url.query["$apply"];
}
const search = url.query["$search"];
if (search) {
url.query["$apply"] = `search(${search})/` + url.query["$apply"];
}
if (url.query["$orderby"]) {
url.query["$orderby"] = url.query["$orderby"]
.split(",")
.map((orderBy) => {
let [name, order] = orderBy.split(" ");
const element = elements[name];
if (element && isMeasureElement(element)) {
name = `${AggregationPrefix}${element.name}`;
}
return name + (order ? ` ${order}` : "");
})
.join(",");
}
delete url.query["$filter"];
delete url.query["$select"];
delete url.query["$expand"];
delete url.query["$search"];
req.context.$apply = {
key: dimensions,
value: values,
filter: req.context.aggregationKey ? req.context.aggregationFilter : filter,
search: req.context.aggregationKey ? req.context.aggregationSearch : search,
};
}
}
function isAnalyticsEntity(entity) {
return (
entity &&
entity.kind === "entity" &&
entity["@cov2ap.analytics"] !== false &&
(entity["@cov2ap.analytics"] ||
entity["@cov2ap.analytics.skipForKey"] ||
entity["@Analytics"] ||
entity["@Analytics.AnalyticalContext"] ||
entity["@Analytics.query"] ||
entity["@AnalyticalContext"] ||
entity["@Aggregation.ApplySupported.PropertyRestrictions"] ||
entity["@sap.semantics"] === "aggregate")
);
}
function isDimensionElement(element) {
return (
element["@Analytics.AnalyticalContext.Dimension"] ||
element["@AnalyticalContext.Dimension"] ||
element["@Analytics.Dimension"] ||
element["@sap.aggregation.role"] === "dimension"
);
}
function isMeasureElement(element) {
return (
element["@Analytics.AnalyticalContext.Measure"] ||
element["@AnalyticalContext.Measure"] ||
element["@Analytics.Measure"] ||
element["@sap.aggregation.role"] === "measure"
);
}
function aggregationDefault(element) {
return element["@Aggregation.default"] || element["@Aggregation.Default"] || element["@DefaultAggregation"];
}
function referenceElement(element) {
return (
element["@Aggregation.referenceElement"] ||
element["@Aggregation.ReferenceElement"] ||
element["@Aggregation.reference"] ||
element["@Aggregation.Reference"]
);
}
function convertValue(url, req) {
if (url.contextPath.endsWith("/$value")) {
url.contextPath = url.contextPath.substring(0, url.contextPath.length - "/$value".length);
const mediaDataElementName =
req.context &&
req.context.definition &&
findElementByAnnotation(req.context.definitionElements, "@Core.MediaType");
const endingElementName = findEndingElementName(req.context.definitionElements, url);
if (!endingElementName) {
url.contextPath += `/${mediaDataElementName}`;
req.context.$entityValue = true;
} else if (endingElementName !== mediaDataElementName) {
req.context.$value = true;
}
}
}
function convertParameters(url, req) {
if (req.context.parameters) {
let context;
let stop = false;
url.contextPath = url.contextPath
.split("/")
.map((part) => {
if (part === "Set" || part.startsWith("Set(")) {
req.context.parameters.kind = "Set";
stop = true;
} else if (part === "Parameters" || part.startsWith("Parameters(")) {
req.context.parameters.kind = "Parameters";
stop = true;
} else if (part === "$count") {
req.context.parameters.count = true;
}
if (stop || !part) {
return "";
}
let name = part;
let keyPart = "";
const keyStart = part.indexOf("(");
const keyEnd = part.lastIndexOf(")");
if (keyStart !== -1 && keyEnd === part.length - 1) {
name = part.substring(0, keyStart);
keyPart = part.substring(keyStart + 1, keyEnd);
}
if (name === req.context.parameters.type) {
name = req.context.parameters.entity;
}
context = lookupContext(name, context, req, false, url.contextPath);
if (!context) {
stop = true;
}
const contextElements = definitionElements(context);
if (context && keyPart) {
const keys = decodeURIComponent(keyPart).split(",");
return (
name +
encodeURIComponent(
`(${keys
.map((key) => {
const [name, value] = key.split("=");
if (name && value) {
if (context.params[name]) {
req.context.parameters.values[name] = unquoteParameter(context.params[name], value, req);
return `${name}=${value}`;
} else {
req.context.parameters.keys[name] = unquoteParameter(contextElements[name], value, req);
}
} else if (name) {
const param = Object.keys(context.params).find(() => true);
if (param) {
if (context.params[param]) {
req.context.parameters.values[param] = unquoteParameter(context.params[param], name, req);
return `${param}=${name}`;
} else {
req.context.parameters.keys[param] = unquoteParameter(contextElements[param], name, req);
}
}
}
return "";
})
.filter((part) => !!part)
.join(",")})`,
)
);
} else {
return name;
}
})
.filter((part) => !!part)
.join("/");
if (!url.contextPath.endsWith("/Set")) {
url.contextPath = `${url.contextPath}/Set`;
}
if (req.context.parameters.count) {
url.contextPath += "/$count";
}
}
}
function convertRequestBody(body, headers, url, req) {
let definition = req.context && req.context.definition;
if (definition) {
if (definition.kind === "action") {
body = Object.assign({}, body, req.context.bodyParameters);
definition = {
elements: definition.params || {},
};
}
if (body.d && typeof body.d === "object") {
body = body.d;
}
convertRequestData(body, headers, definition, req);
}
return JSON.stringify(body);
}
function convertRequestData(data, headers, definition, req) {
if (!Array.isArray(data)) {
return convertRequestData([data], headers, definition, req);
}
const elements = definitionElements(definition);
if (Object.keys(elements).length === 0) {
return;
}
// Modify Payload
data.forEach((data) => {
if (!isObject(data)) {
return;
}
delete data.__metadata;
delete data.__count;
convertDataTypesToV4(data, headers, definition, elements, req);
});
// Recursion
data.forEach((data) => {
if (!isObject(data)) {
return;
}
Object.keys(data).forEach((key) => {
const element = elements[key];
if (!element) {
return;
}
const type = elementType(element, req);
if (!type) {
if (element.items) {
convertRequestData(data[key], headers, element.items, req);
} else if (element.elements) {
convertRequestData(data[key], headers, element, req);
}
}
if (element && (type === "cds.Composition" || type === "cds.Association")) {
if (data[key] && data[key].__deferred) {
delete data[key];
} else {
if (data[key] !== null) {
if (Array.isArray(data[key].results)) {
data[key] = data[key].results;
}
convertRequestData(data[key], headers, element._target, req);
} else {
delete data[key];
}
}
}
});
});
}
function convertDataTypesToV4(data, headers, definition, elements, req) {
Object.keys(data || {}).forEach((key) => {
if (data[key] === null) {
return;
}
const element = elements[key];
if (element) {
data[key] = convertDataTypeToV4(data[key], elementType(element, req), definition, headers);
}
});
}
function replaceConvertDataTypeToV4(value, type, definition, headers = {}) {
if (value === null || value === undefined) {
return value;
}
if (Array.isArray(value)) {
return value.map((entry) => {
return replaceConvertDataTypeToV4(entry, type, definition, headers);
});
}
if (DataTypeMap[type] && typeof value === "string") {
value = value.replace(DataTypeMap[type].v4, "$1");
}
return convertDataTypeToV4(value, type, definition, headers);
}
function convertDataTypeToV4(value, type, definition, headers = {}) {
if (value === null || value === undefined) {
return value;
}
if (Array.isArray(value)) {
return value.map((entry) => {
return convertDataTypeToV4(entry, type, definition, headers);
});
}
const contentType = headers["content-type"];
const contentTypeIEEE754Compatible = contentType && contentType.includes(IEEE754Compatible);
if (["cds.Boolean"].includes(type)) {
if (value === "true") {
value = true;
} else if (value === "false") {
value = false;
}
} else if (["cds.UInt8", "cds.Int16", "cds.Int32", "cds.Integer"].includes(type)) {
value = parseInt(value, 10);
} else if (["cds.Int64", "cds.Integer64", "cds.Decimal", "cds.DecimalFloat"].includes(type)) {
value = ieee754Compatible || contentTypeIEEE754Compatible ? `${value}` : parseFloat(value);
} else if (["cds.Double"].includes(type)) {
value = parseFloat(value);
} else if (["cds.Time"].includes(type)) {
const match = value.match(DurationRegex);
if (match) {
value = `${match[4] || "00"}:${match[5] || "00"}:${match[6] || "00"}`;
}
} else if (["cds.Date", "cds.DateTime", "cds.Timestamp"].includes(type)) {
const match = value.match(/\\?\/Date\((.*)\)\\?\//is);
const ticksAndOffset = match && match.pop();
if (ticksAndOffset !== undefined && ticksAndOffset !== null) {
value = new Date(calculateTicksOffsetSum(ticksAndOffset)).toISOString(); // always UTC
}
if (value) {
if (["cds.DateTime"].includes(type)) {
value = value.slice(0, 19) + "Z"; // Cut millis
} else if (["cds.Date"].includes(type)) {
value = value.slice(0, 10); // Cut time
} else if (["cds.Timestamp"].includes(type)) {
if (!value.endsWith("Z")) {
value += "Z";
}
}
}
}
return value;
}
function calculateTicksOffsetSum(text) {
return (text.replace(/\s/g, "").match(/[+-]?([0-9]+)/g) || []).reduce((sum, value, index) => {
return sum + parseFloat(value) * (index === 0 ? 1 : 60 * 1000); // ticks are milliseconds (0), offset are minutes (1)
}, 0);
}
function convertRequestBodyFromXML(body, req) {
if (typeof body === "string") {
xmlParser.parseString(body, (err, xml) => {
if (err) {
throw err;
}
body = xml;
});
}
return convertRequestDataFromXML(body && body.entry, req);
}
function convertRequestDataFromXML(data, req) {
const result = {};
if (data && data.content && data.content[0] && data.content[0].properties && data.content[0].properties[0]) {
const properties = data.content[0].properties[0];
Object.keys(properties).forEach((name) => {
const property = properties[name] && properties[name][0];
if (typeof property === "object") {
if (property._ !== undefined) {
result[name] = property._;
}
if (property.$ !== undefined) {
Object.keys(property.$).forEach((key) => {
if (key.endsWith("null") && property.$[key] === "true") {
result[name] = null;
}
});
}
} else {
result[name] = property;
}
return result[name];
});
}
if (data && data.link) {
data.link.forEach((link) => {
if (link.$ && link.$.rel && link.inline && link.inline[0]) {
const name = link.$.rel.split("/").pop();
if (link.inline[0].feed && link.inline[0].feed[0]) {
result[name] = result[name] || [];
(link.inline[0].feed[0].entry || []).forEach((entry) => {
result[name].push(convertRequestDataFromXML(entry, req));
});
} else if (link.inline[0].entry && link.inline[0].entry[0]) {
result[name] = convertRequestDataFromXML(link.inline[0].entry[0], req);
} else {
result[name] = null;
}
}
});
}
return result;
}
function initContext(req, index = 0) {
req.context = req.contexts[index] || {};
return req.context.definition && req.context.definition.kind === "entity"
? req.context.definition
: req.context.boundDefinition;
}
/**
* Convert Proxy Response (V4 -> V2)
* @param proxyRes Proxy Request
* @param req Request
* @param res Response
*/
async function convertProxyResponse(proxyRes, req, res) {
try {
req.context = {};
let statusCode = proxyRes.statusCode;
let headers = proxyRes.headers;
if (statusCode < 400 && req.overwriteResponse) {
statusCode = req.overwriteResponse.statusCode;
headers = {
...req.overwriteResponse.headers,
...headers,
};
}
normalizeContentType(headers);
// Pipe Binary Stream
const contentType = headers["content-type"];
const transferEncoding = headers["transfer-encoding"] || "";
if (transferEncoding.includes("chunked") && !isApplicationJSON(contentType) && !isMultipartMixed(contentType)) {
return await processStreamResponse(proxyRes, req, res, headers);
}
// Body
let body = await parseProxyResponseBody(proxyRes, headers, req);
if (statusCode < 400 && req.overwriteResponse) {
body = {
...req.overwriteResponse.body,
...body,
};
}
// Trace
traceResponse(req, "ProxyResponse", proxyRes.statusCode, proxyRes.statusMessage, headers, body);
delete headers["content-encoding"];
convertBasicHeaders(headers);
if (body && statusCode < 400) {
if (isMultipartMixed(contentType)) {
// Multipart
const resContentIdOrder = [];
body = processMultipartMixed(
req,
body,
contentType,
null,
({ index, statusCode, contentType, body, headers }) => {
const serviceDefinition = initContext(req, index);
if (statusCode < 400) {
convertHeaders(body, headers, serviceDefinition, req);
if (body && isApplicationJSON(contentType)) {
if (statusCode === 204) {
body = "";
} else {
body = convertResponseBody(Object.assign({}, body), headers, req);
}
}
} else {
convertHeaders(body, headers, serviceDefinition, req);
body = convertResponseError(body, headers, serviceDefinition, req);
}
let statusCodeText;
if (
req.context.method === "GET" &&
statusCode !== 204 &&
isApplicationJSON(contentType) &&
isEmptyJSON(body)
) {
statusCode = 404;
body = notFoundErrorResponse();
statusCodeText = body.error.message;
body = convertResponseError(body, headers, serviceDefinition, req);
}
if (body && statusCode !== 204) {
setContentLength(headers, body);
} else {
clearContentLength(headers);
}
return { statusCode, statusCodeText, body, headers };
},
resContentIdOrder,
ProcessingDirection.Response,
);
if (
!(
req.contentIdOrder.length === resContentIdOrder.length &&
req.contentIdOrder.every((contentId, index) => contentId === resContentIdOrder[index])
)
) {
log(req, changesetDeviationLogLevel, "Batch", "Changeset order deviation", {
req: req.contentIdOrder,
res: resContentIdOrder,
});
}
if (statusCode === 200) {
// OData V4: 200 => OData V2: 202
statusCode = 202;
}
} else {
// Single
const serviceDefinition = initContext(req);
convertHeaders(body, headers, serviceDefinition, req);
if (isApplicationJSON(contentType)) {
body = convertResponseBody(Object.assign({}, body), headers, req);
}
}
} else {
// No body or failed
const serviceDefinition = initContext(req);
convertHeaders(body, headers, serviceDefinition, req);
body = convertResponseError(body, headers, serviceDefinition, req);
}
if (req.method === "GET" && statusCode !== 204 && isApplicationJSON(contentType) && isEmptyJSON(body)) {
statusCode = 404;
const serviceDefinition = initContext(req);
body = convertResponseError(notFoundErrorResponse(), headers, serviceDefinition, req);
}
if (body && !(headers["transfer-encoding"] || "").includes("chunked") && statusCode !== 204) {
setContentLength(headers, body);
} else {
clearContentLength(headers);
}
respond(req, res, statusCode, headers, body);
} catch (err) {
// Error
if (err.statusCode === 400) {
logWarn(req, "Response", err);
} else {
logError(req, "Response", err);
}
if (proxyRes.body && proxyRes.body.error) {
respond(
req,
res,
proxyRes.statusCode,
proxyRes.headers,
convertResponseError(proxyRes.body, proxyRes.headers, undefined, req),
);
} else {
if (res.writeHead && !res.headersSent) {
res.writeHead(500);
}
if (!res.writableEnded) {
res.end("Internal Server Error");
}
}
}
}
async function processStreamResponse(proxyRes, req, res, headers) {
// Trace
traceResponse(req, "ProxyResponse", proxyRes.statusCode, proxyRes.statusMessage, headers, {});
let streamRes = proxyRes;
convertBasicHeaders(headers);
const context = req.contexts && req.contexts[0];
if (context && context.definition && context.definitionElements) {
const mediaDataElementName = findElementByAnnotation(context.definitionElements, "@Core.MediaType");
if (mediaDataElementName) {
const parts = proxyRes.req.path.split("/");
if (parts[parts.length - 1] === "$value" || parts[parts.length - 1].startsWith("$value?")) {
parts.pop();
}
if (parts[parts.length - 1] === mediaDataElementName) {
parts.pop();
}
// Is Url
const urlElement =
findElementByAnnotation(context.definitionElements, "@Core.IsURL") ||
findElementByAnnotation(context.definitionElements, "@Core.IsUrl");
if (urlElement) {
const mediaResponse = await fetch(target + parts.join("/"), {
method: "GET",
headers: propagateHeaders(req, {
accept: "application/json",
}),
});
if (!mediaResponse.ok) {
throw new Error(await mediaResponse.text());
}
if (mediaResponse) {
const mediaResult = await mediaResponse.json();
const mediaReadLink = mediaResult[`${urlElement}@odata.mediaReadLink`];
if (mediaReadLink) {
try {
const mediaResponse = await fetch(mediaReadLink, {
method: "GET",
headers: propagateHeaders(req),
});
res.status(mediaResponse.status);
headers = convertBasicHeaders(convertToNodeHeaders(mediaResponse.headers));
streamRes = mediaResponse.body;
} catch (err) {
logError(req, "MediaStream", err);
const headers = { "content-type": "application/json" };
const errorBody = convertResponseError(
{ error: { message: err.message } },
headers,
context.definition,
req,
);
respond(req, res, 500, headers, errorBody);
return;
}
}
}
} else {
if (!headers["content-disposition"] || calcContentDisposition) {
// Is Binary
const contentDispositionFilenameElement =
findElementValueByAnnotation(context.definitionElements, "@Core.ContentDisposition.Filename") ||
findElementValueByAnnotation(context.definitionElements, "@Common.ContentDisposition.Filename");
if (contentDispositionFilenameElement) {
const contentDispositionTypeValue =
findElementValueByAnnotation(context.definitionElements, "@Core.ContentDisposition.Type") ||
findElementValueByAnnotation(context.definitionElements, "@Common.ContentDisposition.Type") ||
contentDisposition;
const response = await fetch(target + [...parts, contentDispositionFilenameElement, "$value"].join("/"), {
method: "GET",
headers: propagateHeaders(req, {
accept: "application/json,*/*",
}),
});
if (response.ok) {
const filename = await response.text();
if (filename) {
headers["content-disposition"] = `${contentDispositionTypeValue}; filename="${encodeURIComponent(
filename,
)}"`;
}
} else {
logWarn(req, "ContentDisposition", await response.text());
}
}
}
}
}
}
delete headers["content-encoding"];
Object.entries(headers).forEach(([name, value]) => {
res.setHeader(name, value);
});
pipeline(streamRes, res, (err) => {
if (err) {
logWarn(req, "StreamResponse", err);
}
});
// Trace
traceResponse(req, "Response", res.statusCode, res.statusMessage, headers, {});
}
async function parseProxyResponseBody(proxyRes, headers, req) {
const contentType = headers["content-type"];
let bodyParser;
if (req.method === "HEAD") {
bodyParser = null;
} else if (isApplicationJSON(contentType)) {
bodyParser = express.json({ limit: bodyParserLimit });
} else if (isPlainText(contentType) || isXML(contentType)) {
bodyParser = express.text({ type: () => true, limit: bodyParserLimit });
} else if (isMultipartMixed(contentType)) {
bodyParser = express.text({ type: "multipart/mixed", limit: bodyParserLimit });
}
if (bodyParser) {
await promisify(bodyParser)(proxyRes, null);
return proxyRes.body;
}
}
function convertBasicHeaders(headers) {
delete headers["odata-version"];
delete headers["OData-Version"];
delete headers["odata-entityid"];
delete headers["OData-EntityId"];
headers.dataserviceversion = "2.0";
return headers;
}
function convertHeaders(body, headers, serviceDefinition, req) {
convertBasicHeaders(headers);
const definition = contextFromBody(body, req);
if (definition && definition.kind === "entity") {
const elements = definitionElements(definition);
convertLocation(body, headers, definition, elements, req);
}
convertMessages(body, headers, definition && definition.kind === "entity" ? definition : serviceDefinition, req);
return headers;
}
function convertLocation(body, headers, definition, elements, req) {
if (headers.location || headers.Location) {
headers.location = entityUri(body, definition, elements, req);
}
delete headers.Location;
}
function convertToUnicode(string) {
return string.replace(/[\u007F-\uFFFF]/g, (chr) => {
const unicode = "0000" + chr.charCodeAt(0).toString(16);
return "\\u" + unicode.substring(unicode.length - 4);
});
}
function convertMessages(body, headers, definition, req) {
if (headers["sap-messages"]) {
let messages = JSON.parse(headers["sap-messages"]);
if (messages && messages.length > 0) {
if (req.context.messages === "transientOnly") {
messages = messages.filter((message) => {
return (
!message.target ||
message.target === MessageTargetTransient ||
message.target.startsWith(MessageTargetTransientPrefix)
);
});
}
if (messages && messages.length > 0) {
const rootMessage = messages.shift();
rootMessage.details = Array.isArray(rootMessage.details) ? rootMessage.details : [];
rootMessage.details.push(...messages);
const message = convertMessage(rootMessage, definition, req);
headers["sap-message"] = convertToUnicode(JSON.stringify(message));
}
}
delete headers["sap-messages"];
}
}
function convertMessage(message, definition, req, contentID) {
if (!message) {
return message;
}
message.severity = SeverityMap[message["@Common.numericSeverity"] || message.numericSeverity] || "error";
delete message.numericSeverity;
delete message["@Common.numericSeverity"];
if (message.target) {
message.target = convertMessageTarget(message.target, req, definition);
} else if (message.target === undefined && messageTargetDefault) {
message.target = messageTargetDefault;
}
if (Array.isArray(message["@Common.additionalTargets"])) {
message.additionalTargets = message["@Common.additionalTargets"].map((messageTarget) => {
return convertMessageTarget(messageTarget, req, definition);
});
}
delete message["@Common.additionalTargets"];
contentID = message["@Core.ContentID"] || contentID;
message.ContentID = contentID;
delete message["@Core.ContentID"];
if (Array.isArray(message.details)) {
message.details = message.details.map((detail) => {
return convertMessage(detail, definition, req, contentID);
});
if (propagateMessageToDetails) {
const propagatedDetailMessage = Object.assign({}, message);
delete propagatedDetailMessage.details;
message.details.unshift(propagatedDetailMessage);
}
message.details.forEach((detail) => {
if (detail.code && detail.code.toLowerCase().includes("transition")) {
detail.transition = true;
}
});
}
return message;
}
function convertMessageTarget(messageTarget, req, definition) {
if (!messageTarget) {
return messageTarget;
}
if (messageTarget === MessageTargetTransient) {
return messageTarget;
}
if (req.context.operation && req.context.boundDefinition) {
const bindingParameterName = req.context.operation["@cds.odata.bindingparameter.name"] || "in";
if (messageTarget.startsWith(`${bindingParameterName}/`)) {
messageTarget = messageTarget.substring(bindingParameterName.length + 1);
}
}
let transientTarget = false;
if (messageTarget.startsWith(MessageTargetTransientPrefix)) {
messageTarget = messageTarget.substring(MessageTargetTransientPrefix.length);
transientTarget = true;
}
if (!/^[_a-z-0-9]*$/i.test(messageTarget)) {
let context;
if (definition && definition.kind === "entity") {
context = definition;
}
if (!context && req.context.returnDefinition && req.context.returnDefinition.kind === "entity") {
context = req.context.returnDefinition;
}
if (!context && req.lookupContext.returnDefinition && req.lookupContext.returnDefinition.kind === "entity") {
context = req.lookupContext.returnDefinition;
}
let messageContext;
if (
contextFromUrl(
{
contextPath: messageTarget,
query: {},
},
req,
undefined,
true,
)
) {
// Absolute target (no context)
messageContext = undefined;
} else if (
contextFromUrl(
{
contextPath: messageTarget,
query: {},
},
req,
context,
true,
)
) {
// Relative target (valid context)
messageContext = context;
} else {
const rootContext = compositionRoot(context, req);
if (
context &&
contextFromUrl(
{
contextPath: messageTarget,
query: {},
},
req,
rootContext,
true,
)
) {
// Relative target (composition root context)
messageContext = rootContext;
}
}
messageTarget = convertMessageTargetParts(messageTarget, messageContext, req);
}
return (transientTarget ? MessageTargetTransientPrefix : "") + messageTarget;
}
function convertMessageTargetParts(messageTarget, context, req) {
let stop = false;
return messageTarget
.split("/")
.map((part) => {
if (stop || !part) {
return part;
}
let name = part;
let keyPart = "";
const keyStart = part.indexOf("(");
const keyEnd = part.lastIndexOf(")");
if (keyStart !== -1 && keyEnd === part.length - 1) {
name = part.substring(0, keyStart);
keyPart = part.substring(keyStart + 1, keyEnd);
}
context = lookupContext(name, context, req, "debug", messageTarget);
if (!context) {
stop = true;
}
const contextElements = definitionElements(context);
const contextKeys = definitionKeys(context);
if (context && keyPart) {
const keys = keyPart.split(",");
return `${name}(${keys
.map((key) => {
const [name, value] = key.split("=");
let type;
if (name && value) {
if (context.params && context.params[name]) {
type = context.params[name].type;
}
if (!type) {
type = elementType(contextElements[name], req);
}
return `${name}=${replaceConvertDataTypeToV2(value, type, context)}`;
} else if (name) {
const key = Object.keys(contextKeys).find((key) => {
return contextKeys[key].type !== "cds.Composition" && contextKeys[key].type !== "cds.Association";
});
type = key && elementType(contextElements[key], req);
return type && `${replaceConvertDataTypeToV2(name, type, context)}`;
}
return "";
})
.filter((part) => !!part)
.join(",")})`;
} else {
return part;
}
})
.join("/");
}
function convertResponseError(body, headers, definition, req) {
if (!body) {
if (req.context.serviceResponseAsXML) {
headers["content-type"] = "application/xml;charset=utf-8";
}
return body;
}
if (body.error) {
if (body.error.message) {
body.error.message = {
lang: headers["content-language"] || "en",
value: body.error.message,
};
}
body.error = convertMessage(body.error, definition, req);
body.error.innererror = body.error.innererror || {};
body.error.innererror.errordetails = body.error.innererror.errordetails || [];
body.error.innererror.errordetails.push(...(body.error.details || []));
delete body.error.details;
if (body.error.innererror.errordetails.length === 0) {
const singleDetailError = Object.assign({}, body.error);
delete singleDetailError.innererror;
delete singleDetailError.details;
if (body.error.innererror.errordetails.length === 0) {
body.error.innererror.errordetails.push(singleDetailError);
}
}
}
if (typeof body === "object") {
if (req.context.serviceResponseAsXML) {
body = convertResponseBodyToXML(body, req);
headers["content-type"] = "application/atom+xml;charset=utf-8";
} else {
body = JSON.stringify(body);
}
}
body = `${body}`;
setContentLength(headers, body);
return body;
}
function convertResponseBody(proxyBody, headers, req) {
let body = {
d: {},
};
if (req.context.serviceRoot && proxyBody.value) {
if (req.context.serviceRootAsXML) {
return convertResponseServiceRootToXML(proxyBody, headers, req);
} else {
body.d.EntitySets = proxyBody.value.map((entry) => {
return entry.name;
});
}
} else {
// Context from Body
const definition = contextFromBody(proxyBody, req);
if (definition) {
req.context.requestDefinition = req.context.definition;
req.context.definition = definition;
req.context.definitionElements = definitionElements(definition);
const elements = req.context.definitionElements;
const definitionElement = contextElementFromBody(proxyBody, req);
if (definitionElement) {
body.d[definitionElement.name] = convertDataTypeToV2(
proxyBody.value,
elementType(definitionElement, req),
definition,
headers,
);
convertResponseElementData(body, headers, definition, elements, proxyBody, req);
if (req.context.$value) {
headers["content-type"] = "text/plain";
return `${body.d[definitionElement.name]}`;
}
} else {
const data = convertResponseList(body, headers, definition, proxyBody, req);
convertResponseData(data, headers, definition, proxyBody, req);
}
} else {
// Context from Request
let definition = req.context.definition;
if (definition && (definition.kind === "function" || definition.kind === "action")) {
const returnDefinition = req.context.returnDefinition;
if (!returnDefinition || returnDefinition.name) {
definition = returnDefinition;
} else {
definition = {
kind: "type",
name: contextNameFromBody(proxyBody),
...returnDefinition,
};
}
req.context.requestDefinition = req.context.definition;
req.context.definition = definition;
req.context.definitionElements = definitionElements(definition);
}
const data = convertResponseList(body, headers, definition, proxyBody, req);
convertResponseData(data, headers, definition, proxyBody, req);
}
}
if (req.context.operation) {
let operationLocalName = localName(req.context.operation, req);
if (req.context.boundDefinition) {
operationLocalName = `${localName(req.context.boundDefinition, req)}_${operationLocalName}`;
}
const isArrayResult = Array.isArray(body.d.results) || Array.isArray(body.d);
if (req.context.definition) {
if (req.context.definition.kind === "type") {
if (returnComplexNested && !isArrayResult) {
body.d = {
[operationLocalName]: body.d,
};
req.context.operationNested = true;
}
} else if (
!req.context.definition.kind &&
req.context.definition.name &&
req.context.definitionElements.value
) {
if (returnPrimitivePlain) {
body.d = isArrayResult ? (body.d.results || body.d).map((entry) => entry.value) : body.d.value;
}
if (returnPrimitiveNested) {
if (isArrayResult) {
body.d = {
results: body.d,
};
} else {
body.d = {
[operationLocalName]: body.d,
};
req.context.operationNested = true;
}
}
}
}
}
if (req.context.serviceResponseAsXML) {
body = convertResponseBodyToXML(body, req);
headers["content-type"] = "application/atom+xml;charset=utf-8";
return body;
}
return JSON.stringify(body);
}
function convertResponseList(body, headers, definition, proxyBody, req) {
if (Array.isArray(proxyBody.value)) {
if (req.context.aggregationKey) {
proxyBody = proxyBody.value[0] || {};
} else {
body.d.results = proxyBody.value || [];
if (req.context.$count) {
body.d.__count = String(proxyBody["@odata.count"] || proxyBody["@count"] || 0);
}
if (proxyBody["@odata.nextLink"] !== undefined || proxyBody["@nextLink"] !== undefined) {
const skipToken = URL.parse(proxyBody["@odata.nextLink"] || proxyBody["@nextLink"], true).query["$skiptoken"];
if (skipToken) {
body.d.__next = linkUri(req, {
$skiptoken: skipToken,
});
}
}
let deltaToken;
if (proxyBody["@odata.deltaLink"] !== undefined || proxyBody["@deltaLink"] !== undefined) {
deltaToken = URL.parse(proxyBody["@odata.deltaLink"] || proxyBody["@deltaLink"], true).query["!deltatoken"];
}
if (!deltaToken && definition && definition["@cov2ap.deltaResponse"] === "timestamp") {
deltaToken = `'${new Date().getTime()}'`;
}
if (deltaToken) {
body.d.__delta = linkUri(req, {
"!deltatoken": deltaToken,
});
}
body.d.results = body.d.results.map((entry) => {
return typeof entry == "object" ? entry : { value: entry };
});
if (req.context.parameters) {
if (req.context.parameters.kind === "Parameters") {
body.d = body.d.results[0];
return body.d ? [body.d] : null;
} else if (req.context.parameters.kind === "Set") {
if (Object.keys(req.context.parameters.keys).length > 0) {
body.d = body.d.results.filter((entry) => {
return Object.keys(req.context.parameters.keys).every((key) => {
return entry[key] === req.context.parameters.keys[key];
});
})[0];
return body.d ? [body.d] : null;
}
}
}
if (!returnCollectionNested) {
body.d = body.d.results;
return body.d;
}
return body.d.results;
}
}
body.d = proxyBody;
return [body.d];
}
function convertResponseData(data, headers, definition, proxyBody, req, selects) {
if (data === null) {
return;
}
if (!definition) {
return;
}
if (!Array.isArray(data)) {
return convertResponseData([data], headers, definition, proxyBody, req, selects);
}
selects = selects || req.context.selects || [];
const elements = definitionElements(definition);
// Recursion
data.forEach((data) => {
if (!isObject(data)) {
return;
}
Object.keys(data).forEach((key) => {
let element = elements[key];
if (!element) {
return;
}
const type = elementType(element, req);
if (!type) {
if (element.items) {
const subSelects = determineSubSelects(element, selects);
convertResponseData(data[key], headers, element.items, proxyBody, req, subSelects);
} else if (element.elements) {
const subSelects = determineSubSelects(element, selects);
convertResponseData(data[key], headers, element, proxyBody, req, subSelects);
}
}
if (type === "cds.Composition" || type === "cds.Association") {
const subSelects = determineSubSelects(element, selects);
convertResponseData(data[key], headers, element._target, proxyBody, req, subSelects);
}
});
});
// Structural Changes
data.forEach((data) => {
if (!isObject(data)) {
return;
}
addResultsNesting(data, headers, definition, elements, proxyBody, req);
});
// Deferreds
data.forEach((data) => {
if (!isObject(data)) {
return;
}
addDeferreds(data, headers, definition, elements, proxyBody, req, selects);
});
// Modify Payload
data.forEach((data) => {
if (!isObject(data)) {
return;
}
addMetadata(data, headers, definition, elements, proxyBody, req);
removeMetadata(data, headers, definition, elements, proxyBody, req);
convertMedia(data, headers, definition, elements, proxyBody, req);
removeAnnotations(data, headers, definition, elements, proxyBody, req);
convertDataTypesToV2(data, headers, definition, elements, proxyBody, req);
convertAggregation(data, headers, definition, elements, proxyBody, req);
filterParameters(data, headers, definition, elements, proxyBody, req);
removeNonSelectedKeys(data, headers, definition, elements, proxyBody, req, selects);
});
}
function convertResponseElementData(data, headers, definition, elements, proxyBody, req) {
if (!definition) {
return;
}
// Modify Payload
convertDataTypesToV2(data, headers, definition, elements, proxyBody, req);
}
function determineSubSelects(element, selects) {
return selects.reduce((results, select) => {
if (select.startsWith(`${element.name}/`)) {
results.push(select.substring(element.name.length + 1));
}
return results;
}, []);
}
function contextNameFromBody(body) {
let context = body && (body["@odata.context"] || body["@context"]);
if (!context) {
return;
}
if (context === "$metadata") {
return "";
}
const metadataIndex = context.indexOf("$metadata#");
if (metadataIndex !== -1) {
context = context.substring(metadataIndex + "$metadata#".length);
}
if (context.endsWith("/$entity")) {
context = context.substring(0, context.length - "/$entity".length);
}
if (context.endsWith("/DraftAdministrativeData")) {
return "DraftAdministrativeData";
}
if (context.startsWith("Collection(")) {
context = context.substring("Collection(".length, context.indexOf(")"));
} else {
if (context.indexOf("(") !== -1) {
context = context.substring(0, context.indexOf("("));
}
}
if (context.indexOf("/") !== -1) {
context = context.substring(0, context.indexOf("/"));
}
return context;
}
function contextFromBody(body, req) {
const context = contextNameFromBody(body);
if (context) {
return lookupDefinition(context, req);
}
}
function contextElementFromBody(body, req) {
let context = body["@odata.context"] || body["@context"];
const definition = contextFromBody(body, req);
if (!(context && definition)) {
return null;
}
if (context.lastIndexOf("/") !== -1) {
const name = context.substring(context.lastIndexOf("/") + 1);
if (name && !name.startsWith("$")) {
const element = definitionElements(definition)[name];
if (element) {
return element;
}
}
}
}
function addMetadata(data, headers, definition, elements, body, req) {
const typeSuffix =
definition.kind === "entity" && definition.params && req.context.parameters
? req.context.parameters.kind === "Set"
? "Type"
: "Parameters"
: "";
data.__metadata = {};
if (definition.name) {
data.__metadata.type = qualifiedODataName(definition.name, req) + typeSuffix;
}
if (definition.kind === "entity") {
data.__metadata.uri = entityUri(data, definition, elements, req);
if (data["@odata.etag"] || data["@etag"]) {
data.__metadata.etag = data["@odata.etag"] || data["@etag"];
}
const mediaDataElementName = findElementByAnnotation(elements, "@Core.MediaType");
if (mediaDataElementName) {
data.__metadata.media_src = `${data.__metadata.uri}/$value`;
const mediaDataElement = elements[mediaDataElementName];
const mediaTypeElementName =
(mediaDataElement["@Core.MediaType"] && mediaDataElement["@Core.MediaType"]["="]) ||
findElementByAnnotation(elements, "@Core.IsMediaType");
if (mediaTypeElementName) {
data.__metadata.content_type = data[mediaTypeElementName];
} else if (mediaDataElement["@Core.MediaType"]) {
data.__metadata.content_type = mediaDataElement["@Core.MediaType"];
}
if (!data.__metadata.content_type) {
data.__metadata.content_type = "application/octet-stream";
}
}
}
}
function removeMetadata(data, headers, definition, elements, body, req) {
Object.keys(data).forEach((key) => {
if (key.startsWith("@") || key.startsWith("odata.") || key.includes("@odata.")) {
delete data[key];
}
});
}
function convertMedia(data, headers, definition, elements, proxyBody, req) {
Object.keys(data).forEach((key) => {
if (key.endsWith("@odata.mediaReadLink")) {
data[key.split("@odata.mediaReadLink")[0]] = data[key];
} else if (key.endsWith("@mediaReadLink")) {
data[key.split("@mediaReadLink")[0]] = data[key];
}
});
}
function removeAnnotations(data, headers, definition, elements, proxyBody, req) {
Object.keys(data).forEach((key) => {
if (key.startsWith("@")) {
delete data[key];
}
});
}
function convertAggregation(data, headers, definition, elements, body, req) {
if (!req.context.$apply) {
return;
}
Object.keys(data).forEach((key) => {
let value = data[key];
if (key.startsWith(AggregationPrefix)) {
if (!(key.endsWith("@odata.type") || key.endsWith("@type"))) {
const name = key.substring(AggregationPrefix.length);
let aggregationType = (data[`${key}@odata.type`] || data[`${key}@type`] || "#Decimal").replace("#", "");
if (DataTypeOData[aggregationType]) {
aggregationType = DataTypeOData[aggregationType];
} else {
aggregationType = `cds.${aggregationType}`;
}
if (
[
"cds.UInt8",
"cds.Int16",
"cds.Int32",
"cds.Integer",
"cds.Int64",
"cds.Integer64",
"cds.Double",
"cds.Decimal",
"cds.DecimalFloat",
].includes(aggregationType)
) {
if (value === null || value === "null") {
value = 0;
}
} else if (["cds.String", "cds.LargeString"].includes(aggregationType)) {
if (value === null) {
value = "";
}
} else if (["cds.Boolean"].includes(aggregationType)) {
if (value === null) {
value = false;
}
}
let aggregationValue = convertDataTypeToV2(value, aggregationType, definition, headers);
// Convert to JSON number
const element = req.context.$apply.value.find((entry) => {
return entry.name === name;
});
if (element) {
const type = elementType(element, req);
const contentType = headers["content-type"];
const contentTypeIEEE754Compatible = contentType && contentType.includes(IEEE754Compatible);
if (
["cds.UInt8", "cds.Int16", "cds.Int32", "cds.Integer"].includes(type) ||
(!(ieee754Compatible || contentTypeIEEE754Compatible) &&
["cds.Decimal", "cds.DecimalFloat", "cds.Double", "cds.Int64", "cds.Integer64"].includes(type))
) {
const aggregation =
element["@Aggregation.default"] || element["@Aggregation.Default"] || element["@DefaultAggregation"];
const aggregationName = aggregation ? aggregation["#"] || aggregation : DefaultAggregation;
const aggregationFunction = aggregationName ? AggregationMap[aggregationName.toUpperCase()] : undefined;
if (
aggregationType === "cds.Decimal" &&
aggregationFunction &&
![AggregationMap.AVG].includes(aggregationFunction)
) {
const floatValue = parseFloat(aggregationValue);
if (aggregationValue === `${floatValue}`) {
aggregationValue = floatValue;
}
}
}
}
data[name] = aggregationValue;
delete data[key];
}
}
});
Object.keys(data).forEach((key) => {
if (key.startsWith(AggregationPrefix) && (key.endsWith("@odata.type") || key.endsWith("@type"))) {
delete data[key];
}
});
const aggregationKey = {
key: req.context.$apply.key.reduce((result, keyElement) => {
const type = elementType(keyElement, req);
let value = data[keyElement.name];
if (value !== undefined && value !== null) {
value = encodeURIKey(value);
if (DataTypeMap[type]) {
value = convertDataTypeToV2Uri(String(value), type).replace(/(.*)/s, DataTypeMap[type].v2);
}
}
result[keyElement.name] = value;
return result;
}, {}),
value: req.context.$apply.value.map((valueElement) => {
return valueElement.name;
}),
};
if (req.context.$apply.filter) {
aggregationKey.filter = encodeURIComponent(req.context.$apply.filter);
}
if (req.context.$apply.search) {
aggregationKey.search = encodeURIComponent(req.context.$apply.search);
}
data.ID__ = `aggregation'${JSON.stringify(aggregationKey)}'`;
data.__metadata.uri = entityUriKey(data.ID__, definition, req);
delete data.__metadata.etag;
}
function filterParameters(data, headers, definition, elements, body, req) {
if (
definition &&
definition.kind === "entity" &&
definition.params &&
req.context.parameters &&
req.context.parameters.kind === "Parameters"
) {
const columns = definitionQueryColumns(definition);
Object.keys(elements).forEach((name) => {
const param = columns.find((column) => column.as === name);
const paramName = (param ? param.ref.join("_") : "") || name;
if (!definition.params[paramName]) {
delete data[name];
}
});
}
}
function removeNonSelectedKeys(data, headers, definition, elements, body, req, selects) {
if (!excludeNonSelectedKeys) {
return;
}
Object.keys(data).forEach((key) => {
const element = elements[key];
if (!element) {
return;
}
if (element.key && selects.length > 0 && !selects.includes(element.name)) {
delete data[element.name];
}
});
}
function replaceConvertDataTypeToV2(value, type, definition, headers = {}) {
if (value === null || value === undefined) {
return value;
}
if (Array.isArray(value)) {
return value.map((entry) => {
return replaceConvertDataTypeToV2(entry, type, definition, headers);
});
}
value = convertDataTypeToV2(value, type, definition, headers);
if (DataTypeMap[type]) {
if (!value.match(DataTypeMap[type].v2.replace("$1", ".*"))) {
value = DataTypeMap[type].v2.replace("$1", encodeReplaceValue(value));
}
}
return value;
}
function convertDataTypesToV2(data, headers, definition, elements, body, req) {
Object.keys(data).forEach((key) => {
if (data[key] === null) {
return;
}
const element = elements[key];
if (!element) {
return;
}
data[key] = convertDataTypeToV2(data[key], elementType(element, req), definition, headers);
});
}
function convertDataTypeToV2(value, type, definition, headers = {}) {
if (value === null || value === undefined) {
return value;
}
if (Array.isArray(value)) {
return value.map((entry) => {
return convertDataTypeToV2(entry, type, definition, headers);
});
}
const contentType = headers["content-type"];
const contentTypeIEEE754Compatible = contentType && contentType.includes(IEEE754Compatible);
if (["cds.Decimal", "cds.DecimalFloat", "cds.Double", "cds.Int64", "cds.Integer64"].includes(type)) {
value = ieee754Compatible || contentTypeIEEE754Compatible ? `${value}` : parseFloat(value);
} else if (!isoDate && !definition["@cov2ap.isoDate"] && ["cds.Date"].includes(type)) {
value = value === "" ? "" : `/Date(${new Date(value).getTime()})/`;
} else if (!isoTime && !definition["@cov2ap.isoTime"] && ["cds.Time"].includes(type)) {
value = value === "" ? "" : convertToDayTimeDuration(value);
} else if (
!isoDateTime &&
!definition["@cov2ap.isoDateTime"] &&
!isoDateTimeOffset &&
!definition["@cov2ap.isoDateTimeOffset"] &&
["cds.DateTime"].includes(type)
) {
value = value === "" || value === "Z" ? "" : `/Date(${new Date(value).getTime()}+0000)/`; // always UTC
} else if (
!isoTimestamp &&
!definition["@cov2ap.isoTimestamp"] &&
!isoDateTimeOffset &&
!definition["@cov2ap.isoDateTimeOffset"] &&
["cds.Timestamp"].includes(type)
) {
value = value === "" || value === "Z" ? "" : `/Date(${new Date(value).getTime()}+0000)/`; // always UTC
}
return value;
}
function convertDataTypeToV2Uri(value, type) {
if (["cds.Date"].includes(type)) {
value = `${value}T00:00:00`;
} else if (["cds.Time"].includes(type)) {
value = convertToDayTimeDuration(value);
}
return value;
}
function convertToDayTimeDuration(value) {
const timeParts = value.split(":");
return `PT${timeParts[0] || "00"}H${timeParts[1] || "00"}M${timeParts[2] || "00"}S`;
}
function addResultsNesting(data, headers, definition, elements, root, req) {
if (!returnCollectionNested) {
return;
}
Object.keys(data).forEach((key) => {
const element = elements[key];
if (!element) {
return;
}
if (element.cardinality && element.cardinality.max === "*") {
data[key] = {
results: data[key],
};
}
});
}
function addDeferreds(data, headers, definition, elements, root, req, selects) {
if (definition.kind !== "entity" || req.context.$apply) {
return;
}
selects = selects.map((select) => {
return select.split("/")[0];
});
const _entityUri = entityUri(data, definition, elements, req);
for (const key of Object.keys(elements)) {
const element = elements[key];
const type = elementType(element, req);
if (element && (type === "cds.Composition" || type === "cds.Association")) {
if (data[key] === undefined) {
if (selects.length === 0 || selects.includes(key)) {
if (fixDraftRequests && req.context.expandSiblingEntity && key === "SiblingEntity") {
data[key] = null;
} else {
data[key] = {
__deferred: {
uri: `${_entityUri}/${key}`,
},
};
}
}
}
}
}
if (definition.kind === "entity" && definition.params && req.context.parameters) {
if (req.context.parameters.kind === "Parameters") {
if (selects.length === 0 || selects.includes("Set")) {
data.Set = {
__deferred: {
uri: `${_entityUri}/Set`,
},
};
}
} else if (req.context.parameters.kind === "Set") {
if (selects.length === 0 || selects.includes("Parameters")) {
data.Parameters = {
__deferred: {
uri: `${_entityUri}/Parameters`,
},
};
}
}
}
}
function rootUri(req) {
const protocol = (processForwardedHeaders && req.header("x-forwarded-proto")) || req.protocol || "http";
const host =
(processForwardedHeaders && req.header("x-forwarded-host")) ||
req.headers.host ||
`${req.hostname || DefaultHost}:${req.socket.address().port || DefaultPort}`;
return `${protocol}://${host}`.replace(/^http:\/\/127.0.0.1/, `http://${DefaultHost}`);
}
function serviceUri(req) {
if (req.context.serviceUri) {
return req.context.serviceUri;
}
let serviceUri = rootUri(req);
if (processForwardedHeaders && req.header("x-forwarded-path") !== undefined) {
const path = stripSlashes(URL.parse(req.header("x-forwarded-path") || "").pathname || "");
let resourceStartPath = "";
const definition = req.context.requestDefinition;
if (definition) {
if (definition.kind === "entity") {
resourceStartPath = localName(definition, req);
} else if (definition.kind === "function" || definition.kind === "action") {
resourceStartPath = localName(definition.parent || definition, req);
}
}
const parts = [];
path.split("/").some((part) => {
if (
part === resourceStartPath ||
part.startsWith(`${resourceStartPath}(`) ||
part.startsWith(`${resourceStartPath}?`) ||
part === "$batch" ||
part.startsWith("$batch?")
) {
return true;
}
parts.push(part);
return false;
});
if (parts.length > 0) {
serviceUri += `/${parts.join("/")}`;
}
} else {
const endpointPath = `${sourcePath}/${sourceServicePath(req.servicePath)}`;
serviceUri += (req.endpointRewrite && req.endpointRewrite(endpointPath)) || endpointPath;
}
req.context.serviceUri = serviceUri.endsWith("/") ? serviceUri : `${serviceUri}/`;
return req.context.serviceUri;
}
function entityUriCollection(entity, req) {
return `${serviceUri(req)}${localName(entity, req)}`;
}
function entityUriKey(key, entity, req) {
return `${entityUriCollection(entity, req)}(${key})`;
}
function entityUri(data, entity, elements, req) {
return entityUriKey(entityKey(data, entity, elements, req), entity, req);
}
function entityKey(data, entity, elements, req) {
if (entity.kind === "entity" && entity.params && req.context.parameters) {
return entityKeyParameters(data, entity, elements, req);
}
const keyElements = Object.keys(definitionKeys(entity)).reduce((keys, key) => {
const element = elements[key];
const type = elementType(element, req);
if (!(type === "cds.Composition" || type === "cds.Association")) {
keys.push(element);
}
return keys;
}, []);
return keyElements
.map((keyElement) => {
const type = elementType(keyElement, req);
let value = data[keyElement.name];
if (value !== undefined && value !== null) {
value = encodeURIKey(value);
if (DataTypeMap[type]) {
value = convertDataTypeToV2Uri(String(value), type).replace(/(.*)/s, DataTypeMap[type].v2);
}
}
if (keyElements.length === 1) {
return `${value}`;
} else {
return `${keyElement.name}=${value}`;
}
})
.join(",");
}
function entityKeyParameters(data, entity, elements, req) {
const keys = definitionKeys(entity);
const keyElements = [];
Object.keys(req.context.parameters.values).forEach((param) => {
keyElements.push(entity.params[param]);
});
if (req.context.parameters.kind === "Set") {
Object.keys(req.context.parameters.keys).forEach((key) => {
keyElements.push(keys[key]);
});
const columns = definitionQueryColumns(entity);
Object.keys(keys).forEach((key) => {
const param = columns.find((column) => column.as === key);
const paramName = (param ? param.ref.join("_") : "") || key;
if (!keyElements.find((keyElement) => keyElement.name === paramName)) {
keyElements.push(keys[key]);
}
});
}
data = { ...data, ...req.context.parameters.values, ...req.context.parameters.keys };
return keyElements
.map((keyElement) => {
const type = elementType(keyElement, req);
let value = data[keyElement.name];
if (value !== undefined && value !== null) {
value = encodeURIKey(value);
if (DataTypeMap[type]) {
value = convertDataTypeToV2Uri(String(value), type).replace(/(.*)/s, DataTypeMap[type].v2);
}
}
if (keyElements.length === 1) {
return `${value}`;
} else {
return `${keyElement.name}=${value}`;
}
})
.join(",");
}
function definitionQueryColumns(definition) {
if (definition.query) {
if (definition.query.SELECT && definition.query.SELECT.columns) {
return definition.query.SELECT.columns;
} else if (
definition.query.SET &&
definition.query.SET.args &&
definition.query.SET.args[0] &&
definition.query.SET.args[0].SELECT &&
definition.query.SET.args[0].SELECT.columns
) {
return definition.query.SET.args[0].SELECT.columns;
}
}
return [];
}
function linkUri(req, params) {
const originalUrl = req.context.url.originalUrl;
Object.keys(params || {}).forEach((key) => {
const value = params[key];
if (Array.isArray(value)) {
params[key] = value.pop();
}
if (params[key] === undefined) {
delete params[key];
}
});
return (
serviceUri(req) +
formatLinkUri(
URL.format({
...originalUrl,
search: null,
pathname: originalUrl.contextPath,
query: {
...originalUrl.query,
...params,
},
}),
)
);
}
function sourceServicePath(servicePath) {
if (!targetPath) {
return servicePath;
}
if (servicePath.startsWith(`/${targetPath}`)) {
return servicePath.substring(targetPath.length + 1);
}
if (servicePath.startsWith(`${targetPath}/`)) {
return servicePath.substring(targetPath.length + 1);
}
return servicePath;
}
function respond(req, res, statusCode, headers, body) {
if (!res.headersSent) {
if (!body || statusCode === 204) {
delete headers["content-length"];
}
Object.entries(headers).forEach(([name, value]) => {
res.setHeader(name, value);
});
res.status(statusCode);
if (body && statusCode !== 204) {
if (body.pipe) {
pipeline(body, res, (err) => {
if (err) {
logWarn(req, "MetadataStream", err);
}
});
} else {
res.write(body);
res.end();
}
} else {
res.end();
}
// Trace
traceResponse(req, "Response", res.statusCode, res.statusMessage, headers, body);
}
}
function normalizeContentType(headers) {
let contentType = headers["content-type"];
if (contentType) {
contentType = contentType.trim();
if (isApplicationJSON(contentType)) {
contentType = contentType
.split(";")
.filter((part) => {
return !part.startsWith("odata.");
})
.join(";");
}
headers["content-type"] = contentType;
}
return contentType;
}
function normalizeBody(body) {
if (typeof body === "string" || Buffer.isBuffer(body)) {
return body;
}
if (typeof body === "object") {
return JSON.stringify(body);
}
return String(body);
}
function isEmptyJSON(content) {
return !content || content === "{}";
}
function convertResponseServiceRootToXML(proxyBody, headers, req) {
let xmlBody = `<?xml version="1.0" encoding="utf-8" standalone="yes"?>`;
xmlBody += `<service xml:base="${serviceUri(
req,
)}" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:app="http://www.w3.org/2007/app" xmlns="http://www.w3.org/2007/app">`;
xmlBody += `<workspace><atom:title>Default</atom:title>`;
xmlBody += proxyBody.value
.map((entry) => {
return `<collection href="${entry.name}"><atom:title>${entry.name}</atom:title></collection>`;
})
.join("");
xmlBody += `</workspace></service>`;
headers["content-type"] = "application/xml;charset=utf-8";
return xmlBody;
}
function convertResponseBodyToXML(body, req) {
let xmlBody = `<?xml version="1.0" encoding="utf-8" standalone="yes"?>`;
const namespace = ` xml:base="${serviceUri(
req,
)}" xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns="http://www.w3.org/2005/Atom"`;
const definition = req.context.definition;
if (body && body.d && definition) {
xmlBody += convertResponseListToXML(body.d, definition, req, namespace);
} else if (body && body.error) {
xmlBody += convertResponseErrorToXML(body.error);
} else {
xmlBody += `<feed${namespace}></feed>`;
}
return xmlBody;
}
function convertResponseListToXML(data, definition, req, namespace, element) {
let xmlBody = "";
if (data) {
if (data.results) {
xmlBody += `<feed${namespace || ""}>`;
if (data.__count !== undefined) {
xmlBody += `<m:count>${data.__count}</m:count>`;
}
xmlBody += `<title type="text">${localName(definition, req)}</title>`;
xmlBody += `<id>${entityUriCollection(definition, req)}</id>`;
xmlBody += `<updated>${req.now.toISOString()}</updated>`;
xmlBody += `<link rel="self" title="${element || localName(definition, req)}" href="${entityUriCollection(
definition,
req,
)}${element ? `/${element}` : ""}" />`;
data.results.forEach((entry) => {
if (typeof entry === "object") {
xmlBody += convertResponseDataToXML(entry, definition, req);
} else {
xmlBody += `<entry><content>${entry}</content></entry>`;
}
});
xmlBody += `</feed>`;
} else {
if (req.context.operationNested) {
const operationName = Object.keys(data)[0];
if (operationName) {
data = data[operationName];
}
}
xmlBody += convertResponseDataToXML(data, definition, req, namespace);
}
}
return xmlBody;
}
function convertResponseDataToXML(data, definition, req, namespace) {
let xmlBody = `<entry${namespace || ""}>`;
const uri = data.__metadata && data.__metadata.uri;
if (uri) {
xmlBody += `<id>${uri}</id>`;
}
xmlBody += `<title type="text"></title>`;
xmlBody += `<updated>${req.now.toISOString()}</updated>`;
xmlBody += `<author><name /></author>`;
if (uri) {
xmlBody += `<link rel="edit" title="${localName(definition, req)}" href="${uri}" />`;
}
const elements = definitionElements(definition);
if (typeof data === "object") {
Object.keys(data).forEach((key) => {
const value = data[key];
const element = elements[key];
const type = elementType(element, req);
if (type === "cds.Composition" || type === "cds.Association") {
if (value && value.__deferred) {
xmlBody += `<link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/${key}" type="application/atom+xml;type=entry" title="${key}" href="${uri}/${key}" />`;
} else {
xmlBody += `<link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/${key}" type="application/atom+xml;type=entry" title="${key}" href="${uri}/${key}"><m:inline>`;
xmlBody += convertResponseListToXML(value, element._target, req, "", element.name);
xmlBody += `</m:inline></link>`;
}
}
});
xmlBody += `<category term="${definition.name}" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />`;
xmlBody += `<content type="application/xml"><m:properties>`;
Object.keys(data).forEach((key) => {
if (["__metadata"].includes(key)) {
return;
}
const element = elements[key];
const type = elementType(element, req);
if (!(type === "cds.Composition" || type === "cds.Association")) {
if (data[key] === null) {
xmlBody += `<d:${key} m:null="true" />`;
} else {
let value = data[key];
const match = typeof value === "string" && value.match(/\/Date\((.*)\)\//is);
const date = match && match.pop();
if (date) {
value = new Date(parseInt(date.split("+")[0])).toISOString().slice(0, 19); // Cut millis
} else {
value = escapeXMLEntities(value);
}
xmlBody += `<d:${key} m:type="${ODataType[type]}">${value}</d:${key}>`;
}
}
});
xmlBody += `</m:properties></content>`;
} else {
xmlBody += `<content type="text/plain">${data}</content>`;
}
xmlBody += `</entry>`;
return xmlBody;
}
function convertResponseErrorToXML(error) {
let xmlBody = `<error xmlns="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">`;
xmlBody += convertResponseErrorMessageToXML(error);
xmlBody += `</error>`;
return xmlBody;
}
function convertResponseErrorMessageToXML(message) {
let xmlBody = `<code>${message.code}</code>`;
xmlBody += `<severity>${message.severity}</severity>`;
if (typeof message.message === "string") {
xmlBody += `<message>${message.message}</message>`;
} else {
xmlBody += `<message xml:lang="${message.message.lang}">${message.message.value}</message>`;
}
if (message.additionalTargets) {
xmlBody += `<additionalTargets>`;
message.additionalTargets.forEach((additionalTarget) => {
xmlBody += `<target>${additionalTarget}</target>`;
});
xmlBody += `</additionalTargets>`;
}
if (message.ContentID) {
xmlBody += `<ContentID>${message.ContentID}</ContentID>`;
}
if (message.target) {
xmlBody += `<target>${message.target}</target>`;
}
if (message.transition) {
xmlBody += `<transition>${message.transition}</transition>`;
}
if (message.innererror && message.innererror.errordetails) {
xmlBody += `<innererror><errordetails>`;
message.innererror.errordetails.forEach((errorDetail) => {
xmlBody += `<errordetail>`;
xmlBody += convertResponseErrorMessageToXML(errorDetail);
xmlBody += `</errordetail>`;
});
xmlBody += `</errordetails></innererror>`;
}
return xmlBody;
}
function notFoundErrorResponse() {
return {
error: {
code: "404",
message: "Not Found",
severity: "error",
},
};
}
function propagateHeaders(req, addHeaders = {}) {
const headers = Object.assign({}, req.headers, addHeaders);
headers["x-request-id"] = req.contextId;
headers["x-correlation-id"] = req.contextId;
headers["x-correlationid"] = req.contextId;
delete headers.host;
delete headers.connection;
return headers;
}
function enrichApplicationJSON(contentType) {
const [key] = IEEE754Compatible.split("=");
if (!contentType.includes(key)) {
contentType += ";" + IEEE754Compatible;
}
return contentType;
}
function isApplicationJSON(contentType) {
return contentType && contentType.startsWith("application/json");
}
function isPlainText(contentType) {
return contentType && contentType.startsWith("text/plain");
}
function isXML(contentType) {
return (
contentType &&
(contentType.startsWith("application/xml") ||
contentType.startsWith("application/atom+xml") ||
contentType.startsWith("application/atomsvc+xml") ||
contentType.startsWith("text/xml") ||
contentType.startsWith("text/html"))
);
}
function escapeXMLEntities(value) {
if (!value || typeof value !== "string") {
return value;
}
const escapeEntityMap = {
"<": "<",
">": ">",
"&": "&",
"'": "'",
'"': """,
};
return value.replace(/[<>&'"]/g, function (char) {
return escapeEntityMap[char] || char;
});
}
function isMultipartMixed(contentType) {
return contentType && contentType.replace(/\s/g, "").startsWith("multipart/mixed;boundary=");
}
function isMultipartFormData(contentType) {
return contentType && contentType.replace(/\s/g, "").startsWith("multipart/form-data;boundary=");
}
function encodeURIKey(value) {
return encodeURIComponent(value).replace(/[/]/g, "%2F").replace(/'/g, "''").replace(/%3A/g, ":");
}
function decodeURIKey(value) {
return decodeURIComponent(value).replace(/%2F/g, "/");
}
function formatLinkUri(value) {
return value.replace(/%24/g, "$");
}
function encodeReplaceValue(value) {
if (typeof value === "string") {
return value.replace(/\$/g, "$$$$");
}
return value;
}
function targetUrl(url) {
return url.replace(sourcePath, rewritePath);
}
function lookupReturnDefinition(returns, req) {
returns = (returns && returns.items) || returns;
if (returns && returns.type) {
const definition = lookupDefinition(returns.type, req);
return definition || lookupReturnPrimitiveDefinition(returns);
}
return returns;
}
function lookupReturnPrimitiveDefinition(returns) {
returns = (returns && returns.items) || returns;
return {
name: lookupODataType(returns && returns.type),
elements: {
value: returns,
},
};
}
function lookupODataType(type) {
const odataType = Object.keys(DataTypeOData).find((key) => {
return DataTypeOData[key] === type;
});
if (odataType && odataType.startsWith("_")) {
return odataType.substring(1);
}
return odataType;
}
function definitionElements(definition) {
if (definition && definition.elements) {
if (cacheDefinitions) {
if (definition.hasOwnProperty(cacheSymbol)) {
return definition[cacheSymbol];
}
}
// Collect keys of prototype hierarchy
const keys = [];
for (const key in definition.elements || {}) {
keys.push(key);
}
const elements = Object.freeze(
keys.reduce((elements, key) => {
const element = definition.elements[key];
if (element["@cds.api.ignore"]) {
return elements;
}
elements[key] = element;
if (
(element.type === "cds.Composition" || element.type === "cds.Association") &&
element.keys &&
element._target
) {
element.keys.forEach((key) => {
const targetKey = key.ref[0];
const foreignKey = `${element.name}_${targetKey}`;
if (!elements[foreignKey]) {
elements[foreignKey] = {
key: element.key,
type: element._target.elements[targetKey].type,
name: foreignKey,
parent: element.parent,
kind: element.kind,
};
}
});
}
return elements;
}, {}),
);
if (cacheDefinitions) {
Object.defineProperty(definition, cacheSymbol, {
value: elements,
writable: false,
configurable: true,
});
}
return elements;
}
return {};
}
function definitionKeys(definition) {
const elements = definitionElements(definition);
return Object.keys(elements).reduce((keys, key) => {
if (elements[key].key) {
keys[key] = elements[key];
}
return keys;
}, {});
}
function elementType(element, req) {
let type;
if (element) {
type = element.type;
if (element["@odata.Type"] || element["@odata.type"]) {
const odataType = (element["@odata.Type"] || element["@odata.type"]).split(".").pop();
if (odataType && DataTypeOData[odataType]) {
type = DataTypeOData[odataType];
}
}
if (!type && element.items && element.items.type) {
type = element.items.type;
}
}
return baseElementType(type, req.csn);
}
function baseElementType(type, csn) {
if (type && csn.definitions[type]) {
type = csn.definitions[type].type;
type = baseElementType(type, csn);
}
return type;
}
function findElementByType(elements, type, req) {
return Object.keys(elements)
.filter((key) => {
return !(elements[key].type === "cds.Composition" && elements[key].type === "cds.Association");
})
.find((key) => {
const element = elements[key];
return element && elementType(element, req) === type;
});
}
function findElementByAnnotation(elements, annotation) {
return Object.keys(elements)
.filter((key) => {
return !(elements[key].type === "cds.Composition" && elements[key].type === "cds.Association");
})
.find((key) => {
const element = elements[key];
return element && !!element[annotation];
});
}
function findElementValueByAnnotation(elements, annotation) {
const elementName = findElementByAnnotation(elements, annotation);
if (elementName) {
const elementValue = elements[elementName][annotation];
if (elementValue) {
return elementValue["="] || elementValue;
}
}
return undefined;
}
function findEndingElementName(elements, url) {
return Object.keys(elements)
.filter((key) => {
return !(elements[key].type === "cds.Composition" && elements[key].type === "cds.Association");
})
.find((key) => {
const element = elements[key];
return url.contextPath.endsWith(`/${element.name}`);
});
}
function compositionRoot(entity, req) {
if (!entity || entity.kind !== "entity") {
return;
}
const parentEntity = compositionParent(entity, req);
return parentEntity ? compositionRoot(parentEntity, req) : entity;
}
function compositionParent(entity, req) {
if (!entity || entity.kind !== "entity") {
return;
}
const parentAssociation = compositionParentAssociation(entity, req);
return parentAssociation ? parentAssociation._target : null;
}
function compositionParentAssociation(entity, req) {
if (!entity || entity.kind !== "entity") {
return;
}
const elements = definitionElements(entity);
const parentAssociation = Object.keys(elements).find((name) => {
const element = elements[name];
if (element.type === "cds.Association") {
const parentDefinition = req.csn.definitions[element.target];
const parentElements = definitionElements(parentDefinition);
return !!Object.keys(parentElements).find((name) => {
const parentElement = parentElements[name];
if (parentElement.type === "cds.Composition") {
return parentElement.target === entity.name;
}
});
}
});
if (parentAssociation) {
return elements[parentAssociation];
}
if (entity.name.endsWith(".texts")) {
const parentEntityName = Object.keys(req.csn.definitions).find((name) => {
const definition = req.csn.definitions[name];
if (definition.kind !== "entity") {
return false;
}
const elements = definitionElements(definition);
return !!Object.keys(elements).find((name) => {
const parentElement = elements[name];
if (parentElement.type === "cds.Composition") {
return parentElement.target === entity.name;
}
});
});
if (parentEntityName) {
return {
target: parentEntityName,
_target: req.csn.definitions[parentEntityName],
};
}
}
}
function determineLocale(req) {
let locale = cds.env.i18n && cds.env.i18n.default_language;
try {
locale = require("@sap/cds/lib/req/locale")(req);
} catch {
try {
// CDS 3
// eslint-disable-next-line n/no-missing-require
locale = require("@sap/cds-runtime/lib/cds-services/adapter/utils/locale")({ req });
} catch {
// Default
}
}
if (locale && locale.length >= 2) {
locale = locale.substring(0, 2).toLowerCase() + locale.slice(2);
}
return locale || "en";
}
const isObject = (object) => {
return typeof object === "object" && !Array.isArray(object) && object !== null;
};
const ensureArray = (value) => {
if (!value) {
return [];
}
if (Array.isArray(value)) {
return value;
}
if (typeof value === "string") {
return value.split(",");
}
if (typeof value === "object") {
return Object.keys(value)
.filter((k) => value[k])
.sort();
}
return [];
};
function decodeBase64(b64String) {
return Buffer.from(b64String, "base64").toString();
}
function decodeJwtTokenBody(token) {
const parts = token.split(".");
if (parts.length > 1) {
return JSON.parse(decodeBase64(parts[1]));
}
throw new Error("Invalid JWT token");
}
function setContentLength(headers, body) {
if (body && body.length && !(headers["transfer-encoding"] || "").includes("chunked")) {
headers["content-length"] = Buffer.byteLength(body);
} else {
clearContentLength(headers);
}
}
function clearContentLength(headers) {
delete headers["content-length"];
}
function processMultipartMixed(
req,
multiPartBody,
contentType,
urlProcessor,
bodyHeadersProcessor,
contentIdOrder = [],
direction,
) {
let maxContentId = 1;
function nextContentID() {
return "cov2ap_" + String(maxContentId++);
}
if (!multiPartBody || !(typeof multiPartBody === "string")) {
const error = new Error("Invalid multipart body");
error.statusCode = 400;
throw error;
}
if (!contentType || !(typeof contentType === "string")) {
const error = new Error("Invalid content type");
error.statusCode = 400;
throw error;
}
const match = contentType.replace(/\s/g, "").match(/^multipart\/mixed;boundary=([^;]*)/i);
let boundary = match && match.pop();
if (!boundary) {
return multiPartBody;
}
let boundaryChangeSet = "";
let urlAfterBlank = false;
let bodyAfterBlank = false;
let previousLineIsBlank = false;
let index = 0;
let httpInfo;
let statusCode;
let statusCodeText;
let contentId;
let contentIdMisplaced = false;
let contentTransferEncoding;
let body = "";
let headers = {};
let method = "";
let url = "";
const parts = multiPartBody.split("\r\n");
const newParts = [];
parts.forEach((part) => {
const match = part.replace(/\s/g, "").match(/^content-type:multipart\/mixed;boundary=(.*)$/i);
if (match) {
boundaryChangeSet = match.pop();
}
if (part.startsWith(`--${boundary}`) || (boundaryChangeSet && part.startsWith(`--${boundaryChangeSet}`))) {
// Body & Headers
if (bodyAfterBlank) {
if (bodyHeadersProcessor) {
try {
const contentType = normalizeContentType(headers);
if (isApplicationJSON(contentType)) {
body = (body && JSON.parse(body)) || {};
}
const result = bodyHeadersProcessor({
index,
statusCode,
contentType,
body,
headers,
method,
url,
contentId,
});
statusCode = (result && result.statusCode) || statusCode;
statusCodeText = (result && result.statusCodeText) || statusCodeText;
body = (result && result.body) ?? body;
headers = (result && result.headers) || headers;
if (["HEAD", "OPTIONS", "GET", "DELETE"].includes(method)) {
body = "";
}
} catch (err) {
// Error
logError(req, "Batch", err);
}
}
if (boundaryChangeSet) {
// Inject mandatory content-id for changesets
const addContentIdHeader = contentId === undefined || contentIdMisplaced;
if (contentId === undefined && direction === ProcessingDirection.Request) {
contentId = nextContentID();
}
if (contentId !== undefined) {
if (direction === ProcessingDirection.Request || !contentId.startsWith("cov2ap_")) {
if (addContentIdHeader) {
// Add content-id to headers of changeset (before url = -3)
newParts.splice(-3, 0, `content-id: ${contentId}`);
}
headers["content-id"] = contentId;
}
contentIdOrder.push(contentId);
}
}
// Inject mandatory content-transfer-encoding
if (!contentTransferEncoding) {
contentTransferEncoding = "binary";
// Add content-transfer-encoding to headers (before url = -3)
newParts.splice(-3, 0, `content-transfer-encoding: ${contentTransferEncoding}`);
}
if (httpInfo && statusCode) {
newParts.splice(-1, 0, `${httpInfo[1]} ${statusCode} ${statusCodeText || httpInfo[3]}`);
}
Object.entries(headers).forEach(([name, value]) => {
newParts.splice(-1, 0, `${name}: ${value}`);
});
newParts.push(body);
httpInfo = undefined;
statusCode = undefined;
statusCodeText = undefined;
contentId = undefined;
contentIdMisplaced = false;
contentTransferEncoding = undefined;
body = "";
headers = {};
url = "";
index++;
}
urlAfterBlank = true;
bodyAfterBlank = false;
newParts.push(part);
if (boundaryChangeSet && part === `--${boundaryChangeSet}--`) {
boundaryChangeSet = "";
}
} else if (part && urlAfterBlank && previousLineIsBlank) {
urlAfterBlank = false;
bodyAfterBlank = true;
// Url
const urlParts = part.split(" ");
let partMethod = urlParts[0];
let partUrl = urlParts.slice(1, -1).join(" ");
let partProtocol = urlParts.pop();
if (urlProcessor) {
const result = urlProcessor({ method: partMethod, url: partUrl });
if (result) {
partMethod = result.method;
partUrl = result.url;
part = [partMethod, partUrl, partProtocol].join(" ");
}
}
method = partMethod;
url = partUrl;
if (part.startsWith("HTTP/")) {
httpInfo = part.match(/^(HTTP\/[\d.]+)\s+(\d{3})\s(.*)$/i);
if (httpInfo && httpInfo.length === 4) {
statusCode = parseInt(httpInfo[2]);
} else {
httpInfo = undefined;
}
}
if (!statusCode) {
newParts.push(part);
}
} else if (bodyAfterBlank && (previousLineIsBlank || body !== "")) {
body = body ? `${body}\r\n${part}` : part;
} else if (part !== "") {
const partIsContentId = part.toLowerCase().startsWith("content-id:");
if (partIsContentId) {
const colonIndex = part.indexOf(":");
if (colonIndex !== -1) {
contentId = part.substring(colonIndex + 1).trim();
contentIdMisplaced = !!bodyAfterBlank;
}
}
const partContentTransferEncoding = part.toLowerCase().startsWith("content-transfer-encoding:");
if (partContentTransferEncoding && !bodyAfterBlank) {
const colonIndex = part.indexOf(":");
if (colonIndex !== -1) {
contentTransferEncoding = part.substring(colonIndex + 1).trim();
}
}
if (!bodyAfterBlank) {
if (!(direction === ProcessingDirection.Response && partIsContentId && contentId.startsWith("cov2ap_"))) {
newParts.push(part);
}
} else {
let colonIndex = part.indexOf(":");
if (colonIndex !== -1) {
headers[part.substring(0, colonIndex).toLowerCase()] = part.substring(colonIndex + 1).trim();
}
}
} else {
newParts.push(part);
}
previousLineIsBlank = part === "";
});
return newParts.join("\r\n");
}
const sanitizeHeaders = (headers) => {
headers = { ...(headers || {}) };
if (headers.authorization) {
headers.authorization = headers.authorization.split(" ")[0] + " ***";
}
return headers;
};
function traceRequest(req, name, method, url, headers, body) {
const LOG = cds.log("cov2ap");
if (LOG._debug) {
const _url = decodeURIComponent(url) || "";
const _headers = JSON.stringify(sanitizeHeaders(headers));
const _body = typeof body === "string" ? body : body ? JSON.stringify(body) : "";
logTrace(req, name, `${method} ${_url}`, _headers && "Headers:", _headers, _body && "Body:", _body);
}
}
function traceResponse(req, name, statusCode, statusMessage, headers, body) {
const LOG = cds.log("cov2ap");
if (LOG._debug) {
const _headers = JSON.stringify(sanitizeHeaders(headers));
const _body = typeof body === "string" ? body : body ? JSON.stringify(body) : "";
logTrace(
req,
name,
`${statusCode || ""} ${statusMessage || ""}`,
_headers && "Headers:",
_headers,
_body && "Body:",
_body,
);
}
}
function logError(req, name, error) {
const LOG = cds.log("cov2ap");
if (LOG._error) {
initCDSContext(req);
LOG.error(`${name}:`, error);
}
}
function logWarn(req, name, message, info) {
const LOG = cds.log("cov2ap");
if (LOG._warn) {
initCDSContext(req);
LOG.warn(`${name}:`, message, info ?? "");
}
}
function logInfo(req, name, message, info) {
const LOG = cds.log("cov2ap");
if (LOG._info) {
initCDSContext(req);
LOG.info(`${name}:`, message, info ?? "");
}
}
function logDebug(req, name, message, info) {
const LOG = cds.log("cov2ap");
if (LOG._debug) {
initCDSContext(req);
LOG.debug(`${name}:`, message, info ?? "");
}
}
function logTrace(req, name, ...lines) {
const LOG = cds.log("cov2ap");
if (LOG._debug) {
initCDSContext(req);
LOG.debug(`${name}:`, lines.filter((line) => !!line).join("\n"));
}
}
function log(req, level, name, message, info) {
let error;
switch (level.toLocaleString()) {
case "error":
error = new Error(message);
error.info = info;
logError(req, name, error);
break;
case "warn":
logWarn(req, name, message, info);
break;
case "info":
logInfo(req, name, message, info);
break;
case "debug":
logDebug(req, name, message, info);
break;
case "trace":
logTrace(req, name, message, info);
break;
default:
return;
}
}
function initCDSContext(req) {
if (cds.context) {
return;
}
cds.context = new cds.EventContext({
id: req.contextId,
user: req.user,
tenant: req.tenant,
http: { req, res: req.res },
});
}
return router;
}
cov2ap.singleton = () => {
if (!cov2ap._singleton) {
cov2ap._singleton = cov2ap();
cds.cov2ap = cov2ap._singleton;
}
return cov2ap._singleton;
};
module.exports = cov2ap;