@sap/cds 7.9.2 → 8.0.3

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 (279) hide show
  1. package/CHANGELOG.md +139 -3656
  2. package/_i18n/i18n_en_US_saptrc.properties +113 -0
  3. package/_i18n/i18n_zh_CN.properties +7 -4
  4. package/app/index.css +129 -0
  5. package/app/index.html +16 -64
  6. package/app/index.js +14 -9
  7. package/bin/args.js +34 -0
  8. package/bin/serve.js +18 -24
  9. package/bin/test.js +97 -0
  10. package/common.cds +5 -12
  11. package/eslint.config.mjs +133 -0
  12. package/lib/auth/basic-auth.js +16 -20
  13. package/lib/auth/dummy-auth.js +1 -1
  14. package/lib/auth/ias-auth.js +12 -30
  15. package/lib/auth/index.js +1 -14
  16. package/lib/auth/jwt-auth.js +14 -30
  17. package/lib/compile/cds-compile.js +1 -2
  18. package/lib/compile/cdsc.js +21 -26
  19. package/lib/compile/etc/_localized.js +1 -6
  20. package/lib/compile/etc/csv.js +1 -1
  21. package/lib/compile/etc/properties.js +1 -1
  22. package/lib/compile/for/java.js +1 -1
  23. package/lib/compile/for/lean_drafts.js +4 -6
  24. package/lib/compile/for/nodejs.js +1 -1
  25. package/lib/compile/parse.js +4 -0
  26. package/lib/compile/resolve.js +4 -4
  27. package/lib/compile/to/edm-files.js +16 -23
  28. package/lib/compile/to/hana.js +27 -0
  29. package/lib/compile/to/json.js +1 -1
  30. package/lib/compile/to/sql.js +5 -1
  31. package/lib/compile/to/srvinfo.js +1 -1
  32. package/lib/compile/to/yaml.js +3 -3
  33. package/lib/dbs/cds-deploy.js +4 -2
  34. package/lib/env/cds-env.js +10 -14
  35. package/lib/env/cds-requires.js +29 -13
  36. package/lib/env/defaults.js +46 -16
  37. package/lib/env/plugins.js +1 -1
  38. package/lib/env/schemas/cds-rc.js +8 -4
  39. package/lib/env/schemas/index.js +7 -7
  40. package/lib/env/serviceBindings.js +1 -1
  41. package/lib/index.js +12 -10
  42. package/lib/lazy.js +1 -1
  43. package/lib/linked/classes.js +36 -8
  44. package/lib/linked/entities.js +2 -10
  45. package/lib/linked/models.js +2 -1
  46. package/lib/linked/validate.js +292 -0
  47. package/lib/log/cds-error.js +0 -6
  48. package/lib/log/cds-log.js +3 -3
  49. package/lib/log/format/json.js +1 -1
  50. package/lib/log/service/index.js +0 -1
  51. package/lib/plugins.js +3 -3
  52. package/lib/ql/Query.js +2 -10
  53. package/lib/ql/SELECT.js +1 -1
  54. package/lib/ql/Whereable.js +3 -2
  55. package/lib/req/cds-context.js +14 -25
  56. package/lib/req/context.js +23 -25
  57. package/lib/req/request.js +1 -34
  58. package/lib/req/user.js +47 -35
  59. package/lib/srv/bindings.js +1 -1
  60. package/lib/srv/cds-connect.js +4 -4
  61. package/lib/srv/cds-serve.js +2 -2
  62. package/lib/srv/factory.js +1 -1
  63. package/lib/srv/middlewares/cds-context.js +11 -22
  64. package/lib/srv/middlewares/ctx-model.js +2 -3
  65. package/lib/srv/middlewares/errors.js +41 -8
  66. package/lib/srv/middlewares/index.js +3 -3
  67. package/lib/srv/middlewares/trace.js +0 -2
  68. package/lib/srv/protocols/hcql.js +15 -10
  69. package/lib/srv/protocols/http.js +44 -49
  70. package/lib/srv/protocols/index.js +1 -23
  71. package/lib/srv/protocols/odata-v4.js +12 -74
  72. package/lib/srv/protocols/rest.js +1 -13
  73. package/lib/srv/srv-api.js +0 -20
  74. package/lib/srv/srv-dispatch.js +3 -2
  75. package/lib/srv/srv-handlers.js +22 -11
  76. package/lib/srv/srv-methods.js +2 -2
  77. package/lib/srv/srv-models.js +3 -36
  78. package/lib/test/expect.js +343 -0
  79. package/lib/test/index.js +2 -0
  80. package/lib/test/reporter.js +176 -0
  81. package/lib/utils/axios.js +10 -9
  82. package/lib/utils/cds-test.js +86 -37
  83. package/lib/utils/cds-utils.js +54 -7
  84. package/lib/utils/check-version.js +0 -4
  85. package/lib/utils/colors.js +49 -0
  86. package/lib/utils/data.js +5 -4
  87. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +2 -7
  88. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +3 -30
  89. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +6 -12
  90. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +1 -3
  91. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +0 -1
  92. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +4 -7
  93. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +12 -6
  94. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +2 -4
  95. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +1 -0
  96. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -1
  97. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +0 -1
  98. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +1 -3
  99. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +1 -1
  100. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/edm/AbstractEdmStructuredType.js +1 -2
  101. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/ResourceJsonDeserializer.js +5 -0
  102. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ContextURLFactory.js +1 -1
  103. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +9 -43
  104. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +0 -1
  105. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +8 -3
  106. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +4 -2
  107. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +1 -3
  108. package/libx/_runtime/cds-services/util/assert.js +1 -1
  109. package/libx/_runtime/cds.js +10 -3
  110. package/libx/_runtime/common/Service.js +12 -32
  111. package/libx/_runtime/common/aspects/any.js +1 -0
  112. package/libx/_runtime/common/code-ext/execute.js +1 -1
  113. package/libx/_runtime/common/code-ext/worker.js +0 -1
  114. package/libx/_runtime/common/composition/data.js +0 -1
  115. package/libx/_runtime/common/composition/delete.js +0 -1
  116. package/libx/_runtime/common/composition/insert.js +2 -2
  117. package/libx/_runtime/common/composition/tree.js +0 -1
  118. package/libx/_runtime/common/composition/update.js +3 -3
  119. package/libx/_runtime/common/error/frontend.js +21 -12
  120. package/libx/_runtime/common/error/log.js +36 -0
  121. package/libx/_runtime/common/error/utils.js +2 -5
  122. package/libx/_runtime/common/generic/auth/autoexpose.js +18 -17
  123. package/libx/_runtime/common/generic/auth/expand.js +1 -1
  124. package/libx/_runtime/common/generic/auth/readOnly.js +1 -2
  125. package/libx/_runtime/common/generic/auth/restrict.js +23 -42
  126. package/libx/_runtime/common/generic/auth/restrictions.js +2 -7
  127. package/libx/_runtime/common/generic/auth/utils.js +91 -88
  128. package/libx/_runtime/common/generic/crud.js +6 -5
  129. package/libx/_runtime/common/generic/etag.js +7 -12
  130. package/libx/_runtime/common/generic/input.js +70 -68
  131. package/libx/_runtime/common/generic/paging.js +1 -0
  132. package/libx/_runtime/common/generic/sorting.js +1 -0
  133. package/libx/_runtime/common/generic/temporal.js +8 -2
  134. package/libx/_runtime/common/i18n/index.js +1 -1
  135. package/libx/_runtime/common/i18n/messages.properties +3 -1
  136. package/libx/_runtime/common/utils/binary.js +8 -2
  137. package/libx/_runtime/common/utils/compareJson.js +5 -1
  138. package/libx/_runtime/common/utils/copy.js +6 -11
  139. package/libx/_runtime/common/utils/cqn2cqn4sql.js +16 -14
  140. package/libx/_runtime/common/utils/differ.js +3 -6
  141. package/libx/_runtime/common/utils/keys.js +77 -18
  142. package/libx/_runtime/common/utils/postProcess.js +12 -15
  143. package/libx/_runtime/common/utils/propagateForeignKeys.js +0 -1
  144. package/libx/_runtime/common/utils/resolveView.js +2 -3
  145. package/libx/_runtime/common/utils/restrictions.js +45 -17
  146. package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -8
  147. package/libx/_runtime/common/utils/stream.js +3 -16
  148. package/libx/_runtime/common/utils/streamProp.js +8 -18
  149. package/libx/_runtime/common/utils/structured.js +1 -1
  150. package/libx/_runtime/common/utils/ucsn.js +0 -2
  151. package/libx/_runtime/db/Service.js +0 -72
  152. package/libx/_runtime/db/data-conversion/post-processing.js +0 -1
  153. package/libx/_runtime/db/expand/expandCQNToJoin.js +9 -9
  154. package/libx/_runtime/db/expand/rawToExpanded.js +0 -8
  155. package/libx/_runtime/db/generic/input.js +3 -8
  156. package/libx/_runtime/db/generic/rewrite.js +27 -4
  157. package/libx/_runtime/db/query/read.js +2 -2
  158. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +0 -1
  159. package/libx/_runtime/db/sql-builder/InsertBuilder.js +1 -1
  160. package/libx/_runtime/db/utils/columns.js +2 -6
  161. package/libx/_runtime/fiori/lean-draft.js +138 -56
  162. package/libx/_runtime/hana/Service.js +0 -1
  163. package/libx/_runtime/hana/driver.js +1 -1
  164. package/libx/_runtime/hana/dynatrace.js +1 -2
  165. package/libx/_runtime/hana/pool.js +11 -21
  166. package/libx/_runtime/hana/streaming.js +0 -1
  167. package/libx/_runtime/messaging/common-utils/AMQPClient.js +0 -1
  168. package/libx/_runtime/messaging/common-utils/authorizedRequest.js +1 -1
  169. package/libx/_runtime/messaging/common-utils/normalizeIncomingMessage.js +1 -1
  170. package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +1 -1
  171. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +19 -33
  172. package/libx/_runtime/messaging/event-broker.js +0 -12
  173. package/libx/_runtime/messaging/file-based.js +3 -3
  174. package/libx/_runtime/messaging/http-utils/token.js +1 -1
  175. package/libx/_runtime/messaging/kafka.js +2 -2
  176. package/libx/_runtime/messaging/redis-messaging.js +0 -1
  177. package/libx/_runtime/remote/Service.js +25 -25
  178. package/libx/_runtime/remote/utils/client.js +4 -5
  179. package/libx/_runtime/remote/utils/cloudSdkProvider.js +0 -3
  180. package/libx/_runtime/remote/utils/data.js +0 -1
  181. package/libx/_runtime/sqlite/Service.js +1 -2
  182. package/libx/_runtime/ucl/Service.js +37 -78
  183. package/libx/common/assert/index.js +22 -21
  184. package/libx/common/assert/type-relaxed.js +39 -0
  185. package/libx/common/assert/utils.js +3 -2
  186. package/libx/common/assert/validation.js +3 -8
  187. package/libx/common/utils/index.js +5 -0
  188. package/libx/common/utils/path.js +51 -0
  189. package/libx/odata/ODataAdapter.js +126 -0
  190. package/libx/odata/index.js +15 -2
  191. package/libx/odata/middleware/batch.js +261 -72
  192. package/libx/odata/middleware/body-parser.js +33 -0
  193. package/libx/odata/middleware/create.js +44 -59
  194. package/libx/odata/middleware/delete.js +23 -12
  195. package/libx/odata/middleware/error.js +30 -6
  196. package/libx/odata/middleware/metadata.js +38 -26
  197. package/libx/odata/middleware/operation.js +93 -69
  198. package/libx/odata/middleware/parse.js +6 -8
  199. package/libx/odata/middleware/read.js +117 -93
  200. package/libx/odata/middleware/service-document.js +22 -19
  201. package/libx/odata/middleware/stream.js +54 -56
  202. package/libx/odata/middleware/update.js +79 -87
  203. package/libx/odata/parse/afterburner.js +191 -175
  204. package/libx/odata/parse/cqn2odata.js +8 -8
  205. package/libx/odata/parse/grammar.peggy +27 -20
  206. package/libx/odata/parse/multipartToJson.js +17 -9
  207. package/libx/odata/parse/parser.js +1 -1
  208. package/libx/odata/utils/etag.js +14 -6
  209. package/libx/odata/utils/index.js +84 -12
  210. package/libx/odata/utils/metadata.js +161 -0
  211. package/libx/odata/utils/postProcess.js +89 -0
  212. package/libx/odata/utils/readAfterWrite.js +134 -17
  213. package/libx/odata/utils/result.js +36 -142
  214. package/libx/outbox/index.js +5 -4
  215. package/libx/rest/RestAdapter.js +115 -182
  216. package/libx/rest/middleware/create.js +28 -24
  217. package/libx/rest/middleware/delete.js +7 -10
  218. package/libx/rest/middleware/error.js +19 -16
  219. package/libx/rest/middleware/operation.js +48 -41
  220. package/libx/rest/middleware/parse.js +128 -126
  221. package/libx/rest/middleware/read.js +20 -27
  222. package/libx/rest/middleware/update.js +26 -31
  223. package/package.json +16 -12
  224. package/server.js +4 -2
  225. package/tasks/enterprise-messaging-deploy.js +1 -1
  226. package/apis/cds.d.ts +0 -3
  227. package/apis/core.d.ts +0 -21
  228. package/apis/cqn.d.ts +0 -18
  229. package/apis/csn.d.ts +0 -21
  230. package/apis/events.d.ts +0 -18
  231. package/apis/internal/inference.d.ts +0 -18
  232. package/apis/linked.d.ts +0 -18
  233. package/apis/log.d.ts +0 -20
  234. package/apis/models.d.ts +0 -18
  235. package/apis/ql.d.ts +0 -18
  236. package/apis/reflect.d.ts +0 -32
  237. package/apis/server.d.ts +0 -18
  238. package/apis/services.d.ts +0 -22
  239. package/bin/cds-serve.js +0 -56
  240. package/lib/compile/to/gql.js +0 -15
  241. package/lib/srv/protocols/_legacy.js +0 -44
  242. package/lib/utils/jest.js +0 -43
  243. package/libx/_runtime/auth/index.js +0 -193
  244. package/libx/_runtime/auth/strategies/JWT.js +0 -37
  245. package/libx/_runtime/auth/strategies/basic.js +0 -20
  246. package/libx/_runtime/auth/strategies/dummy.js +0 -14
  247. package/libx/_runtime/auth/strategies/ias-auth.js +0 -1
  248. package/libx/_runtime/auth/strategies/mock.js +0 -77
  249. package/libx/_runtime/auth/strategies/xssecUtils.js +0 -93
  250. package/libx/_runtime/auth/strategies/xsuaa.js +0 -38
  251. package/libx/_runtime/common/perf/index.js +0 -19
  252. package/libx/_runtime/common/utils/ensureIEEE754.js +0 -29
  253. package/libx/_runtime/fiori/draft.js +0 -2
  254. package/libx/_runtime/fiori/generic/activate.js +0 -190
  255. package/libx/_runtime/fiori/generic/before.js +0 -201
  256. package/libx/_runtime/fiori/generic/cancel.js +0 -19
  257. package/libx/_runtime/fiori/generic/delete.js +0 -21
  258. package/libx/_runtime/fiori/generic/edit.js +0 -157
  259. package/libx/_runtime/fiori/generic/index.js +0 -25
  260. package/libx/_runtime/fiori/generic/new.js +0 -82
  261. package/libx/_runtime/fiori/generic/patch.js +0 -101
  262. package/libx/_runtime/fiori/generic/prepare.js +0 -57
  263. package/libx/_runtime/fiori/generic/read.js +0 -1340
  264. package/libx/_runtime/fiori/generic/readOverDraft.js +0 -146
  265. package/libx/_runtime/fiori/utils/csn.js +0 -13
  266. package/libx/_runtime/fiori/utils/delete.js +0 -114
  267. package/libx/_runtime/fiori/utils/handler.js +0 -264
  268. package/libx/_runtime/fiori/utils/lockInfo.js +0 -27
  269. package/libx/_runtime/fiori/utils/req.js +0 -23
  270. package/libx/_runtime/fiori/utils/stream.js +0 -36
  271. package/libx/_runtime/fiori/utils/where.js +0 -254
  272. package/libx/_runtime/index.js +0 -22
  273. package/libx/odata/utils/handler.js +0 -120
  274. package/libx/odata/utils/metaInfo.js +0 -410
  275. package/libx/odata/utils/path.js +0 -75
  276. package/libx/rest/RestRequest.js +0 -32
  277. package/libx/rest/index.js +0 -3
  278. package/libx/rest/readme.md +0 -1
  279. /package/libx/common/assert/{type.js → type-strict.js} +0 -0
