@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,29 +1,27 @@
1
1
  const cds = require('../../../')
2
2
  const { INSERT } = cds.ql
3
3
 
4
- const { toODataResult, postProcess } = require('../utils/result')
5
- const {
6
- calculateLocationHeader,
7
- getKeysAndParamsFromPath,
8
- handleSapMessages,
9
- getPreferReturnHeader
10
- } = require('../utils')
11
- const { getDeepSelect, getSimpleSelectCQN } = require('../utils/handler')
12
-
13
- const { deepCopy } = require('../../_runtime/common/utils/copy')
14
-
15
- const readAfterWrite = require('../utils/readAfterWrite')
16
- const metaInfo = require('../utils/metaInfo')
17
-
18
- module.exports = srv =>
19
- function create(req, res, next) {
4
+ const { calculateLocationHeader, handleSapMessages, getPreferReturnHeader } = require('../utils')
5
+ const getODataMetadata = require('../utils/metadata')
6
+ const postProcess = require('../utils/postProcess')
7
+ const readAfterWrite4 = require('../utils/readAfterWrite')
8
+ const getODataResult = require('../utils/result')
9
+
10
+ const { getKeysAndParamsFromPath } = require('../../common/utils')
11
+
12
+ module.exports = (adapter, isUpsert) => {
13
+ // REVISIT: adapter should be this
14
+ const { service } = adapter
15
+ const _readAfterWrite = readAfterWrite4(adapter, 'create')
16
+
17
+ return function create(req, res, next) {
20
18
  const {
21
19
  SELECT: { one, from },
22
20
  target
23
21
  } = req._query
24
22
 
25
23
  // req.__proto__.method is set in case of upsert
26
- const isUpsert = req.__proto__.method in { PUT: 1, PATCH: 1 }
24
+ // const isUpsert = req.__proto__.method in { PUT: 1, PATCH: 1 }
27
25
 
28
26
  if (one && !isUpsert) {
29
27
  const msg = 'Method POST is not allowed for singletons and individual entities'
@@ -31,18 +29,12 @@ module.exports = srv =>
31
29
  }
32
30
 
33
31
  // payload & params
34
- const data = deepCopy(req.body)
35
- const { keys, params } = getKeysAndParamsFromPath(from, srv)
32
+ const data = req.body
33
+ const { keys, params } = getKeysAndParamsFromPath(from, service)
36
34
  // add keys from url into payload (overwriting if already present)
37
35
  Object.assign(data, keys)
38
36
 
39
- // assert payload
40
- const assertOptions = { filter: true, http: { req }, mandatories: true }
41
- const errs = cds.assert(data, target, assertOptions)
42
- if (errs) {
43
- if (errs.length === 1) throw errs[0]
44
- throw Object.assign(new Error('MULTIPLE_ERRORS'), { statusCode: 400, details: errs })
45
- }
37
+ const _isDraft = target.drafts && data.IsActiveEntity !== true
46
38
 
47
39
  // query
48
40
  const query = INSERT.into(from).entries(data)
@@ -51,56 +43,49 @@ module.exports = srv =>
51
43
  const headers = { ...cds.context.http.req.headers, ...req.headers }
52
44
 
53
45
  // we need a cds.Request for multiple reasons, incl. params, headers, sap-messages, read after write, ...
54
- const cdsReq = new cds.Request({ query, params, headers, req, res })
55
- Object.defineProperty(cdsReq, 'protocol', { value: 'odata-v4' })
56
-
57
- // API for subrequests of $batch (or incoming request)
58
- cdsReq.req = req
59
- cdsReq.res = res
46
+ const cdsReq = adapter.request4({ query, params, headers, req, res })
60
47
 
61
48
  // rewrite event for draft-enabled entities
62
- if (target._isDraftEnabled) cdsReq.event = 'NEW'
63
-
64
- // REVISIT: only via srv.run in combination with srv.dispatch inside
65
- // we automatically either use a single auto-managed tx for the req (i.e., insert and read after write in same tx)
66
- // or the auto-managed tx opened for the respective atomicity group, if exists
67
- return srv
49
+ if (_isDraft) cdsReq.event = 'NEW'
50
+
51
+ // NOTES:
52
+ // - only via srv.run in combination with srv.dispatch inside,
53
+ // we automatically either use a single auto-managed tx for the req (i.e., insert and read after write in same tx)
54
+ // or the auto-managed tx opened for the respective atomicity group, if exists
55
+ // - in the then block of .run(), the transaction is committed (i.e., before sending the response) if a single auto-managed tx is used
56
+ return service
68
57
  .run(() => {
69
- return srv.dispatch(cdsReq).then(result => {
70
- handleSapMessages(cdsReq, req, res)
71
-
58
+ return service.dispatch(cdsReq).then(result => {
59
+ // REVISIT: shouldn't read after write be the default behavior (that could be suppressed)?
72
60
  // generic handlers indicate that read after write is required
73
- if (cdsReq._.readAfterWrite) {
74
- // const keys = cdsReq.target.keys?.filter(k => !k.isAssociation)?.reduce((prev, k) => { prev[k] = result[k]; return prev}, {} )
75
- // const query = SELECT.one(cdsReq.query.INSERT.into, keys)
76
- const query = cdsReq.event === 'NEW' ? getSimpleSelectCQN(cdsReq.target, result) : getDeepSelect(cdsReq)
77
- return readAfterWrite(cdsReq, srv, query)
78
- }
79
-
61
+ if (cdsReq._.readAfterWrite) return _readAfterWrite(cdsReq)
80
62
  return result
81
63
  })
82
64
  })
83
65
  .then(result => {
84
- // we use an extra then block, after getting the result, so the transaction is commited, before sending the response
85
-
86
- // determine calculation based on result with req.data as fallback
87
- if (!target._isSingleton)
88
- res.set('location', calculateLocationHeader(cdsReq.target, srv, result || cdsReq.data))
66
+ handleSapMessages(cdsReq, req, res)
89
67
 
68
+ // case: read after write returns no results, e.g., due to auth (academic but possible)
90
69
  if (result == null) return res.sendStatus(204)
91
- const isMinimal = getPreferReturnHeader(req) === 'minimal'
92
- postProcess(cdsReq.target, srv, result, isMinimal)
93
70
 
94
- if (result['$etag']) res.set('etag', result['$etag'])
71
+ if (!target._isSingleton) {
72
+ // determine calculation based on result with req.data as fallback
73
+ res.set('location', calculateLocationHeader(cdsReq.target, service, result || cdsReq.data))
74
+ }
75
+
76
+ const isMinimal = getPreferReturnHeader(req) === 'minimal'
77
+ postProcess(cdsReq.target, service, result, isMinimal)
78
+ if (result?.$etag) res.set('ETag', result.$etag) //> must be done after post processing
95
79
  if (isMinimal) return res.sendStatus(204)
96
80
 
97
- const info = metaInfo(query, 'CREATE', srv, result, req)
98
- result = toODataResult(result, info)
99
- res.set('content-type', 'application/json;IEEE754Compatible=true')
81
+ const metadata = getODataMetadata(query, { result })
82
+ result = getODataResult(result, metadata)
100
83
  res.status(201).send(result)
101
84
  })
102
85
  .catch(err => {
103
86
  handleSapMessages(cdsReq, req, res)
87
+
104
88
  next(err)
105
89
  })
106
90
  }
91
+ }
@@ -1,10 +1,14 @@
1
1
  const cds = require('../../../')
2
2
  const { UPDATE, DELETE } = cds.ql
3
3
 
4
- const { getKeysAndParamsFromPath, handleSapMessages, getPreferReturnHeader } = require('../utils')
4
+ const { handleSapMessages, getPreferReturnHeader } = require('../utils')
5
5
 
6
- module.exports = srv =>
7
- function deleete(req, res, next) {
6
+ const { getKeysAndParamsFromPath } = require('../../common/utils')
7
+
8
+ module.exports = adapter => {
9
+ const { service } = adapter
10
+
11
+ return function deleet(req, res, next) {
8
12
  if (getPreferReturnHeader(req)) {
9
13
  const msg = "The 'return' preference is not allowed in DELETE requests"
10
14
  throw Object.assign(new Error(msg), { statusCode: 400 })
@@ -22,7 +26,7 @@ module.exports = srv =>
22
26
  }
23
27
 
24
28
  // payload & params
25
- const { keys, params } = getKeysAndParamsFromPath(from, srv)
29
+ const { keys, params } = getKeysAndParamsFromPath(from, service)
26
30
  const data = keys //> for read and delete, we provide keys in req.data
27
31
  if (_propertyAccess) data[_propertyAccess] = null //> delete of property -> set to null
28
32
 
@@ -33,18 +37,23 @@ module.exports = srv =>
33
37
  const headers = { ...cds.context.http.req.headers, ...req.headers }
34
38
 
35
39
  // we need a cds.Request for multiple reasons, incl. params, headers, sap-messages, read after write, ...
36
- const cdsReq = new cds.Request({ query, data, headers, params, req, res })
37
- Object.defineProperty(cdsReq, 'protocol', { value: 'odata-v4' })
38
-
39
- // API for subrequests of $batch (or incoming request)
40
- cdsReq.req = req
41
- cdsReq.res = res
40
+ const cdsReq = adapter.request4({ query, data, headers, params, req, res })
42
41
 
43
42
  // rewrite event for draft-enabled entities
44
43
  if (target._isDraftEnabled && cdsReq.data.IsActiveEntity === false) cdsReq.event = 'CANCEL'
45
44
 
46
- return srv
47
- .dispatch(cdsReq)
45
+ // NOTES:
46
+ // - only via srv.run in combination with srv.dispatch inside,
47
+ // we automatically either use a single auto-managed tx for the req (i.e., insert and read after write in same tx)
48
+ // or the auto-managed tx opened for the respective atomicity group, if exists
49
+ // - in the then block of .run(), the transaction is committed (i.e., before sending the response) if a single auto-managed tx is used
50
+ return service
51
+ .run(() => {
52
+ return service.dispatch(cdsReq).then(result => {
53
+ // nothing to do
54
+ return result
55
+ })
56
+ })
48
57
  .then(() => {
49
58
  handleSapMessages(cdsReq, req, res)
50
59
 
@@ -52,6 +61,8 @@ module.exports = srv =>
52
61
  })
53
62
  .catch(err => {
54
63
  handleSapMessages(cdsReq, req, res)
64
+
55
65
  next(err)
56
66
  })
57
67
  }
68
+ }
@@ -1,9 +1,33 @@
1
- const { normalizeError } = require('../../_runtime/common/error/frontend')
1
+ const cds = require('../../../lib')
2
2
 
3
- module.exports = _srv =>
4
- function error(err, req, res, _next) {
5
- const { error, statusCode } = normalizeError(err, req)
3
+ const _log = require('../../_runtime/common/error/log')
6
4
 
7
- // NOTE: normalizeError already does sanatization -> we can use as is
8
- res.status(statusCode).json({ error })
5
+ const { normalizeError, unwrapMultipleErrors } = require('../../_runtime/common/error/frontend')
6
+
7
+ module.exports = () => {
8
+ return function odata_error(err, req, res, next) {
9
+ if (err == 401 || err.code == 401) return next(err) // speed up logins, at least temporary until we reviewed and eliminated overhead that may be involved below
10
+ // REVISIT: keep?
11
+ // log the error (4xx -> warn)
12
+ _log(err)
13
+
14
+ const { error, statusCode } = normalizeError(err, req) // REVISIT: that's killing error objects and stacks -> get rid of it
15
+
16
+ const content_id = req.headers['content-id']
17
+ if (content_id) {
18
+ error['@Core.ContentID'] = content_id
19
+ error.details?.forEach(e => (e['@Core.ContentID'] = content_id))
20
+ }
21
+
22
+ if (error.details && cds.env.fiori.wrap_multiple_errors === false) {
23
+ unwrapMultipleErrors(error)
24
+ }
25
+
26
+ // NOTE: express also looks for numbers in err.status or err.statusCode
27
+ error.statusCode = statusCode
28
+ // if (err.stack) Object.defineProperty (error, 'stack', { value: err.stack })
29
+ // REVISIT: we can't do that ^^^^^ currently because the _http_common parsers we use for $batch requests can't handle that
30
+
31
+ next(error)
9
32
  }
33
+ }
@@ -6,6 +6,7 @@ const crypto = require('crypto')
6
6
  const _requestedFormat = (queryOption, header) => {
7
7
  if (queryOption) return queryOption.match(/json/i) ? 'json' : 'xml'
8
8
  if (header) {
9
+ if (header.indexOf('*/*') > -1) return 'xml' //> default to xml for backward compatibility
9
10
  const jsonIndex = header.indexOf('application/json')
10
11
  if (jsonIndex === -1) return 'xml'
11
12
  const xmlIndex = header.indexOf('application/xml')
@@ -17,12 +18,12 @@ const _requestedFormat = (queryOption, header) => {
17
18
 
18
19
  const _metadataFromFile = async srv => {
19
20
  const fs = require('fs')
20
- const filePath = cds.root + `/srv/odata/v4/${srv.name}.xml`
21
+ const filePath = cds.root + `/srv/odata/v4/${srv.definition.name}.xml`
21
22
  let exists
22
23
  try {
23
24
  exists = !(await fs.promises.access(filePath, fs.constants.F_OK))
24
- } catch (e) {
25
- LOG._debug && LOG.debug(`No metadata file found for service ${srv.name} at ${filePath}`)
25
+ } catch {
26
+ LOG._debug && LOG.debug(`No metadata file found for service ${srv.definition.name} at ${filePath}`)
26
27
  }
27
28
  if (exists) {
28
29
  const file = await fs.promises.readFile(filePath)
@@ -49,19 +50,20 @@ const mpSupportsEmptyLocale = () => {
49
50
  return major > 1 || (major === 1 && minor >= 12)
50
51
  }
51
52
 
52
- module.exports = srv =>
53
- async function metadata(req, res, _next) {
53
+ module.exports = adapter => {
54
+ const { service } = adapter
55
+
56
+ return async function metadata(req, res, next) {
54
57
  if (req.method !== 'GET') {
55
58
  const msg = `Method ${req.method} is not allowed for calls to the metadata endpoint`
56
- throw Object.assign(new Error(msg), { statusCode: 405 })
59
+ return next(Object.assign(new Error(msg), { statusCode: 405 }))
57
60
  }
58
61
 
59
- const tenant = cds.context.tenant
60
- const locale = cds.context.locale
61
62
  const format = _requestedFormat(req.query['$format'], req.headers['accept'])
63
+ const locale = cds.context.locale
62
64
 
63
65
  // REVISIT: edm(x) and etag cache is only evicted with model
64
- const csnService = (cds.context.model || cds.model).definitions[srv.name]
66
+ const csnService = (cds.context.model || cds.model).definitions[service.definition.name]
65
67
  const metadataCache = (csnService.metadataCache = csnService.metadataCache || { jsonEtag: {}, xmlEtag: {} }) // REVISIT: yet another uncontrolled cache?
66
68
 
67
69
  const etag = format === 'json' ? metadataCache.jsonEtag?.[locale] : metadataCache.xmlEtag?.[locale]
@@ -77,8 +79,8 @@ module.exports = srv =>
77
79
  if (etag) {
78
80
  const unchanged = validate_etag(req.headers['if-none-match'], etag)
79
81
  if (unchanged) {
80
- res.set('etag', etag)
81
- return res.status(304).end()
82
+ res.set('ETag', etag)
83
+ return res.sendStatus(304)
82
84
  }
83
85
  }
84
86
  }
@@ -86,28 +88,35 @@ module.exports = srv =>
86
88
  const { 'cds.xt.ModelProviderService': mps } = cds.services
87
89
  if (mps) {
88
90
  if (format === 'json') {
89
- LOG._warn && LOG.warn('JSON metadata is not supported in case of cds.requires.extensibilty: true')
91
+ LOG._warn && LOG.warn('JSON metadata is not supported in case of cds.requires.extensibility: true')
90
92
  const msg = 'JSON metadata is not supported for this service'
91
- throw Object.assign(new Error(msg), { statusCode: 501 })
93
+ return next(Object.assign(new Error(msg), { statusCode: 501 }))
92
94
  }
93
95
 
96
+ const { tenant, features } = cds.context
97
+
94
98
  try {
95
99
  let edmx
96
100
  // REVISIT: remove check later
97
101
  if (mpSupportsEmptyLocale()) {
98
102
  // If no extensibility nor fts, do not provide model to mtxs
99
- const modelNeeded = cds.env.requires.extensibility || cds.context.features?.given
103
+ const modelNeeded = cds.env.requires.extensibility || features?.given
100
104
  edmx =
101
105
  metadataCache.edm ||
102
- (await mps.getEdmx({ tenant, model: modelNeeded && srv.model, service: srv.definition.name }))
106
+ (await mps.getEdmx({
107
+ tenant,
108
+ model: modelNeeded ? await mps.getCsn(tenant, features) : undefined,
109
+ service: service.definition.name
110
+ }))
103
111
  metadataCache.edm = edmx
104
112
  const extBundle = cds.env.requires.extensibility && (await mps.getI18n({ tenant, locale }))
105
- edmx = cds.localize(srv.model, locale, edmx, extBundle)
113
+ edmx = cds.localize(service.model, locale, edmx, extBundle)
106
114
  } else {
107
- edmx = await mps.getEdmx({ tenant, model: srv.model, service: srv.definition.name, locale })
115
+ edmx = await mps.getEdmx({ tenant, model: service.model, service: service.definition.name, locale })
108
116
  }
109
117
  metadataCache.xmlEtag[locale] = generateEtag(edmx)
110
- res.set('content-type', 'application/xml')
118
+ res.set('ETag', metadataCache.xmlEtag[locale])
119
+ res.set('Content-Type', 'application/xml')
111
120
  res.send(edmx)
112
121
  return
113
122
  } catch (e) {
@@ -115,25 +124,28 @@ module.exports = srv =>
115
124
  e.message = 'Unable to get EDMX for tenant ' + tenant + ' due to error: ' + e.message
116
125
  LOG.error(e)
117
126
  }
118
- throw Object.assign(new Error('503'), { statusCode: 503 })
127
+ return next(Object.assign(new Error('503'), { statusCode: 503 }))
119
128
  }
120
129
  }
121
130
 
122
131
  if (format === 'json') {
123
132
  const edm =
124
- metadataCache.edm || (metadataCache.edm = cds.compile.to.edm(srv.model, { service: srv.definition.name }))
125
- const localized = cds.localize(srv.model, locale, edm)
133
+ metadataCache.edm ||
134
+ (metadataCache.edm = cds.compile.to.edm(service.model, { service: service.definition.name }))
135
+ const localized = cds.localize(service.model, locale, edm)
126
136
  metadataCache.jsonEtag[locale] = generateEtag(localized)
137
+ res.set('ETag', metadataCache.jsonEtag[locale])
127
138
  return res.json(JSON.parse(localized))
128
139
  }
129
140
 
130
141
  const edmx =
131
142
  metadataCache.edmx ||
132
- (await _metadataFromFile(srv)) ||
133
- (metadataCache.edmx = cds.compile.to.edmx(srv.model, { service: srv.definition.name }))
134
- const localized = cds.localize(srv.model, locale, edmx)
143
+ (await _metadataFromFile(service)) ||
144
+ (metadataCache.edmx = cds.compile.to.edmx(service.model, { service: service.definition.name }))
145
+ const localized = cds.localize(service.model, locale, edmx)
135
146
  metadataCache.xmlEtag[locale] = generateEtag(localized)
136
- res.set('etag', metadataCache.xmlEtag[locale])
137
- res.set('content-type', 'application/xml')
147
+ res.set('ETag', metadataCache.xmlEtag[locale])
148
+ res.set('Content-Type', 'application/xml')
138
149
  return res.send(localized)
139
150
  }
151
+ }
@@ -1,48 +1,82 @@
1
1
  const cds = require('../../../')
2
2
 
3
- const { toODataResult, postProcess } = require('../utils/result')
4
- const { cds2edm, getKeysAndParamsFromPath, handleSapMessages } = require('../utils')
5
-
6
- const { deepCopy } = require('../../_runtime/common/utils/copy')
3
+ const { cds2edm, handleSapMessages, calculateLocationHeader } = require('../utils')
4
+ const getODataMetadata = require('../utils/metadata')
5
+ const postProcess = require('../utils/postProcess')
6
+ const getODataResult = require('../utils/result')
7
+
8
+ const { getKeysAndParamsFromPath } = require('../../common/utils')
9
+
10
+ const _findEdmNameFor = (definition, namespace, fullyQualified = false) => {
11
+ let name
12
+ if (!definition) return ''
13
+ if (definition._isStructured) {
14
+ const structured = [definition.name]
15
+ while (definition.parent) {
16
+ definition = definition.parent
17
+ structured.unshift(definition.name)
18
+ }
19
+ name = structured.join('_')
20
+ } else {
21
+ name = definition.name
22
+ }
23
+ if (!name.startsWith(`${namespace}.`)) return name
24
+ return fullyQualified ? name : name.replace(new RegExp(`^${namespace}\\.`), '')
25
+ }
26
+
27
+ const _opResultName = ({ service, returnType, operation }) => {
28
+ const { definition: { name: namespace } } = service // prettier-ignore
29
+ if (returnType.name) {
30
+ const resultName = _findEdmNameFor(returnType, namespace)
31
+ if (returnType.name.startsWith(`${namespace}.`)) {
32
+ return `${namespace}.${resultName.replace(/\./g, '_')}`
33
+ }
34
+ return resultName
35
+ }
36
+ // bound action / function returns inline structure
37
+ if (operation.parent) {
38
+ const boundEntityName = _findEdmNameFor(operation.parent, namespace, true).replace(/\./g, '_')
39
+ // REVISIT exactly this return type name is generated in edm by compiler
40
+ return `${namespace}.return_${boundEntityName}_${_findEdmNameFor(operation, namespace)}`
41
+ }
42
+ // unbound action / function returns inline structure
43
+ // REVISIT exactly this return type name is generated in edm by compiler
44
+ return `${namespace}.return_${_findEdmNameFor(operation, namespace, true).replace(/\./g, '_')}`
45
+ }
7
46
 
8
- const metaInfo = require('../utils/metaInfo')
47
+ module.exports = adapter => {
48
+ const { service } = adapter
9
49
 
10
- module.exports = srv =>
11
- function operation(req, res, next) {
50
+ return function odata_operation(req, res, next) {
12
51
  let { operation, args } = req._query.SELECT?.from.ref?.slice(-1)[0] || {}
13
52
  if (!operation) return next() //> create or read
14
53
 
54
+ // REVISIT: should not be necessary
55
+ const _originalQuery = JSON.parse(JSON.stringify(req._query))
56
+
15
57
  // unbound vs. bound
16
- let entity, /* keys, */ params
17
- if (srv.model.definitions[operation]) {
18
- operation = srv.model.definitions[operation]
58
+ let entity, params
59
+ if (service.model.definitions[operation]) {
60
+ operation = service.model.definitions[operation]
19
61
  } else {
20
62
  req._query.SELECT.from.ref.pop()
21
- let cur = { elements: srv.model.definitions }
63
+ let cur = { elements: service.model.definitions }
22
64
  for (const each of req._query.SELECT.from.ref) {
23
65
  cur = cur.elements[each.id || each]
24
66
  if (cur._target) cur = cur._target
25
67
  }
26
68
  operation = cur.actions[operation]
27
69
  entity = cur
28
- const keysAndParams = getKeysAndParamsFromPath(req._query.SELECT.from, srv)
70
+ const keysAndParams = getKeysAndParamsFromPath(req._query.SELECT.from, service)
29
71
  params = keysAndParams.params
30
72
  }
31
73
 
32
74
  // payload & params
33
- const data = args || deepCopy(req.body)
34
-
35
- // assert payload
36
- const assertOptions = { filter: true, http: { req }, mandatories: true }
37
- const errs = cds.assert(data, operation, assertOptions)
38
- if (errs) {
39
- if (errs.length === 1) throw errs[0]
40
- throw Object.assign(new Error('MULTIPLE_ERRORS'), { statusCode: 400, details: errs })
41
- }
75
+ const data = args || req.body
42
76
 
43
77
  // event
44
78
  // REVISIT: when is operation.name actually prefixed with the service name?
45
- const event = operation.name.replace(`${srv.name}.`, '')
79
+ const event = operation.name.replace(`${service.definition.name}.`, '')
46
80
 
47
81
  const query = entity ? req._query : undefined
48
82
 
@@ -50,63 +84,52 @@ module.exports = srv =>
50
84
  const headers = { ...cds.context.http.req.headers, ...req.headers }
51
85
 
52
86
  // we need a cds.Request for multiple reasons, incl. params, headers, sap-messages, read after write, ...
53
- const cdsReq = new cds.Request({ query, event, data, params, headers, target: query?.target, req, res })
54
- Object.defineProperty(cdsReq, 'protocol', { value: 'odata-v4' })
55
-
56
- // API for subrequests of $batch (or incoming request)
57
- cdsReq.req = req
58
- cdsReq.res = res
59
-
60
- // REVISIT: only via srv.run in combination with srv.dispatch inside
61
- // we automatically either use a single auto-managed tx for the req (i.e., insert and read after write in same tx)
62
- // or the auto-managed tx opened for the respective atomicity group, if exists
63
- return srv
64
- .run(() => {
65
- return srv.dispatch(cdsReq).then(result => {
66
- handleSapMessages(cdsReq, req, res)
67
- return result
68
- })
69
- })
87
+ const cdsReq = adapter.request4({ query, event, data, params, headers, target: query?.target, req, res })
88
+
89
+ // NOTES:
90
+ // - only via srv.run in combination with srv.dispatch inside,
91
+ // we automatically either use a single auto-managed tx for the req (i.e., insert and read after write in same tx)
92
+ // or the auto-managed tx opened for the respective atomicity group, if exists
93
+ // - in the then block of .run(), the transaction is committed (i.e., before sending the response) if a single auto-managed tx is used
94
+ return service
95
+ .run(() => service.dispatch(cdsReq))
70
96
  .then(result => {
71
- // we use an extra then block, after getting the result, so the transaction is commited, before sending the response
72
-
73
- if (!operation.returns || result == null) return res.status(204).end()
97
+ handleSapMessages(cdsReq, req, res)
74
98
 
75
- if (operation.returns._type?.match?.(/^cds\./)) {
76
- res.set('content-type', 'application/json;IEEE754Compatible=true')
77
- // TODO: check result type
78
- return res.send({
79
- '@odata.context': `${'../'.repeat(query?.SELECT?.from?.ref?.length)}$metadata#${cds2edm[operation.returns._type]}`,
80
- value: result
81
- })
82
- }
99
+ if (operation.returns?.items && result == null) result = []
83
100
 
84
- const info = metaInfo(req._query, event, srv, result, req)
101
+ if (!operation.returns || result == null) return res.sendStatus(204)
85
102
 
86
- // FIXME: info.metadata.isCollection is incorrect
87
- if (!operation.returns.items) info.metadata.isCollection = false
103
+ if (operation.returns._type?.match?.(/^cds\./)) {
104
+ const context = `${'../'.repeat(query?.SELECT?.from?.ref?.length)}$metadata#${cds2edm[operation.returns._type]}`
88
105
 
89
- // REVISIT impl of context url generation
90
- if (
91
- !info.metadata.isCollection &&
92
- info.metadata.isServiceEntity &&
93
- !info.metadata.contextUrl.endsWith('$entity')
94
- ) {
95
- info.metadata.contextUrl += '/$entity'
106
+ result = { '@odata.context': context, value: result }
107
+ return res.send(result)
96
108
  }
97
109
 
98
- if (info.metadata.returnType) {
99
- postProcess(info.metadata.returnType, srv, result)
100
- if (result['$etag']) res.set('etag', result['$etag'])
110
+ if (res.statusCode === 201 && !res.hasHeader('location')) {
111
+ const locationHeader = calculateLocationHeader(operation.returns, service, result)
112
+ if (locationHeader) res.set('location', locationHeader)
101
113
  }
102
114
 
103
- result = toODataResult(result, info)
104
-
105
- // FIXME: toODataResult() doesn't seem to handle this case
106
- if (entity && !result['@odata.context'].match(/^\.\.\//))
107
- result['@odata.context'] = '../' + result['@odata.context']
115
+ if (operation.returns) {
116
+ postProcess(operation.returns, service, result)
117
+ if (result?.$etag) res.set('ETag', result.$etag) //> must be done after post processing
118
+ }
108
119
 
109
- res.set('content-type', 'application/json;IEEE754Compatible=true')
120
+ // REVISIT: enterprise search result? -> simply return what was provided
121
+ if (operation.returns.type !== 'sap.esh.SearchResult') {
122
+ const isCollection = !!operation.returns.items
123
+ const _target = operation.returns.items ?? operation.returns
124
+ // REVISIT: when is edmName needed?
125
+ const edmName = _opResultName({ service, operation, returnType: _target })
126
+ const metadata = getODataMetadata(
127
+ { SELECT: { from: _originalQuery?.SELECT?.from, one: !isCollection }, _target },
128
+
129
+ { result, isCollection, edmName }
130
+ )
131
+ result = getODataResult(result, metadata, { isCollection })
132
+ }
110
133
  res.send(result)
111
134
  })
112
135
  .catch(err => {
@@ -114,3 +137,4 @@ module.exports = srv =>
114
137
  next(err)
115
138
  })
116
139
  }
140
+ }
@@ -1,18 +1,16 @@
1
1
  const cds = require('../../../')
2
2
 
3
- module.exports = srv =>
4
- function parse(req, _, next) {
3
+ module.exports = adapter => {
4
+ const { service } = adapter
5
+
6
+ return function odata_parse_url(req, _, next) {
5
7
  // REVISIT: can't we register the batch handler before the parse handler to avoid this?
6
8
  if (req.path.startsWith('/$batch')) return next()
7
9
 
8
10
  if (req._query) return next() //> already parsed (e.g., upsert)
9
11
 
10
- // if not a GET, use req.path instead of req.url to ignore query parameters
11
- req._query = cds.odata.parse(req.method === 'GET' ? req.url : req.path, {
12
- service: srv,
13
- baseUrl: req.baseUrl,
14
- strict: true
15
- })
12
+ req._query = cds.odata.parse(req.url, { service, baseUrl: req.baseUrl, strict: true })
16
13
 
17
14
  next()
18
15
  }
16
+ }