@sap/cds 7.9.4 → 8.0.4

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