@@ -1,18 +1,20 @@
1
1
  const cds = require('../../../')
2
- const { AsyncResource } = require('async_hooks')
3
2
 
4
- // eslint-disable-next-line cds/no-missing-dependencies
3
+ const { AsyncResource } = require('async_hooks')
5
4
  const express = require('express')
6
5
  const { STATUS_CODES } = require('http')
7
6
  const qs = require('querystring')
8
7
  const { URL } = require('url')
9
8
 
10
9
  const multipartToJson = require('../parse/multipartToJson')
10
+ const { getBoundary } = require('../utils')
11
11
 
12
12
  const HTTP_METHODS = { GET: 1, POST: 1, PUT: 1, PATCH: 1, DELETE: 1 }
13
13
  const CT = { JSON: 'application/json', MULTIPART: 'multipart/mixed' }
14
14
  const CRLF = '\r\n'
15
15
 
16
+ const CONTINUE_ON_ERROR = /odata\.continue-on-error/
17
+
16
18
  /*
17
19
  * common
18
20
  */
@@ -47,7 +49,7 @@ const _validateBatch = body => {
47
49
  if (typeof request !== 'object')
48
50
  throw _deserializationError(`Element of 'requests' array at index ${i} must be type of 'object'.`)
49
51
 
50
- const { id, method, url, body, atomicityGroup, dependsOn } = request
52
+ const { id, method, url, body, atomicityGroup } = request
51
53
 
52
54
  _validateProperty('id', id, 'string')
53
55
 
@@ -83,9 +85,17 @@ const _validateBatch = body => {
83
85
  }
84
86
  }
