@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,11 +1,16 @@
1
1
  const cds = require('../../../')
2
- const { toODataResult, postProcess } = require('../utils/result')
2
+
3
3
  const querystring = require('node:querystring')
4
- const { getKeysAndParamsFromPath, handleSapMessages, validateIfNoneMatch, getPreferReturnHeader } = require('../utils')
5
- const { handleStreamProperties } = require('../../_runtime/common/utils/streamProp')
6
4
 
7
- const metaInfo = require('../utils/metaInfo')
5
+ const { handleSapMessages, validateIfNoneMatch, getPreferReturnHeader } = require('../utils')
6
+ const getODataMetadata = require('../utils/metadata')
7
+ const postProcess = require('../utils/postProcess')
8
+ const getODataResult = require('../utils/result')
9
+
10
+ const { getKeysAndParamsFromPath } = require('../../common/utils')
11
+
8
12
  const { getPageSize } = require('../../_runtime/common/generic/paging')
13
+ const { handleStreamProperties } = require('../../_runtime/common/utils/streamProp')
9
14
 
10
15
  const _getCount = result =>
11
16
  Array.isArray(result)
@@ -89,7 +94,7 @@ const resolveProxyExpands = ({ SELECT: { columns }, target: entity }, service) =
89
94
 
90
95
  for (const column of columns) {
91
96
  if (column.expand) {
92
- _checkExpandDeep(column, entity.elements[column.ref[0]]._target, service.namespace)
97
+ _checkExpandDeep(column, entity.elements[column.ref[0]]._target, service.definition.name)
93
98
  }
94
99
  }
95
100
  }
@@ -107,143 +112,162 @@ const _count = result => {
107
112
  : result.$count ?? result._counted_ ?? 0
108
113
  }
109
114
 
