@sap/cds 5.7.5 → 5.8.2

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 (151) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/app/fiori/routes.js +1 -1
  3. package/bin/deploy/to-hana/cfUtil.js +251 -138
  4. package/bin/deploy/to-hana/gitUtil.js +55 -0
  5. package/bin/deploy/to-hana/hana.js +92 -93
  6. package/bin/deploy/to-hana/hdiDeployUtil.js +42 -27
  7. package/bin/deploy/to-hana/index.js +14 -13
  8. package/bin/mtx/in-cds.js +1 -0
  9. package/bin/serve.js +1 -1
  10. package/bin/version.js +1 -0
  11. package/lib/compile/cdsc.js +0 -6
  12. package/lib/compile/resolve.js +1 -1
  13. package/lib/compile/to/srvinfo.js +1 -1
  14. package/lib/core/classes.js +21 -1
  15. package/lib/env/index.js +3 -2
  16. package/lib/env/requires.js +4 -0
  17. package/lib/i18n/localize.js +5 -8
  18. package/lib/index.js +1 -0
  19. package/lib/log/errors.js +1 -1
  20. package/lib/log/format/kibana.js +3 -3
  21. package/lib/ql/SELECT.js +2 -2
  22. package/lib/req/cds-context.js +1 -1
  23. package/lib/req/context.js +1 -1
  24. package/lib/serve/Transaction.js +9 -5
  25. package/lib/serve/index.js +13 -21
  26. package/lib/utils/tests.js +90 -66
  27. package/libx/_runtime/audit/generic/personal/modification.js +0 -8
  28. package/libx/_runtime/auth/index.js +7 -6
  29. package/libx/_runtime/auth/strategies/dwc.js +43 -0
  30. package/libx/_runtime/auth/utils.js +24 -0
  31. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -3
  32. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +11 -32
  33. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +12 -5
  34. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +7 -4
  35. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +24 -3
  36. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +43 -38
  37. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +1 -1
  38. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +11 -5
  39. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/boundToCQN.js +1 -2
  40. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/deleteToCQN.js +0 -1
  41. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -2
  42. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/orderByToCQN.js +9 -0
  43. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +17 -30
  44. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +12 -1
  45. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/edm/AbstractEdmStructuredType.js +2 -1
  46. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/ResourcePathParser.js +23 -2
  47. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriHelper.js +7 -6
  48. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriTokenizer.js +2 -5
  49. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +19 -47
  50. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +4 -11
  51. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/ResourceJsonDeserializer.js +7 -1
  52. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/CommandExecutor.js +0 -3
  53. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/ConditionalRequestControlCommand.js +0 -1
  54. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ContextURLFactory.js +2 -2
  55. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ErrorJsonSerializer.js +2 -0
  56. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/TrustedResourceJsonSerializer.js +2 -5
  57. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +6 -6
  58. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
  59. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -4
  60. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +41 -17
  61. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +1 -17
  62. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +60 -18
  63. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +47 -10
  64. package/libx/_runtime/cds-services/adapter/rest/Rest.js +22 -1
  65. package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +8 -3
  66. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +3 -0
  67. package/libx/_runtime/cds-services/services/utils/columns.js +5 -1
  68. package/libx/_runtime/cds-services/services/utils/compareJson.js +15 -16
  69. package/libx/_runtime/cds-services/services/utils/differ.js +2 -8
  70. package/libx/_runtime/common/aspects/Association.js +16 -0
  71. package/libx/_runtime/common/composition/data.js +28 -37
  72. package/libx/_runtime/common/composition/delete.js +107 -58
  73. package/libx/_runtime/common/composition/index.js +3 -3
  74. package/libx/_runtime/common/composition/insert.js +14 -27
  75. package/libx/_runtime/common/composition/tree.js +1 -1
  76. package/libx/_runtime/common/composition/update.js +39 -34
  77. package/libx/_runtime/common/error/frontend.js +19 -5
  78. package/libx/_runtime/common/generic/auth.js +20 -85
  79. package/libx/_runtime/common/generic/crud.js +22 -1
  80. package/libx/_runtime/common/i18n/messages.properties +2 -1
  81. package/libx/_runtime/common/utils/cqn.js +2 -6
  82. package/libx/_runtime/common/utils/cqn2cqn4sql.js +95 -122
  83. package/libx/_runtime/common/utils/csn.js +29 -6
  84. package/libx/_runtime/common/utils/foreignKeyPropagations.js +21 -1
  85. package/libx/_runtime/common/utils/keys.js +2 -1
  86. package/libx/_runtime/common/utils/path.js +1 -1
  87. package/libx/_runtime/common/utils/resolveView.js +12 -4
  88. package/libx/_runtime/common/utils/rewriteAsterisks.js +27 -13
  89. package/libx/_runtime/common/utils/search2cqn4sql.js +11 -6
  90. package/libx/_runtime/common/utils/structured.js +10 -4
  91. package/libx/_runtime/common/utils/vcap.js +27 -10
  92. package/libx/_runtime/db/data-conversion/post-processing.js +20 -13
  93. package/libx/_runtime/db/expand/expand-v2.js +21 -12
  94. package/libx/_runtime/db/expand/expandCQNToJoin.js +67 -26
  95. package/libx/_runtime/db/expand/index.js +3 -0
  96. package/libx/_runtime/db/generic/create.js +0 -10
  97. package/libx/_runtime/db/generic/index.js +3 -0
  98. package/libx/_runtime/db/generic/read.js +2 -24
  99. package/libx/_runtime/db/generic/rewrite.js +1 -3
  100. package/libx/_runtime/db/generic/update.js +1 -1
  101. package/libx/_runtime/db/query/delete.js +10 -4
  102. package/libx/_runtime/db/query/insert.js +3 -4
  103. package/libx/_runtime/db/query/read.js +4 -1
  104. package/libx/_runtime/db/query/update.js +5 -5
  105. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +9 -2
  106. package/libx/_runtime/db/sql-builder/FunctionBuilder.js +3 -0
  107. package/libx/_runtime/db/sql-builder/SelectBuilder.js +7 -3
  108. package/libx/_runtime/db/sql-builder/index.js +3 -0
  109. package/libx/_runtime/db/utils/columns.js +5 -2
  110. package/libx/_runtime/db/utils/deep.js +16 -14
  111. package/libx/_runtime/db/utils/generateAliases.js +56 -6
  112. package/libx/_runtime/fiori/generic/before.js +73 -49
  113. package/libx/_runtime/fiori/generic/edit.js +14 -18
  114. package/libx/_runtime/fiori/generic/patch.js +8 -11
  115. package/libx/_runtime/fiori/generic/read.js +20 -19
  116. package/libx/_runtime/fiori/generic/readOverDraft.js +1 -4
  117. package/libx/_runtime/fiori/utils/handler.js +1 -11
  118. package/libx/_runtime/hana/Service.js +1 -1
  119. package/libx/_runtime/hana/conversion.js +12 -1
  120. package/libx/_runtime/hana/dynatrace.js +11 -5
  121. package/libx/_runtime/hana/execute.js +132 -19
  122. package/libx/_runtime/hana/search.js +3 -3
  123. package/libx/_runtime/hana/search2cqn4sql.js +23 -25
  124. package/libx/_runtime/hana/searchToContains.js +1 -1
  125. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +1 -1
  126. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +0 -1
  127. package/libx/_runtime/messaging/enterprise-messaging.js +1 -0
  128. package/libx/_runtime/messaging/file-based.js +3 -1
  129. package/libx/_runtime/messaging/service.js +4 -1
  130. package/libx/_runtime/remote/utils/client.js +41 -24
  131. package/libx/_runtime/remote/utils/data.js +54 -12
  132. package/libx/_runtime/sqlite/Service.js +1 -1
  133. package/libx/_runtime/sqlite/conversion.js +10 -0
  134. package/libx/_runtime/types/api.js +2 -2
  135. package/libx/gql/resolvers/crud/update.js +8 -5
  136. package/libx/gql/resolvers/parse/ast/enrich.js +1 -0
  137. package/libx/odata/afterburner.js +29 -6
  138. package/libx/odata/cqn2odata.js +9 -0
  139. package/libx/odata/grammar.pegjs +49 -21
  140. package/libx/odata/index.js +2 -2
  141. package/libx/odata/parser.js +1 -1
  142. package/libx/odata/utils.js +2 -2
  143. package/libx/rest/RestAdapter.js +29 -1
  144. package/libx/rest/middleware/auth.js +1 -3
  145. package/libx/rest/middleware/parse.js +1 -0
  146. package/package.json +1 -1
  147. package/server.js +1 -1
  148. package/bin/deploy/to-hana/logger.js +0 -27
  149. package/bin/deploy/to-hana/runCommand.js +0 -113
  150. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/selectHelper.js +0 -37
  151. package/libx/_runtime/common/utils/auth.js +0 -16