85
87
 
86
- if (dependsOn) {
87
- _validateProperty('dependsOn', dependsOn, 'Array')
88
- dependsOn.forEach(dependsOnId => {
88
+ if (url.startsWith('$')) {
89
+ request.dependsOn ??= []
90
+ const dependencyId = url.split('/')[0].replace(/^\$/, '')
91
+ if (!request.dependsOn.includes(dependencyId)) {
92
+ request.dependsOn.push(dependencyId)
93
+ }
94
+ }
95
+
96
+ if (request.dependsOn) {
97
+ _validateProperty('dependsOn', request.dependsOn, 'Array')
98
+ request.dependsOn.forEach(dependsOnId => {
89
99
  _validateProperty('dependent request ID', dependsOnId, 'string')
90
100
 
91
101
  const dependency = ids[dependsOnId]
@@ -93,10 +103,12 @@ const _validateBatch = body => {
93
103
  throw _deserializationError(`Request ID '${dependsOnId}' used in dependsOn has not been defined before.`)
94
104
 
95
105
  const dependencyAtomicityGroup = dependency.atomicityGroup
96
- if (dependencyAtomicityGroup && !dependsOn.includes(dependencyAtomicityGroup))
97
- throw _deserializationError(
98
- `The group '${dependencyAtomicityGroup}' of the referenced request '${dependsOnId}' must be listed in dependsOn of request '${id}'.`
99
- )
106
+ if (
107
+ dependencyAtomicityGroup &&
108
+ dependencyAtomicityGroup !== atomicityGroup &&
109
+ !request.dependsOn.includes(dependencyAtomicityGroup)
110
+ )
111
+ request.dependsOn.push(dependencyAtomicityGroup)
100
112
  })
101
113
  }
102
114
 
@@ -108,6 +120,7 @@ const _validateBatch = body => {
108
120
  return ids
109
121
  }
110
122
 
123
+ // REVISIT: Why not simply use {__proto__:req, ...}?
111
124
  const _createExpressReqResLookalike = (request, _req, _res) => {
112
125
  const { id, method, url } = request
113
126
  const ret = { id }
@@ -123,11 +136,16 @@ const _createExpressReqResLookalike = (request, _req, _res) => {
123
136
  const u = new URL(url, 'http://cap')
124
137
  req.query = qs.parse(u.search.slice(1))
125
138
  req.headers = request.headers || {}
139
+ if (request.content_id) req.headers['content-id'] = request.content_id
126
140
  req.body = request.body
141
+ if (_req._login) req._login = _req._login
127
142
 
128
143
  const res = (ret.res = new express.response.constructor(req))
129
144
  res.__proto__ = express.response
130
145
 
146
+ // REVISIT: mark as subrequest
147
+ req._subrequest = true
148
+
131
149
  // express internals
132
150
  res.app = _res.app
133
151
 
@@ -135,10 +153,11 @@ const _createExpressReqResLookalike = (request, _req, _res) => {
135
153
  req.res = res
136
154
 
137
155
  // resolve promise for subrequest via res.end()
138
- ret.promise = new Promise((resolve, _reject) => {
156
+ ret.promise = new Promise((resolve, reject) => {
139
157
  res.end = (chunk, encoding) => {
140
158
  res._chunk = chunk
141
159
  res._encoding = encoding
160
+ if (res.statusCode >= 400) return reject(ret)
142
161
  resolve(ret)
143
162
  }
144
163
  })
@@ -146,22 +165,91 @@ const _createExpressReqResLookalike = (request, _req, _res) => {
146
165
  return ret
147
166
  }
148
167
 
149
- const _transaction = async (srv, router) => {
168
+ const _writeResponseMultipart = (responses, res, rejected, group, boundary) => {
169
+ if (group) {
170
+ res.write(`--${boundary}${CRLF}`)
171
+ res.write(`content-type: multipart/mixed;boundary=${group}${CRLF}${CRLF}`)
172
+ }
173
+ const header = group || boundary
174
+ if (rejected) {
175
+ const resp = responses.find(r => r.status === 'fail')
176
+ if (resp.separator && res._writeSeparator) res.write(resp.separator)
177
+ resp.txt.forEach(txt => {
178
+ res.write(`--${header}${CRLF}`)
179
+ res.write(`${txt}`)
180
+ })
181
+ } else {
182
+ for (const resp of responses) {
183
+ if (resp.separator) res.write(resp.separator)
184
+ resp.txt.forEach(txt => {
185
+ res.write(`--${header}${CRLF}`)
186
+ res.write(`${txt}`)
187
+ })
188
+ }
189
+ }
190
+ if (group) res.write(`${CRLF}--${group}--${CRLF}`)
191
+ // indicates that we need to write a potential separator before the next error response
192
+ res._writeSeparator = true
193
+ }
194
+
195
+ const _writeResponseJson = (responses, res) => {
196
+ for (const resp of responses) {
197
+ if (resp.separator) res.write(resp.separator)
198
+ resp.txt.forEach(txt => res.write(txt))
199
+ }
200
+ }
201
+
202
+ let error_mws
203
+ const _getNextForLookalike = lookalike => {
204
+ error_mws ??= cds.middlewares.after.filter(mw => mw.length === 4) // error middleware has 4 params
205
+ return err => {
206
+ let _err = err
207
+ let _next_called
208
+ const _next = e => {
209
+ _err = e
210
+ _next_called = true
211
+ }
212
+ for (const mw of error_mws) {
213
+ _next_called = false
214
+ mw(_err, lookalike.req, lookalike.res, _next)
215
+ if (!_next_called) break //> next chain was interrupted -> done
216
+ }
217
+ if (_next_called) {
218
+ // here, final error middleware called next (which actually shouldn't happen!)
219
+ if (_err.statusCode) lookalike.res.status(_err.statusCode)
220
+ if (typeof _err === 'object') lookalike.res.json({ error: _err })
221
+ else lookalike.res.send(_err)
222
+ }
223
+ }
224
+ }
225
+
226
+ // REVISIT: This looks frightening -> need to review
227
+ const _transaction = async srv => {
150
228
  return new Promise(res => {
151
229
  const ret = {}
152
- srv.tx(
230
+ const _tx = srv.tx(
153
231
  async () =>
154
232
  (ret.promise = new Promise((resolve, reject) => {
155
233
  const proms = []
156
- ret.add = AsyncResource.bind(function (request, req, res) {
157
- const lookalike = _createExpressReqResLookalike(request, req, res)
158
- router.handle(lookalike.req, lookalike.res)
159
- request.promise = lookalike.promise
160
- proms.push(request.promise)
161
- return request.promise
234
+ // It's important to run `makePromise` in the current execution context (cb of srv.tx),
235
+ // otherwise, it will use a different transaction.
236
+ // REVISIT: This looks frightening -> need to review
237
+ ret.add = AsyncResource.bind(function (makePromise) {
238
+ const p = makePromise()
239
+ proms.push(p)
240
+ return p
162
241
  })
163
- ret.done = function () {
164
- return Promise.allSettled(proms).then(resolve, reject)
242
+ ret.done = async function () {
243
+ const result = await Promise.allSettled(proms)
244
+ if (result.some(r => r.status === 'rejected')) {
245
+ reject()
246
+ // REVISIT: workaround to wait for commit/rollback
247
+ await _tx
248
+ return 'rejected'
249
+ }
250
+ resolve(result)
251
+ // REVISIT: workaround to wait for commit/rollback
252
+ await _tx
165
253
  }
166
254
  res(ret)
167
255
  }))
@@ -172,7 +260,12 @@ const _transaction = async (srv, router) => {
172
260
  const _processBatch = async (srv, router, req, res, next, body, ct, boundary) => {
173
261
  body ??= req.body
174
262
  ct ??= 'JSON'
175
- const isJson = ct === 'JSON'
263
+ // respond with requested content type (i.e., accept) with fallback to the content type used in the request
264
+ let isJson = ct === 'JSON'
265
+ if (req.headers.accept) {
266
+ if (req.headers.accept.indexOf('multipart/mixed') > -1) isJson = false
267
+ else if (req.headers.accept.indexOf('application/json') > -1) isJson = true
268
+ }
176
269
  const _formatResponse = isJson ? _formatResponseJson : _formatResponseMultipart
177
270
 
178
271
  try {
@@ -183,41 +276,129 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
183
276
  let previousAtomicityGroup
184
277
  let separator
185
278
  let tx
186
-
187
- res.setHeader('content-type', CT[ct] + (!isJson ? ';boundary=' + boundary : ''))
188
- res.setHeader('OData-Version', '4.0') //> REVISIT: Fiori/ UI5 wants this
189
- res.status(200)
190
- res.write(isJson ? '{"responses":[' : '')
279
+ let responses
280
+
281
+ // IMPORTANT: Avoid sending headers and responses too eagerly, as we might still have to send a 401
282
+ let sendPreludeOnce = () => {
283
+ res.setHeader('Content-Type', isJson ? CT.JSON : CT.MULTIPART + ';boundary=' + boundary)
284
+ res.status(200)
285
+ res.write(isJson ? '{"responses":[' : '')
286
+ sendPreludeOnce = () => {} //> only once
287
+ }
191
288
 
192
289
  const { requests } = body
193
290
  for await (const request of requests) {
291
+ // for json payloads, normalize headers to lowercase
292
+ if (ct === 'JSON') {
293
+ request.headers = request.headers
294
+ ? Object.keys(request.headers).reduce((acc, cur) => {
295
+ acc[cur.toLowerCase()] = request.headers[cur]
296
+ return acc
297
+ }, {})
298
+ : {}
299
+ }
300
+
194
301
  const { atomicityGroup } = request
195
302
 
196
303
  if (!atomicityGroup || atomicityGroup !== previousAtomicityGroup) {
197
- if (tx) await tx.done()
198
- tx = await _transaction(srv, router)
304
+ if (tx) {
305
+ // Each change in `atomicityGroup` results in a new transaction. We execute them in sequence to avoid too many database connections.
306
+ // In the future, we might make this configurable (e.g. allow X parallel connections per HTTP request).
307
+ const rejected = await tx.done()
308
+ if (tx.failed?.res.statusCode === 401 && req._login) return req._login()
309
+ else sendPreludeOnce()
310
+ isJson
311
+ ? _writeResponseJson(responses, res)
312
+ : _writeResponseMultipart(responses, res, rejected, previousAtomicityGroup, boundary)
313
+ if (rejected && !CONTINUE_ON_ERROR.test(req.headers.prefer)) {
314
+ tx = null
315
+ break
316
+ }
317
+ }
318
+
319
+ responses = []
320
+ tx = await _transaction(srv)
199
321
  if (atomicityGroup) ids[atomicityGroup].promise = tx.promise
200
322
  }
201
323
 
202
- const dependencies = request.dependsOn?.map(id => ids[id].promise)
203
- if (dependencies) {
204
- // TODO: fail the dependent request if dependency fails
205
- await Promise.allSettled(dependencies)
206
- }
324
+ tx.add(() => {
325
+ return (request.promise = (async () => {
326
+ const dependencies = request.dependsOn?.filter(id => id !== request.atomicityGroup).map(id => ids[id].promise)
327
+ if (dependencies) {
328
+ // first, wait for dependencies
329
+ const results = await Promise.allSettled(dependencies)
330
+ const dependendOnFailed = results.some(({ status }) => status === 'rejected')
331
+ if (dependendOnFailed) {
332
+ tx.res = {
333
+ getHeaders: () => {},
334
+ statusCode: 404,
335
+ _chunk: JSON.stringify({
336
+ status: 404,
337
+ message: `Dependency for Request ${request.id} failed to execute`
338
+ })
339
+ }
340
+ throw tx
341
+ }
342
+
343
+ const dependsOnId = request.url.split('/')[0].replace(/^\$/, '')
344
+ if (dependsOnId in ids) {
345
+ const dependentResult = results.find(r => r.value.id === dependsOnId)
346
+ const dependentOnUrl = dependentResult.value.req.originalUrl
347
+ const dependentOnResult = JSON.parse(dependentResult.value.res._chunk)
348
+ const recentUrl = request.url
349
+ const cqn = cds.odata.parse(dependentOnUrl, { service: srv, baseUrl: req.baseUrl, strict: true })
350
+ const { target } = cqn
351
+ const keyString =
352
+ '(' +
353
+ target.keys
354
+ .filter(k => !k.isAssociation)
355
+ .map(k => {
356
+ let v = dependentOnResult[k.name]
357
+ if (typeof v === 'string' && k._type !== 'cds.UUID') v = `'${v}'`
358
+ return k.name + '=' + v
359
+ })
360
+ .join(',') +
361
+ ')'
362
+ request.url = recentUrl.replace(`$${dependsOnId}`, dependentOnUrl + keyString)
363
+ }
364
+ }
207
365
 
208
- tx.add(request, req, res).then(request => {
209
- if (separator) res.write(separator)
210
- else separator = isJson ? Buffer.from(',') : Buffer.from(CRLF)
211
- _formatResponse(request, res, boundary)
366
+ // REVIST: That sends each request through the whole middleware chain again and again, including authentication and authorization.
367
+ // -> We should optimize this!
368
+ const lookalike = _createExpressReqResLookalike(request, req, res)
369
+ const lookalike_next = _getNextForLookalike(lookalike)
370
+ router.handle(lookalike.req, lookalike.res, lookalike_next)
371
+ return lookalike.promise
372
+ })())
212
373
  })
213
-
214
- if (!atomicityGroup) tx.done()
374
+ .then(req => {
375
+ const resp = { status: 'ok' }
376
+ if (separator) resp.separator = separator
377
+ else separator = isJson ? Buffer.from(',') : Buffer.from(CRLF)
378
+ resp.txt = _formatResponse(req, boundary)
379
+ responses.push(resp)
380
+ })
381
+ .catch(failedReq => {
382
+ const resp = { status: 'fail' }
383
+ if (separator) resp.separator = separator
384
+ else separator = isJson ? Buffer.from(',') : Buffer.from(CRLF)
385
+ resp.txt = _formatResponse(failedReq, boundary)
386
+ tx.failed = failedReq
387
+ responses.push(resp)
388
+ })
215
389
 
216
390
  previousAtomicityGroup = atomicityGroup
217
391
  }
218
392
 
219
- if (tx) await tx.done()
220
-
393
+ if (tx) {
394
+ // The last open transaction must be finished
395
+ const rejected = await tx.done()
396
+ if (tx.failed?.res.statusCode === 401 && req._login) return req._login()
397
+ else sendPreludeOnce()
398
+ isJson
399
+ ? _writeResponseJson(responses, res)
400
+ : _writeResponseMultipart(responses, res, rejected, previousAtomicityGroup, boundary)
401
+ } else sendPreludeOnce()
221
402
  res.write(isJson ? ']}' : `${CRLF}--${boundary}--${CRLF}`)
222
403
  res.end()
223
404
 
@@ -231,8 +412,8 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
231
412
  * multipart/mixed
232
413
  */
233
414
 
234
- const _multipartBatch = (srv, router) => async (req, res, next) => {
235
- const boundary = req.headers['content-type']?.match(/boundary=([\w_-]+)/i)?.[1]
415
+ const _multipartBatch = async (srv, router, req, res, next) => {
416
+ const boundary = getBoundary(req)
236
417
  if (!boundary) return next(cds.error('No boundary found in Content-Type header', { code: 400 }))
237
418
 
238
419
  try {
@@ -243,20 +424,22 @@ const _multipartBatch = (srv, router) => async (req, res, next) => {
243
424
  }
244
425
  }
245
426
 
246
- const _formatResponseMultipart = (request, res, boundary) => {
247
- const { /* id, */ res: response } = request
427
+ const _formatResponseMultipart = request => {
428
+ const { res: response } = request
429
+ const content_id = request.req?.headers['content-id']
248
430
 
249
- let txt = `--${boundary}${CRLF}content-type: application/http${CRLF}content-transfer-encoding: binary${CRLF}${CRLF}`
431
+ let txt = `content-type: application/http${CRLF}content-transfer-encoding: binary${CRLF}`
432
+ if (content_id) txt += `content-id: ${content_id}${CRLF}`
433
+ txt += CRLF
250
434
  txt += `HTTP/1.1 ${response.statusCode} ${STATUS_CODES[response.statusCode]}${CRLF}`
251
435
 
252
436
  // REVISIT: tests require specific sequence
253
437
  const headers = {
254
- 'odata-version': '4.0',
255
- 'content-type': 'DUMMY',
256
- ...response.getHeaders()
438
+ ...response.getHeaders(),
439
+ 'content-type': 'application/json;odata.metadata=minimal' //> REVISIT: expected by tests
257
440
  }
258
- headers['content-type'] = 'application/json;odata.metadata=minimal' //> REVISIT: expected by tests
259
441
  delete headers['content-length'] //> REVISIT: expected by tests
442
+
260
443
  for (const key in headers) {
261
444
  txt += key + ': ' + headers[key] + CRLF
262
445
  }
@@ -271,7 +454,7 @@ const _formatResponseMultipart = (request, res, boundary) => {
271
454
  let meta = [],
272
455
  data = []
273
456
  for (const [k, v] of Object.entries(_json)) {
274
- if (k.startsWith('@')) meta.push(`"${k}":"${v.replaceAll('"', '\\"')}"`)
457
+ if (k.startsWith('@')) meta.push(`"${k}":${typeof v === 'string' ? `"${v.replaceAll('"', '\\"')}"` : v}`)
275
458
  else data.push(JSON.stringify({ [k]: v }).slice(1, -1))
276
459
  }
277
460
  const _json_as_txt = '{' + meta.join(',') + (meta.length && data.length ? ',' : '') + data.join(',') + '}'
@@ -283,7 +466,7 @@ const _formatResponseMultipart = (request, res, boundary) => {
283
466
  }
284
467
  }
285
468
 
286
- res.write(txt)
469
+ return [txt]
287
470
  }
288
471
 
289
472
  /*
@@ -296,46 +479,52 @@ const _formatStatics = {
296
479
  close: Buffer.from('}')
297
480
  }
298
481
 
299
- const _formatResponseJson = (request, res) => {
482
+ const _formatResponseJson = request => {
300
483
  const { id, res: response } = request
484
+
301
485
  const raw = Buffer.from(
302
486
  JSON.stringify({
303
487
  id,
304
488
  status: response.statusCode,
305
489
  headers: {
306
- 'odata-version': '4.0',
307
- 'content-type': 'application/json',
308
- ...response.getHeaders()
490
+ ...response.getHeaders(),
491
+ 'content-type': 'application/json' //> REVISIT: why?
309
492
  }
310
493
  })
311
494
  )
495
+
496
+ // body?
497
+ if (!response._chunk) return [raw]
498
+
312
499
  // change last "}" into ","
313
500
  raw[raw.byteLength - 1] = _formatStatics.comma
314
- res.write(raw)
315
- res.write(_formatStatics.body)
316
- res.write(response._chunk)
317
- res.write(_formatStatics.close)
501
+ return [raw, _formatStatics.body, response._chunk, _formatStatics.close]
318
502
  }
319
503
 
320
- const _jsonBatch = (srv, router) => (req, res, next) => {
321
- _processBatch(srv, router, req, res, next)
322
- }
504
+ /*
505
+ * exports
506
+ */
507
+
508
+ module.exports = adapter => {
509
+ const { options: config, router, service } = adapter
510
+ const { max_content_length } = config //> max_content_length is unofficial config
323
511
 
324
- module.exports = (srv, router) => {
325
- const handleJsonBatch = _jsonBatch(srv, router)
326
- const handleMultipartBatch = _multipartBatch(srv, router)
512
+ const options = { type: '*/*' }
513
+ if (max_content_length) options.limit = max_content_length
514
+ const textBodyParser = express.text(options)
327
515
 
328
- return function batch(req, res, next) {
516
+ return function odata_batch(req, res, next) {
329
517
  if (req.headers['content-type'].includes('application/json')) {
330
- return handleJsonBatch(req, res, next)
518
+ return _processBatch(service, router, req, res, next)
331
519
  }
332
520
 
333
521
  if (req.headers['content-type'].includes('multipart/mixed')) {
334
- return express.text({ type: '*/*' })(req, res, () => {
335
- handleMultipartBatch(req, res, next)
522
+ return textBodyParser(req, res, function odata_batch_next(err) {
523
+ if (err) return next(err)
524
+ return _multipartBatch(service, router, req, res, next)
336
525
  })
337
526
  }
338
527
 
339
- throw cds.error('Batch requests must have content type multipart/mixed or application/json', { code: 400 })
528
+ throw cds.error('Batch requests must have content type multipart/mixed or application/json', { statusCode: 400 })
340
529
  }
341
530
  }
@@ -0,0 +1,33 @@
1
+ const express = require('express')
2
+
3
+ // basically express.json() with string representation of body stored in req._raw for recovery
4
+ // REVISIT: why do we need our own body parser? Only because of req._raw?
5
+ module.exports = function bodyParser4(adapter, options = {}) {
6
+ if (!options.type) options.type = 'json'
7
+ const { max_content_length } = adapter.options //> max_content_length is unofficial config
8
+ if (!options.limit && max_content_length) options.limit = max_content_length
9
+ const textParser = express.text(options)
10
+ return function http_body_parser(req, res, next) {
11
+ if (typeof req.body === 'object') {
12
+ //> body already deserialized (e.g., batch subrequest or custom body parser)
13
+ if (!req._raw) req._raw = JSON.stringify(req.body) //> ensure req._raw is set
14
+ return next()
15
+ }
16
+ textParser(req, res, function http_body_parser_next(err) {
17
+ if (err) return next(Object.assign(err, { statusCode: 400 }))
18
+ if (typeof req.body !== 'string') return next()
19
+
20
+ req._raw = req.body || '{}'
21
+ try {
22
+ req.body = JSON.parse(req._raw)
23
+ if (typeof req.body !== 'object') {
24
+ res.status(400).json({ error: { message: 'Expected JSON object in body', statusCode: 400, code: '400' } })
25
+ return
26
+ }
27
+ } catch (e) {
28
+ return next(Object.assign(e, { statusCode: 400 }))
29
+ }
30
+ next()
31
+ })
32
+ }
33
+ }