@sap/cds 7.3.1 → 7.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +58 -3
  2. package/_i18n/i18n_es_MX.properties +110 -0
  3. package/apis/cds.d.ts +13 -12
  4. package/apis/core.d.ts +27 -108
  5. package/apis/cqn.d.ts +15 -18
  6. package/apis/csn.d.ts +95 -60
  7. package/apis/env.d.ts +25 -0
  8. package/apis/events.d.ts +124 -0
  9. package/apis/{reflect.d.ts → linked.d.ts} +27 -38
  10. package/apis/models.d.ts +60 -45
  11. package/apis/ql.d.ts +11 -5
  12. package/apis/{serve.d.ts → server.d.ts} +57 -31
  13. package/apis/services.d.ts +74 -145
  14. package/apis/test.d.ts +1 -1
  15. package/bin/serve.js +3 -0
  16. package/lib/compile/cds-compile.js +2 -2
  17. package/lib/compile/to/edm.js +8 -3
  18. package/lib/compile/to/gql.js +4 -0
  19. package/lib/dbs/cds-deploy.js +52 -4
  20. package/lib/env/cds-requires.js +27 -15
  21. package/lib/env/defaults.js +1 -0
  22. package/lib/env/schemas/index.js +10 -0
  23. package/lib/index.js +7 -4
  24. package/lib/linked/models.js +8 -5
  25. package/lib/ql/CREATE.js +2 -0
  26. package/lib/ql/DELETE.js +1 -0
  27. package/lib/ql/DROP.js +2 -0
  28. package/lib/ql/INSERT.js +2 -22
  29. package/lib/ql/Query.js +59 -22
  30. package/lib/ql/SELECT.js +5 -0
  31. package/lib/ql/STREAM.js +2 -0
  32. package/lib/ql/UPDATE.js +2 -0
  33. package/lib/ql/UPSERT.js +3 -1
  34. package/lib/ql/cds-ql.js +21 -5
  35. package/lib/ql/infer.js +129 -0
  36. package/lib/req/cds-context.js +8 -5
  37. package/lib/srv/cds-connect.js +3 -1
  38. package/lib/utils/axios.js +4 -2
  39. package/lib/utils/data.js +3 -0
  40. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +12 -0
  41. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +26 -8
  42. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/batch/BatchProcessor.js +1 -1
  43. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +11 -8
  44. package/libx/_runtime/common/code-ext/worker.js +5 -16
  45. package/libx/_runtime/common/generic/auth/capabilities.js +11 -2
  46. package/libx/_runtime/common/i18n/messages.properties +1 -0
  47. package/libx/_runtime/common/utils/postProcessing.js +1 -1
  48. package/libx/_runtime/common/utils/resolveView.js +20 -1
  49. package/libx/{common → _runtime/common}/utils/ucsn.js +19 -11
  50. package/libx/_runtime/db/expand/expandCQNToJoin.js +2 -2
  51. package/libx/_runtime/db/sql-builder/InsertBuilder.js +6 -1
  52. package/libx/_runtime/db/sql-builder/UpdateBuilder.js +6 -1
  53. package/libx/_runtime/db/sql-builder/dollar.js +7 -7
  54. package/libx/_runtime/fiori/generic/activate.js +2 -2
  55. package/libx/_runtime/fiori/generic/edit.js +25 -45
  56. package/libx/_runtime/fiori/generic/read.js +3 -5
  57. package/libx/_runtime/fiori/lean-draft.js +142 -64
  58. package/libx/_runtime/fiori/utils/delete.js +7 -1
  59. package/libx/_runtime/fiori/utils/handler.js +4 -6
  60. package/libx/_runtime/fiori/utils/lockInfo.js +27 -0
  61. package/libx/_runtime/fiori/utils/where.js +20 -1
  62. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +3 -2
  63. package/libx/_runtime/messaging/Outbox.js +12 -47
  64. package/libx/_runtime/messaging/common-utils/AMQPClient.js +1 -3
  65. package/libx/_runtime/messaging/common-utils/authorizedRequest.js +3 -0
  66. package/libx/_runtime/messaging/common-utils/connections.js +1 -1
  67. package/libx/_runtime/messaging/enterprise-messaging.js +12 -13
  68. package/libx/_runtime/messaging/file-based.js +7 -5
  69. package/libx/_runtime/messaging/redis-messaging.js +10 -11
  70. package/libx/_runtime/messaging/service.js +12 -26
  71. package/libx/_runtime/remote/Service.js +52 -36
  72. package/libx/_runtime/remote/utils/client.js +22 -123
  73. package/libx/odata/afterburner.js +14 -5
  74. package/libx/odata/grammar.peggy +26 -7
  75. package/libx/odata/metadata.js +18 -1
  76. package/libx/odata/parser.js +1 -1
  77. package/libx/odata/service-document.js +0 -1
  78. package/libx/odata/utils.js +19 -3
  79. package/libx/{_runtime/messaging/outbox/utils.js → outbox/index.js} +94 -24
  80. package/libx/rest/middleware/parse.js +1 -1
  81. package/package.json +2 -2
  82. package/apis/connect.d.ts +0 -39
  83. package/bin/utils/modules.js +0 -7
  84. package/bin/utils/term.js +0 -56
  85. package/lib/env/schema.js +0 -9
  86. package/lib/linked/queries.js +0 -41
  87. package/lib/srv/protocols/odata-v2-proxy.js +0 -3699
  88. package/libx/common/asserts.js +0 -0
  89. package/libx/common/crud.js +0 -0
  90. package/libx/common/etag.js +0 -0
  91. package/libx/common/localized.js +0 -0
  92. package/libx/common/managed.js +0 -0
  93. package/libx/common/paging.js +0 -0
  94. package/libx/common/readme.md +0 -4
  95. package/libx/common/sorting.js +0 -0
  96. package/libx/common/temporal.js +0 -0
  97. package/libx/connect/auth.js +0 -0
  98. package/libx/connect/perf.js +0 -0
  99. package/libx/connect/readme.md +0 -3
  100. package/libx/fiori/draft/readme.md +0 -1
  101. package/libx/fiori/readme.md +0 -1
  102. package/libx/hana/readme.md +0 -1
  103. package/libx/msg/readme.md +0 -3
  104. package/libx/readme.md +0 -1
  105. package/libx/sqlite/readme.md +0 -1
  106. /package/libx/_runtime/{messaging/common-utils → common/utils}/waitingTime.js +0 -0
  107. /package/libx/{_runtime/messaging/outbox → outbox}/OutboxRunner.js +0 -0