@@ -5,13 +5,15 @@ const {
5
5
  Components: { DATA_CREATE_HANDLER }
6
6
  } = require('../okra/odata-server')
7
7
 
8
- const { getSapMessages } = require('../../../../common/error/frontend')
9
8
  const { validateResourcePath } = require('../utils/request')
10
9
  const { isReturnMinimal } = require('../utils/handlerUtils')
11
10
  const readAfterWrite = require('../utils/readAfterWrite')
12
11
  const { toODataResult, postProcess, postProcessMinimal } = require('../utils/result')
12
+
13
13
  const { mergeJson } = require('../../../services/utils/compareJson')
14
14
 
15
+ const { getSapMessages } = require('../../../../common/error/frontend')
16
+
15
17
  /**
16
18
  * The handler that will be registered with odata-v4.
17
19
  *
@@ -21,6 +23,7 @@ const { mergeJson } = require('../../../services/utils/compareJson')
21
23
  const create = service => {
22
24
  return async (odataReq, odataRes, next) => {
23
25
  let req
26
+
24
27
  try {
25
28
  validateResourcePath(odataReq, service)
26
29
  req = new ODataRequest(DATA_CREATE_HANDLER, service, odataReq, odataRes)
@@ -33,9 +36,12 @@ const create = service => {
33
36
  cds.context = tx
34
37
 
35
38
  let result, err
39
+
36
40
  try {
37
41
  result = await tx.dispatch(req)
38
42
 
43
+ // REVISIT readAfterWrite -> commit -> postProcessing
44
+
39
45
  if (isReturnMinimal(req)) {
40
46
  postProcessMinimal(req, result)
41
47
  } else {
@@ -60,12 +66,13 @@ const create = service => {
60
66
  }
61
67
  } catch (e) {
62
68
  err = e
63
- if (!changeset) {
64
- // REVISIT: rollback needed if error occured before commit attempted -> how to distinguish?
65
- await tx.rollback(e).catch(() => {})
66
- } else if (changeset) {
69
+
70
+ if (changeset) {
67
71
  // for passing into rollback
68
72
  odataReq.getBatchApplicationData().errors[changeset].push({ error: e, req })
73
+ } else {
74
+ // REVISIT: rollback needed if an error occurred before commit attempted -> how to distinguish?
75
+ await tx.rollback(e).catch(() => {})
69
76
  }
70
77
  } finally {
71
78
  req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req._.req))
@@ -17,6 +17,7 @@ const { validateResourcePath } = require('../utils/request')
17
17
  const del = service => {
18
18
  return async (odataReq, odataRes, next) => {
19
19
  let req
20
+
20
21
  try {
21
22
  validateResourcePath(odataReq, service)
22
23
  req = new ODataRequest(DATA_DELETE_HANDLER, service, odataReq, odataRes)
@@ -29,6 +30,7 @@ const del = service => {
29
30
  cds.context = tx
30
31
 
31
32
  let err
33
+
32
34
  try {
33
35
  await tx.dispatch(req)
34
36
  const result = null
@@ -41,12 +43,13 @@ const del = service => {
41
43
  }
42
44
  } catch (e) {
43
45
  err = e
44
- if (!changeset) {
45
- // REVISIT: rollback needed if error occured before commit attempted -> how to distinguish?
46
- await tx.rollback(e).catch(() => {})
47
- } else if (changeset) {
46
+
47
+ if (changeset) {
48
48
  // for passing into rollback
49
49
  odataReq.getBatchApplicationData().errors[changeset].push({ error: e, req })
50
+ } else {
51
+ // REVISIT: rollback needed if an error occurred before commit attempted -> how to distinguish?
52
+ await tx.rollback(e).catch(() => {})
50
53
  }
51
54
  } finally {
52
55
  req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req._.req))
@@ -49,6 +49,19 @@ const _betterOkraError = err => {
49
49
  if (err.name === 'DeserializationError') {
50
50
  err.message = err.message.replace('Error while deserializing payload.', 'Deserialization Error:')
51
51
  err.message = err.message.replace(' An error occurred during deserialization of the entity.', '')
52
+
53
+ // add parsed data to error for req.data
54
+ let e = err
55
+ let depth = 0
56
+ while (e._rootCause && depth < 10) {
57
+ const rootCause = e._rootCause
58
+ if (rootCause._data) {
59
+ err._data = rootCause._data
60
+ break
61
+ }
62
+ e = rootCause
63
+ depth++
64
+ }
52
65
  } else if (err.name === 'SerializationError') {
53
66
  err.message = err.message.replace('An error occurred during serialization of the entity.', 'Serialization Error:')
54
67
  err.message = err.message.replace(
@@ -68,10 +81,16 @@ const _betterOkraError = err => {
68
81
  * @returns {Function}
69
82
  */
