@sap/cds 6.1.2 → 6.2.1

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