@@ -1,3699 +0,0 @@
1
- "use strict";
2
-
3
- // OData V2/V4 Delta: http://docs.oasis-open.org/odata/new-in-odata/v4.0/cn01/new-in-odata-v4.0-cn01.html
4
-
5
- const URL = require("url");
6
- const express = require("express"); // eslint-disable-line cds/no-missing-dependencies
7
- const expressFileUpload = require("express-fileupload"); // eslint-disable-line cds/no-missing-dependencies
8
- const fetch = require("node-fetch"); // eslint-disable-line cds/no-missing-dependencies
9
- const cds = require("../../index"); // eslint-disable-line cds/no-missing-dependencies
10
- const { promisify } = require("util");
11
- const { createProxyMiddleware } = require("http-proxy-middleware"); // eslint-disable-line cds/no-missing-dependencies
12
-
13
- const LOG = cds.log("cov2ap");
14
-
15
- const SeverityMap = {
16
- 1: "success",
17
- 2: "info",
18
- 3: "warning",
19
- 4: "error",
20
- };
21
-
22
- // NOTE: we want to support HANA's SYSUUID, which does not conform to real UUID formats
23
- const UUIDLikeRegex = /guid'([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'/gi;
24
- // https://www.w3.org/TR/xmlschema11-2/#nt-duDTFrag
25
- const DurationRegex =
26
- /^P(?:(\d)Y)?(?:(\d{1,2})M)?(?:(\d{1,2})D)?T(?:(\d{1,2})H)?(?:(\d{2})M)?(?:(\d{2}(?:\.\d+)?)S)?$/i;
27
- // Unsupported Draft Filter
28
- const UnsupportedDraftFilterRegex =
29
- /\(IsActiveEntity eq true and (.*?)\) or \(IsActiveEntity eq false and \((.*?) or HasActiveEntity eq false\)\)/;
30
-
31
- // https://cap.cloud.sap/docs/cds/types
32
- const DataTypeMap = {
33
- "cds.UUID": { v2: `guid'$1'`, v4: UUIDLikeRegex },
34
- // "cds.Boolean" - no transformation
35
- // "cds.Integer" - no transformation
36
- "cds.Integer64": { v2: `$1L`, v4: /([-]?[0-9]+?)L/gi },
37
- "cds.Decimal": { v2: `$1m`, v4: /([-]?[0-9]+?\.?[0-9]*)m/gi },
38
- "cds.DecimalFloat": { v2: `$1f`, v4: /([-]?[0-9]+?\.?[0-9]*)f/gi },
39
- "cds.Double": { v2: `$1d`, v4: /([-]?[0-9]+?\.?[0-9]*(?:E[+-]?[0-9]+?)?)d/gi },
40
- "cds.Date": { v2: `datetime'$1'`, v4: /datetime'(.+?)'/gi },
41
- "cds.Time": { v2: `time'$1'`, v4: /time'(.+?)'/gi },
42
- "cds.DateTime": { v2: `datetimeoffset'$1'`, v4: /datetime(?:offset)?'(.+?)'/gi },
43
- "cds.Timestamp": { v2: `datetimeoffset'$1'`, v4: /datetime(?:offset)?'(.+?)'/gi },
44
- "cds.String": { v2: `'$1'`, v4: /(.*)/gis },
45
- "cds.Binary": { v2: `binary'$1'`, v4: /X'(?:[0-9a-f][0-9a-f])+?'/gi },
46
- "cds.LargeBinary": { v2: `binary'$1'`, v4: /X'(?:[0-9a-f][0-9a-f])+?'/gi },
47
- "cds.LargeString": { v2: `'$1'`, v4: /(.*)/gis },
48
- };
49
-
50
- // https://www.odata.org/documentation/odata-version-2-0/overview/ (6. Primitive Data Types)
51
- // https://cap.cloud.sap/docs/advanced/odata#type-mapping
52
- const DataTypeOData = {
53
- Binary: "cds.Binary",
54
- Boolean: "cds.Boolean",
55
- Byte: "cds.Integer",
56
- DateTime: "cds.DateTime",
57
- Decimal: "cds.Decimal",
58
- Double: "cds.Double",
59
- Single: "cds.Double",
60
- Guid: "cds.UUID",
61
- Int16: "cds.Integer",
62
- Int32: "cds.Integer",
63
- Int64: "cds.Integer64",
64
- SByte: "cds.Integer",
65
- String: "cds.String",
66
- Time: "cds.Time",
67
- DateTimeOffset: "cds.Timestamp",
68
- Date: "cds.Date",
69
- TimeOfDay: "cds.Time",
70
- _Decimal: "cds.DecimalFloat",
71
- _Binary: "cds.LargeBinary",
72
- _String: "cds.LargeString",
73
- };
74
-
75
- const AggregationMap = {
76
- SUM: "sum",
77
- MIN: "min",
78
- MAX: "max",
79
- AVG: "average",
80
- COUNT: "countdistinct",
81
- COUNT_DISTINCT: "countdistinct",
82
- $COUNT: "$count",
83
- NONE: "none",
84
- NOP: "nop",
85
- };
86
-
87
- const DefaultAggregation = AggregationMap.SUM;
88
-
89
- const FilterFunctions = {
90
- "substringof($,$)": "contains($2,$1)",
91
- "gettotaloffsetminutes($)": "totaloffsetminutes($1)",
92
- };
93
-
94
- const FilterFunctionsCaseInsensitive = {
95
- "substringof($,$)": "contains(tolower($2),tolower($1))",
96
- "startswith($,$)": "startswith(tolower($1),tolower($2))",
97
- "endswith($,$)": "endswith(tolower($1),tolower($2))",
98
- };
99
-
100
- const ProcessingDirection = {
101
- Request: "req",
102
- Response: "res",
103
- };
104
-
105
- const DefaultHost = "localhost";
106
- const DefaultPort = 4004;
107
- const DefaultTenant = "00000000-0000-0000-0000-000000000000";
108
- const AggregationPrefix = "__AGGREGATION__";
109
- const IEEE754Compatible = "IEEE754Compatible=true";
110
-
111
- function convertToNodeHeaders(webHeaders) {
112
- return Array.from(webHeaders.entries()).reduce((result, [key, value]) => {
113
- result[key] = value;
114
- return result;
115
- }, {});
116
- }
117
-
118
- /**
119
- * Instantiates a CDS OData V2 Adapter Proxy Express Router for a CDS-based OData V4 Server:
120
- * @param {object} options CDS OData V2 Adapter Proxy options object.
121
- * @param {string} options.base Base path under which the service is reachable. Default is ''.
122
- * @param {string} options.path Path under which the proxy is reachable. Default is 'v2'.
123
- * @param {string|string[]|object} options.model CDS service model (path(s) or CSN). Default is 'all'.
124
- * @param {number} options.port Target port which points to OData V4 backend port. Default is process.env.PORT or 4004.
125
- * @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. 'http://localhost:4004'.
126
- * @param {string} options.targetPath Target path to which is redirected. Default is ''.
127
- * @param {object} options.services Service mapping object from url path name to service name. Default is {}.
128
- * @param {boolean} options.ieee754Compatible Edm.Decimal and Edm.Int64 are serialized IEEE754 compatible. Default is true.
129
- * @param {number} options.fileUploadSizeLimit File upload file size limit (in bytes). Default is 10485760 (10 MB).
130
- * @param {boolean} options.continueOnError Indicates to OData V4 backend to continue on error. Default is false.
131
- * @param {boolean} options.isoTime Use ISO 8601 format for type cds.Time (Edm.Time). Default is false.
132
- * @param {boolean} options.isoDate Use ISO 8601 format for type cds.Date (Edm.DateTime). Default is false.
133
- * @param {boolean} options.isoDateTime Use ISO 8601 format for type cds.DateTime (Edm.DateTimeOffset). Default is false.
134
- * @param {boolean} options.isoTimestamp Use ISO 8601 format for type cds.Timestamp (Edm.DateTimeOffset). Default is false.
135
- * @param {boolean} options.isoDateTimeOffset Use ISO 8601 format for type Edm.DateTimeOffset (cds.DateTime, cds.Timestamp). Default is false.
136
- * @param {string} options.bodyParserLimit Request and response body parser size limit. Default is '100mb'.
137
- * @param {boolean} options.returnCollectionNested Collection of entity type is returned nested into a results section. Default is true.
138
- * @param {boolean} options.returnComplexNested Function import return structure of complex type (non collection) is nested using function import name. Default is true.
139
- * @param {boolean} options.returnPrimitiveNested Function import return structure of primitive type (non collection) is nested using function import name. Default is true.
140
- * @param {boolean} options.returnPrimitivePlain Function import return value of primitive type is rendered as plain JSON value. Default is true.
141
- * @param {string} options.messageTargetDefault Specifies the message target default, if target is undefined. Default is '/#TRANSIENT#'.
142
- * @param {boolean} options.caseInsensitive: Transforms search functions i.e. substringof, startswith, endswith to case-insensitive variant. Default is false.
143
- * @param {boolean} options.propagateMessageToDetails: Propagates root error or message always to details section. Default is false.
144
- * @param {boolean} options.contentDisposition: Default content disposition for media streams (inline, attachment), if not available or calculated. Default is 'attachment'.
145
- * @param {boolean} options.calcContentDisposition: Calculate content disposition for media streams even if already available. Default is false.
146
- * @param {boolean} options.quoteSearch: Specifies if search expression is quoted automatically. Default is true.
147
- * @param {boolean} options.fixDraftRequests: Specifies if unsupported draft requests are converted to a working version. Default is false.
148
- * @returns {express.Router} CDS OData V2 Adapter Proxy Express Router
149
- */
150
- function cov2ap(options = {}) {
151
- const optionWithFallback = (name, fallback) => {
152
- if (options && Object.prototype.hasOwnProperty.call(options, name)) {
153
- return options[name];
154
- }
155
- if (cds.env.cov2ap && Object.prototype.hasOwnProperty.call(cds.env.cov2ap, name)) {
156
- return cds.env.cov2ap[name];
157
- }
158
- return fallback;
159
- };
160
-
161
- const proxyCache = {};
162
- const router = express.Router();
163
- const base = optionWithFallback("base", "");
164
- const _with_leading_slash = s => s.replace(/^([^/])/,'/$1')
165
- const path = _with_leading_slash(optionWithFallback("path", "/v2"))
166
- const sourcePath = _with_leading_slash(optionWithFallback("sourcePath", `${base ? "/" + base : ""}/${path}`))
167
- const targetPath = _with_leading_slash(optionWithFallback("targetPath", ""))
168
- const pathRewrite = { [`^${sourcePath}`]: targetPath };
169
- let port = optionWithFallback("port", process.env.PORT || DefaultPort);
170
- let target = optionWithFallback("target", `http://${DefaultHost}:${port}`);
171
- const logLevel = optionWithFallback("logLevel", 'info');
172
- const services = optionWithFallback("services", {});
173
- const ieee754Compatible = optionWithFallback("ieee754Compatible", true);
174
- const fileUploadSizeLimit = optionWithFallback("fileUploadSizeLimit", 10 * 1024 * 1024);
175
- const continueOnError = optionWithFallback("continueOnError", false);
176
- const isoTime = optionWithFallback("isoTime", false);
177
- const isoDate = optionWithFallback("isoDate", false);
178
- const isoDateTime = optionWithFallback("isoDateTime", false);
179
- const isoTimestamp = optionWithFallback("isoTimestamp", false);
180
- const isoDateTimeOffset = optionWithFallback("isoDateTimeOffset", false);
181
- const bodyParserLimit = optionWithFallback("bodyParserLimit", "100mb");
182
- const returnCollectionNested = optionWithFallback("returnCollectionNested", true);
183
- const returnComplexNested = optionWithFallback("returnComplexNested", true);
184
- const returnPrimitiveNested = optionWithFallback("returnPrimitiveNested", true);
185
- const returnPrimitivePlain = optionWithFallback("returnPrimitivePlain", true);
186
- const messageTargetDefault = optionWithFallback("messageTargetDefault", "/#TRANSIENT#");
187
- const caseInsensitive = optionWithFallback("caseInsensitive", false);
188
- const propagateMessageToDetails = optionWithFallback("propagateMessageToDetails", false);
189
- const contentDisposition = optionWithFallback("contentDisposition", "attachment");
190
- const calcContentDisposition = optionWithFallback("calcContentDisposition", false);
191
- const quoteSearch = optionWithFallback("quoteSearch", true);
192
- const fixDraftRequests = optionWithFallback("fixDraftRequests", false);
193
-
194
- if (caseInsensitive) {
195
- Object.assign(FilterFunctions, FilterFunctionsCaseInsensitive);
196
- }
197
-
198
- const fileUpload = expressFileUpload({
199
- abortOnLimit: true,
200
- limits: {
201
- files: 1,
202
- fileSize: fileUploadSizeLimit,
203
- },
204
- });
205
-
206
- let model = optionWithFallback("model", "all");
207
- if (Array.isArray(model)) {
208
- model = model.map((entry) => (entry === "all" ? "*" : entry));
209
- } else {
210
- model = model === "all" ? "*" : model;
211
- }
212
- model = cds.resolve(model);
213
-
214
- cds.on("serving", (service) => {
215
- const isOData = Object.keys(service._adapters).find((adapter) => adapter.startsWith("odata"));
216
- if (!isOData) {
217
- return;
218
- }
219
- const provider = (entity) => {
220
- const href = `${sourcePath}${service.path}/${entity || "$metadata"}`;
221
- return { href, name: `${entity || "$metadata"} (V2)`, title: "OData V2" };
222
- };
223
- service.$linkProviders = service.$linkProviders || [];
224
- service.$linkProviders.push(provider);
225
- });
226
-
227
- // TODO: Cache invalidation for Streamlined MTX (when extensibility is supported)
228
-
229
- router.use(`${path}/:service`, async (req, res, next) => {
230
- req.contextId =
231
- req.headers["x-correlation-id"] ||
232
- req.headers["x-correlationid"] ||
233
- req.headers["x-request-id"] ||
234
- req.headers["x-vcap-request-id"] ||
235
- cds.utils.uuid();
236
- res.set("x-request-id", req.contextId);
237
- res.set("x-correlation-id", req.contextId);
238
- res.set("x-correlationid", req.contextId);
239
- try {
240
- const [authType, token] = (req.headers.authorization && req.headers.authorization.split(" ")) || [];
241
- if (authType && token) {
242
- let jwtBody;
243
- switch (authType) {
244
- case "Basic":
245
- req.user = {
246
- id: decodeBase64(token).split(":")[0],
247
- };
248
- if (req.user.id && cds.env.requires.auth && cds.env.requires.auth.strategy === "mock") {
249
- const user = (cds.env.requires.auth.users || {})[req.user.id];
250
- req.tenant = user && (user.tenant || (user.jwt && user.jwt.zid));
251
- }
252
- break;
253
- case "Bearer":
254
- jwtBody = decodeJwtTokenBody(token);
255
- req.user = {
256
- id: jwtBody.user_name || jwtBody.client_id,
257
- };
258
- req.tenant = jwtBody.zid;
259
- break;
260
- }
261
- }
262
- } catch (err) {
263
- logError(req, "Authorization", err);
264
- }
265
- next();
266
- });
267
-
268
- router.get(`${path}/*\\$metadata`, async (req, res) => {
269
- let serviceValid = true;
270
- try {
271
- const metadataUrlPath = targetUrl(req);
272
-
273
- // Trace
274
- traceRequest(req, "Request", req.method, req.originalUrl, req.headers, req.body);
275
- traceRequest(req, "ProxyRequest", req.method, metadataUrlPath, req.headers, req.body);
276
-
277
- const result = await Promise.all([
278
- fetch(target + metadataUrlPath, {
279
- method: "GET",
280
- headers: propagateHeaders(req),
281
- }),
282
- (async () => {
283
- const { csn } = await getMetadata(req);
284
- req.csn = csn;
285
- const service = serviceFromRequest(req);
286
- if (service && service.name) {
287
- serviceValid = service.valid;
288
- const { edmx } = await getMetadata(req, service.name);
289
- return edmx;
290
- }
291
- })(),
292
- ]);
293
- const [metadataResponse, edmx] = result;
294
- const headers = convertBasicHeaders(convertToNodeHeaders(metadataResponse.headers));
295
- delete headers["content-encoding"];
296
- const metadataBody = await metadataResponse.text();
297
- let body;
298
- if (metadataResponse.ok) {
299
- body = edmx;
300
- } else {
301
- body = metadataBody;
302
- }
303
- setContentLength(headers, body);
304
-
305
- // Trace
306
- traceResponse(
307
- req,
308
- "Proxy Response",
309
- metadataResponse.status,
310
- metadataResponse.statusMessage,
311
- headers,
312
- metadataBody
313
- );
314
-
315
- respond(req, res, metadataResponse.status, headers, body);
316
- } catch (err) {
317
- if (serviceValid) {
318
- // Error
319
- logError(req, "MetadataRequest", err);
320
- res.status(500).send("Internal Server Error");
321
- } else {
322
- res.status(404).send("Not Found");
323
- }
324
- }
325
- });
326
-
327
- router.use(`${path}/:service`,
328
-
329
- // Body Parsers
330
- (req, res, next) => {
331
- const contentType = req.header("content-type");
332
- if (!contentType) {
333
- return next();
334
- }
335
-
336
- if (isApplicationJSON(contentType)) {
337
- express.json({ limit: bodyParserLimit })(req, res, next);
338
- } else if (isMultipartMixed(contentType)) {
339
- express.text({ type: "multipart/mixed", limit: bodyParserLimit })(req, res, next);
340
- } else {
341
- req.checkUploadBinary = req.method === "POST";
342
- next();
343
- }
344
- },
345
-
346
- // Inject Context
347
- async (req, res, next) => {
348
- try {
349
- const { csn } = await getMetadata(req);
350
- req.csn = csn;
351
- } catch (err) {
352
- // Error
353
- logError(req, "Request", err);
354
- res.status(500).send("Internal Server Error");
355
- return;
356
- }
357
- const service = serviceFromRequest(req);
358
- req.base = base;
359
- req.service = service.name;
360
- req.servicePath = service.path;
361
- req.context = {};
362
- req.contexts = [];
363
- req.contentId = {};
364
- req.lookupContext = {};
365
- next();
366
- },
367
-
368
- // File Upload
369
- async (req, res, next) => {
370
- if (!req.checkUploadBinary) {
371
- return next();
372
- }
373
-
374
- const targetPath = targetUrl(req);
375
- const url = parseUrl(targetPath, req);
376
- const definition = contextFromUrl(url, req);
377
- if (!definition) {
378
- return next();
379
- }
380
- const elements = definitionElements(definition);
381
- const mediaDataElementName =
382
- findElementByAnnotation(elements, "@Core.MediaType") ||
383
- findElementByType(elements, DataTypeOData._Binary, req) ||
384
- findElementByType(elements, DataTypeOData.Binary, req);
385
- if (!mediaDataElementName) {
386
- return next();
387
- }
388
-
389
- const handleMediaEntity = async (contentType, filename, headers = {}) => {
390
- try {
391
- contentType = contentType || "application/octet-stream";
392
- const body = {};
393
- // Custom body
394
- const caseInsensitiveElements = structureKeys(elements).reduce((result, name) => {
395
- result[name.toLowerCase()] = elements[name];
396
- return result;
397
- }, {});
398
- Object.keys(headers).forEach((name) => {
399
- const element = caseInsensitiveElements[name.toLowerCase()];
400
- if (element) {
401
- const value = convertDataTypeToV4(headers[name], elementType(element, req), definition, headers);
402
- body[element.name] = decodeHeaderValue(definition, element, element.name, value);
403
- }
404
- });
405
- const mediaDataElement = elements[mediaDataElementName];
406
- const mediaTypeElementName =
407
- (mediaDataElement["@Core.MediaType"] && mediaDataElement["@Core.MediaType"]["="]) ||
408
- findElementByAnnotation(elements, "@Core.IsMediaType");
409
- if (mediaTypeElementName) {
410
- body[mediaTypeElementName] = contentType;
411
- }
412
- const contentDispositionFilenameElementName =
413
- findElementValueByAnnotation(elements, "@Core.ContentDisposition.Filename") ||
414
- findElementValueByAnnotation(elements, "@Common.ContentDisposition.Filename");
415
- if (contentDispositionFilenameElementName && filename) {
416
- const element = elements[contentDispositionFilenameElementName];
417
- body[contentDispositionFilenameElementName] = decodeHeaderValue(
418
- definition,
419
- element,
420
- element.name,
421
- filename
422
- );
423
- }
424
- const url = target + targetPath;
425
- const postHeaders = propagateHeaders(req, {
426
- ...headers,
427
- "content-type": "application/json",
428
- });
429
- delete postHeaders["transfer-encoding"];
430
-
431
- // Trace
432
- traceRequest(req, "ProxyRequest", "POST", url, postHeaders, body);
433
-
434
- const response = await fetch(url, {
435
- method: "POST",
436
- headers: postHeaders,
437
- body: JSON.stringify(body),
438
- });
439
- const responseBody = await response.json();
440
- const responseHeaders = convertToNodeHeaders(response.headers);
441
- if (!response.ok) {
442
- res
443
- .status(response.status)
444
- .set({
445
- "content-type": "application/json",
446
- })
447
- .send(convertResponseError(responseBody, responseHeaders, definition, req));
448
- return;
449
- }
450
-
451
- // Rewrite
452
- req.method = "PUT";
453
- req.originalUrl += `(${entityKey(responseBody, definition, elements, req)})/${mediaDataElementName}`;
454
- req.baseUrl = req.originalUrl;
455
- req.overwriteResponse = {
456
- kind: "uploadBinary",
457
- statusCode: response.status,
458
- headers: responseHeaders,
459
- body: responseBody,
460
- };
461
-
462
- // Trace
463
- traceResponse(req, "ProxyResponse", response.status, response.statusText, responseHeaders, responseBody);
464
-
465
- next();
466
- } catch (err) {
467
- // Error
468
- logError(req, "FileUpload", err);
469
- res.status(500).send("Internal Server Error");
470
- }
471
- };
472
-
473
- const headers = req.headers;
474
- if (isMultipartFormData(headers["content-type"])) {
475
- fileUpload(req, res, async () => {
476
- await handleMediaEntity(
477
- req.body && req.body["content-type"],
478
- req.body &&
479
- (req.body["slug"] ||
480
- req.body["filename"] ||
481
- contentDispositionFilename(req.body) ||
482
- contentDispositionFilename(headers) ||
483
- req.body["name"]),
484
- req.body
485
- );
486
- });
487
- } else {
488
- await handleMediaEntity(
489
- headers["content-type"],
490
- headers["slug"] || headers["filename"] || contentDispositionFilename(headers) || headers["name"],
491
- headers
492
- );
493
- }
494
- }
495
- );
496
-
497
- // Proxy Middleware
498
- function setupProxyMiddleware() {
499
- return createProxyMiddleware({
500
- target,
501
- changeOrigin: true,
502
- selfHandleResponse: true,
503
- logLevel,
504
- pathRewrite,
505
- onProxyReq: (proxyReq, req, res) => {
506
- convertProxyRequest(proxyReq, req, res);
507
- },
508
- onProxyRes: (proxyRes, req, res) => {
509
- convertProxyResponse(proxyRes, req, res);
510
- },
511
- });
512
- }
513
-
514
- if (target === "auto") {
515
- cds.on("listening", ({ server, url }) => {
516
- port = server.address().port;
517
- target = url;
518
- router.use(`${path}/*`, setupProxyMiddleware());
519
- });
520
- } else {
521
- router.use(`${path}/*`, setupProxyMiddleware());
522
- }
523
-
524
- function contentDispositionFilename(headers) {
525
- const contentDispositionHeader = headers["content-disposition"] || headers["Content-Disposition"];
526
- if (contentDispositionHeader) {
527
- const filenameMatch = contentDispositionHeader.match(/^.*filename="(.*)"$/is);
528
- return filenameMatch && filenameMatch.pop();
529
- }
530
- return null;
531
- }
532
-
533
- function decodeHeaderValue(entity, element, name, value) {
534
- if (value === undefined || value === null || value === "" || typeof value !== "string") {
535
- return value;
536
- }
537
- let decodes = [];
538
- if (Array.isArray(element["@cov2ap.headerDecode"])) {
539
- decodes = element["@cov2ap.headerDecode"];
540
- } else if (typeof element["@cov2ap.headerDecode"] === "string") {
541
- decodes = [element["@cov2ap.headerDecode"]];
542
- }
543
- if (decodes.length > 0) {
544
- decodes.forEach((decode) => {
545
- switch (decode.toLowerCase()) {
546
- case "uri":
547
- value = decodeURI(value);
548
- break;
549
- case "uricomponent":
550
- value = decodeURIComponent(value);
551
- break;
552
- case "base64":
553
- value = decodeBase64(value);
554
- break;
555
- }
556
- });
557
- }
558
- return value;
559
- }
560
-
561
- function serviceFromRequest(req) {
562
- let serviceName;
563
- let serviceValid = true;
564
- let servicePath = req.params.service
565
- let path = '/'+ servicePath
566
- Object.keys(services).find((path) => {
567
- if (servicePath.toLowerCase().startsWith(normalizeSlashes(path).toLowerCase())) {
568
- serviceName = services[path];
569
- servicePath = stripSlashes(path);
570
- return true;
571
- }
572
- return false;
573
- });
574
- if (!serviceName) {
575
- let srv = cds.service.providers.find(service => service.path === path)
576
- if (srv) serviceName = srv.name
577
- }
578
- if (!serviceName) {
579
- let srv = cds.service.providers.find(service => service.path === "/")
580
- if (srv) servicePath = ""
581
- }
582
- if (!serviceName || !req.csn.definitions[serviceName] || req.csn.definitions[serviceName].kind !== "service") {
583
- logWarning(req, "Service", "Service definition not found for request path", {
584
- requestPath: servicePath,
585
- serviceName,
586
- });
587
- serviceValid = false;
588
- }
589
- return {
590
- name: serviceName,
591
- path: servicePath,
592
- valid: serviceValid,
593
- };
594
- }
595
-
596
- async function getMetadata(req, service) {
597
- let metadata;
598
- if (req.tenant && cds.env.requires?.["cds.xt.ModelProviderService"]) {
599
- metadata = await getTenantMetadataStreamlined(req, service);
600
- }
601
- if (!metadata) {
602
- metadata = await getDefaultMetadata(req, service);
603
- }
604
- return metadata;
605
- }
606
-
607
- async function getTenantMetadataStreamlined(req, service) {
608
- const { "cds.xt.ModelProviderService": mps } = cds.services;
609
- proxyCache[req.tenant] = proxyCache[req.tenant] || {};
610
- const isExtended = await callCached(proxyCache[req.tenant], "isExtended", () => {
611
- return mps.isExtended(req.tenant);
612
- });
613
- if (isExtended) {
614
- return await prepareMetadata(
615
- req.tenant,
616
- async (tenant) => {
617
- return await mps.getCsn(tenant, ensureArray(req.features), "nodejs"); // TODO: getExtCsn()? (when extensibility is supported)
618
- },
619
- async (tenant, service, locale) => {
620
- return await mps.getEdmx(tenant, ensureArray(req.features), service, locale, "v2", "nodejs");
621
- },
622
- service,
623
- determineLocale(req)
624
- );
625
- }
626
- }
627
-
628
- async function getDefaultMetadata(req, service) {
629
- return await prepareMetadata(
630
- DefaultTenant,
631
- async () => {
632
- if (typeof model === "object" && !Array.isArray(model)) {
633
- return model;
634
- }
635
- return await cds.load(model);
636
- },
637
- async () => {},
638
- service,
639
- determineLocale(req)
640
- );
641
- }
642
-
643
- async function prepareMetadata(tenant, loadCsn, loadEdmx, service, locale) {
644
- proxyCache[tenant] = proxyCache[tenant] || {};
645
- const csn = await callCached(proxyCache[tenant], "csn", () => {
646
- return prepareCSN(tenant, loadCsn);
647
- });
648
- if (!service) {
649
- return { csn };
650
- }
651
- proxyCache[tenant].edmx = proxyCache[tenant].edmx || {};
652
- proxyCache[tenant].edmx[service] = proxyCache[tenant].edmx[service] || {};
653
- const edmx = await callCached(proxyCache[tenant].edmx[service], locale, () => {
654
- return prepareEdmx(tenant, csn, loadEdmx, service, locale);
655
- });
656
- return { csn, edmx };
657
- }
658
-
659
- async function prepareCSN(tenant, loadCsn) {
660
- let csnRaw;
661
- if (cds.server && cds.model && tenant === DefaultTenant) {
662
- csnRaw = cds.model;
663
- } else {
664
- csnRaw = await loadCsn(tenant);
665
- }
666
- let csn;
667
- if (cds.compile.for.nodejs) {
668
- csn = cds.compile.for.nodejs(csnRaw);
669
- } else {
670
- csn = csnRaw.meta && csnRaw.meta.transformation === "odata" ? csnRaw : cds.linked(cds.compile.for.odata(csnRaw));
671
- }
672
- return csn;
673
- }
674
-
675
- async function prepareEdmx(tenant, csn, loadEdmx, service, locale) {
676
- let edmx;
677
- if (tenant !== DefaultTenant) {
678
- edmx = await loadEdmx(tenant, service, locale);
679
- }
680
- if (!edmx) {
681
- edmx = await cds.compile.to.edmx(csn, {
682
- service,
683
- version: "v2",
684
- });
685
- edmx = cds.localize(csn, locale, edmx);
686
- }
687
- return edmx;
688
- }
689
-
690
- function localName(name) {
691
- return name.split(".").pop();
692
- }
693
-
694
- async function callCached(cache, field, call) {
695
- if (!cache[field]) {
696
- cache[field] = call();
697
- }
698
- try {
699
- return await cache[field];
700
- } catch (err) {
701
- delete cache[field];
702
- throw err;
703
- }
704
- }
705
-
706
- function localEntityName(definition, req) {
707
- const parts = definition.name.split(".");
708
- const localName = [parts.pop()];
709
- while (parts.length > 0) {
710
- const context = req.csn.definitions[parts.join(".")];
711
- if (context && context.kind === "entity") {
712
- localName.unshift(parts.pop());
713
- } else {
714
- break;
715
- }
716
- }
717
- const nameSuffix =
718
- definition.kind === "entity" &&
719
- definition.params &&
720
- req.context.parameters &&
721
- req.context.parameters.kind === "Set"
722
- ? "Set"
723
- : "";
724
- return localName.join("_") + nameSuffix;
725
- }
726
-
727
- function qualifiedName(name, req) {
728
- const serviceNamespacePrefix = `${req.service}.`;
729
- return (name.startsWith(serviceNamespacePrefix) ? "" : serviceNamespacePrefix) + name;
730
- }
731
-
732
- function lookupDefinition(name, req) {
733
- const definitionName = qualifiedName(name, req);
734
- return req.csn.definitions[definitionName] || req.csn.definitions[name];
735
- }
736
-
737
- function lookupBoundDefinition(name, req) {
738
- let boundAction = undefined;
739
- structureKeys(req.csn.definitions).find((definitionName) => {
740
- const definition = req.csn.definitions[definitionName];
741
- return structureKeys(definition.actions).find((actionName) => {
742
- if (name.endsWith(`_${actionName}`)) {
743
- const entityName = name.substr(0, name.length - `_${actionName}`.length);
744
- const entityDefinition = lookupDefinition(entityName, req);
745
- if (entityDefinition === definition) {
746
- boundAction = definition.actions[actionName];
747
- req.lookupContext.boundDefinition = definition;
748
- req.lookupContext.operation = boundAction;
749
- const returnDefinition = lookupReturnDefinition(boundAction.returns, req);
750
- if (returnDefinition) {
751
- req.lookupContext.returnDefinition = returnDefinition;
752
- }
753
- return true;
754
- }
755
- }
756
- return false;
757
- });
758
- });
759
- return boundAction;
760
- }
761
-
762
- function lookupParametersDefinition(name, req) {
763
- const definitionTypeName = qualifiedName(name, req);
764
- let definitionKind;
765
- if (definitionTypeName.endsWith("Set")) {
766
- definitionKind = "Set";
767
- } else if (definitionTypeName.endsWith("Parameters")) {
768
- definitionKind = "Parameters";
769
- }
770
- if (definitionKind) {
771
- const definitionName = definitionTypeName.substring(0, definitionTypeName.length - definitionKind.length);
772
- const definition = req.csn.definitions[definitionName] || req.csn.definitions[name];
773
- if (definition && definition.kind === "entity" && definition.params) {
774
- req.lookupContext.parameters = {
775
- kind: definitionKind,
776
- entity: localName(definitionName),
777
- type: localName(definitionTypeName),
778
- values: {},
779
- keys: {},
780
- count: false,
781
- };
782
- return definition;
783
- }
784
- }
785
- }
786
-
787
- function enhanceParametersDefinition(context, req) {
788
- if (context && context.kind === "entity" && context.params) {
789
- req.lookupContext.parameters = req.lookupContext.parameters || {
790
- kind: "Parameters",
791
- entity: localName(context.name),
792
- type: localName(context.name),
793
- values: {},
794
- keys: {},
795
- count: false,
796
- };
797
- }
798
- }
799
-
800
- /**
801
- * Convert Proxy Request (v2 -> v4)
802
- * @param proxyReq Proxy Request
803
- * @param req Request
804
- * @param res Response
805
- */
806
- async function convertProxyRequest(proxyReq, req, res) {
807
- try {
808
- // Trace
809
- traceRequest(req, "Request", req.method, req.originalUrl, req.headers, req.body);
810
-
811
- const headers = propagateHeaders(req);
812
- let body = req.body;
813
- let contentType = req.header("content-type");
814
-
815
- if (isMultipartMixed(contentType)) {
816
- // Multipart
817
- req.contentIdOrder = [];
818
- body =
819
- req.method === "HEAD"
820
- ? ""
821
- : processMultipartMixed(
822
- req,
823
- req.body,
824
- contentType,
825
- ({ method, url }) => {
826
- return {
827
- method: method === "MERGE" ? "PATCH" : method,
828
- url: convertUrl(url, req),
829
- };
830
- },
831
- ({ contentType, body, headers, url, contentId }) => {
832
- if (contentId) {
833
- req.contentId[`$${contentId}`] = req.context.url;
834
- }
835
- delete headers.dataserviceversion;
836
- delete headers.DataServiceVersion;
837
- delete headers.maxdataserviceversion;
838
- delete headers.MaxDataServiceVersion;
839
- if (isApplicationJSON(contentType)) {
840
- if (ieee754Compatible) {
841
- contentType = enrichApplicationJSON(contentType);
842
- headers["content-type"] = contentType;
843
- }
844
- body = convertRequestBody(body, headers, url, req);
845
- }
846
- return { body, headers };
847
- },
848
- req.contentIdOrder,
849
- ProcessingDirection.Request
850
- );
851
- headers.accept = "multipart/mixed,application/json";
852
- proxyReq.setHeader("accept", headers.accept);
853
- } else {
854
- // Single
855
- proxyReq.path = convertUrl(proxyReq.path, req);
856
- if (req.context.serviceRoot && (!headers.accept || headers.accept.includes("xml"))) {
857
- req.context.serviceRootAsXML = true;
858
- headers.accept = "application/json";
859
- proxyReq.setHeader("accept", headers.accept);
860
- } else if (headers.accept && !headers.accept.includes("application/json")) {
861
- headers.accept = "application/json," + headers.accept;
862
- proxyReq.setHeader("accept", headers.accept);
863
- }
864
- if (isApplicationJSON(contentType)) {
865
- if (ieee754Compatible) {
866
- contentType = enrichApplicationJSON(contentType);
867
- headers["content-type"] = contentType;
868
- }
869
- body = convertRequestBody(req.body, req.headers, proxyReq.path, req);
870
- }
871
- }
872
-
873
- Object.keys(headers).forEach(name => {
874
- if (
875
- name === "dataserviceversion" ||
876
- name === "DataServiceVersion" ||
877
- name === "maxdataserviceversion" ||
878
- name === "MaxDataServiceVersion"
879
- ) {
880
- delete headers[name];
881
- proxyReq.removeHeader(name);
882
- }
883
- });
884
-
885
- if (continueOnError) {
886
- headers["prefer"] = "odata.continue-on-error";
887
- proxyReq.setHeader("prefer", "odata.continue-on-error");
888
- }
889
- headers["x-cds-odata-version"] = "v2";
890
- proxyReq.setHeader("x-cds-odata-version", "v2");
891
-
892
- if (req.body) {
893
- delete req.body;
894
- }
895
- if (headers["x-http-method"]) {
896
- proxyReq.method = headers["x-http-method"].toUpperCase();
897
- }
898
- proxyReq.method = proxyReq.method === "MERGE" ? "PATCH" : proxyReq.method;
899
-
900
- if (contentType) {
901
- if (body !== undefined) {
902
- // File Upload
903
- if (req.files && Object.keys(req.files).length === 1) {
904
- const file = req.files[Object.keys(req.files)[0]];
905
- contentType = body["content-type"] || file.mimetype;
906
- body = file.data;
907
- }
908
- proxyReq.setHeader("content-type", contentType);
909
- body = normalizeBody(body);
910
- proxyReq.setHeader("content-length", Buffer.byteLength(body));
911
- proxyReq.write(body);
912
- proxyReq.end();
913
- } else if ((req.header("transfer-encoding") || "").includes("chunked")) {
914
- proxyReq.setHeader("content-type", contentType);
915
- req.pipe(proxyReq);
916
- }
917
- }
918
-
919
- // Trace
920
- traceRequest(req, "ProxyRequest", proxyReq.method, proxyReq.path, headers, body);
921
- } catch (err) {
922
- // Error
923
- logError(req, "Request", err);
924
- if (err.statusCode) {
925
- res.status(err.statusCode).send(err.message);
926
- } else {
927
- res.status(500).send("Internal Server Error");
928
- }
929
- }
930
- }
931
-
932
- function convertUrl(urlPath, req) {
933
- let url = parseUrl(urlPath, req);
934
- const definition = lookupContextFromUrl(url, req);
935
- enrichRequest(definition, url, urlPath, req);
936
-
937
- // Order is important
938
- convertUrlLinks(url, req);
939
- convertUrlDataTypes(url, req);
940
- convertUrlCount(url, req);
941
- convertDraft(url, req);
942
- convertActionFunction(url, req);
943
- convertFilter(url, req);
944
- convertExpandSelect(url, req);
945
- convertSearch(url, req);
946
- convertAnalytics(url, req);
947
- convertValue(url, req);
948
- convertParameters(url, req);
949
-
950
- delete url.search;
951
- url.pathname = url.basePath + url.servicePath + url.contextPath;
952
- return URL.format(url);
953
- }
954
-
955
- function parseUrl(urlPath, req) {
956
- const url = URL.parse(urlPath, true);
957
- url.pathname = (url.pathname && url.pathname.replace(/%27/g, "'")) || "";
958
- url.originalUrl = { ...url, query: { ...url.query } };
959
- url.basePath = "";
960
- url.servicePath = "";
961
- url.contextPath = url.pathname;
962
- if (req.base && url.contextPath.startsWith(`/${req.base}`)) {
963
- url.basePath = `/${req.base}`;
964
- url.contextPath = url.contextPath.substr(url.basePath.length);
965
- }
966
- if (targetPath && url.contextPath.startsWith(targetPath)) {
967
- url.basePath = targetPath;
968
- url.contextPath = url.contextPath.substr(url.basePath.length);
969
- }
970
- if (url.contextPath.startsWith(`/${req.servicePath}`)) {
971
- url.servicePath = `/${req.servicePath}`;
972
- url.contextPath = url.contextPath.substr(url.servicePath.length);
973
- }
974
- if (url.contextPath.startsWith("/")) {
975
- url.servicePath += "/";
976
- url.contextPath = url.contextPath.substr(1);
977
- }
978
- url.originalUrl.servicePath = url.servicePath;
979
- url.originalUrl.contextPath = url.contextPath;
980
- // Normalize system and reserved query parameters (no array), others not (if array)
981
- Object.keys(url.query || {}).forEach((name) => {
982
- if (Array.isArray(url.query[name])) {
983
- if (name.startsWith("$") || ["search", "SideEffectsQualifier"].includes(name)) {
984
- url.query[name] = url.query[name][0];
985
- }
986
- }
987
- });
988
- return url;
989
- }
990
-
991
- function lookupContextFromUrl(url, req, context) {
992
- req.lookupContext = {};
993
- return contextFromUrl(url, req, context);
994
- }
995
-
996
- function contextFromUrl(url, req, context, suppressWarning) {
997
- let stop = false;
998
- return url.contextPath.split("/").reduce((context, part) => {
999
- if (stop) {
1000
- return context;
1001
- }
1002
- const keyStart = part.indexOf("(");
1003
- if (keyStart !== -1) {
1004
- part = part.substr(0, keyStart);
1005
- }
1006
- context = lookupContext(part, context, req, suppressWarning);
1007
- if (!context) {
1008
- stop = true;
1009
- }
1010
- return context;
1011
- }, context);
1012
- }
1013
-
1014
- function lookupContext(name, context, req, suppressWarning) {
1015
- if (!name) {
1016
- return context;
1017
- }
1018
- if (!context) {
1019
- if (name.startsWith("$") && req.contentId[name]) {
1020
- return contextFromUrl(req.contentId[name], req, undefined, suppressWarning);
1021
- } else {
1022
- context = lookupDefinition(name, req);
1023
- if (!context) {
1024
- context = lookupBoundDefinition(name, req);
1025
- }
1026
- if (!context) {
1027
- context = lookupParametersDefinition(name, req);
1028
- }
1029
- enhanceParametersDefinition(context, req);
1030
- if (!context && !suppressWarning) {
1031
- logWarning(req, "Context", "Definition name not found", {
1032
- name,
1033
- });
1034
- }
1035
- if (context && (context.kind === "function" || context.kind === "action")) {
1036
- req.lookupContext.operation = context;
1037
- const returnDefinition = lookupReturnDefinition(context.returns, req);
1038
- if (returnDefinition) {
1039
- req.lookupContext.returnDefinition = returnDefinition;
1040
- }
1041
- }
1042
- return context;
1043
- }
1044
- } else {
1045
- if (name.startsWith("$")) {
1046
- return context;
1047
- }
1048
- if (context.kind === "function" || context.kind === "action") {
1049
- req.lookupContext.operation = context;
1050
- const returnDefinition = lookupReturnDefinition(context.returns, req);
1051
- if (returnDefinition) {
1052
- context = returnDefinition;
1053
- req.lookupContext.returnDefinition = context;
1054
- }
1055
- }
1056
- const element = definitionElements(context)[name];
1057
- if (element) {
1058
- const type = elementType(element, req);
1059
- if (type === "cds.Composition" || type === "cds.Association") {
1060
- // Navigation
1061
- return element._target;
1062
- } else {
1063
- // Element
1064
- return context;
1065
- }
1066
- }
1067
- if (context && context.kind === "entity" && context.params && ["Set", "Parameters"].includes(name)) {
1068
- return context;
1069
- }
1070
- if (!suppressWarning) {
1071
- logWarning(req, "Context", "Definition name not found", {
1072
- name,
1073
- });
1074
- }
1075
- }
1076
- }
1077
-
1078
- function enrichRequest(definition, url, urlPath, req) {
1079
- req.context = {
1080
- url,
1081
- urlPath,
1082
- serviceRoot: url.contextPath.length === 0,
1083
- serviceRootAsXML: false,
1084
- definition: definition,
1085
- definitionElements: definitionElements(definition),
1086
- requestDefinition: definition,
1087
- serviceUri: "",
1088
- operation: null,
1089
- boundDefinition: null,
1090
- returnDefinition: null,
1091
- bodyParameters: {},
1092
- $entityValue: false,
1093
- $value: false,
1094
- $count: false,
1095
- $apply: null,
1096
- aggregationKey: false,
1097
- aggregationFilter: "",
1098
- parameters: null,
1099
- expandSiblingEntity: false,
1100
- ...req.lookupContext,
1101
- };
1102
- req.contexts.push(req.context);
1103
- return req.context;
1104
- }
1105
-
1106
- function convertUrlLinks(url) {
1107
- url.contextPath = url.contextPath.replace(/\/\$links\//gi, "/");
1108
- }
1109
-
1110
- function convertUrlDataTypes(url, req) {
1111
- // Keys & Parameters
1112
- let context;
1113
- let stop = false;
1114
- url.contextPath = url.contextPath
1115
- .split("/")
1116
- .map((part) => {
1117
- if (stop) {
1118
- return part;
1119
- }
1120
- let keyPart = "";
1121
- const keyStart = part.indexOf("(");
1122
- const keyEnd = part.lastIndexOf(")");
1123
- if (keyStart !== -1 && keyEnd === part.length - 1) {
1124
- keyPart = part.substring(keyStart + 1, keyEnd);
1125
- part = part.substr(0, keyStart);
1126
- }
1127
- context = lookupContext(part, context, req);
1128
- if (!context) {
1129
- stop = true;
1130
- }
1131
- const contextElements = definitionElements(context);
1132
- const contextKeys = definitionKeys(context);
1133
- if (context && keyPart) {
1134
- const aggregationMatch =
1135
- keyPart.match(/^aggregation'(.*)'$/is) ||
1136
- keyPart.match(/^'aggregation'(.*)''$/is) ||
1137
- keyPart.match(/^ID__='aggregation'(.*)''$/is);
1138
- const aggregationKey = aggregationMatch && aggregationMatch.pop();
1139
- if (aggregationKey) {
1140
- // Aggregation Key
1141
- try {
1142
- const aggregation = JSON.parse(decodeURIKey(aggregationKey));
1143
- url.query["$select"] = (aggregation.value || []).join(",");
1144
- delete url.query["$filter"];
1145
- if (Object.keys(aggregation.key || {}).length > 0) {
1146
- url.query["$filter"] = Object.keys(aggregation.key || {})
1147
- .map((name) => {
1148
- return `${name} eq ${aggregation.key[name]}`;
1149
- })
1150
- .join(" and ");
1151
- }
1152
- if (aggregation.filter) {
1153
- if (!url.query["$filter"]) {
1154
- url.query["$filter"] = aggregation.filter;
1155
- } else {
1156
- url.query["$filter"] = `(${url.query["$filter"]}) and (${aggregation.filter})`;
1157
- }
1158
- req.context.aggregationFilter = aggregation.filter;
1159
- }
1160
- if (aggregation.search) {
1161
- url.query["search"] = aggregation.search;
1162
- req.context.aggregationSearch = aggregation.search;
1163
- }
1164
- req.context.aggregationKey = true;
1165
- return part;
1166
- } catch (err) {
1167
- // Error
1168
- logError(req, "AggregationKey", err);
1169
- return part;
1170
- }
1171
- } else {
1172
- const keys = decodeURIComponent(keyPart).split(",");
1173
- return encodeURIComponent(
1174
- `${part}(${keys
1175
- .map((key) => {
1176
- const [name, value] = key.split("=");
1177
- let type;
1178
- if (name && value) {
1179
- if (context.params && context.params[name]) {
1180
- type = context.params[name].type;
1181
- }
1182
- if (!type) {
1183
- type = elementType(contextElements[name], req);
1184
- }
1185
- return `${name}=${replaceConvertDataTypeToV4(value, type)}`;
1186
- } else if (name) {
1187
- const key = structureKeys(contextKeys).find((key) => {
1188
- return contextKeys[key].type !== "cds.Composition" && contextKeys[key].type !== "cds.Association";
1189
- });
1190
- type = key && elementType(contextElements[key], req);
1191
- return type && `${replaceConvertDataTypeToV4(name, type)}`;
1192
- }
1193
- return "";
1194
- })
1195
- .filter((part) => !!part)
1196
- .join(",")})`
1197
- );
1198
- }
1199
- } else {
1200
- return part;
1201
- }
1202
- })
1203
- .join("/");
1204
-
1205
- // Query
1206
- Object.keys(url.query).forEach((name) => {
1207
- if (name === "$filter") {
1208
- url.query[name] = convertUrlDataTypesForFilter(url.query[name], context, req);
1209
- } else if (!name.startsWith("$")) {
1210
- const contextElements = definitionElements(context);
1211
- if (contextElements[name]) {
1212
- const element = contextElements[name];
1213
- const type = elementType(element, req);
1214
- if (DataTypeMap[type]) {
1215
- url.query[name] = replaceConvertDataTypeToV4(url.query[name], type);
1216
- }
1217
- }
1218
- if (context && (context.kind === "function" || context.kind === "action")) {
1219
- if (context.params && context.params[name]) {
1220
- const element = context.params[name];
1221
- const type = elementType(element, req);
1222
- if (DataTypeMap[type]) {
1223
- url.query[name] = replaceConvertDataTypeToV4(url.query[name], type);
1224
- }
1225
- }
1226
- if (context.parent && context.parent.kind === "entity") {
1227
- const parentElements = definitionElements(context.parent);
1228
- if (parentElements[name]) {
1229
- const element = parentElements[name];
1230
- const type = elementType(element, req);
1231
- if (DataTypeMap[type]) {
1232
- url.query[name] = replaceConvertDataTypeToV4(url.query[name], type);
1233
- }
1234
- }
1235
- }
1236
- }
1237
- }
1238
- });
1239
- }
1240
-
1241
- function buildQuoteParts(input) {
1242
- let quote = false;
1243
- let quoteEscape = false;
1244
- let quoteTypeStart = false;
1245
- let part = "";
1246
- const parts = [];
1247
- input.split("").forEach((char, index) => {
1248
- part += char;
1249
- if (char === "'") {
1250
- if (quote) {
1251
- if (quoteEscape) {
1252
- quoteEscape = false;
1253
- return;
1254
- }
1255
- const nextChar = input.substr(index + 1, 1);
1256
- if (nextChar === "'") {
1257
- quoteEscape = true;
1258
- return;
1259
- }
1260
- }
1261
- const typeStart = !!Object.keys(DataTypeMap)
1262
- .filter((type) => !["cds.String", "cds.LargeString"].includes(type))
1263
- .find((type) => {
1264
- const v2Pattern = DataTypeMap[type].v2;
1265
- return v2Pattern.includes("'") && part.endsWith(v2Pattern.split("'").shift() + "'");
1266
- });
1267
- if (!typeStart && !quoteTypeStart) {
1268
- if (part.length > 0) {
1269
- parts.push({
1270
- content: part,
1271
- quote: quote,
1272
- });
1273
- part = "";
1274
- }
1275
- quote = !quote;
1276
- }
1277
- quoteTypeStart = typeStart;
1278
- }
1279
- });
1280
- if (part.length > 0) {
1281
- parts.push({
1282
- content: part,
1283
- quote: quote,
1284
- });
1285
- }
1286
- return parts;
1287
- }
1288
-
1289
- function convertUrlDataTypesForFilter(filter, context, req) {
1290
- if (filter === null || filter === undefined) {
1291
- return filter;
1292
- }
1293
- return buildQuoteParts(filter)
1294
- .map((part) => {
1295
- if (!part.quote) {
1296
- convertUrlDataTypesForFilterElements(part, context, req);
1297
- }
1298
- return part.content;
1299
- })
1300
- .join("");
1301
- }
1302
-
1303
- function convertUrlDataTypesForFilterElements(part, entity, req, path = "", depth = 0) {
1304
- const elements = definitionElements(entity);
1305
- for (let name of structureKeys(elements)) {
1306
- const namePath = (path ? `${path}/` : "") + name;
1307
- if (part.content.includes(namePath)) {
1308
- const element = elements[name];
1309
- const type = elementType(element, req);
1310
- if (type !== "cds.Composition" && type !== "cds.Association") {
1311
- if (DataTypeMap[type]) {
1312
- const v4Regex = new RegExp(
1313
- `(${namePath})(\\)?\\s+?(?:eq|ne|gt|ge|lt|le)\\s+?)${DataTypeMap[type].v4.source}`,
1314
- DataTypeMap[type].v4.flags
1315
- );
1316
- if (v4Regex.test(part.content)) {
1317
- part.content = part.content.replace(v4Regex, (_, name, op, value) => {
1318
- return `${name}${op}${convertDataTypeToV4(value, type)}`;
1319
- });
1320
- }
1321
- }
1322
- } else if (depth < 3 && (!element.cardinality || element.cardinality.max !== "*")) {
1323
- convertUrlDataTypesForFilterElements(part, element._target, req, namePath, depth + 1);
1324
- }
1325
- }
1326
- }
1327
- }
1328
-
1329
- function convertUrlCount(url, req) {
1330
- if (url.query["$inlinecount"]) {
1331
- url.query["$count"] = url.query["$inlinecount"] === "allpages";
1332
- req.context.$count = url.query["$count"];
1333
- delete url.query["$inlinecount"];
1334
- }
1335
- return url;
1336
- }
1337
-
1338
- function convertDraft(url, req) {
1339
- if (
1340
- req.context &&
1341
- req.context.definition &&
1342
- req.context.definition.kind === "action" &&
1343
- req.context.definition.params &&
1344
- req.context.definition.params.SideEffectsQualifier
1345
- ) {
1346
- url.query.SideEffectsQualifier = url.query.SideEffectsQualifier || "";
1347
- }
1348
- }
1349
-
1350
- function convertActionFunction(url, req) {
1351
- const definition = req.context && (req.context.operation || req.context.definition);
1352
- if (!(definition && (definition.kind === "function" || definition.kind === "action"))) {
1353
- return;
1354
- }
1355
- const operationLocalName = localEntityName(definition, req);
1356
- let reqContextPathSuffix = "";
1357
- if (url.contextPath.startsWith(operationLocalName)) {
1358
- reqContextPathSuffix = url.contextPath.substr(operationLocalName.length);
1359
- url.contextPath = url.contextPath.substr(0, operationLocalName.length);
1360
- }
1361
- // Key Parameters
1362
- if (definition.parent && definition.parent.kind === "entity") {
1363
- url.contextPath = localEntityName(definition.parent, req);
1364
- url.contextPath += `(${structureKeys(definitionKeys(definition.parent))
1365
- .reduce((result, name) => {
1366
- const parentElements = definitionElements(definition.parent);
1367
- const element = parentElements[name];
1368
- const type = elementType(element, req);
1369
- if (!(type === "cds.Composition" || type === "cds.Association")) {
1370
- const value = url.query[name];
1371
- result.push(`${name}=${quoteParameter(element, value, req)}`);
1372
- delete url.query[name];
1373
- }
1374
- return result;
1375
- }, [])
1376
- .join(",")})`;
1377
- url.contextPath += `/${req.service}.${definition.name}`;
1378
- }
1379
- // Function Parameters
1380
- if (definition.kind === "function") {
1381
- url.contextPath += `(${Object.keys(url.query)
1382
- .reduce((result, name) => {
1383
- if (!name.startsWith("$")) {
1384
- const element = definition.params && definition.params[name];
1385
- if (element) {
1386
- let value = url.query[name];
1387
- if (Array.isArray(value)) {
1388
- value = value.map((entry) => {
1389
- return quoteParameter(element, encodeURIComponent(entry), req);
1390
- });
1391
- } else {
1392
- value = quoteParameter(element, encodeURIComponent(value), req);
1393
- if (element.items && element.items.type) {
1394
- value = [value];
1395
- }
1396
- }
1397
- if (Array.isArray(value)) {
1398
- result.push(`${name}=@${name}Col`);
1399
- value = value.map((entry) => {
1400
- return quoteParameter(element, entry, req, '"');
1401
- });
1402
- url.query[`@${name}Col`] = `[${value}]`;
1403
- } else {
1404
- result.push(`${name}=${value}`);
1405
- }
1406
- delete url.query[name];
1407
- }
1408
- }
1409
- return result;
1410
- }, [])
1411
- .join(",")})`;
1412
- }
1413
- url.contextPath += reqContextPathSuffix;
1414
- // Action Body
1415
- if (definition.kind === "action") {
1416
- Object.keys(url.query).forEach((name) => {
1417
- if (!name.startsWith("$")) {
1418
- const element = definition.params && definition.params[name];
1419
- if (element) {
1420
- let value = url.query[name];
1421
- if (Array.isArray(value)) {
1422
- value = value.map((entry) => {
1423
- return unescapeSingleQuote(element, unquoteParameter(element, entry, req), req);
1424
- });
1425
- } else {
1426
- value = unescapeSingleQuote(element, unquoteParameter(element, value, req), req);
1427
- if (element.items && element.items.type) {
1428
- value = [value];
1429
- }
1430
- }
1431
- req.context.bodyParameters[name] = value;
1432
- delete url.query[name];
1433
- }
1434
- }
1435
- });
1436
- }
1437
- }
1438
-
1439
- function unescapeSingleQuote(element, value, req) {
1440
- if (element && value && ["cds.String", "cds.LargeString"].includes(elementType(element, req))) {
1441
- return value.replace(/''/g, "'");
1442
- }
1443
- return value;
1444
- }
1445
-
1446
- function quoteParameter(element, value, req, quote = "'") {
1447
- if (element && ["cds.String", "cds.LargeString"].includes(elementType(element, req))) {
1448
- return `${quote}${unquoteParameter(element, value, req)}${quote}`;
1449
- }
1450
- return value;
1451
- }
1452
-
1453
- function unquoteParameter(element, value, req) {
1454
- if (
1455
- element &&
1456
- value &&
1457
- [
1458
- "cds.String",
1459
- "cds.LargeString",
1460
- "cds.UUID",
1461
- "cds.Binary",
1462
- "cds.LargeBinary",
1463
- "cds.Date",
1464
- "cds.Time",
1465
- "cds.DateTime",
1466
- "cds.Timestamp",
1467
- ].includes(elementType(element, req))
1468
- ) {
1469
- return value.replace(/^['](.*)[']$/s, "$1");
1470
- }
1471
- return value;
1472
- }
1473
-
1474
- function stripSlashes(path) {
1475
- return path && path.replace(/^\/|\/$/g, "");
1476
- }
1477
-
1478
- function normalizeSlashes(path) {
1479
- path = stripSlashes(path);
1480
- return path ? `/${path}/` : "/";
1481
- }
1482
-
1483
- function convertExpandSelect(url, req) {
1484
- const definition = req.context && req.context.definition;
1485
- if (definition) {
1486
- const context = { select: {}, expand: {} };
1487
- if (url.query["$expand"]) {
1488
- let expands = url.query["$expand"].split(",");
1489
- if (definition.kind === "entity" && definition.params) {
1490
- expands = expands.filter((expand) => !["Set", "Parameters"].includes(expand));
1491
- }
1492
- expands.forEach((expand) => {
1493
- if (fixDraftRequests && expand === "SiblingEntity") {
1494
- req.context.expandSiblingEntity = true;
1495
- return;
1496
- }
1497
- let current = context.expand;
1498
- expand.split("/").forEach((part) => {
1499
- current[part] = current[part] || { select: {}, expand: {} };
1500
- current = current[part].expand;
1501
- });
1502
- });
1503
- }
1504
- if (url.query["$select"]) {
1505
- const selects = url.query["$select"].split(",");
1506
- selects.forEach((select) => {
1507
- let current = context;
1508
- let currentDefinition = definition;
1509
- select.split("/").forEach((part) => {
1510
- if (!current) {
1511
- return;
1512
- }
1513
- const element = definitionElements(currentDefinition)[part];
1514
- if (element) {
1515
- const type = elementType(element, req);
1516
- if (type === "cds.Composition" || type === "cds.Association") {
1517
- current = current && current.expand[part];
1518
- currentDefinition = element._target;
1519
- } else if (current && current.select) {
1520
- current.select[part] = true;
1521
- }
1522
- }
1523
- });
1524
- });
1525
- if (Object.keys(context.select).length > 0) {
1526
- url.query["$select"] = Object.keys(context.select).join(",");
1527
- } else {
1528
- delete url.query["$select"];
1529
- }
1530
- }
1531
- if (url.query["$expand"]) {
1532
- const serializeExpand = (expand) => {
1533
- return Object.keys(expand || {})
1534
- .map((name) => {
1535
- let value = expand[name];
1536
- let result = name;
1537
- const selects = Object.keys(value.select);
1538
- const expands = Object.keys(value.expand);
1539
- if (selects.length > 0 || expands.length > 0) {
1540
- result += "(";
1541
- if (selects.length > 0) {
1542
- result += `$select=${selects.join(",")}`;
1543
- }
1544
- if (expands.length > 0) {
1545
- if (selects.length > 0) {
1546
- result += ";";
1547
- }
1548
- result += `$expand=${serializeExpand(value.expand)}`;
1549
- }
1550
- result += ")";
1551
- }
1552
- return result;
1553
- })
1554
- .join(",");
1555
- };
1556
- if (Object.keys(context.expand).length > 0) {
1557
- url.query["$expand"] = serializeExpand(context.expand);
1558
- } else {
1559
- delete url.query["$expand"];
1560
- }
1561
- }
1562
- }
1563
- }
1564
-
1565
- function convertFilter(url) {
1566
- const _ = "§§";
1567
-
1568
- let filter = url.query["$filter"];
1569
- if (filter) {
1570
- // Fix unsupported draft requests
1571
- if (fixDraftRequests) {
1572
- const match = filter.match(UnsupportedDraftFilterRegex);
1573
- if (match && match.length === 3 && match[1] === match[2]) {
1574
- filter = filter.replace(match[0], match[1]);
1575
- }
1576
- }
1577
- // Convert functions
1578
- let quote = false;
1579
- let lookBehind = "";
1580
- let bracket = 0;
1581
- let brackets = [];
1582
- let bracketMax = 0;
1583
-
1584
- filter = filter
1585
- .split("")
1586
- .map((char) => {
1587
- if (char === "'") {
1588
- quote = !quote;
1589
- }
1590
- if (!quote) {
1591
- if (char === "(") {
1592
- bracket++;
1593
- const filterFunctionStart = !!Object.keys(FilterFunctions).find((name) => {
1594
- return lookBehind.endsWith(name.split("(").shift());
1595
- });
1596
- if (filterFunctionStart) {
1597
- brackets.push(bracket - 1);
1598
- bracketMax = Math.max(bracketMax, brackets.length);
1599
- return `${_}(${brackets.length}${_}`;
1600
- }
1601
- } else if (char === ")") {
1602
- bracket--;
1603
- const [mark] = brackets.slice(-1);
1604
- if (mark === bracket) {
1605
- brackets.pop();
1606
- return `${_})${brackets.length + 1}${_}`;
1607
- }
1608
- } else if (char === ",") {
1609
- if (brackets.length > 0) {
1610
- return `${_},${brackets.length}${_}`;
1611
- }
1612
- }
1613
- lookBehind += char;
1614
- } else {
1615
- lookBehind = "";
1616
- }
1617
- return char;
1618
- })
1619
- .join("");
1620
-
1621
- if (bracketMax > 0) {
1622
- for (let i = 1; i <= bracketMax; i++) {
1623
- Object.keys(FilterFunctions).forEach((name) => {
1624
- let pattern = name
1625
- .replace(/([()])/g, `${_}\\$1${i}${_}`)
1626
- .replace(/([,])/g, `${_}$1${i}${_}`)
1627
- .replace(/[$]/g, "(.*?)");
1628
- filter = filter.replace(new RegExp(pattern, "gi"), FilterFunctions[name]);
1629
- });
1630
- filter = filter.replace(new RegExp(`${_}([(),])${i}${_}`, "g"), "$1");
1631
- }
1632
- url.query["$filter"] = filter;
1633
- }
1634
- }
1635
- }
1636
-
1637
- function convertSearch(url) {
1638
- if (url.query.search) {
1639
- let search = url.query.search;
1640
- if (quoteSearch) {
1641
- search = `"${search.replace(/"/g, `\\"`)}"`;
1642
- } else {
1643
- if (!/^".*"$/s.test(search) && search.includes('"')) {
1644
- search = `"${search.replace(/"/g, `\\"`)}"`;
1645
- }
1646
- }
1647
- url.query["$search"] = search;
1648
- delete url.query.search;
1649
- }
1650
- }
1651
-
1652
- function convertAnalytics(url, req) {
1653
- const definition = req.context && req.context.definition;
1654
- if (
1655
- !(
1656
- definition &&
1657
- definition.kind === "entity" &&
1658
- definition["@cov2ap.analytics"] !== false &&
1659
- url.query["$select"] &&
1660
- (definition["@cov2ap.analytics"] === true ||
1661
- definition["@Analytics"] ||
1662
- definition["@Analytics.AnalyticalContext"] ||
1663
- definition["@Analytics.query"] ||
1664
- definition["@AnalyticalContext"] ||
1665
- definition["@Aggregation.ApplySupported.PropertyRestrictions"] ||
1666
- definition["@sap.semantics"] === "aggregate")
1667
- )
1668
- ) {
1669
- return;
1670
- }
1671
- const elements = req.context.definitionElements;
1672
- const measures = [];
1673
- const dimensions = [];
1674
- const selects = url.query["$select"].split(",");
1675
- const values = [];
1676
- selects.forEach((select) => {
1677
- const element = elements[select];
1678
- if (element) {
1679
- values.push(element);
1680
- }
1681
- });
1682
- selects.forEach((select) => {
1683
- const element = elements[select];
1684
- if (element) {
1685
- if (
1686
- element["@Analytics.AnalyticalContext.Measure"] ||
1687
- element["@AnalyticalContext.Measure"] ||
1688
- element["@Analytics.Measure"] ||
1689
- element["@sap.aggregation.role"] === "measure"
1690
- ) {
1691
- measures.push(element);
1692
- } else {
1693
- // element["@Analytics.AnalyticalContext.Dimension"] || element["@AnalyticalContext.Dimension"] || element["@Analytics.Dimension"] || element["@sap.aggregation.role"] === "dimension"
1694
- dimensions.push(element);
1695
- }
1696
- }
1697
- });
1698
-
1699
- if (dimensions.length > 0 || measures.length > 0) {
1700
- url.query["$apply"] = "";
1701
- if (dimensions.length) {
1702
- url.query["$apply"] = "groupby(";
1703
- url.query["$apply"] += `(${dimensions
1704
- .map((dimension) => {
1705
- return dimension.name;
1706
- })
1707
- .join(",")})`;
1708
- }
1709
- if (measures.length > 0) {
1710
- if (url.query["$apply"]) {
1711
- url.query["$apply"] += ",";
1712
- }
1713
- url.query["$apply"] += `aggregate(${measures
1714
- .map((measure) => {
1715
- const aggregation = measure["@Aggregation.default"] || measure["@DefaultAggregation"];
1716
- const aggregationName = aggregation ? aggregation["#"] || aggregation : DefaultAggregation;
1717
- const aggregationFunction = aggregationName ? AggregationMap[aggregationName.toUpperCase()] : undefined;
1718
- if (!aggregationFunction) {
1719
- throw new Error(`Aggregation '${aggregationName}' is not supported`);
1720
- }
1721
- if ([AggregationMap.NONE, AggregationMap.NOP].includes(aggregationFunction)) {
1722
- return null;
1723
- }
1724
- if (aggregationFunction.startsWith("$")) {
1725
- return `${aggregationFunction} as ${AggregationPrefix}${measure.name}`;
1726
- } else {
1727
- return `${measure.name} with ${aggregationFunction} as ${AggregationPrefix}${measure.name}`;
1728
- }
1729
- })
1730
- .filter((aggregation) => !!aggregation)
1731
- .join(",")})`;
1732
- }
1733
- if (dimensions.length) {
1734
- url.query["$apply"] += ")";
1735
- }
1736
-
1737
- const filter = url.query["$filter"];
1738
- if (filter) {
1739
- url.query["$apply"] = `filter(${filter})/` + url.query["$apply"];
1740
- }
1741
- const search = url.query["$search"];
1742
- if (search) {
1743
- url.query["$apply"] = `search(${search})/` + url.query["$apply"];
1744
- }
1745
-
1746
- if (url.query["$orderby"]) {
1747
- url.query["$orderby"] = url.query["$orderby"]
1748
- .split(",")
1749
- .map((orderBy) => {
1750
- let [name, order] = orderBy.split(" ");
1751
- const element = elements[name];
1752
- if (
1753
- element &&
1754
- (element["@Analytics.AnalyticalContext.Measure"] ||
1755
- element["@AnalyticalContext.Measure"] ||
1756
- element["@Analytics.Measure"] ||
1757
- element["@sap.aggregation.role"] === "measure")
1758
- ) {
1759
- name = `${AggregationPrefix}${element.name}`;
1760
- }
1761
- return name + (order ? ` ${order}` : "");
1762
- })
1763
- .join(",");
1764
- }
1765
-
1766
- delete url.query["$filter"];
1767
- delete url.query["$select"];
1768
- delete url.query["$expand"];
1769
- delete url.query["$search"];
1770
-
1771
- req.context.$apply = {
1772
- key: dimensions,
1773
- value: values,
1774
- filter: req.context.aggregationKey ? req.context.aggregationFilter : filter,
1775
- search: req.context.aggregationKey ? req.context.aggregationSearch : search,
1776
- };
1777
- }
1778
- }
1779
-
1780
- function convertValue(url, req) {
1781
- if (url.contextPath.endsWith("/$value")) {
1782
- url.contextPath = url.contextPath.substr(0, url.contextPath.length - "/$value".length);
1783
- const mediaDataElementName =
1784
- req.context &&
1785
- req.context.definition &&
1786
- findElementByAnnotation(req.context.definitionElements, "@Core.MediaType");
1787
- const endingElementName = findEndingElementName(req.context.definitionElements, url);
1788
- if (!endingElementName) {
1789
- url.contextPath += `/${mediaDataElementName}`;
1790
- req.context.$entityValue = true;
1791
- } else if (endingElementName !== mediaDataElementName) {
1792
- req.context.$value = true;
1793
- }
1794
- }
1795
- }
1796
-
1797
- function convertParameters(url, req) {
1798
- if (req.context.parameters) {
1799
- let context;
1800
- let stop = false;
1801
- url.contextPath = url.contextPath
1802
- .split("/")
1803
- .map((part) => {
1804
- if (part === "Set" || part.startsWith("Set(")) {
1805
- req.context.parameters.kind = "Set";
1806
- stop = true;
1807
- } else if (part === "Parameters" || part.startsWith("Parameters(")) {
1808
- req.context.parameters.kind = "Parameters";
1809
- stop = true;
1810
- } else if (part === "$count") {
1811
- req.context.parameters.count = true;
1812
- }
1813
- if (stop) {
1814
- return "";
1815
- }
1816
- let keyPart = "";
1817
- const keyStart = part.indexOf("(");
1818
- const keyEnd = part.lastIndexOf(")");
1819
- if (keyStart !== -1 && keyEnd === part.length - 1) {
1820
- keyPart = part.substring(keyStart + 1, keyEnd);
1821
- part = part.substr(0, keyStart);
1822
- }
1823
- if (part === req.context.parameters.type) {
1824
- part = req.context.parameters.entity;
1825
- }
1826
- context = lookupContext(part, context, req);
1827
- if (!context) {
1828
- stop = true;
1829
- }
1830
- const contextElements = definitionElements(context);
1831
- if (context && keyPart) {
1832
- const keys = decodeURIComponent(keyPart).split(",");
1833
- return encodeURIComponent(
1834
- `${part}(${keys
1835
- .map((key) => {
1836
- const [name, value] = key.split("=");
1837
- if (name && value) {
1838
- if (context.params[name]) {
1839
- req.context.parameters.values[name] = unquoteParameter(context.params[name], value, req);
1840
- return `${name}=${value}`;
1841
- } else {
1842
- req.context.parameters.keys[name] = unquoteParameter(contextElements[name], value, req);
1843
- }
1844
- } else if (name) {
1845
- const param = structureKeys(context.params).find(() => true);
1846
- if (param) {
1847
- if (context.params[param]) {
1848
- req.context.parameters.values[param] = unquoteParameter(context.params[param], name, req);
1849
- return `${param}=${name}`;
1850
- } else {
1851
- req.context.parameters.keys[param] = unquoteParameter(contextElements[param], name, req);
1852
- }
1853
- }
1854
- }
1855
- return "";
1856
- })
1857
- .filter((part) => !!part)
1858
- .join(",")})`
1859
- );
1860
- } else {
1861
- return part;
1862
- }
1863
- })
1864
- .filter((part) => !!part)
1865
- .join("/");
1866
- if (!url.contextPath.endsWith("/Set")) {
1867
- url.contextPath = `${url.contextPath}/Set`;
1868
- }
1869
- if (req.context.parameters.count) {
1870
- url.contextPath += "/$count";
1871
- }
1872
- }
1873
- }
1874
-
1875
- function convertRequestBody(body, headers, url, req) {
1876
- let definition = req.context && req.context.definition;
1877
- if (definition) {
1878
- if (definition.kind === "action") {
1879
- body = Object.assign({}, body, req.context.bodyParameters);
1880
- definition = {
1881
- elements: definition.params || {},
1882
- };
1883
- }
1884
- convertRequestData(body, headers, definition, req);
1885
- }
1886
- return JSON.stringify(body);
1887
- }
1888
-
1889
- function convertRequestData(data, headers, definition, req) {
1890
- if (!Array.isArray(data)) {
1891
- return convertRequestData([data], headers, definition, req);
1892
- }
1893
- const elements = definitionElements(definition);
1894
- if (structureKeys(elements).length === 0) {
1895
- return;
1896
- }
1897
- // Modify Payload
1898
- data.forEach((data) => {
1899
- delete data.__metadata;
1900
- delete data.__count;
1901
- convertDataTypesToV4(data, headers, definition, elements, req);
1902
- });
1903
- // Recursion
1904
- data.forEach((data) => {
1905
- Object.keys(data).forEach((key) => {
1906
- const element = elements[key];
1907
- const type = elementType(element, req);
1908
- if (element && (type === "cds.Composition" || type === "cds.Association")) {
1909
- if (data[key] && data[key].__deferred) {
1910
- delete data[key];
1911
- } else {
1912
- if (data[key] !== null) {
1913
- if (Array.isArray(data[key].results)) {
1914
- data[key] = data[key].results;
1915
- }
1916
- convertRequestData(data[key], headers, element._target, req);
1917
- } else {
1918
- delete data[key];
1919
- }
1920
- }
1921
- }
1922
- });
1923
- });
1924
- }
1925
-
1926
- function convertDataTypesToV4(data, headers, definition, elements, req) {
1927
- Object.keys(data || {}).forEach((key) => {
1928
- if (data[key] === null) {
1929
- return;
1930
- }
1931
- const element = elements[key];
1932
- if (element) {
1933
- data[key] = convertDataTypeToV4(data[key], elementType(element, req), definition, headers);
1934
- }
1935
- });
1936
- }
1937
-
1938
- function replaceConvertDataTypeToV4(value, type, definition, headers = {}) {
1939
- if (value === null || value === undefined) {
1940
- return value;
1941
- }
1942
- if (Array.isArray(value)) {
1943
- return value.map((entry) => {
1944
- return replaceConvertDataTypeToV4(entry, type, definition, headers);
1945
- });
1946
- }
1947
- if (DataTypeMap[type]) {
1948
- value = value.replace(DataTypeMap[type].v4, "$1");
1949
- }
1950
- return convertDataTypeToV4(value, type);
1951
- }
1952
-
1953
- function convertDataTypeToV4(value, type, definition, headers = {}) {
1954
- if (value === null || value === undefined) {
1955
- return value;
1956
- }
1957
- if (Array.isArray(value)) {
1958
- return value.map((entry) => {
1959
- return convertDataTypeToV4(entry, type, definition, headers);
1960
- });
1961
- }
1962
- const contentType = headers["content-type"];
1963
- const ieee754Compatible = contentType && contentType.includes(IEEE754Compatible);
1964
- if (["cds.Boolean"].includes(type)) {
1965
- if (value === "true") {
1966
- value = true;
1967
- } else if (value === "false") {
1968
- value = false;
1969
- }
1970
- } else if (["cds.Integer"].includes(type)) {
1971
- value = parseInt(value, 10);
1972
- } else if (["cds.Integer64", "cds.Decimal", "cds.DecimalFloat"].includes(type)) {
1973
- value = ieee754Compatible ? `${value}` : parseFloat(value);
1974
- } else if (["cds.Double"].includes(type)) {
1975
- value = parseFloat(value);
1976
- } else if (["cds.Time"].includes(type)) {
1977
- const match = value.match(DurationRegex);
1978
- if (match) {
1979
- value = `${match[4] || "00"}:${match[5] || "00"}:${match[6] || "00"}`;
1980
- }
1981
- } else if (["cds.Date", "cds.DateTime", "cds.Timestamp"].includes(type)) {
1982
- const match = value.match(/\/Date\((.*)\)\//is);
1983
- const ticksAndOffset = match && match.pop();
1984
- if (ticksAndOffset !== undefined && ticksAndOffset !== null) {
1985
- value = new Date(calculateTicksOffsetSum(ticksAndOffset)).toISOString(); // always UTC
1986
- }
1987
- if (["cds.DateTime"].includes(type)) {
1988
- value = value.slice(0, 19) + "Z"; // Cut millis
1989
- } else if (["cds.Date"].includes(type)) {
1990
- value = value.slice(0, 10); // Cut time
1991
- }
1992
- }
1993
- return value;
1994
- }
1995
-
1996
- function calculateTicksOffsetSum(text) {
1997
- return (text.replace(/\s/g, "").match(/[+-]?([0-9]+)/g) || []).reduce((sum, value, index) => {
1998
- return sum + parseFloat(value) * (index === 0 ? 1 : 60 * 1000); // ticks are milliseconds (0), offset are minutes (1)
1999
- }, 0);
2000
- }
2001
-
2002
- function initContext(req, index = 0) {
2003
- req.context = req.contexts[index] || {};
2004
- return req.context.definition && req.context.definition.kind === "entity"
2005
- ? req.context.definition
2006
- : req.context.boundDefinition;
2007
- }
2008
-
2009
- /**
2010
- * Convert Proxy Response (v4 -> v2)
2011
- * @param proxyRes Proxy Request
2012
- * @param req Request
2013
- * @param res Response
2014
- */
2015
- async function convertProxyResponse(proxyRes, req, res) {
2016
- try {
2017
- req.context = {};
2018
- let statusCode = proxyRes.statusCode;
2019
- let headers = proxyRes.headers;
2020
- if (statusCode < 400 && req.overwriteResponse) {
2021
- statusCode = req.overwriteResponse.statusCode;
2022
- headers = {
2023
- ...req.overwriteResponse.headers,
2024
- ...headers,
2025
- };
2026
- }
2027
-
2028
- normalizeContentType(headers);
2029
-
2030
- // Pipe Binary Stream
2031
- const contentType = headers["content-type"];
2032
- const transferEncoding = headers["transfer-encoding"] || "";
2033
- if (transferEncoding.includes("chunked") && !isApplicationJSON(contentType) && !isMultipartMixed(contentType)) {
2034
- return await processStreamResponse(proxyRes, req, res, headers);
2035
- }
2036
-
2037
- // Body
2038
- let body = await parseProxyResponseBody(proxyRes, headers, req);
2039
- if (statusCode < 400 && req.overwriteResponse) {
2040
- body = {
2041
- ...req.overwriteResponse.body,
2042
- ...body,
2043
- };
2044
- }
2045
-
2046
- // Trace
2047
- traceResponse(req, "ProxyResponse", proxyRes.statusCode, proxyRes.statusMessage, headers, body);
2048
- delete headers["content-encoding"];
2049
-
2050
- convertBasicHeaders(headers);
2051
- if (body && statusCode < 400) {
2052
- if (isMultipartMixed(contentType)) {
2053
- // Multipart
2054
- const resContentIdOrder = [];
2055
- body = processMultipartMixed(
2056
- req,
2057
- body,
2058
- contentType,
2059
- null,
2060
- ({ index, statusCode, contentType, body, headers }) => {
2061
- const serviceDefinition = initContext(req, index);
2062
- if (statusCode < 400) {
2063
- convertHeaders(body, headers, serviceDefinition, req);
2064
- if (body && isApplicationJSON(contentType)) {
2065
- body = convertResponseBody(Object.assign({}, body), headers, req);
2066
- }
2067
- } else {
2068
- convertHeaders(body, headers, serviceDefinition, req);
2069
- body = convertResponseError(body, headers, serviceDefinition, req);
2070
- }
2071
- return { body, headers };
2072
- },
2073
- resContentIdOrder,
2074
- ProcessingDirection.Response
2075
- );
2076
- if (
2077
- !(
2078
- req.contentIdOrder.length === resContentIdOrder.length &&
2079
- req.contentIdOrder.every((contentId, index) => contentId === resContentIdOrder[index])
2080
- )
2081
- ) {
2082
- logWarning(req, "Batch", "Response changeset order does not match request changeset order", {
2083
- requestContentIds: req.contentIdOrder,
2084
- responseContentIds: resContentIdOrder,
2085
- });
2086
- }
2087
- if (statusCode === 200) {
2088
- // OData V4: 200 => OData V2: 202
2089
- statusCode = 202;
2090
- }
2091
- } else {
2092
- // Single
2093
- const serviceDefinition = initContext(req);
2094
- convertHeaders(body, headers, serviceDefinition, req);
2095
- if (isApplicationJSON(contentType)) {
2096
- body = convertResponseBody(Object.assign({}, body), headers, req);
2097
- }
2098
- }
2099
- if (body && !(headers["transfer-encoding"] || "").includes("chunked") && statusCode !== 204) {
2100
- setContentLength(headers, body);
2101
- }
2102
- } else {
2103
- // Failed
2104
- const serviceDefinition = initContext(req);
2105
- convertHeaders(body, headers, serviceDefinition, req);
2106
- body = convertResponseError(body, headers, serviceDefinition, req);
2107
- setContentLength(headers, body);
2108
- }
2109
- respond(req, res, statusCode, headers, body);
2110
- } catch (err) {
2111
- // Error
2112
- logError(req, "Response", err);
2113
- if (proxyRes.body && proxyRes.body.error) {
2114
- respond(
2115
- req,
2116
- res,
2117
- proxyRes.statusCode,
2118
- proxyRes.headers,
2119
- convertResponseError(proxyRes.body, proxyRes.headers, undefined, req)
2120
- );
2121
- } else {
2122
- res.status(500).send("Internal Server Error");
2123
- }
2124
- }
2125
- }
2126
-
2127
- async function processStreamResponse(proxyRes, req, res, headers) {
2128
- // Trace
2129
- traceResponse(req, "ProxyResponse", proxyRes.statusCode, proxyRes.statusMessage, headers, {});
2130
-
2131
- let streamRes = proxyRes;
2132
- convertBasicHeaders(headers);
2133
- const context = req.contexts && req.contexts[0];
2134
- if (context && context.definition && context.definitionElements) {
2135
- const mediaDataElementName = findElementByAnnotation(context.definitionElements, "@Core.MediaType");
2136
- if (mediaDataElementName) {
2137
- const parts = proxyRes.req.path.split("/");
2138
- if (parts[parts.length - 1] === "$value" || parts[parts.length - 1].startsWith("$value?")) {
2139
- parts.pop();
2140
- }
2141
- if (parts[parts.length - 1] === mediaDataElementName) {
2142
- parts.pop();
2143
- }
2144
-
2145
- // Is Url
2146
- const urlElement =
2147
- findElementByAnnotation(context.definitionElements, "@Core.IsURL") ||
2148
- findElementByAnnotation(context.definitionElements, "@Core.IsUrl");
2149
- if (urlElement) {
2150
- const mediaResponse = await fetch(target + parts.join("/"), {
2151
- method: "GET",
2152
- headers: propagateHeaders(req, {
2153
- accept: "application/json",
2154
- }),
2155
- });
2156
- if (!mediaResponse.ok) {
2157
- throw new Error(await mediaResponse.text());
2158
- }
2159
- if (mediaResponse) {
2160
- const mediaResult = await mediaResponse.json();
2161
- const mediaReadLink = mediaResult[`${urlElement}@odata.mediaReadLink`];
2162
- if (mediaReadLink) {
2163
- try {
2164
- const mediaResponse = await fetch(mediaReadLink, {
2165
- method: "GET",
2166
- headers: propagateHeaders(req),
2167
- });
2168
- res.status(mediaResponse.status);
2169
- headers = convertBasicHeaders(convertToNodeHeaders(mediaResponse.headers));
2170
- streamRes = mediaResponse.body;
2171
- } catch (err) {
2172
- logError(req, "MediaStream", err);
2173
- const errorBody = convertResponseError({ error: err }, {}, context.definition, req);
2174
- respond(req, res, 500, { "content-type": "application/json" }, errorBody);
2175
- return;
2176
- }
2177
- }
2178
- }
2179
- } else {
2180
- if (!headers["content-disposition"] || calcContentDisposition) {
2181
- // Is Binary
2182
- const contentDispositionFilenameElement =
2183
- findElementValueByAnnotation(context.definitionElements, "@Core.ContentDisposition.Filename") ||
2184
- findElementValueByAnnotation(context.definitionElements, "@Common.ContentDisposition.Filename");
2185
- if (contentDispositionFilenameElement) {
2186
- const contentDispositionTypeValue =
2187
- findElementValueByAnnotation(context.definitionElements, "@Core.ContentDisposition.Type") ||
2188
- findElementValueByAnnotation(context.definitionElements, "@Common.ContentDisposition.Type") ||
2189
- contentDisposition;
2190
- const response = await fetch(target + [...parts, contentDispositionFilenameElement, "$value"].join("/"), {
2191
- method: "GET",
2192
- headers: propagateHeaders(req, {
2193
- accept: "application/json,*/*",
2194
- }),
2195
- });
2196
- if (response.ok) {
2197
- const filename = await response.text();
2198
- if (filename) {
2199
- headers["content-disposition"] = `${contentDispositionTypeValue}; filename="${encodeURIComponent(
2200
- filename
2201
- )}"`;
2202
- }
2203
- } else {
2204
- logWarning(req, "ContentDisposition", await response.text());
2205
- }
2206
- }
2207
- }
2208
- }
2209
- }
2210
- }
2211
-
2212
- delete headers["content-encoding"];
2213
- Object.entries(headers).forEach(([name, value]) => {
2214
- res.setHeader(name, value);
2215
- });
2216
- streamRes.pipe(res);
2217
-
2218
- // Trace
2219
- traceResponse(req, "Response", res.statusCode, res.statusMessage, headers, {});
2220
- }
2221
-
2222
- async function parseProxyResponseBody(proxyRes, headers, req) {
2223
- const contentType = headers["content-type"];
2224
- let bodyParser;
2225
- if (req.method === "HEAD") {
2226
- bodyParser = null;
2227
- } else if (isApplicationJSON(contentType)) {
2228
- bodyParser = express.json({ limit: bodyParserLimit });
2229
- } else if (isPlainText(contentType) || isXML(contentType)) {
2230
- bodyParser = express.text({ type: () => true, limit: bodyParserLimit });
2231
- } else if (isMultipartMixed(contentType)) {
2232
- bodyParser = express.text({ type: "multipart/mixed", limit: bodyParserLimit });
2233
- }
2234
- if (bodyParser) {
2235
- await promisify(bodyParser)(proxyRes, null);
2236
- return proxyRes.body;
2237
- }
2238
- }
2239
-
2240
- function convertBasicHeaders(headers) {
2241
- delete headers["odata-version"];
2242
- delete headers["OData-Version"];
2243
- delete headers["odata-entityid"];
2244
- delete headers["OData-EntityId"];
2245
- headers.dataserviceversion = "2.0";
2246
- return headers;
2247
- }
2248
-
2249
- function convertHeaders(body, headers, serviceDefinition, req) {
2250
- convertBasicHeaders(headers);
2251
- const definition = contextFromBody(body, req);
2252
- if (definition && definition.kind === "entity") {
2253
- const elements = definitionElements(definition);
2254
- convertLocation(body, headers, definition, elements, req);
2255
- }
2256
- convertMessages(body, headers, definition && definition.kind === "entity" ? definition : serviceDefinition, req);
2257
- return headers;
2258
- }
2259
-
2260
- function convertLocation(body, headers, definition, elements, req) {
2261
- if (headers.location || headers.Location) {
2262
- headers.location = entityUri(body, definition, elements, req);
2263
- }
2264
- delete headers.Location;
2265
- }
2266
-
2267
- function convertMessages(body, headers, definition, req) {
2268
- if (headers["sap-messages"]) {
2269
- const messages = JSON.parse(headers["sap-messages"]);
2270
- if (messages && messages.length > 0) {
2271
- const rootMessage = messages.shift();
2272
- rootMessage.details = Array.isArray(rootMessage.details) ? rootMessage.details : [];
2273
- rootMessage.details.push(...messages);
2274
- const message = convertMessage(rootMessage, definition, req);
2275
- headers["sap-message"] = JSON.stringify(message);
2276
- }
2277
- delete headers["sap-messages"];
2278
- }
2279
- }
2280
-
2281
- function convertMessage(message, definition, req, contentID) {
2282
- if (!message) {
2283
- return message;
2284
- }
2285
- message.severity = SeverityMap[message["@Common.numericSeverity"] || message.numericSeverity] || "error";
2286
- delete message.numericSeverity;
2287
- delete message["@Common.numericSeverity"];
2288
- if (message.target) {
2289
- message.target = convertMessageTarget(message.target, req, definition);
2290
- } else if (message.target === undefined && messageTargetDefault) {
2291
- message.target = messageTargetDefault;
2292
- }
2293
- if (Array.isArray(message["@Common.additionalTargets"])) {
2294
- message.additionalTargets = message["@Common.additionalTargets"].map((messageTarget) => {
2295
- return convertMessageTarget(messageTarget, req, definition);
2296
- });
2297
- }
2298
- delete message["@Common.additionalTargets"];
2299
- contentID = message["@Core.ContentID"] || contentID;
2300
- message.ContentID = contentID;
2301
- delete message["@Core.ContentID"];
2302
- if (Array.isArray(message.details)) {
2303
- message.details = message.details.map((detail) => {
2304
- return convertMessage(detail, definition, req, contentID);
2305
- });
2306
- if (propagateMessageToDetails) {
2307
- const propagatedDetailMessage = Object.assign({}, message);
2308
- delete propagatedDetailMessage.details;
2309
- message.details.unshift(propagatedDetailMessage);
2310
- }
2311
- message.details.forEach((detail) => {
2312
- if (detail.code && detail.code.toLowerCase().includes("transition")) {
2313
- detail.transition = true;
2314
- }
2315
- });
2316
- }
2317
- return message;
2318
- }
2319
-
2320
- function convertMessageTarget(messageTarget, req, definition) {
2321
- if (!messageTarget) {
2322
- return messageTarget;
2323
- }
2324
- if (req.context.operation && req.context.boundDefinition) {
2325
- const bindingParamaterName = req.context.operation["@cds.odata.bindingparameter.name"] || "in";
2326
- if (messageTarget.startsWith(`${bindingParamaterName}/`)) {
2327
- messageTarget = messageTarget.substr(bindingParamaterName.length + 1);
2328
- }
2329
- }
2330
- let context;
2331
- definition = definition && definition.kind === "entity" ? definition : undefined;
2332
- if (
2333
- contextFromUrl(
2334
- {
2335
- contextPath: messageTarget,
2336
- query: {},
2337
- },
2338
- req,
2339
- undefined,
2340
- true
2341
- )
2342
- ) {
2343
- context = undefined;
2344
- } else if (
2345
- contextFromUrl(
2346
- {
2347
- contextPath: messageTarget,
2348
- query: {},
2349
- },
2350
- req,
2351
- definition,
2352
- true
2353
- )
2354
- ) {
2355
- context = definition;
2356
- }
2357
- let stop = false;
2358
- return messageTarget
2359
- .split("/")
2360
- .map((part) => {
2361
- if (stop) {
2362
- return part;
2363
- }
2364
- let keyPart = "";
2365
- const keyStart = part.indexOf("(");
2366
- const keyEnd = part.lastIndexOf(")");
2367
- if (keyStart !== -1 && keyEnd === part.length - 1) {
2368
- keyPart = part.substring(keyStart + 1, keyEnd);
2369
- part = part.substr(0, keyStart);
2370
- }
2371
- context = lookupContext(part, context, req);
2372
- if (!context) {
2373
- stop = true;
2374
- }
2375
- const contextElements = definitionElements(context);
2376
- const contextKeys = definitionKeys(context);
2377
- if (context && keyPart) {
2378
- const keys = keyPart.split(",");
2379
- return `${part}(${keys
2380
- .map((key) => {
2381
- const [name, value] = key.split("=");
2382
- let type;
2383
- if (name && value) {
2384
- if (context.params && context.params[name]) {
2385
- type = context.params[name].type;
2386
- }
2387
- if (!type) {
2388
- type = elementType(contextElements[name], req);
2389
- }
2390
- return `${name}=${replaceConvertDataTypeToV2(value, type, context)}`;
2391
- } else if (name) {
2392
- const key = structureKeys(contextKeys).find((key) => {
2393
- return contextKeys[key].type !== "cds.Composition" && contextKeys[key].type !== "cds.Association";
2394
- });
2395
- type = key && elementType(contextElements[key], req);
2396
- return type && `${replaceConvertDataTypeToV2(name, type, context)}`;
2397
- }
2398
- return "";
2399
- })
2400
- .filter((part) => !!part)
2401
- .join(",")})`;
2402
- } else {
2403
- return part;
2404
- }
2405
- })
2406
- .join("/");
2407
- }
2408
-
2409
- function convertResponseError(body, headers, definition, req) {
2410
- if (!body) {
2411
- return body;
2412
- }
2413
- if (body.error) {
2414
- if (body.error.message) {
2415
- body.error.message = {
2416
- lang: headers["content-language"] || "en",
2417
- value: body.error.message,
2418
- };
2419
- }
2420
- body.error = convertMessage(body.error, definition, req);
2421
- body.error.innererror = body.error.innererror || {};
2422
- body.error.innererror.errordetails = body.error.innererror.errordetails || [];
2423
- body.error.innererror.errordetails.push(...(body.error.details || []));
2424
- delete body.error.details;
2425
- if (body.error.innererror.errordetails.length === 0) {
2426
- const singleDetailError = Object.assign({}, body.error);
2427
- delete singleDetailError.innererror;
2428
- delete singleDetailError.details;
2429
- if (body.error.innererror.errordetails.length === 0) {
2430
- body.error.innererror.errordetails.push(singleDetailError);
2431
- }
2432
- }
2433
- }
2434
- if (typeof body === "object") {
2435
- body = JSON.stringify(body);
2436
- }
2437
- body = `${body}`;
2438
- setContentLength(headers, body);
2439
- return body;
2440
- }
2441
-
2442
- function convertResponseBody(proxyBody, headers, req) {
2443
- const body = {
2444
- d: {},
2445
- };
2446
- if (req.context.serviceRoot && proxyBody.value) {
2447
- if (req.context.serviceRootAsXML) {
2448
- // Service Root XML
2449
- let xmlBody = `<?xml version="1.0" encoding="utf-8" standalone="yes" ?>`;
2450
- xmlBody += `<service xml:base="${serviceUri(
2451
- req
2452
- )}" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:app="http://www.w3.org/2007/app" xmlns="http://www.w3.org/2007/app">`;
2453
- xmlBody += `<workspace><atom:title>Default</atom:title>`;
2454
- xmlBody += proxyBody.value
2455
- .map((entry) => {
2456
- return `<collection href="${entry.name}"><atom:title>${entry.name}</atom:title></collection>`;
2457
- })
2458
- .join("");
2459
- xmlBody += `</workspace></service>`;
2460
- headers["content-type"] = "application/xml";
2461
- return xmlBody;
2462
- } else {
2463
- // Service Root JSON
2464
- body.d.EntitySets = proxyBody.value.map((entry) => {
2465
- return entry.name;
2466
- });
2467
- }
2468
- } else {
2469
- // Context from Body
2470
- const definition = contextFromBody(proxyBody, req);
2471
- if (definition) {
2472
- req.context.requestDefinition = req.context.definition;
2473
- req.context.definition = definition;
2474
- req.context.definitionElements = definitionElements(definition);
2475
- const elements = req.context.definitionElements;
2476
- const definitionElement = contextElementFromBody(proxyBody, req);
2477
- if (definitionElement) {
2478
- body.d[definitionElement.name] = proxyBody.value;
2479
- convertResponseElementData(body, headers, definition, elements, proxyBody, req);
2480
- if (req.context.$value) {
2481
- headers["content-type"] = "text/plain";
2482
- return `${body.d[definitionElement.name]}`;
2483
- }
2484
- } else {
2485
- const data = convertResponseList(body, headers, definition, proxyBody, req);
2486
- convertResponseData(data, headers, definition, proxyBody, req);
2487
- }
2488
- } else {
2489
- // Context from Request
2490
- let definition = req.context.definition;
2491
- if (definition && (definition.kind === "function" || definition.kind === "action")) {
2492
- const returnDefinition = req.context.returnDefinition;
2493
- if (!returnDefinition || returnDefinition.name) {
2494
- definition = returnDefinition;
2495
- } else {
2496
- definition = {
2497
- kind: "type",
2498
- name: contextNameFromBody(proxyBody),
2499
- ...returnDefinition,
2500
- };
2501
- }
2502
- req.context.requestDefinition = req.context.definition;
2503
- req.context.definition = definition;
2504
- req.context.definitionElements = definitionElements(definition);
2505
- }
2506
- const data = convertResponseList(body, headers, definition, proxyBody, req);
2507
- convertResponseData(data, headers, definition, proxyBody, req);
2508
- }
2509
- }
2510
-
2511
- if (req.context.operation) {
2512
- const localOperationName = localName(req.context.operation.name);
2513
- const isArrayResult = Array.isArray(body.d.results) || Array.isArray(body.d);
2514
- if (req.context.definition.kind === "type") {
2515
- if (returnComplexNested && !isArrayResult) {
2516
- body.d = {
2517
- [localOperationName]: body.d,
2518
- };
2519
- }
2520
- } else if (!req.context.definition.kind && req.context.definition.name && req.context.definitionElements.value) {
2521
- if (returnPrimitivePlain) {
2522
- body.d = isArrayResult ? (body.d.results || body.d).map((entry) => entry.value) : body.d.value;
2523
- }
2524
- if (returnPrimitiveNested) {
2525
- body.d = isArrayResult
2526
- ? {
2527
- results: body.d,
2528
- }
2529
- : {
2530
- [localOperationName]: body.d,
2531
- };
2532
- }
2533
- }
2534
- }
2535
-
2536
- return JSON.stringify(body);
2537
- }
2538
-
2539
- function convertResponseList(body, headers, definition, proxyBody, req) {
2540
- if (Array.isArray(proxyBody.value)) {
2541
- if (req.context.aggregationKey) {
2542
- proxyBody = proxyBody.value[0] || {};
2543
- } else {
2544
- body.d.results = proxyBody.value || [];
2545
- if (req.context.$count) {
2546
- body.d.__count = String(proxyBody["@odata.count"] || proxyBody["@count"] || 0);
2547
- }
2548
- if (proxyBody["@odata.nextLink"] !== undefined || proxyBody["@nextLink"] !== undefined) {
2549
- const skipToken = URL.parse(proxyBody["@odata.nextLink"] || proxyBody["@nextLink"], true).query["$skiptoken"];
2550
- if (skipToken) {
2551
- body.d.__next = linkUri(req, {
2552
- $skiptoken: skipToken,
2553
- });
2554
- }
2555
- }
2556
- let deltaToken;
2557
- if (proxyBody["@odata.deltaLink"] !== undefined || proxyBody["@deltaLink"] !== undefined) {
2558
- deltaToken = URL.parse(proxyBody["@odata.deltaLink"] || proxyBody["@deltaLink"], true).query["!deltatoken"];
2559
- }
2560
- if (!deltaToken && definition && definition["@cov2ap.deltaResponse"] === "timestamp") {
2561
- deltaToken = `'${new Date().getTime()}'`;
2562
- }
2563
- if (deltaToken) {
2564
- body.d.__delta = linkUri(req, {
2565
- "!deltatoken": deltaToken,
2566
- });
2567
- }
2568
- body.d.results = body.d.results.map((entry) => {
2569
- return typeof entry == "object" ? entry : { value: entry };
2570
- });
2571
- if (req.context.parameters) {
2572
- if (req.context.parameters.kind === "Parameters") {
2573
- body.d.results = body.d.results.slice(0, 1);
2574
- } else if (req.context.parameters.kind === "Set") {
2575
- if (Object.keys(req.context.parameters.keys).length > 0) {
2576
- body.d.results = body.d.results.filter((entry) => {
2577
- return Object.keys(req.context.parameters.keys).every((key) => {
2578
- return entry[key] === req.context.parameters.keys[key];
2579
- });
2580
- });
2581
- }
2582
- }
2583
- }
2584
- if (!returnCollectionNested) {
2585
- body.d = body.d.results;
2586
- return body.d;
2587
- }
2588
- return body.d.results;
2589
- }
2590
- }
2591
- body.d = proxyBody;
2592
- return [body.d];
2593
- }
2594
-
2595
- function convertResponseData(data, headers, definition, proxyBody, req) {
2596
- if (data === null) {
2597
- return;
2598
- }
2599
- if (!Array.isArray(data)) {
2600
- return convertResponseData([data], headers, definition, proxyBody, req);
2601
- }
2602
- if (!definition) {
2603
- return;
2604
- }
2605
- const elements = definitionElements(definition);
2606
- // Recursion
2607
- data.forEach((data) => {
2608
- Object.keys(data).forEach((key) => {
2609
- let element = elements[key];
2610
- if (!element) {
2611
- return;
2612
- }
2613
- const type = elementType(element, req);
2614
- if (type === "cds.Composition" || type === "cds.Association") {
2615
- convertResponseData(data[key], headers, element._target, proxyBody, req);
2616
- }
2617
- });
2618
- });
2619
- // Structural Changes
2620
- data.forEach((data) => {
2621
- addResultsNesting(data, headers, definition, elements, proxyBody, req);
2622
- });
2623
- // Deferreds
2624
- data.forEach((data) => {
2625
- addDeferreds(data, headers, definition, elements, proxyBody, req);
2626
- });
2627
- // Modify Payload
2628
- data.forEach((data) => {
2629
- addMetadata(data, headers, definition, elements, proxyBody, req);
2630
- removeMetadata(data, headers, definition, elements, proxyBody, req);
2631
- convertMedia(data, headers, definition, elements, proxyBody, req);
2632
- removeAnnotations(data, headers, definition, elements, proxyBody, req);
2633
- convertDataTypesToV2(data, headers, definition, elements, proxyBody, req);
2634
- convertAggregation(data, headers, definition, elements, proxyBody, req);
2635
- filterParameters(data, headers, definition, elements, proxyBody, req);
2636
- });
2637
- }
2638
-
2639
- function convertResponseElementData(data, headers, definition, elements, proxyBody, req) {
2640
- if (!definition) {
2641
- return;
2642
- }
2643
- // Modify Payload
2644
- convertDataTypesToV2(data, headers, definition, elements, proxyBody, req);
2645
- }
2646
-
2647
- function contextNameFromBody(body) {
2648
- let context = body && (body["@odata.context"] || body["@context"]);
2649
- if (!context) {
2650
- return;
2651
- }
2652
- context = context.substr(context.indexOf("#") + 1);
2653
- if (context.startsWith("Collection(")) {
2654
- context = context.substring("Collection(".length, context.indexOf(")"));
2655
- } else {
2656
- if (context.indexOf("(") !== -1) {
2657
- context = context.substr(0, context.indexOf("("));
2658
- }
2659
- }
2660
- if (context.indexOf("/") !== -1) {
2661
- context = context.substr(0, context.indexOf("/"));
2662
- }
2663
- return context;
2664
- }
2665
-
2666
- function contextFromBody(body, req) {
2667
- const context = contextNameFromBody(body);
2668
- if (context) {
2669
- return lookupDefinition(context, req);
2670
- }
2671
- }
2672
-
2673
- function contextElementFromBody(body, req) {
2674
- let context = body["@odata.context"] || body["@context"];
2675
- const definition = contextFromBody(body, req);
2676
- if (!(context && definition)) {
2677
- return null;
2678
- }
2679
- if (context.lastIndexOf("/") !== -1) {
2680
- const name = context.substr(context.lastIndexOf("/") + 1);
2681
- if (name && !name.startsWith("$")) {
2682
- const element = definitionElements(definition)[name];
2683
- if (element) {
2684
- return element;
2685
- }
2686
- }
2687
- }
2688
- }
2689
-
2690
- function addMetadata(data, headers, definition, elements, body, req) {
2691
- const typeSuffix =
2692
- definition.kind === "entity" && definition.params && req.context.parameters
2693
- ? req.context.parameters.kind === "Set"
2694
- ? "Type"
2695
- : "Parameters"
2696
- : "";
2697
- data.__metadata = {
2698
- type: definition.name + typeSuffix,
2699
- };
2700
- if (definition.kind === "entity") {
2701
- data.__metadata.uri = entityUri(data, definition, elements, req);
2702
- if (data["@odata.etag"] || data["@etag"]) {
2703
- data.__metadata.etag = data["@odata.etag"] || data["@etag"];
2704
- }
2705
- const mediaDataElementName = findElementByAnnotation(elements, "@Core.MediaType");
2706
- if (mediaDataElementName) {
2707
- data.__metadata.media_src = `${data.__metadata.uri}/$value`;
2708
- const mediaDataElement = elements[mediaDataElementName];
2709
- const mediaTypeElementName =
2710
- (mediaDataElement["@Core.MediaType"] && mediaDataElement["@Core.MediaType"]["="]) ||
2711
- findElementByAnnotation(elements, "@Core.IsMediaType");
2712
- if (mediaTypeElementName) {
2713
- data.__metadata.content_type = data[mediaTypeElementName];
2714
- } else if (mediaDataElement["@Core.MediaType"]) {
2715
- data.__metadata.content_type = mediaDataElement["@Core.MediaType"];
2716
- }
2717
- if (!data.__metadata.content_type) {
2718
- data.__metadata.content_type = "application/octet-stream";
2719
- }
2720
- }
2721
- }
2722
- }
2723
-
2724
- function removeMetadata(data) {
2725
- Object.keys(data).forEach((key) => {
2726
- if (key.startsWith("@") || key.startsWith("odata.") || key.includes("@odata.")) {
2727
- delete data[key];
2728
- }
2729
- });
2730
- }
2731
-
2732
- function convertMedia(data) {
2733
- Object.keys(data).forEach((key) => {
2734
- if (key.endsWith("@odata.mediaReadLink")) {
2735
- data[key.split("@odata.mediaReadLink")[0]] = data[key];
2736
- } else if (key.endsWith("@mediaReadLink")) {
2737
- data[key.split("@mediaReadLink")[0]] = data[key];
2738
- }
2739
- });
2740
- }
2741
-
2742
- function removeAnnotations(data) {
2743
- Object.keys(data).forEach((key) => {
2744
- if (key.startsWith("@")) {
2745
- delete data[key];
2746
- }
2747
- });
2748
- }
2749
-
2750
- function convertAggregation(data, headers, definition, elements, body, req) {
2751
- if (!req.context.$apply) {
2752
- return;
2753
- }
2754
- Object.keys(data).forEach((key) => {
2755
- let value = data[key];
2756
- if (key.startsWith(AggregationPrefix)) {
2757
- if (!(key.endsWith("@odata.type") || key.endsWith("@type"))) {
2758
- const name = key.substr(AggregationPrefix.length);
2759
- let aggregationType = (data[`${key}@odata.type`] || data[`${key}@type`] || "#Decimal").replace("#", "");
2760
- if (DataTypeOData[aggregationType]) {
2761
- aggregationType = DataTypeOData[aggregationType];
2762
- } else {
2763
- aggregationType = `cds.${aggregationType}`;
2764
- }
2765
- if (
2766
- ["cds.Integer", "cds.Integer64", "cds.Double", "cds.Decimal", "cds.DecimalFloat"].includes(aggregationType)
2767
- ) {
2768
- if (value === null || value === "null") {
2769
- value = 0;
2770
- }
2771
- } else if (["cds.String", "cds.LargeString"].includes(aggregationType)) {
2772
- if (value === null) {
2773
- value = "";
2774
- }
2775
- } else if (["cds.Boolean"].includes(aggregationType)) {
2776
- if (value === null) {
2777
- value = false;
2778
- }
2779
- }
2780
- let aggregationValue = convertDataTypeToV2(value, aggregationType, definition);
2781
- // Convert to JSON number
2782
- const element = req.context.$apply.value.find((entry) => {
2783
- return entry.name === name;
2784
- });
2785
- if (element && elementType(element, req) === "cds.Integer") {
2786
- const aggregation = element["@Aggregation.default"] || element["@DefaultAggregation"];
2787
- const aggregationName = aggregation ? aggregation["#"] || aggregation : DefaultAggregation;
2788
- const aggregationFunction = aggregationName ? AggregationMap[aggregationName.toUpperCase()] : undefined;
2789
- if (
2790
- aggregationType === "cds.Decimal" &&
2791
- aggregationFunction &&
2792
- ![AggregationMap.AVG, AggregationMap.COUNT_DISTINCT].includes(aggregationFunction)
2793
- ) {
2794
- const floatValue = parseFloat(aggregationValue);
2795
- if (aggregationValue === `${floatValue}`) {
2796
- aggregationValue = floatValue;
2797
- }
2798
- }
2799
- }
2800
- data[name] = aggregationValue;
2801
- delete data[key];
2802
- }
2803
- }
2804
- });
2805
- Object.keys(data).forEach((key) => {
2806
- if (key.startsWith(AggregationPrefix) && (key.endsWith("@odata.type") || key.endsWith("@type"))) {
2807
- delete data[key];
2808
- }
2809
- });
2810
- const aggregationKey = {
2811
- key: req.context.$apply.key.reduce((result, keyElement) => {
2812
- const type = elementType(keyElement, req);
2813
- let value = data[keyElement.name];
2814
- if (value !== undefined && value !== null) {
2815
- value = encodeURIKey(value);
2816
- if (DataTypeMap[type]) {
2817
- value = convertDataTypeToV2Uri(String(value), type).replace(/(.*)/s, DataTypeMap[type].v2);
2818
- }
2819
- }
2820
- result[keyElement.name] = value;
2821
- return result;
2822
- }, {}),
2823
- value: req.context.$apply.value.map((valueElement) => {
2824
- return valueElement.name;
2825
- }),
2826
- };
2827
- if (req.context.$apply.filter) {
2828
- aggregationKey.filter = encodeURIComponent(req.context.$apply.filter);
2829
- }
2830
- if (req.context.$apply.search) {
2831
- aggregationKey.search = encodeURIComponent(req.context.$apply.search);
2832
- }
2833
- data.ID__ = `aggregation'${JSON.stringify(aggregationKey)}'`;
2834
- data.__metadata.uri = entityUriKey(data.ID__, definition, req);
2835
- delete data.__metadata.etag;
2836
- }
2837
-
2838
- function filterParameters(data, headers, definition, elements, body, req) {
2839
- if (
2840
- definition &&
2841
- definition.kind === "entity" &&
2842
- definition.params &&
2843
- req.context.parameters &&
2844
- req.context.parameters.kind === "Parameters"
2845
- ) {
2846
- Object.keys(elements).forEach((name) => {
2847
- if (!definition.params[name]) {
2848
- delete data[name];
2849
- }
2850
- });
2851
- }
2852
- }
2853
-
2854
- function replaceConvertDataTypeToV2(value, type, definition) {
2855
- if (value === null || value === undefined) {
2856
- return value;
2857
- }
2858
- if (Array.isArray(value)) {
2859
- return value.map((entry) => {
2860
- return replaceConvertDataTypeToV2(entry, type, definition);
2861
- });
2862
- }
2863
- value = convertDataTypeToV2(value, type, definition);
2864
- if (DataTypeMap[type]) {
2865
- if (!value.match(DataTypeMap[type].v2.replace("$1", ".*"))) {
2866
- value = DataTypeMap[type].v2.replace("$1", value);
2867
- }
2868
- }
2869
- return value;
2870
- }
2871
-
2872
- function convertDataTypesToV2(data, headers, definition, elements, body, req) {
2873
- Object.keys(data).forEach((key) => {
2874
- if (data[key] === null) {
2875
- return;
2876
- }
2877
- const element = elements[key];
2878
- if (!element) {
2879
- return;
2880
- }
2881
- data[key] = convertDataTypeToV2(data[key], elementType(element, req), definition);
2882
- });
2883
- }
2884
-
2885
- function convertDataTypeToV2(value, type, definition) {
2886
- if (value === null || value === undefined) {
2887
- return value;
2888
- }
2889
- if (Array.isArray(value)) {
2890
- return value.map((entry) => {
2891
- return convertDataTypeToV2(entry, type, definition);
2892
- });
2893
- }
2894
- if (["cds.Decimal", "cds.DecimalFloat", "cds.Double", "cds.Integer64"].includes(type)) {
2895
- value = `${value}`;
2896
- } else if (!isoDate && !definition["@cov2ap.isoDate"] && ["cds.Date"].includes(type)) {
2897
- value = `/Date(${new Date(value).getTime()})/`;
2898
- } else if (!isoTime && !definition["@cov2ap.isoTime"] && ["cds.Time"].includes(type)) {
2899
- value = convertToDayTimeDuration(value);
2900
- } else if (
2901
- !isoDateTime &&
2902
- !definition["@cov2ap.isoDateTime"] &&
2903
- !isoDateTimeOffset &&
2904
- !definition["@cov2ap.isoDateTimeOffset"] &&
2905
- ["cds.DateTime"].includes(type)
2906
- ) {
2907
- value = `/Date(${new Date(value).getTime()}+0000)/`; // always UTC
2908
- } else if (
2909
- !isoTimestamp &&
2910
- !definition["@cov2ap.isoTimestamp"] &&
2911
- !isoDateTimeOffset &&
2912
- !definition["@cov2ap.isoDateTimeOffset"] &&
2913
- ["cds.Timestamp"].includes(type)
2914
- ) {
2915
- value = `/Date(${new Date(value).getTime()}+0000)/`; // always UTC
2916
- }
2917
- return value;
2918
- }
2919
-
2920
- function convertDataTypeToV2Uri(value, type) {
2921
- if (["cds.Date"].includes(type)) {
2922
- value = `${value}T00:00:00`;
2923
- } else if (["cds.Time"].includes(type)) {
2924
- value = convertToDayTimeDuration(value);
2925
- }
2926
- return value;
2927
- }
2928
-
2929
- function convertToDayTimeDuration(value) {
2930
- const timeParts = value.split(":");
2931
- return `PT${timeParts[0] || "00"}H${timeParts[1] || "00"}M${timeParts[2] || "00"}S`;
2932
- }
2933
-
2934
- function addResultsNesting(data, headers, definition, elements) {
2935
- if (!returnCollectionNested) {
2936
- return;
2937
- }
2938
- Object.keys(data).forEach((key) => {
2939
- const element = elements[key];
2940
- if (!element) {
2941
- return;
2942
- }
2943
- if (element.cardinality && element.cardinality.max === "*") {
2944
- data[key] = {
2945
- results: data[key],
2946
- };
2947
- }
2948
- });
2949
- }
2950
-
2951
- function addDeferreds(data, headers, definition, elements, root, req) {
2952
- if (definition.kind !== "entity" || req.context.$apply) {
2953
- return;
2954
- }
2955
- const _entityUri = entityUri(data, definition, elements, req);
2956
- for (let key of structureKeys(elements)) {
2957
- const element = elements[key];
2958
- const type = elementType(element, req);
2959
- if (element && (type === "cds.Composition" || type === "cds.Association")) {
2960
- if (data[key] === undefined) {
2961
- if (fixDraftRequests && req.context.expandSiblingEntity && key === "SiblingEntity") {
2962
- data[key] = null;
2963
- } else {
2964
- data[key] = {
2965
- __deferred: {
2966
- uri: `${_entityUri}/${key}`,
2967
- },
2968
- };
2969
- }
2970
- }
2971
- }
2972
- }
2973
- if (definition.kind === "entity" && definition.params && req.context.parameters) {
2974
- if (req.context.parameters.kind === "Parameters") {
2975
- data.Set = {
2976
- __deferred: {
2977
- uri: `${_entityUri}/Set`,
2978
- },
2979
- };
2980
- } else if (req.context.parameters.kind === "Set") {
2981
- data.Parameters = {
2982
- __deferred: {
2983
- uri: `${_entityUri}/Parameters`,
2984
- },
2985
- };
2986
- }
2987
- }
2988
- }
2989
-
2990
- function rootUri(req) {
2991
- const protocol = req.header("x-forwarded-proto") || req.protocol || "http";
2992
- const host =
2993
- req.header("x-forwarded-host") ||
2994
- req.headers.host ||
2995
- `${req.hostname || DefaultHost}:${req.socket.address().port || DefaultPort}`;
2996
- return `${protocol}://${host}`.replace(/^http:\/\/127.0.0.1/, `http://${DefaultHost}`);
2997
- }
2998
-
2999
- function serviceUri(req) {
3000
- if (req.context.serviceUri) {
3001
- return req.context.serviceUri;
3002
- }
3003
- let serviceUri = rootUri(req);
3004
- if (req.header("x-forwarded-path") === undefined) {
3005
- serviceUri += `${sourcePath}/${req.servicePath}`;
3006
- } else {
3007
- const path = stripSlashes(URL.parse(req.header("x-forwarded-path") || "").pathname || "");
3008
- let resourceStartPath = "";
3009
- const definition = req.context.requestDefinition;
3010
- if (definition) {
3011
- if (definition.kind === "entity") {
3012
- resourceStartPath = localEntityName(definition, req);
3013
- } else if (definition.kind === "function" || definition.kind === "action") {
3014
- resourceStartPath = localEntityName(definition.parent || definition, req);
3015
- }
3016
- }
3017
- const parts = [];
3018
- path.split("/").some((part) => {
3019
- if (
3020
- part === resourceStartPath ||
3021
- part.startsWith(`${resourceStartPath}(`) ||
3022
- part.startsWith(`${resourceStartPath}?`) ||
3023
- part === "$batch" ||
3024
- part.startsWith("$batch?")
3025
- ) {
3026
- return true;
3027
- }
3028
- parts.push(part);
3029
- return false;
3030
- });
3031
- if (parts.length > 0) {
3032
- serviceUri += `/${parts.join("/")}`;
3033
- }
3034
- }
3035
- req.context.serviceUri = serviceUri.endsWith("/") ? serviceUri : `${serviceUri}/`;
3036
- return req.context.serviceUri;
3037
- }
3038
-
3039
- function entityUriCollection(entity, req) {
3040
- return `${serviceUri(req)}${localEntityName(entity, req)}`;
3041
- }
3042
-
3043
- function entityUriKey(key, entity, req) {
3044
- return `${entityUriCollection(entity, req)}(${key})`;
3045
- }
3046
-
3047
- function entityUri(data, entity, elements, req) {
3048
- return entityUriKey(entityKey(data, entity, elements, req), entity, req);
3049
- }
3050
-
3051
- function entityKey(data, entity, elements, req) {
3052
- if (entity.kind === "entity" && entity.params && req.context.parameters) {
3053
- return entityKeyParameters(data, entity, elements, req);
3054
- }
3055
- const keyElements = structureKeys(definitionKeys(entity)).reduce((keys, key) => {
3056
- const element = elements[key];
3057
- const type = elementType(element, req);
3058
- if (!(type === "cds.Composition" || type === "cds.Association")) {
3059
- keys.push(element);
3060
- }
3061
- return keys;
3062
- }, []);
3063
- return keyElements
3064
- .map((keyElement) => {
3065
- const type = elementType(keyElement, req);
3066
- let value = data[keyElement.name];
3067
- if (value !== undefined && value !== null) {
3068
- value = encodeURIKey(value);
3069
- if (DataTypeMap[type]) {
3070
- value = convertDataTypeToV2Uri(String(value), type).replace(/(.*)/s, DataTypeMap[type].v2);
3071
- }
3072
- }
3073
- if (keyElements.length === 1) {
3074
- return `${value}`;
3075
- } else {
3076
- return `${keyElement.name}=${value}`;
3077
- }
3078
- })
3079
- .join(",");
3080
- }
3081
-
3082
- function entityKeyParameters(data, entity, elements, req) {
3083
- const keys = definitionKeys(entity);
3084
- const keyElements = [];
3085
- Object.keys(req.context.parameters.values).forEach((param) => {
3086
- keyElements.push(entity.params[param]);
3087
- });
3088
- if (req.context.parameters.kind === "Set") {
3089
- Object.keys(req.context.parameters.keys).forEach((key) => {
3090
- keyElements.push(keys[key]);
3091
- });
3092
- const columns = entity.query.SELECT.columns || [];
3093
- Object.keys(keys).forEach((key) => {
3094
- const param = columns.find((column) => column.as === key);
3095
- const paramName = (param ? param.ref.join("_") : "") || key;
3096
- if (!keyElements.find((keyElement) => keyElement.name === paramName)) {
3097
- keyElements.push(keys[key]);
3098
- }
3099
- });
3100
- }
3101
- data = { ...data, ...req.context.parameters.values, ...req.context.parameters.keys };
3102
- return keyElements
3103
- .map((keyElement) => {
3104
- const type = elementType(keyElement, req);
3105
- let value = data[keyElement.name];
3106
- if (value !== undefined && value !== null) {
3107
- value = encodeURIKey(value);
3108
- if (DataTypeMap[type]) {
3109
- value = convertDataTypeToV2Uri(String(value), type).replace(/(.*)/s, DataTypeMap[type].v2);
3110
- }
3111
- }
3112
- if (keyElements.length === 1) {
3113
- return `${value}`;
3114
- } else {
3115
- return `${keyElement.name}=${value}`;
3116
- }
3117
- })
3118
- .join(",");
3119
- }
3120
-
3121
- function linkUri(req, params) {
3122
- const originalUrl = req.context.url.originalUrl;
3123
- Object.keys(params || {}).forEach((key) => {
3124
- const value = params[key];
3125
- if (Array.isArray(value)) {
3126
- params[key] = value.pop();
3127
- }
3128
- if (params[key] === undefined) {
3129
- delete params[key];
3130
- }
3131
- });
3132
- return (
3133
- serviceUri(req) +
3134
- decodeURIComponent(
3135
- URL.format({
3136
- ...originalUrl,
3137
- search: null,
3138
- pathname: originalUrl.contextPath,
3139
- query: {
3140
- ...originalUrl.query,
3141
- ...params,
3142
- },
3143
- })
3144
- )
3145
- );
3146
- }
3147
-
3148
- function respond(req, res, statusCode, headers, body) {
3149
- if (!res.headersSent) {
3150
- if (!body || statusCode === 204) {
3151
- delete headers["content-length"];
3152
- }
3153
- Object.entries(headers).forEach(([name, value]) => {
3154
- res.setHeader(name, value);
3155
- });
3156
- res.status(statusCode);
3157
- if (body && statusCode !== 204) {
3158
- res.write(body);
3159
- }
3160
- res.end();
3161
-
3162
- // Trace
3163
- traceResponse(req, "Response", res.statusCode, res.statusMessage, headers, body);
3164
- }
3165
- }
3166
-
3167
- function normalizeContentType(headers) {
3168
- let contentType = headers["content-type"];
3169
- if (contentType) {
3170
- contentType = contentType.trim();
3171
- if (isApplicationJSON(contentType)) {
3172
- contentType = contentType
3173
- .split(";")
3174
- .filter((part) => {
3175
- return !part.startsWith("odata.");
3176
- })
3177
- .join(";");
3178
- }
3179
- headers["content-type"] = contentType;
3180
- }
3181
- return contentType;
3182
- }
3183
-
3184
- function normalizeBody(body) {
3185
- if (typeof body === "string" || Buffer.isBuffer(body)) {
3186
- return body;
3187
- }
3188
- if (typeof body === "object") {
3189
- return JSON.stringify(body);
3190
- }
3191
- return String(body);
3192
- }
3193
-
3194
- function propagateHeaders(req, addHeaders = {}) {
3195
- const headers = Object.assign({}, req.headers, addHeaders);
3196
- headers["x-request-id"] = req.contextId;
3197
- headers["x-correlation-id"] = req.contextId;
3198
- headers["x-correlationid"] = req.contextId;
3199
- delete headers.host;
3200
- return headers;
3201
- }
3202
-
3203
- function enrichApplicationJSON(contentType) {
3204
- const [key] = IEEE754Compatible.split("=");
3205
- if (!contentType.includes(key)) {
3206
- contentType += ";" + IEEE754Compatible;
3207
- }
3208
- return contentType;
3209
- }
3210
-
3211
- function isApplicationJSON(contentType) {
3212
- return contentType && contentType.startsWith("application/json");
3213
- }
3214
-
3215
- function isPlainText(contentType) {
3216
- return contentType && contentType.startsWith("text/plain");
3217
- }
3218
-
3219
- function isXML(contentType) {
3220
- return (
3221
- contentType &&
3222
- (contentType.startsWith("application/xml") ||
3223
- contentType.startsWith("application/atomsvc+xml") ||
3224
- contentType.startsWith("text/xml") ||
3225
- contentType.startsWith("text/html"))
3226
- );
3227
- }
3228
-
3229
- function isMultipartMixed(contentType) {
3230
- return contentType && contentType.replace(/\s/g, "").startsWith("multipart/mixed;boundary=");
3231
- }
3232
-
3233
- function isMultipartFormData(contentType) {
3234
- return contentType && contentType.replace(/\s/g, "").startsWith("multipart/form-data;boundary=");
3235
- }
3236
-
3237
- function encodeURIKey(value) {
3238
- return encodeURIComponent(value).replace(/[/]/g, "%2F").replace(/'/g, "''").replace(/%3A/g, ":");
3239
- }
3240
-
3241
- function decodeURIKey(value) {
3242
- return decodeURIComponent(value).replace(/%2F/g, "/");
3243
- }
3244
-
3245
- function targetUrl(req) {
3246
- // Non-batch scenario only
3247
- let path = req.originalUrl;
3248
- Object.entries(pathRewrite).forEach(([key, value]) => {
3249
- path = path.replace(new RegExp(key, "g"), value);
3250
- });
3251
- return path;
3252
- }
3253
-
3254
- function lookupReturnDefinition(returns, req) {
3255
- returns = (returns && returns.items) || returns;
3256
- if (returns && returns.type) {
3257
- const definition = lookupDefinition(returns.type, req);
3258
- return definition || lookupReturnPrimitiveDefinition(returns);
3259
- }
3260
- return returns;
3261
- }
3262
-
3263
- function lookupReturnPrimitiveDefinition(returns) {
3264
- returns = (returns && returns.items) || returns;
3265
- return {
3266
- name: lookupODataType(returns && returns.type),
3267
- elements: {
3268
- value: returns,
3269
- },
3270
- };
3271
- }
3272
-
3273
- function lookupODataType(type) {
3274
- const odataType = Object.keys(DataTypeOData).find((key) => {
3275
- return DataTypeOData[key] === type;
3276
- });
3277
- if (odataType && odataType.startsWith("_")) {
3278
- return odataType.substr(1);
3279
- }
3280
- return odataType;
3281
- }
3282
-
3283
- function structureKeys(structure) {
3284
- const keys = [];
3285
- for (const key in structure || {}) {
3286
- keys.push(key);
3287
- }
3288
- return keys;
3289
- }
3290
-
3291
- function definitionElements(definition) {
3292
- if (definition && definition.elements) {
3293
- return structureKeys(definition.elements).reduce((elements, key) => {
3294
- const element = definition.elements[key];
3295
- if (element["@cds.api.ignore"]) {
3296
- return elements;
3297
- }
3298
- elements[key] = element;
3299
- if (
3300
- (element.type === "cds.Composition" || element.type === "cds.Association") &&
3301
- element.keys &&
3302
- element._target
3303
- ) {
3304
- element.keys.forEach((key) => {
3305
- const targetKey = key.ref[0];
3306
- const foreignKey = `${element.name}_${targetKey}`;
3307
- if (!elements[foreignKey]) {
3308
- elements[foreignKey] = {
3309
- key: element.key,
3310
- type: element._target.elements[targetKey].type,
3311
- name: foreignKey,
3312
- parent: element.parent,
3313
- kind: element.kind,
3314
- };
3315
- }
3316
- });
3317
- }
3318
- return elements;
3319
- }, {});
3320
- }
3321
- return {};
3322
- }
3323
-
3324
- function definitionKeys(definition) {
3325
- const elements = definitionElements(definition);
3326
- return structureKeys(elements).reduce((keys, key) => {
3327
- if (elements[key].key) {
3328
- keys[key] = elements[key];
3329
- }
3330
- return keys;
3331
- }, {});
3332
- }
3333
-
3334
- function elementType(element, req) {
3335
- let type;
3336
- if (element) {
3337
- type = element.type;
3338
- if (element["@odata.Type"] || element["@odata.type"]) {
3339
- const odataType = localName(element["@odata.Type"] || element["@odata.type"]);
3340
- if (odataType && DataTypeOData[odataType]) {
3341
- type = DataTypeOData[odataType];
3342
- }
3343
- }
3344
- if (!type && element.items && element.items.type) {
3345
- type = element.items.type;
3346
- }
3347
- }
3348
- return baseElementType(type, req.csn);
3349
- }
3350
-
3351
- function baseElementType(type, csn) {
3352
- if (type && csn.definitions[type]) {
3353
- type = csn.definitions[type].type;
3354
- type = baseElementType(type, csn);
3355
- }
3356
- return type;
3357
- }
3358
-
3359
- function findElementByType(elements, type, req) {
3360
- return structureKeys(elements)
3361
- .filter((key) => {
3362
- return !(elements[key].type === "cds.Composition" && elements[key].type === "cds.Association");
3363
- })
3364
- .find((key) => {
3365
- const element = elements[key];
3366
- return element && elementType(element, req) === type;
3367
- });
3368
- }
3369
-
3370
- function findElementByAnnotation(elements, annotation) {
3371
- return structureKeys(elements)
3372
- .filter((key) => {
3373
- return !(elements[key].type === "cds.Composition" && elements[key].type === "cds.Association");
3374
- })
3375
- .find((key) => {
3376
- const element = elements[key];
3377
- return element && !!element[annotation];
3378
- });
3379
- }
3380
-
3381
- function findElementValueByAnnotation(elements, annotation) {
3382
- const elementName = findElementByAnnotation(elements, annotation);
3383
- if (elementName) {
3384
- const elementValue = elements[elementName][annotation];
3385
- if (elementValue) {
3386
- return elementValue["="] || elementValue;
3387
- }
3388
- }
3389
- return undefined;
3390
- }
3391
-
3392
- function findEndingElementName(elements, url) {
3393
- return structureKeys(elements)
3394
- .filter((key) => {
3395
- return !(elements[key].type === "cds.Composition" && elements[key].type === "cds.Association");
3396
- })
3397
- .find((key) => {
3398
- const element = elements[key];
3399
- return url.contextPath.endsWith(`/${element.name}`);
3400
- });
3401
- }
3402
-
3403
- function determineLocale(req) {
3404
- let locale = cds.env.i18n && cds.env.i18n.default_language;
3405
- try {
3406
- locale = require("../../req/locale")(req);
3407
- } catch {
3408
- try {
3409
- // CDS 3
3410
- locale = require("@sap/cds-runtime/lib/cds-services/adapter/utils/locale")({ req }); // eslint-disable-line cds/no-missing-dependencies
3411
- } catch {
3412
- // Default
3413
- }
3414
- }
3415
- if (locale && locale.length >= 2) {
3416
- locale = locale.substr(0, 2).toLowerCase() + locale.slice(2);
3417
- }
3418
- return locale || "en";
3419
- }
3420
-
3421
- const ensureArray = (value) => {
3422
- if (!value) {
3423
- return [];
3424
- }
3425
- if (Array.isArray(value)) {
3426
- return value;
3427
- }
3428
- if (typeof value === "string") {
3429
- return value.split(",");
3430
- }
3431
- if (typeof value === "object") {
3432
- return Object.keys(value)
3433
- .filter((k) => value[k])
3434
- .sort();
3435
- }
3436
- return [];
3437
- };
3438
-
3439
- function decodeBase64(b64String) {
3440
- return Buffer.from(b64String, "base64").toString();
3441
- }
3442
-
3443
- function decodeJwtTokenBody(token) {
3444
- const parts = token.split(".");
3445
- if (parts.length > 1) {
3446
- return JSON.parse(decodeBase64(parts[1]));
3447
- }
3448
- throw new Error("Invalid JWT token");
3449
- }
3450
-
3451
- function setContentLength(headers, body) {
3452
- if (body && !(headers["transfer-encoding"] || "").includes("chunked")) {
3453
- headers["content-length"] = Buffer.byteLength(body);
3454
- } else {
3455
- delete headers["content-length"];
3456
- }
3457
- }
3458
-
3459
- function processMultipartMixed(
3460
- req,
3461
- multiPartBody,
3462
- contentType,
3463
- urlProcessor,
3464
- bodyHeadersProcessor,
3465
- contentIdOrder = [],
3466
- direction
3467
- ) {
3468
- let maxContentId = 1;
3469
-
3470
- function nextContentID() {
3471
- return "cov2ap_" + String(maxContentId++);
3472
- }
3473
-
3474
- if (!multiPartBody || !(typeof multiPartBody === "string")) {
3475
- const error = new Error("Invalid multipart body");
3476
- error.statusCode = 400;
3477
- throw error;
3478
- }
3479
-
3480
- if (!contentType || !(typeof contentType === "string")) {
3481
- const error = new Error("Invalid content type");
3482
- error.statusCode = 400;
3483
- throw error;
3484
- }
3485
-
3486
- const match = contentType.replace(/\s/g, "").match(/^multipart\/mixed;boundary=([^;]*)/i);
3487
- let boundary = match && match.pop();
3488
- if (!boundary) {
3489
- return multiPartBody;
3490
- }
3491
- let boundaryChangeSet = "";
3492
- let urlAfterBlank = false;
3493
- let bodyAfterBlank = false;
3494
- let previousLineIsBlank = false;
3495
- let index = 0;
3496
- let statusCode;
3497
- let contentId;
3498
- let contentIdMisplaced = false;
3499
- let contentTransferEncoding;
3500
- let body = "";
3501
- let headers = {};
3502
- let method = "";
3503
- let url = "";
3504
- const parts = multiPartBody.split("\r\n");
3505
- const newParts = [];
3506
- parts.forEach((part) => {
3507
- const match = part.replace(/\s/g, "").match(/^content-type:multipart\/mixed;boundary=(.*)$/i);
3508
- if (match) {
3509
- boundaryChangeSet = match.pop();
3510
- }
3511
- if (part.startsWith(`--${boundary}`) || (boundaryChangeSet && part.startsWith(`--${boundaryChangeSet}`))) {
3512
- // Body & Headers
3513
- if (bodyAfterBlank) {
3514
- if (bodyHeadersProcessor) {
3515
- try {
3516
- const contentType = normalizeContentType(headers);
3517
- if (isApplicationJSON(contentType)) {
3518
- body = (body && JSON.parse(body)) || {};
3519
- }
3520
- const result = bodyHeadersProcessor({
3521
- index,
3522
- statusCode,
3523
- contentType,
3524
- body,
3525
- headers,
3526
- method,
3527
- url,
3528
- contentId,
3529
- });
3530
- body = (result && result.body) || body;
3531
- headers = (result && result.headers) || headers;
3532
- } catch (err) {
3533
- // Error
3534
- logError(req, "Batch", err);
3535
- }
3536
- }
3537
- if (boundaryChangeSet) {
3538
- // Inject mandatory content-id for changesets
3539
- const addContentIdHeader = contentId === undefined || contentIdMisplaced;
3540
- if (contentId === undefined && direction === ProcessingDirection.Request) {
3541
- contentId = nextContentID();
3542
- }
3543
- if (contentId !== undefined) {
3544
- if (direction === ProcessingDirection.Request || !contentId.startsWith("cov2ap_")) {
3545
- if (addContentIdHeader) {
3546
- // Add content-id to headers of changeset (before url = -3)
3547
- newParts.splice(-3, 0, `content-id: ${contentId}`);
3548
- }
3549
- headers["content-id"] = contentId;
3550
- }
3551
- contentIdOrder.push(contentId);
3552
- }
3553
- }
3554
- // Inject mandatory content-transfer-encoding
3555
- if (!contentTransferEncoding) {
3556
- contentTransferEncoding = "binary";
3557
- // Add content-transfer-encoding to headers (before url = -3)
3558
- newParts.splice(-3, 0, `content-transfer-encoding: ${contentTransferEncoding}`);
3559
- }
3560
- Object.entries(headers).forEach(([name, value]) => {
3561
- newParts.splice(-1, 0, `${name}: ${value}`);
3562
- });
3563
- newParts.push(body);
3564
- statusCode = undefined;
3565
- contentId = undefined;
3566
- contentIdMisplaced = false;
3567
- contentTransferEncoding = undefined;
3568
- body = "";
3569
- headers = {};
3570
- url = "";
3571
- index++;
3572
- }
3573
- urlAfterBlank = true;
3574
- bodyAfterBlank = false;
3575
- newParts.push(part);
3576
- if (boundaryChangeSet && part === `--${boundaryChangeSet}--`) {
3577
- boundaryChangeSet = "";
3578
- }
3579
- } else if (urlAfterBlank && previousLineIsBlank) {
3580
- urlAfterBlank = false;
3581
- bodyAfterBlank = true;
3582
- // Url
3583
- const urlParts = part.split(" ");
3584
- let partMethod = urlParts[0];
3585
- let partUrl = urlParts.slice(1, -1).join(" ");
3586
- let partProtocol = urlParts.pop();
3587
- if (urlProcessor) {
3588
- const result = urlProcessor({ method: partMethod, url: partUrl });
3589
- if (result) {
3590
- partMethod = result.method;
3591
- partUrl = result.url;
3592
- part = [partMethod, partUrl, partProtocol].join(" ");
3593
- }
3594
- }
3595
- method = partMethod;
3596
- url = partUrl;
3597
-
3598
- newParts.push(part);
3599
- if (part.startsWith("HTTP/")) {
3600
- const statusCodeMatch = part.match(/^HTTP\/[\d.]+\s+(\d{3})\s.*$/i);
3601
- if (statusCodeMatch) {
3602
- statusCode = parseInt(statusCodeMatch.pop());
3603
- }
3604
- }
3605
- } else if (bodyAfterBlank && (previousLineIsBlank || body !== "")) {
3606
- body = body ? `${body}\r\n${part}` : part;
3607
- } else if (part !== "") {
3608
- const partIsContentId = part.toLowerCase().startsWith("content-id:");
3609
- if (partIsContentId) {
3610
- const colonIndex = part.indexOf(":");
3611
- if (colonIndex !== -1) {
3612
- contentId = part.substr(colonIndex + 1).trim();
3613
- contentIdMisplaced = !!bodyAfterBlank;
3614
- }
3615
- }
3616
- const partContentTransferEncoding = part.toLowerCase().startsWith("content-transfer-encoding:");
3617
- if (partContentTransferEncoding && !bodyAfterBlank) {
3618
- const colonIndex = part.indexOf(":");
3619
- if (colonIndex !== -1) {
3620
- contentTransferEncoding = part.substr(colonIndex + 1).trim();
3621
- }
3622
- }
3623
- if (!bodyAfterBlank) {
3624
- if (!(direction === ProcessingDirection.Response && partIsContentId && contentId.startsWith("cov2ap_"))) {
3625
- newParts.push(part);
3626
- }
3627
- } else {
3628
- let colonIndex = part.indexOf(":");
3629
- if (colonIndex !== -1) {
3630
- headers[part.substr(0, colonIndex).toLowerCase()] = part.substr(colonIndex + 1).trim();
3631
- }
3632
- }
3633
- } else {
3634
- newParts.push(part);
3635
- }
3636
- previousLineIsBlank = part === "";
3637
- });
3638
- return newParts.join("\r\n");
3639
- }
3640
-
3641
- function traceRequest(req, name, method, url, headers, body) {
3642
- if (LOG._debug) {
3643
- const _url = url || "";
3644
- const _headers = JSON.stringify(headers || {});
3645
- const _body = typeof body === "string" ? body : body ? JSON.stringify(body) : "";
3646
- trace(req, name, `${method} ${_url}`, _headers && "Headers:", _headers, _body && "Body:", _body);
3647
- }
3648
- }
3649
-
3650
- function traceResponse(req, name, statusCode, statusMessage, headers, body) {
3651
- if (LOG._debug) {
3652
- const _headers = JSON.stringify(headers || {});
3653
- const _body = typeof body === "string" ? body : body ? JSON.stringify(body) : "";
3654
- trace(
3655
- req,
3656
- name,
3657
- `${statusCode || ""} ${statusMessage || ""}`,
3658
- _headers && "Headers:",
3659
- _headers,
3660
- _body && "Body:",
3661
- _body
3662
- );
3663
- }
3664
- }
3665
-
3666
- function trace(req, name, ...lines) {
3667
- if (LOG._debug) {
3668
- initCDSContext(req);
3669
- LOG.debug(name, lines.filter((line) => !!line).join("\n"));
3670
- }
3671
- }
3672
-
3673
- function logError(req, name, error) {
3674
- if (LOG._error) {
3675
- initCDSContext(req);
3676
- LOG.error(name, error);
3677
- }
3678
- }
3679
-
3680
- function logWarning(req, name, message, info) {
3681
- if (LOG._warn) {
3682
- initCDSContext(req);
3683
- LOG.warn(name, message, info);
3684
- }
3685
- }
3686
-
3687
- function initCDSContext(req) {
3688
- cds.context = cds.context || {
3689
- id: req.contextId,
3690
- tenant: req.tenant,
3691
- user: req.user,
3692
- _: { req, res: req.res },
3693
- };
3694
- }
3695
-
3696
- return router;
3697
- }
3698
-
3699
- module.exports = cov2ap;