70
83
  const getErrorHandler = (crashOnError = true, srv) => {
71
- return (odataReq, odataRes, next, err) => {
84
+ return async (odataReq, odataRes, next, err) => {
72
85
  // REVISIT: crashOnError
73
86
  if (isStandardError(err) && crashOnError) {
74
- err.__crashOnError = true
87
+ // first rollback in case of atomicity groups
88
+ const changeset = odataReq.getAtomicityGroupId()
89
+ if (changeset) {
90
+ const tx = odataReq.getBatchApplicationData().txs[changeset]
91
+ await tx.rollback().catch(() => {})
92
+ }
93
+
75
94
  throw err
76
95
  }
77
96
 
@@ -84,9 +103,11 @@ const getErrorHandler = (crashOnError = true, srv) => {
84
103
  req = odataReq.getIncomingRequest()
85
104
  }
86
105
 
87
- if (err.getRootCause && typeof err.getRootCause === 'function') {
106
+ if (typeof err.getRootCause === 'function') {
88
107
  // > an OKRA error
89
108
  err = _betterOkraError(err)
109
+ // add req.data for use in custom error handler in case of okra deserialization error
110
+ if ('_data' in err && !('data' in req)) req.data = err._data
90
111
  }
91
112
 
92
113
  // invoke srv.on('error', function (err, req) { ... }) here in special situations
@@ -14,12 +14,13 @@ const {
14
14
  }
15
15
  } = require('../okra/odata-server')
16
16
 
17
- const { isCustomOperation, skipToken } = require('../utils/request')
17
+ const { isCustomOperation } = require('../utils/request')
18
18
  const { actionAndFunctionQueries, getActionOrFunctionReturnType } = require('../utils/handlerUtils')
19
19
  const { validateResourcePath } = require('../utils/request')
20
20
  const { toODataResult, postProcess } = require('../utils/result')
21
21
  const { isStreaming, getStreamProperties } = require('../utils/stream')
22
22
  const { resolveStructuredName } = require('../utils/handlerUtils')
23
+
23
24
  const getError = require('../../../../common/error')
24
25
  const { getSapMessages } = require('../../../../common/error/frontend')
25
26
 
@@ -46,7 +47,7 @@ const _selectOrExpandInQueryOptions = queryOptions => {
46
47
  * @private
47
48
  */
48
49
  const _invokeFunction = async (tx, req, odataReq) => {
49
- const result = await tx.dispatch(req)
50
+ let result = await tx.dispatch(req)
50
51
 
51
52
  const functionReturnType = getActionOrFunctionReturnType(
52
53
  odataReq.getUriInfo().getPathSegments(),
@@ -59,7 +60,7 @@ const _invokeFunction = async (tx, req, odataReq) => {
59
60
  functionReturnType.kind === 'entity' &&
60
61
  _selectOrExpandInQueryOptions(odataReq.getQueryOptions())
61
62
  ) {
62
- await actionAndFunctionQueries(req, odataReq, result, tx, functionReturnType)
63
+ result = await actionAndFunctionQueries(req, odataReq, result, tx, functionReturnType)
63
64
  }
64
65
 
65
66
  return toODataResult(result, req)
@@ -142,9 +143,9 @@ const _isCollection = segments => {
142
143
  * @returns {boolean}
143
144
  * @private
144
145
  */
145
- const _isNavigationToOne = segments => {
146
- return segments[segments.length - 1].getKind() === NAVIGATION_TO_ONE
147
- }
146
+ const _isNavigationToOne = segments =>
147
+ segments[segments.length - 1].getKind() === NAVIGATION_TO_ONE &&
148
+ segments[segments.length - 1].getKeyPredicates().length === 0
148
149
 
149
150
  const _hasRedirectProperty = elements => {
150
151
  return Object.values(elements).some(val => {
@@ -229,22 +230,23 @@ const _readEntityOrProperty = async (tx, req, segments) => {
229
230
 
230
231
  if (propertyElement === null) {
231
232
  _transformRedirectProperties(req, result)
233
+
232
234
  return toODataResult(result[0])
233
235
  }
234
236
 
235
- const name = resolveStructuredName(segments, segments.length - 2)
236
- const res = _getResult(name, result[0])
237
+ result = _getResult(resolveStructuredName(segments, segments.length - 2), result[0])
237
238
 
238
- const odataResult = toODataResult(typeof res === 'object' ? res[propertyElement.getName()] : res)
239
- if (req.target._etag) odataResult['*@odata.etag'] = res[req.target._etag]
239
+ if (req.target._etag) result.$etag = result[req.target._etag]
240
+
241
+ const odataResult = toODataResult(result, req)
240
242
 
241
243
  // property is read via a to one association and last segment is not $value
242
244
  if (index !== 2 && segments.length > 2 && segments[segments.length - 2].getKind() === NAVIGATION_TO_ONE) {
243
245
  // find keys in result
244
- const keys = Object.keys(result[0])
246
+ const keys = Object.keys(result)
245
247
  .filter(k => segments[segments.length - index - 1].getEdmType().getOwnKeyPropertyRefs().has(k))
246
248
  .reduce((res, curr) => {
247
- res[curr] = result[0][curr]
249
+ res[curr] = result[curr]
248
250
  return res
249
251
  }, {})
250
252
 
@@ -266,20 +268,18 @@ const _readEntityOrProperty = async (tx, req, segments) => {
266
268
  */
267
269
  const _readCollection = async (tx, req, odataReq) => {
268
270
  const result = (await tx.dispatch(req)) || []
269
- const odataResult = toODataResult(result, req)
270
271
 
271
- // REVISIT: better
272
- if (!odataResult['*@odata.count'] && req.query.SELECT.count) {
273
- odataResult['*@odata.count'] = 0
274
- }
272
+ if (req.query.SELECT.count && !('$count' in result)) result.$count = 0
275
273
 
276
- if (!odataResult['*@odata.nextLink']) {
277
- const limit = req.query && req.query.SELECT.limit && req.query.SELECT.limit.rows && req.query.SELECT.limit.rows.val
278
- if (limit && limit === result.length && limit !== odataReq.getUriInfo().getQueryOption(QueryOptions.TOP)) {
279
- odataResult['*@odata.nextLink'] = skipToken(odataReq.getUriInfo()) + limit
280
- }
274
+ const limit = req.query.SELECT.limit && req.query.SELECT.limit.rows && req.query.SELECT.limit.rows.val
275
+ const top = odataReq.getUriInfo().getQueryOption(QueryOptions.TOP)
276
+ if (limit && limit === result.length && limit !== top && !('$nextLink' in result)) {
277
+ const token = odataReq.getUriInfo().getQueryOption(QueryOptions.SKIPTOKEN)
278
+ result.$nextLink = (token ? parseInt(token) : 0) + limit
281
279
  }
282
280
 
281
+ const odataResult = toODataResult(result, req)
282
+
283
283
  _transformRedirectProperties(req, result)
284
284
 
285
285
  return odataResult
@@ -298,7 +298,7 @@ const _readCollection = async (tx, req, odataReq) => {
298
298
  const _readStream = async (tx, req) => {
299
299
  req.query._streaming = true
300
300
 
301
- const { contentType, contentDisposition } = await getStreamProperties(req, tx.model)
301
+ const { contentType, contentDispositionFilename, contentDispositionType } = await getStreamProperties(req, tx.model)
302
302
 
303
303
  let result = await tx.dispatch(req)
304
304
 
@@ -312,14 +312,14 @@ const _readStream = async (tx, req) => {
312
312
 
313
313
  if (result[0] === null) return null
314
314
 
315
- const streamObj = result[0]
316
- const stream = streamObj.value
315
+ result = result[0]
316
+ const { value: readable } = result
317
317
 
318
- if (stream) {
319
- stream.on('error', () => {
320
- stream.removeAllListeners('error')
321
- // stream.destroy() does not end stream in node 10 and 12
322
- stream.push(null)
318
+ if (readable) {
319
+ readable.on('error', () => {
320
+ readable.removeAllListeners('error')
321
+ // readable.destroy() does not end stream in node 10 and 12
322
+ readable.push(null)
323
323
  })
324
324
  }
325
325
 
@@ -336,11 +336,13 @@ const _readStream = async (tx, req) => {
336
336
  req.reject(406, `Content type "${contentType}" not listed in accept header "${headers.accept}".`)
337
337
  }
338
338
 
339
- if (contentType) streamObj['*@odata.mediaContentType'] = contentType
340
- if (contentDisposition)
341
- req._.odataRes.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(contentDisposition)}"`)
339
+ if (contentType) result.$mediaContentType = contentType
340
+ if (contentDispositionFilename) {
341
+ result.$mediaContentDispositionFilename = contentDispositionFilename
342
+ if (contentDispositionType) result.$mediaContentDispositionType = contentDispositionType
343
+ }
342
344
 
343
- return streamObj
345
+ return toODataResult(result, req)
344
346
  }
345
347
 
346
348
  const _readSingleton = async (tx, req, lastSegment) => {
@@ -439,6 +441,7 @@ const _removeKeysForParams = result => {
439
441
  const read = service => {
440
442
  return async (odataReq, odataRes, next) => {
441
443
  let req
444
+
442
445
  try {
443
446
  validateResourcePath(odataReq, service)
444
447
  req = new ODataRequest(DATA_READ_HANDLER, service, odataReq, odataRes)
@@ -452,6 +455,7 @@ const read = service => {
452
455
 
453
456
  let result, err
454
457
  let additional = {}
458
+
455
459
  try {
456
460
  // REVISIT: refactor _readAndTransform
457
461
  result = await _readAndTransform(tx, req, odataReq)
@@ -471,12 +475,13 @@ const read = service => {
471
475
  }
472
476
  } catch (e) {
473
477
  err = e
474
- if (!changeset) {
475
- // REVISIT: rollback needed if error occured before commit attempted -> how to distinguish?
476
- await tx.rollback(e).catch(() => {})
477
- } else if (changeset) {
478
+
479
+ if (changeset) {
478
480
  // for passing into rollback
479
481
  odataReq.getBatchApplicationData().errors[changeset].push({ error: e, req })
482
+ } else {
483
+ // REVISIT: rollback needed if an error occurred before commit attempted -> how to distinguish?
484
+ await tx.rollback(e).catch(() => {})
480
485
  }
481
486
  } finally {
482
487
  req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req._.req))
@@ -1,6 +1,6 @@
1
1
  const cds = require('../../../../cds')
2
2
 
3
- const { UNAUTHORIZED, FORBIDDEN, getRequiresAsArray } = require('../../../../common/utils/auth')
3
+ const { UNAUTHORIZED, FORBIDDEN, getRequiresAsArray } = require('../../../../auth/utils')
4
4
 
5
5
  module.exports = srv => {
6
6
  const requires = getRequiresAsArray(srv.definition)
@@ -6,14 +6,16 @@ const {
6
6
  Components: { DATA_UPDATE_HANDLER, DATA_CREATE_HANDLER }
7
7
  } = require('../okra/odata-server')
8
8
 
9
- const { getSapMessages } = require('../../../../common/error/frontend')
10
9
  const { validateResourcePath } = require('../utils/request')
11
10
  const { isReturnMinimal } = require('../utils/handlerUtils')
12
11
  const readAfterWrite = require('../utils/readAfterWrite')
13
12
  const { toODataResult, postProcess, postProcessMinimal } = require('../utils/result')
14
13
  const { hasOmitValuesPreference } = require('../utils/omitValues')
14
+
15
15
  const { mergeJson } = require('../../../services/utils/compareJson')
16
16
 
17
+ const { getSapMessages } = require('../../../../common/error/frontend')
18
+
17
19
  const _isUpsertAllowed = target => {
18
20
  return !(cds.env.runtime && cds.env.runtime.allow_upsert === false) && !(target && target._isDraftEnabled)
19
21
  }
@@ -126,6 +128,7 @@ const _shouldReadPreviousResult = req =>
126
128
  const update = service => {
127
129
  return async (odataReq, odataRes, next) => {
128
130
  let req
131
+
129
132
  try {
130
133
  validateResourcePath(odataReq, service)
131
134
  req = new ODataRequest(DATA_UPDATE_HANDLER, service, odataReq, odataRes)
@@ -141,8 +144,10 @@ const update = service => {
141
144
  const primitive = odataReq.getUriInfo().getLastSegment().getKind() === 'PRIMITIVE.PROPERTY'
142
145
 
143
146
  let result, err
147
+
144
148
  try {
145
149
  let previousResult
150
+
146
151
  if (_shouldReadPreviousResult(req)) {
147
152
  previousResult = await _readAfterWriteAndVirtuals(req, service, result)
148
153
  }
@@ -173,12 +178,13 @@ const update = service => {
173
178
  }
174
179
  } catch (e) {
175
180
  err = e
176
- if (!changeset) {
177
- // REVISIT: rollback needed if error occured before commit attempted -> how to distinguish?
178
- await tx.rollback(e).catch(() => {})
179
- } else if (changeset) {
181
+
182
+ if (changeset) {
180
183
  // for passing into rollback
181
184
  odataReq.getBatchApplicationData().errors[changeset].push({ error: e, req })
185
+ } else {
186
+ // REVISIT: rollback needed if an error occurred before commit attempted -> how to distinguish?
187
+ await tx.rollback(e).catch(() => {})
182
188
  }
183
189
  } finally {
184
190
  req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req._.req))
@@ -1,8 +1,7 @@
1
1
  const cds = require('../../../../cds')
2
2
  const { SELECT } = cds.ql
3
3
 
4
- const { isPathSupported } = require('./selectHelper')
5
- const { convertUrlPathToCqn } = require('./utils')
4
+ const { convertUrlPathToCqn, isPathSupported } = require('./utils')
6
5
 
7
6
  const {
8
7
  BOUND_ACTION,
@@ -26,7 +26,6 @@ const deleteToCQN = (service, odataReq) => {
26
26
  if (SUPPORTED_KINDS[segment.getKind()]) {
27
27
  return DELETE.from(convertUrlPathToCqn(segments, service))
28
28
  }
29
-
30
29
  if (segment.getKind() === 'PRIMITIVE.PROPERTY') {
31
30
  return UPDATE(convertUrlPathToCqn(segments, service)).data({ [segment.getProperty().getName()]: null })
32
31
  }
@@ -13,7 +13,6 @@ const { getColumns } = require('../../../services/utils/columns')
13
13
  const { addLimit, isSameArray } = require('./utils')
14
14
  const { findCsnTargetFor } = require('../../../../common/utils/csn')
15
15
  const getError = require('../../../../common/error')
16
-
17
16
  /**
18
17
  * Check which element(s) of the entity has been expanded.
19
18
  *
@@ -234,7 +233,7 @@ const expandToCQN = (model, expandItems, type, options) => {
234
233
  const allElements = []
235
234
  const isAll = expandItems.some(item => item.isAll())
236
235
 
237
- for (const [name, navigationProperty] of type.getNavigationProperties()) {
236
+ for (const [name, navigationProperty] of type.getNavigationProperties(isAll)) {
238
237
  const expandItem = _getExpandItem(isAll, expandItems, name)
239
238
 
240
239
  if (isAll || expandItem) {
@@ -1,10 +1,12 @@
1
1
  const ExpressionToCQN = require('./ExpressionToCQN')
2
2
  const { getFeatureNotSupportedError } = require('../../../util/errors')
3
3
  const odata = require('../okra/odata-server')
4
+ const { ResourceKind } = require('../okra/odata-commons/uri/UriResource')
4
5
  const {
5
6
  QueryOptions: { ORDERBY }
6
7
  } = odata
7
8
  const ExpressionKind = odata.uri.Expression.ExpressionKind
9
+ const getError = require('../../../../common/error')
8
10
 
9
11
  const _buildNavRef = pathSegment => {
10
12
  return pathSegment.getProperty() ? pathSegment.getProperty().getName() : pathSegment.getNavigationProperty().getName()
@@ -13,6 +15,13 @@ const _buildNavRef = pathSegment => {
13
15
  const _orderExpression = order => {
14
16
  if (order.getExpression().getKind() === ExpressionKind.MEMBER) {
15
17
  const ref = []
18
+ const lastSegment = order.getExpression().getPathSegments()[order.getExpression().getPathSegments().length - 1]
19
+ if (
20
+ lastSegment.getKind() === ResourceKind.ANY_EXPRESSION ||
21
+ lastSegment.getKind() === ResourceKind.ALL_EXPRESSION
22
+ ) {
23
+ throw getError(501, 'ORDERBY_LAMBDA_UNSUPPORTED')
24
+ }
16
25
  for (let i = 0; i < order.getExpression().getPathSegments().length; i++) {
17
26
  ref.push(_buildNavRef(order.getExpression().getPathSegments()[i]))
18
27
  }
@@ -1,22 +1,14 @@
1
1
  const cds = require('../../../../cds')
2
2
  const { SELECT } = cds.ql
3
3
 
4
- const QueryOptions = require('../okra/odata-server').QueryOptions
5
- const { isNavigation, isPathSupported } = require('./selectHelper')
6
- const { isViewWithParams, getValidationQuery } = require('./selectHelper')
7
- const { ensureUnlocalized } = require('../../../../fiori/utils/handler')
8
4
  const ExpressionToCQN = require('./ExpressionToCQN')
9
5
  const orderByToCQN = require('./orderByToCQN')
10
6
  const selectToCQN = require('./selectToCQN')
11
7
  const searchToCQN = require('./searchToCQN')
12
8
  const applyToCQN = require('./applyToCQN')
13
- const { _expand } = require('../utils/handlerUtils')
14
- const { resolveStructuredName } = require('../utils/handlerUtils')
15
- const { isStreaming } = require('../utils/stream')
16
- const { convertUrlPathToCqn, getAllKeys } = require('./utils')
17
- const { getMaxPageSize, getDefaultPageSize } = require('../../../../common/utils/page')
18
- const { isAsteriskColumn } = require('../../../../common/utils/rewriteAsterisks')
9
+ const { convertUrlPathToCqn, getAllKeys, isPathSupported } = require('./utils')
19
10
 
11
+ const QueryOptions = require('../okra/odata-server').QueryOptions
20
12
  const {
21
13
  COUNT,
22
14
  ENTITY,
@@ -28,7 +20,6 @@ const {
28
20
  VALUE,
29
21
  SINGLETON
30
22
  } = require('../okra/odata-server').uri.UriResource.ResourceKind
31
-
32
23
  const SUPPORTED_SEGMENT_KINDS = {
33
24
  [ENTITY]: 1,
34
25
  [ENTITY_COLLECTION]: 1,
@@ -41,9 +32,16 @@ const SUPPORTED_SEGMENT_KINDS = {
41
32
  [SINGLETON]: 1
42
33
  }
43
34
 
44
- const _applyOnlyContainsFilter = apply => {
45
- return Object.keys(apply).length === 1 && apply.filter
46
- }
35
+ const { _expand } = require('../utils/handlerUtils')
36
+ const { resolveStructuredName } = require('../utils/handlerUtils')
37
+ const { isStreaming } = require('../utils/stream')
38
+
39
+ const { getMaxPageSize, getDefaultPageSize } = require('../../../../common/utils/page')
40
+ const { isAsteriskColumn } = require('../../../../common/utils/rewriteAsterisks')
41
+
42
+ const { ensureUnlocalized } = require('../../../../fiori/utils/handler')
43
+
44
+ const _applyOnlyContainsFilter = apply => Object.keys(apply).length === 1 && apply.filter
47
45
 
48
46
  /**
49
47
  *
@@ -232,14 +230,6 @@ const _checkViewWithParamCall = (isView, segments, kind, name) => {
232
230
  }
233
231
  }
234
232
 
235
- const addValidationQueryIfRequired = (segments, isView, cqn, service, kind) => {
236
- if (isNavigation(segments) && !isView && (kind === NAVIGATION_TO_MANY || kind === NAVIGATION_TO_ONE)) {
237
- cqn._validationQuery = getValidationQuery(cqn.SELECT.from.ref, service.model)
238
- cqn._validationQuery.__navToManyWithKeys =
239
- kind === NAVIGATION_TO_ONE && segments[segments.length - 1].getKeyPredicates().length !== 0
240
- }
241
- }
242
-
243
233
  const _addKeysToSelectIfNoStreaming = (entity, select, streaming) => {
244
234
  // might also be singleton w/o keys
245
235
  if (!streaming && entity.keys) {
@@ -381,7 +371,7 @@ const readToCQN = (service, target, odataReq) => {
381
371
  select.push(...expand)
382
372
  }
383
373
 
384
- const isView = isViewWithParams(target)
374
+ const isView = target.params && Object.keys(target.params).length > 0
385
375
  const kind = segments[segments.length - 1].getKind()
386
376
  const isCollectionOrToMany = _isCollectionOrToMany(kind)
387
377
 
@@ -390,13 +380,10 @@ const readToCQN = (service, target, odataReq) => {
390
380
 
391
381
  // keep target as input because of localized view
392
382
  const cqn = SELECT.from(isView ? _convertUrlPathToViewCqn(segments) : convertUrlPathToCqn(segments, service), select)
393
- addValidationQueryIfRequired(segments, isView, cqn, service, kind)
394
383
 
395
384
  if (isCollectionOrToMany && queryOptions && queryOptions[QueryOptions.COUNT]) cqn.SELECT.count = true
396
385
 
397
- if (Object.keys(apply).length) {
398
- _extendCqnWithApply(cqn, apply, entity)
399
- }
386
+ if (Object.keys(apply).length) _extendCqnWithApply(cqn, apply, entity)
400
387
 
401
388
  if (isCollectionOrToMany || _isCount(kind)) {
402
389
  _filter(service.model, entity, uriInfo, apply, cqn)
@@ -408,13 +395,13 @@ const readToCQN = (service, target, odataReq) => {
408
395
  _orderby(uriInfo, cqn)
409
396
  }
410
397
 
411
- if (!isCollectionOrToMany || entity._isSingleton) {
412
- cqn.SELECT.one = true
413
- }
398
+ if (!isCollectionOrToMany || entity._isSingleton) cqn.SELECT.one = true
414
399
 
415
400
  _cleanupForApply(apply, cqn)
401
+
416
402
  // just like in new parser
417
403
  if (cqn.SELECT.columns.length === 1 && cqn.SELECT.columns[0] === '*') delete cqn.SELECT.columns
404
+
418
405
  return cqn
419
406
  }
420
407
 
@@ -1,3 +1,5 @@
1
+ const { getFeatureNotSupportedError } = require('../../../util/errors')
2
+
1
3
  const { findCsnTargetFor } = require('../../../../common/utils/csn')
2
4
 
3
5
  const CDL_KEYWORDS = new Set(require('@sap/cds-compiler/lib/base/keywords').cdl)
@@ -108,9 +110,18 @@ const getAllKeys = (entity, joinStructured = true) => {
108
110
  return allKeys
109
111
  }
110
112
 
113
+ const isPathSupported = (supported, pathSegments) => {
114
+ for (const segment of pathSegments) {
115
+ if (!supported[segment.getKind()]) {
116
+ throw getFeatureNotSupportedError(`Request parameter "${segment.getKind()}"`)
117
+ }
118
+ }
119
+ }
120
+
111
121
  module.exports = {
112
122
  addLimit,
113
123
  convertUrlPathToCqn,
114
124
  isSameArray,
115
- getAllKeys
125
+ getAllKeys,
126
+ isPathSupported
116
127
  }
@@ -190,10 +190,11 @@ class AbstractEdmStructuredType extends EdmType {
190
190
  * Returns a map of navigation properties including those from base type.
191
191
  * @returns {Map.<string, EdmNavigationProperty>} A Map of navigation properties
192
192
  */
193
- getNavigationProperties () {
193
+ getNavigationProperties (ignoreSibling) {
194
194
  if (!this._navigationProperties) {
195
195
  this._navigationProperties = new Map(this.getBaseType() ? this.getBaseType().getNavigationProperties() : [])
196
196
  for (const [name, property] of this.getOwnNavigationProperties()) {
197
+ if (ignoreSibling && name === 'SiblingEntity') continue;
197
198
  this._navigationProperties.set(name, property)
198
199
  }
199
200
  }
@@ -396,7 +396,18 @@ class ResourcePathParser {
396
396
  throw new UriSyntaxError(UriSyntaxError.Message.PREVIOUS_TYPE_HAS_NO_MEDIA, currentType.getName())
397
397
  }
398
398
 
399
- const uriResources = this._parsePropertyPath(uriPathSegments, currentResource, tokenizer)
399
+ let uriResources
400
+ try {
401
+ uriResources = this._parsePropertyPath(uriPathSegments, currentResource, tokenizer)
402
+ } catch (e) {
403
+ try {
404
+ uriResources = this._parseBoundOperation(uriPathSegments, currentResource, tokenizer)
405
+ } catch (e1) {
406
+ // throw first error
407
+ throw e
408
+ }
409
+ }
410
+
400
411
  return result.concat(uriResources)
401
412
  }
402
413
 
@@ -409,7 +420,17 @@ class ResourcePathParser {
409
420
  * @private
410
421
  */
411
422
  _parseBoundOperation (uriPathSegments, currentResource, tokenizer) {
412
- const fqn = FullQualifiedName.createFromNameSpaceAndName(tokenizer.getText())
423
+ // allow short names for bound operations
424
+ let name = tokenizer.getText()
425
+ if (typeof name === 'string' && !name.match(/\./)) {
426
+ const namespace = currentResource._entitySet &&
427
+ currentResource._entitySet._target &&
428
+ currentResource._entitySet._target.type &&
429
+ currentResource._entitySet._target.type.namespace
430
+ if (namespace) name = namespace + '.' + name
431
+ }
432
+
433
+ const fqn = FullQualifiedName.createFromNameSpaceAndName(name)
413
434
  const bindingParamTypeFqn = currentResource.getEdmType().getFullQualifiedName()
414
435
 
415
436
  // parse bound action
@@ -22,12 +22,13 @@ class UriHelper {
22
22
  return uriLiteral.substring(1, uriLiteral.length - 1).replace(REGEXP_TWO_SINGLE_QUOTES, "'")
23
23
  }
24
24
 
25
- if (
26
- edmType === EdmPrimitiveTypeKind.Duration ||
27
- edmType === EdmPrimitiveTypeKind.Binary ||
28
- edmType.getKind() === EdmTypeKind.ENUM ||
29
- edmType.getName().startsWith('Geo')
30
- ) {
25
+ if (edmType === EdmPrimitiveTypeKind.Binary) {
26
+ // convert the URL-safe base64 encoding to the standard variant (with padding, if necessary)
27
+ let val = uriLiteral.substring(uriLiteral.indexOf("'") + 1, uriLiteral.length - 1).replace(/_/g, '/').replace(/-/g, '+')
28
+ return val.padEnd(val.length + val.length % 4, '=')
29
+ }
30
+
31
+ if (edmType === EdmPrimitiveTypeKind.Duration || edmType.getKind() === EdmTypeKind.ENUM || edmType.getName().startsWith('Geo')) {
31
32
  return uriLiteral.substring(uriLiteral.indexOf("'") + 1, uriLiteral.length - 1)
32
33
  }
33
34