110
- // basically stolen from old read handler without understanding it ^^
111
- const _handleArrayOfQueries = (srv, req, res, next) => {
112
- const info = metaInfo(req._query, 'READ', srv, {}, req, false)
113
- const cdsReq = new cds.Request({ query: req._query, req, res })
114
- srv
115
- .dispatch(cdsReq)
116
- .then(result => {
117
- handleSapMessages(cdsReq, req, res)
118
-
119
- if (req.url.match(/\/\$count/)) return res.set('content-type', 'text/plain').send(_count(result).toString())
120
-
121
- for (let i = 0; i < result.length; i++) {
122
- // Add OData context, if it deviates from main context
123
- if (i !== 0 && info.metadata.contextUrl !== info.metadata.additionalContextUrl[i - 1])
124
- result[i].forEach(entry => (entry['@odata.context'] = info.metadata.additionalContextUrl[i - 1]))
125
- }
115
+ // REVISIT: integrate with default handler
116
+ const _handleArrayOfQueriesFactory = adapter => {
117
+ const { service } = adapter
118
+
119
+ return (req, res, next) => {
120
+ const cdsReq = adapter.request4({ query: req._query, req, res })
121
+
122
+ // NOTES:
123
+ // - only via srv.run in combination with srv.dispatch inside,
124
+ // we automatically either use a single auto-managed tx for the req (i.e., insert and read after write in same tx)
125
+ // or the auto-managed tx opened for the respective atomicity group, if exists
126
+ // - in the then block of .run(), the transaction is committed (i.e., before sending the response) if a single auto-managed tx is used
127
+ return service
128
+ .run(() => {
129
+ return service.dispatch(cdsReq).then(result => {
130
+ // nothing to do
131
+ return result
132
+ })
133
+ })
134
+ .then(result => {
135
+ handleSapMessages(cdsReq, req, res)
136
+
137
+ if (req.url.match(/\/\$count/)) return res.set('Content-Type', 'text/plain').send(_count(result).toString())
138
+
139
+ const { context: mainOdataContext } = getODataMetadata(req._query[0], {
140
+ result: result[0],
141
+ isCollection: !req._query[0].SELECT.one
142
+ })
143
+ for (let i = 0; i < result.length; i++) {
144
+ const { context: subOdataContext } = getODataMetadata(req._query[i], {
145
+ result: result[i],
146
+ isCollection: !req._query[i].SELECT.one
147
+ })
148
+ // Add OData context, if it deviates from main context
149
+ if (i !== 0 && mainOdataContext !== subOdataContext) {
150
+ result[i].forEach(entry => (entry['@odata.context'] = subOdataContext))
151
+ }
152
+ }
126
153
 
127
- res.set('content-type', 'application/json;IEEE754Compatible=true')
128
- const flatRes = result.flat(Infinity)
129
- if (cdsReq.query[0].SELECT.count) flatRes.$count = flatRes.length
130
- res.send(toODataResult(flatRes, info))
131
- })
132
- .catch(next)
154
+ result = result.flat(Infinity)
155
+ if (cdsReq.query[0].SELECT.count) result.$count = result.length
156
+
157
+ result = getODataResult(
158
+ result,
159
+ { context: mainOdataContext },
160
+ { isCollection: !req._query[0].SELECT.one, property: req._query[0]._propertyAccess }
161
+ )
162
+ res.send(result)
163
+ })
164
+ .catch(err => {
165
+ handleSapMessages(cdsReq, req, res)
166
+
167
+ next(err)
168
+ })
169
+ }
133
170
  }
134
171
 
135
- module.exports = srv =>
136
- function read(req, res, next) {
172
+ module.exports = adapter => {
173
+ const { service } = adapter
174
+
175
+ const _handleArrayOfQueries = _handleArrayOfQueriesFactory(adapter)
176
+
177
+ return function read(req, res, next) {
137
178
  if (getPreferReturnHeader(req)) {
138
179
  const msg = `The 'return' preference is not allowed in ${req.method} requests`
139
180
  throw Object.assign(new Error(msg), { statusCode: 400 })
140
181
  }
141
182
 
142
183
  // $apply with concat -> multiple queries with special handling
143
- if (Array.isArray(req._query)) return _handleArrayOfQueries(srv, req, res, next)
184
+ if (Array.isArray(req._query)) return _handleArrayOfQueries(req, res, next)
144
185
 
145
186
  // REVISIT: better solution for _propertyAccess
146
187
  let {
147
- SELECT: { from },
188
+ SELECT: { from, one },
148
189
  target,
149
190
  _propertyAccess
150
191
  } = req._query
151
192
  const { _query: query } = req
152
193
 
153
194
  // payload & params
154
- const { keys, params } = getKeysAndParamsFromPath(from, srv)
195
+ const { keys, params } = getKeysAndParamsFromPath(from, service)
155
196
  const data = keys //> for read and delete, we provide keys in req.data
156
197
 
157
198
  // cdsReq.headers should contain merged headers of envelope and subreq
158
199
  const headers = { ...cds.context.http.req.headers, ...req.headers }
159
200
 
160
201
  // we need a cds.Request for multiple reasons, incl. params, headers, sap-messages, read after write, ...
161
- const cdsReq = new cds.Request({ query, data, params, headers, req, res })
162
- Object.defineProperty(cdsReq, 'protocol', { value: 'odata-v4' })
163
-
164
- // API for subrequests of $batch (or incoming request)
165
- cdsReq.req = req
166
- cdsReq.res = res
202
+ const cdsReq = adapter.request4({ query, data, params, headers, req, res })
167
203
 
168
204
  // REVISIT: what is this for? some tests fail without it... we should find a better solution!
169
205
  Object.defineProperty(query.SELECT, '_4odata', { value: true })
170
206
 
171
- // do now to get meta info before the query is rewritten + to know return type
172
- const info = metaInfo(query, 'READ', srv, {}, req, false)
173
-
174
- // FIXME: wrong contextUrl for SiblingEntity
175
- if (info.metadata.contextUrl.match(/\/SiblingEntity\//)) {
176
- const split = info.metadata.contextUrl.split('/')
177
- const i = split.findIndex(s => s === 'SiblingEntity')
178
- split.splice(i, 1)
179
- if (split[i - 1].match(/IsActiveEntity=false/)) {
180
- split[i - 1] = split[i - 1].replace('IsActiveEntity=false', 'IsActiveEntity=true')
181
- info.metadata.contextUrl = split.join('/')
182
- } else {
183
- info.metadata.contextUrl = split.join('/').replace(/IsActiveEntity=true/g, 'IsActiveEntity=false')
184
- }
185
- }
186
-
187
- const lastPathElement = req.path.split('/').slice(-1)[0]
188
-
189
207
  if (cds.env.effective.odata.proxies && cds.env.effective.odata.xrefs) {
190
208
  // REVISIT check above is still not perfect solution
191
- resolveProxyExpands(query, srv)
209
+ resolveProxyExpands(query, service)
192
210
  }
193
211
 
194
- handleStreamProperties(target, query.SELECT.columns, srv.model)
212
+ if (!query.SELECT.columns) query.SELECT.columns = ['*']
213
+
214
+ handleStreamProperties(target, query.SELECT.columns, service.model)
195
215
 
196
216
  // REVISIT: what is this for? some tests fail without it... we should find a better solution!
197
217
  Object.defineProperty(query.SELECT, '_4odata', { value: true })
198
218
 
199
- return srv
200
- .dispatch(cdsReq)
201
- .then(result => {
202
- handleSapMessages(cdsReq, req, res)
203
-
204
- if (result == null) {
205
- if (!query.SELECT.one) {
206
- result = []
207
- if (req.query.$count) result.$count = 0
208
- } else if (_isNullableSingleton(query) || _isToOneAssoc(query)) {
209
- return res.sendStatus(204)
210
- } else {
219
+ // NOTES:
220
+ // - only via srv.run in combination with srv.dispatch inside,
221
+ // we automatically either use a single auto-managed tx for the req (i.e., insert and read after write in same tx)
222
+ // or the auto-managed tx opened for the respective atomicity group, if exists
223
+ // - in the then block of .run(), the transaction is committed (i.e., before sending the response) if a single auto-managed tx is used
224
+ return service
225
+ .run(() => {
226
+ return service.dispatch(cdsReq).then(result => {
227
+ // 404
228
+ if (result == null && query.SELECT.one && !(_isNullableSingleton(query) || _isToOneAssoc(query))) {
211
229
  throw Object.assign(new Error('404'), { statusCode: 404 })
212
230
  }
213
- }
214
-
215
- if (validateIfNoneMatch(cdsReq, req, result)) {
216
- return res.status(304).end()
217
- }
218
231
 
219
- // express always handles if-none-match header (see req.fresh)
220
- if (!cdsReq.target._etag && req.headers['if-none-match']) {
221
- delete req.headers['if-none-match']
222
- }
232
+ return result
233
+ })
234
+ })
235
+ .then(result => {
236
+ handleSapMessages(cdsReq, req, res)
223
237
 
238
+ // 204
239
+ if (result == null && query.SELECT.one) return res.sendStatus(204)
224
240
  if (_propertyAccess && result[_propertyAccess] === null) return res.sendStatus(204)
225
241
 
226
- if (lastPathElement === '$count') {
227
- result = _getCount(result)
228
- return res.set('content-type', 'text/plain').send(result.toString())
229
- }
242
+ // 304
243
+ if (validateIfNoneMatch(cdsReq.target, req.headers?.['if-none-match'], result)) return res.sendStatus(304)
230
244
 
231
- if (lastPathElement === '$value' && _propertyAccess) {
232
- return res.set('content-type', 'text/plain').send(result[_propertyAccess].toString())
245
+ if (result == null) {
246
+ result = []
247
+ if (req.query.$count) result.$count = 0
233
248
  }
234
249
 
235
- if (info.metadata.isCollection) _calculateNextLink(cdsReq, result)
236
- postProcess(cdsReq.target, srv, result)
237
- if (result['$etag']) res.set('etag', result['$etag'])
238
- result = toODataResult(result, info)
250
+ if (!one) _calculateNextLink(cdsReq, result)
251
+ postProcess(cdsReq.target, service, result)
252
+ if (result?.$etag) res.set('ETag', result.$etag) //> must be done after post processing
253
+
254
+ const lastSeg = req.path.split('/').slice(-1)[0]
255
+ if (lastSeg === '$count') return res.set('Content-Type', 'text/plain').send(_getCount(result).toString())
256
+ if (lastSeg === '$value' && _propertyAccess) {
257
+ if (cdsReq.target.elements[_propertyAccess].type === 'cds.Binary')
258
+ return res.set('Content-Type', 'application/octet-stream').send(result[_propertyAccess])
259
+ else return res.set('Content-Type', 'text/plain').send(result[_propertyAccess].toString())
260
+ }
239
261
 
240
- // Express interprets numbers as HTTP status codes
241
- const isNumber = typeof result === 'number'
242
- res.set('content-type', isNumber ? 'text/plain' : 'application/json;IEEE754Compatible=true')
243
- res.send(isNumber ? result.toString() : result)
262
+ const metadata = getODataMetadata(query, { result, isCollection: !one })
263
+ result = getODataResult(result, metadata, { isCollection: !one, property: _propertyAccess })
264
+ res.send(result)
244
265
  })
245
266
  .catch(err => {
267
+ // REVISIT: move error middleware -> applies to all these anti patterns
246
268
  handleSapMessages(cdsReq, req, res)
269
+
247
270
  next(err)
248
271
  })
249
272
  }
273
+ }
@@ -1,31 +1,29 @@
1
1
  const cds = require('../../../')
2
2
 
3
3
  const crypto = require('crypto')
4
- const metaInfo = require('../utils/metaInfo')
5
4
 
6
- const normalize_header = value => {
7
- return value.split(',').map(str => str.trim())
8
- }
5
+ const getODataMetadata = require('../utils/metadata')
6
+
7
+ const normalize_header = value => value.split(',').map(str => str.trim())
9
8
 
10
9
  const validate_etag = (header, etag) => {
11
10
  const normalized = normalize_header(header)
12
11
  return normalized.includes(etag) || normalized.includes('*') || normalized.includes('"*"')
13
12
  }
14
13
 
15
- const generateEtag = s => {
16
- return `W/"${crypto.createHash('sha256').update(s).digest('base64')}"`
17
- }
14
+ const generateEtag = s => `W/"${crypto.createHash('sha256').update(s).digest('base64')}"`
15
+
16
+ module.exports = adapter => {
17
+ const { service } = adapter
18
18
 
19
- module.exports = srv =>
20
- function service_document(req, res) {
21
- if (req.method === 'HEAD') return res.end()
22
- if (req.method !== 'GET') {
19
+ return function service_document(req, res) {
20
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
23
21
  const msg = `Method ${req.method} is not allowed for calls to the service endpoint`
24
22
  throw Object.assign(new Error(msg), { statusCode: 405 })
25
23
  }
26
24
 
27
- const m = cds.context.model || cds.model
28
- const csnService = (cds.context.model || cds.model).definitions[srv.name]
25
+ const model = cds.context.model || cds.model
26
+ const csnService = model.definitions[service.definition.name]
29
27
 
30
28
  if (req.headers['if-match']) {
31
29
  if (csnService.srvDocEtag) {
@@ -38,25 +36,29 @@ module.exports = srv =>
38
36
  if (csnService.srvDocEtag) {
39
37
  const unchanged = validate_etag(req.headers['if-none-match'], csnService.srvDocEtag)
40
38
  if (unchanged) {
41
- res.set('etag', csnService.srvDocEtag)
39
+ res.set('ETag', csnService.srvDocEtag)
42
40
  return res.status(304).end()
43
41
  }
44
42
  }
45
43
  }
46
44
 
47
- const srvEntities = m.childrenOf(srv.name)
45
+ const srvEntities = model.entities(service.definition.name)
46
+
48
47
  // REVISIT: How to identify the exposed entities? api.ignore, autoexposed, ...
49
48
  const exposedEntities = Object.keys(srvEntities).filter(
50
- e => !srvEntities[e]['@cds.api.ignore'] && e !== 'DraftAdministrativeData'
49
+ entityName => !srvEntities[entityName]['@cds.api.ignore'] && entityName !== 'DraftAdministrativeData'
51
50
  )
52
51
 
53
52
  csnService.srvDocEtag = generateEtag(JSON.stringify(exposedEntities))
54
- res.set('etag', csnService.srvDocEtag)
53
+ res.set('ETag', csnService.srvDocEtag)
55
54
 
56
- const info = metaInfo({ SELECT: { from: { ref: [srv.name] } } }, 'READ', srv, {}, req, false)
55
+ const { context: odataContext } = getODataMetadata({
56
+ SELECT: { from: { ref: [service.definition.name] } },
57
+ _target: service.definition
58
+ })
57
59
 
58
60
  return res.json({
59
- '@odata.context': info.metadata.contextUrl,
61
+ '@odata.context': odataContext,
60
62
  '@odata.metadataEtag': csnService.srvDocEtag,
61
63
  value: exposedEntities.map(e => {
62
64
  const e_ = e.replace(/\./g, '_')
@@ -64,3 +66,4 @@ module.exports = srv =>
64
66
  })
65
67
  })
66
68
  }
69
+ }
@@ -1,9 +1,14 @@
1
1
  const cds = require('../../../')
2
+ const LOG = cds.log('odata')
3
+
2
4
  const { Readable } = require('node:stream')
5
+
6
+ const { handleSapMessages, validateIfNoneMatch, isStream, isRedirect } = require('../utils')
7
+
8
+ const { getKeysAndParamsFromPath } = require('../../common/utils')
9
+
3
10
  const getError = require('../../_runtime/common/error')
4
11
  const { getTransition } = require('../../_runtime/common/utils/resolveView')
5
- const LOG = cds.log('odata')
6
- const { getKeysAndParamsFromPath, handleSapMessages, validateIfNoneMatch } = require('../utils')
7
12
 
8
13
  const _resolveContentProperty = (target, annotName, resolvedProp) => {
9
14
  if (target.elements[resolvedProp]) {
@@ -18,18 +23,6 @@ const _resolveContentProperty = (target, annotName, resolvedProp) => {
18
23
  return key?.length && key[0]
19
24
  }
20
25
 
21
- const isStream = query => {
22
- const { _propertyAccess, target } = query
23
- if (!_propertyAccess) return
24
-
25
- const element = target.elements[_propertyAccess]
26
- return element._type === 'cds.LargeBinary' && element['@Core.MediaType']
27
- }
28
-
29
- const isStreamByDollarValue = (query, previous, last) => {
30
- return query.SELECT?.one && last === '$value' && !(previous in query.target.elements)
31
- }
32
-
33
26
  const _addMetadataProperty = (query, property, annotName, odataName) => {
34
27
  if (typeof property[annotName] === 'object') {
35
28
  const contentProperty = _resolveContentProperty(
@@ -46,7 +39,7 @@ const _addMetadataProperty = (query, property, annotName, odataName) => {
46
39
  }
47
40
  }
48
41
 
49
- const addStreamMetadata = query => {
42
+ const _addStreamMetadata = query => {
50
43
  // new odata parser sets streaming property in SELECT.from
51
44
  const ref = query.SELECT.columns?.[0].ref || query.SELECT.from.ref
52
45
  const propertyName = ref.at(-1)
@@ -78,7 +71,7 @@ const addStreamMetadata = query => {
78
71
  }
79
72
  }
80
73
 
81
- const validateStream = (req, result) => {
74
+ const _validateStream = (req, result) => {
82
75
  // REVISIT: compat, should actually be treated as object
83
76
  if (!Array.isArray(result)) result = [result]
84
77
 
@@ -115,7 +108,7 @@ const _ensureStream = stream => {
115
108
  return stream_
116
109
  }
117
110
 
118
- const normalizeStream = (result, propertyName, lastPathElement, target) => {
111
+ const _normalizeStream = (result, propertyName, lastPathElement, target) => {
119
112
  if (!result) return null
120
113
 
121
114
  let readable = result
@@ -151,8 +144,8 @@ const normalizeStream = (result, propertyName, lastPathElement, target) => {
151
144
  return readable
152
145
  }
153
146
 
154
- const setStreamingHeaders = (result, res) => {
155
- // backwards compatibility for content-type in stream
147
+ const _setStreamingHeaders = (result, res) => {
148
+ // backwards compatibility for Content-Type in stream
156
149
  if (result['$mediaContentType']) res.setHeader('Content-Type', result.$mediaContentType)
157
150
  else if (result['*@odata.mediaContentType']) res.setHeader('Content-Type', result['*@odata.mediaContentType'])
158
151
  else res.setHeader('Content-Type', 'application/octet-stream')
@@ -160,22 +153,32 @@ const setStreamingHeaders = (result, res) => {
160
153
  if ('$mediaContentDispositionFilename' in result) {
161
154
  const cdt = result.$mediaContentDispositionType || 'attachment'
162
155
  res.setHeader(
163
- 'Content-Disposition',
156
+ 'content-disposition',
164
157
  `${cdt}; filename="${encodeURIComponent(result.$mediaContentDispositionFilename)}"`
165
158
  )
166
159
  }
167
160
  }
168
161
 
169
- const stream = srv =>
170
- function streamHandler(req, res, next) {
162
+ module.exports = adapter => {
163
+ const { service } = adapter
164
+
165
+ return function stream(req, res, next) {
171
166
  const { _query: query } = req
172
167
 
173
168
  // $apply with concat -> multiple queries with special handling -> read only, no stream?
174
169
  if (Array.isArray(query)) return next()
175
170
 
176
- const [previous, lastPathElement] = req.path.split('/').slice(-2)
177
- const _isStreamByDollarValue = isStreamByDollarValue(query, previous, lastPathElement)
171
+ if (isRedirect(query)) {
172
+ const cdsReq = adapter.request4({ query, req, res })
173
+ service.dispatch(cdsReq).then(result => {
174
+ if (result[query._propertyAccess]) res.set('Location', result[query._propertyAccess])
175
+ return res.sendStatus(307)
176
+ })
177
+ }
178
178
 
179
+ const [previous, lastPathElement] = req.path.split('/').slice(-2)
180
+ const _isStreamByDollarValue =
181
+ query.SELECT?.one && lastPathElement === '$value' && !(previous in query.target.elements)
179
182
  if (_isStreamByDollarValue) {
180
183
  for (const k in query.target.elements) {
181
184
  if (query.target.elements[k]['@Core.MediaType']) {
@@ -193,64 +196,59 @@ const stream = srv =>
193
196
  if (!_isStream) return next()
194
197
 
195
198
  if (!query.target['@cds.persistence.skip'] && !isMimeTypeStreamedByDefault) {
196
- addStreamMetadata(query)
199
+ _addStreamMetadata(query)
197
200
  }
198
201
 
199
202
  // we need the cds request, so we can access the modified query, which is cloned due to lean-draft, so we need to use dispatch here and pass a cds req
200
- const cdsReq = new cds.Request({ query, req, res })
201
- Object.defineProperty(cdsReq, 'protocol', { value: 'odata-v4' })
203
+ const cdsReq = adapter.request4({ query, req, res })
202
204
 
203
205
  // for read and delete, we provide keys in req.data
204
206
  // payload & params
205
- const { keys } = getKeysAndParamsFromPath(query.SELECT.from, srv)
207
+ const { keys } = getKeysAndParamsFromPath(query.SELECT.from, service)
206
208
  cdsReq.data = keys
207
209
 
208
210
  // REVISIT: what is this for? some tests fail without it... we should find a better solution!
209
211
  Object.defineProperty(query.SELECT, '_4odata', { value: true })
210
212
 
211
- return srv
212
- .tx(() => {
213
- return srv.dispatch(cdsReq).then(async result => {
214
- handleSapMessages(cdsReq, req, res)
215
- validateStream(req, result)
213
+ // NOTES:
214
+ // - only via srv.run in combination with srv.dispatch inside,
215
+ // we automatically either use a single auto-managed tx for the req (i.e., insert and read after write in same tx)
216
+ // or the auto-managed tx opened for the respective atomicity group, if exists
217
+ // - in the then block of .run(), the transaction is committed (i.e., before sending the response) if a single auto-managed tx is used
218
+ return service
219
+ .run(() => {
220
+ return service.dispatch(cdsReq).then(async result => {
221
+ _validateStream(req, result)
216
222
 
217
- if (validateIfNoneMatch(cdsReq, req, result)) {
218
- return res.status(304).end()
219
- }
223
+ if (validateIfNoneMatch(cdsReq.target, req.headers?.['if-none-match'], result)) return res.sendStatus(304)
220
224
 
221
- const stream = normalizeStream(result, query._propertyAccess, lastPathElement, query.target)
222
- if (stream === null) {
223
- res.status(204)
224
- return
225
- }
225
+ const stream = _normalizeStream(result, query._propertyAccess, lastPathElement, query.target)
226
+ if (stream === null) return res.sendStatus(204)
226
227
 
227
- if (pdfMimeType) {
228
- if (!result.$mediaContentType) result.$mediaContentType = 'application/pdf'
229
- }
228
+ if (pdfMimeType && !result.$mediaContentType) result.$mediaContentType = 'application/pdf'
230
229
 
231
- setStreamingHeaders(result, res)
230
+ _setStreamingHeaders(result, res)
232
231
 
233
232
  return new Promise((resolve, reject) => {
234
- if (res.destroyed) return reject(new Error('Response is closed while streaming'))
233
+ if (res.destroyed) return reject(new Error('Response was closed while streaming'))
235
234
  stream.pipe(res)
236
235
  stream.on('end', () => resolve(result))
237
236
  stream.once('error', reject)
238
237
  let finished = false
239
- res.on('finish', () => {
240
- finished = true
241
- })
242
- res.on('close', () => !finished && reject(new Error('Response is closed while streaming')))
238
+ res.on('finish', () => (finished = true))
239
+ res.on('close', () => !finished && reject(new Error('Response was closed while streaming')))
243
240
  })
244
241
  })
245
242
  })
246
243
  .then(() => {
247
- // we use an extra then block, after getting the result, so the transaction is commited, before sending the response
244
+ handleSapMessages(cdsReq, req, res)
245
+
248
246
  res.end()
249
247
  })
250
- .catch(next) // catch outside of transaction, so tx is rolled back automatically in case of error
251
- }
248
+ .catch(err => {
249
+ handleSapMessages(cdsReq, req, res)
252
250
 
253
- module.exports = {
254
- stream,
255
- isStream
251
+ next(err)
252
+ })
253
+ }
256
254
  }