@sap/cds 5.5.3 → 5.6.1

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 (227) hide show
  1. package/CHANGELOG.md +134 -1
  2. package/apis/services.d.ts +27 -1
  3. package/app/index.js +22 -11
  4. package/bin/build/buildTaskFactory.js +1 -1
  5. package/bin/build/provider/buildTaskProviderInternal.js +1 -1
  6. package/bin/build/provider/fiori/index.js +1 -1
  7. package/bin/build/provider/hana/2migration.js +8 -7
  8. package/bin/build/provider/java-cf/index.js +1 -1
  9. package/bin/deploy/to-hana/hana.js +1 -17
  10. package/common.cds +8 -0
  11. package/lib/compile/index.js +1 -1
  12. package/lib/compile/to/sql.js +22 -2
  13. package/lib/connect/bindings.js +2 -1
  14. package/lib/connect/index.js +1 -1
  15. package/lib/core/infer.js +1 -1
  16. package/lib/core/reflect.js +3 -1
  17. package/lib/env/index.js +175 -41
  18. package/lib/env/requires.js +24 -3
  19. package/lib/i18n/localize.js +33 -5
  20. package/lib/index.js +7 -6
  21. package/lib/log/format/kibana.js +6 -2
  22. package/lib/ql/DELETE.js +1 -1
  23. package/lib/ql/INSERT.js +1 -1
  24. package/lib/ql/Query.js +13 -10
  25. package/lib/ql/SELECT.js +15 -8
  26. package/lib/ql/UPDATE.js +1 -1
  27. package/lib/ql/Whereable.js +5 -0
  28. package/lib/req/context.js +87 -37
  29. package/lib/req/{impl.js → request.js} +1 -1
  30. package/lib/req/{res.js → response.js} +0 -0
  31. package/lib/serve/Service-api.js +1 -1
  32. package/lib/serve/Service-dispatch.js +12 -2
  33. package/lib/serve/Service-handlers.js +21 -7
  34. package/lib/serve/Service-methods.js +1 -1
  35. package/lib/serve/Transaction.js +7 -6
  36. package/lib/serve/index.js +1 -1
  37. package/lib/utils/axios.js +7 -0
  38. package/lib/utils/data.js +1 -1
  39. package/libx/_runtime/audit/Service.js +18 -18
  40. package/libx/_runtime/audit/generic/personal/access.js +1 -1
  41. package/libx/_runtime/audit/generic/personal/modification.js +3 -2
  42. package/libx/_runtime/audit/generic/personal/utils.js +23 -63
  43. package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +6 -0
  44. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +37 -35
  45. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -1
  46. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +3 -1
  47. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +5 -5
  48. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +1 -1
  49. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +13 -7
  50. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +84 -34
  51. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +10 -4
  52. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +9 -3
  53. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +8 -6
  54. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +1 -3
  55. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +13 -11
  56. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/selectHelper.js +11 -95
  57. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/ResourcePathParser.js +17 -11
  58. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +2 -1
  59. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +6 -2
  60. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +3 -34
  61. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +3 -3
  62. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +48 -18
  63. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +10 -5
  64. package/libx/_runtime/cds-services/adapter/rest/handlers/operation.js +1 -1
  65. package/libx/_runtime/cds-services/adapter/rest/handlers/update.js +1 -1
  66. package/libx/_runtime/cds-services/adapter/rest/rest-to-cqn/index.js +1 -3
  67. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +9 -2
  68. package/libx/_runtime/cds-services/adapter/rest/utils/validation-checks.js +20 -21
  69. package/libx/_runtime/cds-services/services/utils/columns.js +6 -1
  70. package/libx/_runtime/cds-services/services/utils/compareJson.js +1 -8
  71. package/libx/_runtime/cds-services/services/utils/differ.js +7 -26
  72. package/libx/_runtime/cds-services/services/utils/handlerUtils.js +2 -4
  73. package/libx/_runtime/cds-services/util/assert.js +29 -13
  74. package/libx/_runtime/cds.js +2 -1
  75. package/libx/_runtime/common/aspects/Association.js +72 -0
  76. package/libx/_runtime/common/aspects/any.js +8 -45
  77. package/libx/_runtime/common/aspects/entity.js +0 -1
  78. package/libx/_runtime/common/aspects/relation.js +40 -0
  79. package/libx/_runtime/common/aspects/utils.js +73 -1
  80. package/libx/_runtime/common/auth/strategies/utils/uaa.js +10 -14
  81. package/libx/_runtime/common/composition/data.js +3 -2
  82. package/libx/_runtime/common/composition/delete.js +3 -1
  83. package/libx/_runtime/common/composition/tree.js +23 -18
  84. package/libx/_runtime/common/composition/utils.js +34 -8
  85. package/libx/_runtime/common/error/frontend.js +6 -1
  86. package/libx/_runtime/common/generic/auth.js +15 -13
  87. package/libx/_runtime/common/generic/crud.js +2 -2
  88. package/libx/_runtime/common/generic/etag.js +11 -8
  89. package/libx/_runtime/common/generic/input.js +3 -3
  90. package/libx/_runtime/common/generic/paging.js +9 -5
  91. package/libx/_runtime/common/generic/put.js +3 -2
  92. package/libx/_runtime/common/generic/sorting.js +3 -3
  93. package/libx/_runtime/common/generic/temporal.js +3 -3
  94. package/libx/_runtime/common/toggles/alpha.js +1 -1
  95. package/libx/_runtime/common/utils/cqn.js +20 -1
  96. package/libx/_runtime/common/utils/cqn2cqn4sql.js +125 -139
  97. package/libx/_runtime/common/utils/csn.js +50 -52
  98. package/libx/_runtime/common/utils/foreignKeyPropagations.js +41 -176
  99. package/libx/_runtime/common/utils/generateOnCond.js +40 -70
  100. package/libx/_runtime/common/utils/{enrichWithKeysFromWhere.js → keys.js} +29 -28
  101. package/libx/_runtime/common/utils/postProcessing.js +3 -0
  102. package/libx/_runtime/common/utils/propagateForeignKeys.js +84 -0
  103. package/libx/_runtime/common/utils/resolveStructured.js +1 -1
  104. package/libx/_runtime/common/utils/resolveView.js +19 -9
  105. package/libx/_runtime/common/utils/rewriteAsterisks.js +94 -0
  106. package/libx/_runtime/common/utils/search2cqn4sql.js +9 -8
  107. package/libx/_runtime/common/utils/template.js +54 -46
  108. package/libx/_runtime/db/Service.js +9 -2
  109. package/libx/_runtime/db/expand/expandCQNToJoin.js +10 -24
  110. package/libx/_runtime/db/expand/rawToExpanded.js +2 -1
  111. package/libx/_runtime/db/generic/create.js +1 -0
  112. package/libx/_runtime/db/generic/input.js +7 -11
  113. package/libx/_runtime/db/generic/integrity.js +2 -2
  114. package/libx/_runtime/db/generic/rewrite.js +2 -5
  115. package/libx/_runtime/db/generic/update.js +1 -0
  116. package/libx/_runtime/db/query/read.js +10 -5
  117. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +6 -0
  118. package/libx/_runtime/db/sql-builder/SelectBuilder.js +7 -2
  119. package/libx/_runtime/db/sql-builder/annotations.js +1 -0
  120. package/libx/_runtime/db/utils/columns.js +14 -43
  121. package/libx/_runtime/db/utils/deep.js +5 -7
  122. package/libx/_runtime/fiori/generic/activate.js +3 -2
  123. package/libx/_runtime/fiori/generic/before.js +2 -2
  124. package/libx/_runtime/fiori/generic/cancel.js +3 -2
  125. package/libx/_runtime/fiori/generic/delete.js +3 -2
  126. package/libx/_runtime/fiori/generic/edit.js +2 -2
  127. package/libx/_runtime/fiori/generic/new.js +2 -2
  128. package/libx/_runtime/fiori/generic/patch.js +2 -2
  129. package/libx/_runtime/fiori/generic/prepare.js +2 -2
  130. package/libx/_runtime/fiori/generic/read.js +17 -63
  131. package/libx/_runtime/fiori/generic/readOverDraft.js +4 -4
  132. package/libx/_runtime/fiori/uiflex/extensibility/index.cds +15 -0
  133. package/libx/_runtime/fiori/uiflex/extensibility/index.js +148 -0
  134. package/libx/_runtime/fiori/uiflex/handler/transformREAD.js +119 -0
  135. package/libx/_runtime/fiori/uiflex/handler/transformRESULT.js +43 -0
  136. package/libx/_runtime/fiori/uiflex/handler/transformWRITE.js +62 -0
  137. package/libx/_runtime/fiori/uiflex/index.js +35 -0
  138. package/libx/_runtime/fiori/uiflex/utils.js +78 -0
  139. package/libx/_runtime/fiori/utils/handler.js +3 -13
  140. package/libx/_runtime/fiori/utils/where.js +6 -1
  141. package/libx/_runtime/hana/Service.js +5 -2
  142. package/libx/_runtime/hana/execute.js +1 -1
  143. package/libx/_runtime/hana/pool.js +12 -11
  144. package/libx/_runtime/hana/search2cqn4sql.js +34 -43
  145. package/libx/_runtime/hana/searchToContains.js +3 -3
  146. package/libx/_runtime/index.js +5 -2
  147. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +1 -1
  148. package/libx/_runtime/messaging/common-utils/AMQPClient.js +16 -3
  149. package/libx/_runtime/messaging/common-utils/connections.js +11 -14
  150. package/libx/_runtime/messaging/common-utils/naming-conventions.js +1 -1
  151. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -1
  152. package/libx/_runtime/messaging/enterprise-messaging.js +1 -1
  153. package/libx/_runtime/messaging/message-queuing.js +18 -0
  154. package/libx/_runtime/remote/Service.js +14 -2
  155. package/libx/_runtime/remote/utils/client-types.d.ts +7 -0
  156. package/libx/_runtime/remote/utils/client.js +117 -23
  157. package/libx/_runtime/sqlite/Service.js +4 -3
  158. package/libx/_runtime/sqlite/convertAssocToOneManaged.js +1 -3
  159. package/libx/_runtime/sqlite/execute.js +1 -1
  160. package/libx/gql/GraphQLAdapter.js +33 -0
  161. package/libx/gql/constants/adapter.js +69 -0
  162. package/libx/gql/constants/cds.js +18 -0
  163. package/libx/gql/constants/graphql.js +33 -0
  164. package/libx/gql/resolvers/crud/create.js +15 -0
  165. package/libx/gql/resolvers/crud/delete.js +24 -0
  166. package/libx/gql/resolvers/crud/index.js +6 -0
  167. package/libx/gql/resolvers/crud/read.js +25 -0
  168. package/libx/gql/resolvers/crud/update.js +31 -0
  169. package/libx/gql/resolvers/crud/utils/index.js +36 -0
  170. package/libx/gql/resolvers/field.js +5 -0
  171. package/libx/gql/resolvers/index.js +7 -0
  172. package/libx/gql/resolvers/mutation.js +23 -0
  173. package/libx/gql/resolvers/parse/ast/enrich.js +51 -0
  174. package/libx/gql/resolvers/parse/ast/fragment.js +11 -0
  175. package/libx/gql/resolvers/parse/ast/fromObject.js +39 -0
  176. package/libx/gql/resolvers/parse/ast/index.js +3 -0
  177. package/libx/gql/resolvers/parse/ast/meta.js +4 -0
  178. package/libx/gql/resolvers/parse/ast/variable.js +7 -0
  179. package/libx/gql/resolvers/parse/ast2cqn/columns.js +42 -0
  180. package/libx/gql/resolvers/parse/ast2cqn/entries.js +31 -0
  181. package/libx/gql/resolvers/parse/ast2cqn/index.js +8 -0
  182. package/libx/gql/resolvers/parse/ast2cqn/limit.js +6 -0
  183. package/libx/gql/resolvers/parse/ast2cqn/orderBy.js +24 -0
  184. package/libx/gql/resolvers/parse/ast2cqn/utils/index.js +3 -0
  185. package/libx/gql/resolvers/parse/ast2cqn/where.js +70 -0
  186. package/libx/gql/resolvers/parse/utils/index.js +8 -0
  187. package/libx/gql/resolvers/query.js +13 -0
  188. package/libx/gql/resolvers/root.js +34 -0
  189. package/libx/gql/schema/generate.js +18 -0
  190. package/libx/gql/schema/index.js +5 -0
  191. package/libx/gql/schema/mutation.js +76 -0
  192. package/libx/gql/schema/query.js +108 -0
  193. package/libx/gql/schema/typeDefMap.js +45 -0
  194. package/libx/gql/schema/utils/index.js +54 -0
  195. package/libx/gql/utils/index.js +12 -0
  196. package/libx/{_runtime/odata/cqn2odata.js → odata/cqn2odata/index.js} +39 -100
  197. package/libx/odata/index.js +80 -0
  198. package/libx/odata/odata2cqn/afterburner.js +170 -0
  199. package/libx/{_runtime/odata/odata2cqn.pegjs → odata/odata2cqn/grammar.pegjs} +102 -123
  200. package/libx/odata/odata2cqn/index.js +3 -0
  201. package/libx/odata/odata2cqn/parser.js +1 -0
  202. package/libx/odata/utils/index.js +64 -0
  203. package/libx/rest/RestAdapter.js +101 -0
  204. package/libx/rest/RestRequest.js +30 -0
  205. package/libx/rest/index.js +3 -0
  206. package/libx/rest/middleware/auth.js +22 -0
  207. package/libx/rest/middleware/content.js +15 -0
  208. package/libx/rest/middleware/create.js +40 -0
  209. package/libx/rest/middleware/delete.js +20 -0
  210. package/libx/rest/middleware/error.js +56 -0
  211. package/libx/rest/middleware/operation.js +39 -0
  212. package/libx/rest/middleware/parse.js +90 -0
  213. package/libx/rest/middleware/read.js +29 -0
  214. package/libx/rest/middleware/update.js +42 -0
  215. package/libx/rest/utils/data.js +65 -0
  216. package/package.json +4 -1
  217. package/server.js +42 -29
  218. package/lib/req/cls.js +0 -39
  219. package/libx/_runtime/cds-services/services/utils/diff.js +0 -53
  220. package/libx/_runtime/cds-services/util/auditlog.js +0 -247
  221. package/libx/_runtime/cds-services/util/xsenv.js +0 -51
  222. package/libx/_runtime/common/utils/backlinks.js +0 -83
  223. package/libx/_runtime/common/utils/rewriteAsterisk.js +0 -72
  224. package/libx/_runtime/odata/index.js +0 -55
  225. package/libx/_runtime/odata/odata2cqn.js +0 -1
  226. package/libx/_runtime/odata/readToCqn.js +0 -129
  227. package/libx/_runtime/remote/cqn2odata/index.js +0 -2
@@ -159,6 +159,7 @@ class OData {
159
159
 
160
160
  this._odataService.on(ATOMICITY_GROUP_START, (odataContext, done) => {
161
161
  const data = odataContext.applicationData
162
+
162
163
  // start tx
163
164
  const txs = (data.txs = data.txs || {})
164
165
  const {
@@ -179,28 +180,14 @@ class OData {
179
180
  this._odataService.on(ATOMICITY_GROUP_END, async (odataErr, odataContext, done) => {
180
181
  const tx = odataContext.applicationData.txs[odataContext.id]
181
182
  let errors = odataErr || odataContext.failedRequests.length > 0
182
- if (!errors) {
183
- try {
184
- await tx.commit(odataContext.applicationData.results[odataContext.id])
185
- done()
186
- } catch (e) {
187
- // tx gets rolled back automatically
188
- // set error on each request of changeset, if commit failed
189
- const changesetResults = odataContext.applicationData.results[odataContext.id]
190
- const failedRequests = changesetResults.reduce((obj, resultEntry) => {
191
- const requestId = resultEntry.req._.odataReq.getOdataRequestId()
192
- const { error, statusCode } = normalizeError(e, resultEntry.req)
193
- obj[requestId] = Object.assign(error, { statusCode })
194
- return obj
195
- }, {})
196
- done(e, { failedRequests })
197
- }
198
- } else {
183
+
184
+ if (errors) {
199
185
  // rollback without errors to not trigger srv.on('error') with array
200
186
  await tx.rollback()
201
187
  // invoke srv.on('error') for each error and build failedRequests that reflects error modifications
202
188
  errors = odataContext.applicationData.errors[odataContext.id]
203
189
  const failedRequests = {}
190
+
204
191
  for (const e of errors) {
205
192
  const { error: err, req } = e
206
193
  for (const each of cdsService._handlers._error) each.handler.call(cdsService, err, req)
@@ -208,7 +195,26 @@ class OData {
208
195
  const { error, statusCode } = normalizeError(err, req)
209
196
  failedRequests[requestId] = Object.assign(error, { statusCode })
210
197
  }
198
+
211
199
  done(new Error(`Atomicity group "${odataContext.id}" failed`), { failedRequests })
200
+ return
201
+ }
202
+
203
+ try {
204
+ await tx.commit(odataContext.applicationData.results[odataContext.id])
205
+ done()
206
+ } catch (e) {
207
+ // tx gets rolled back automatically
208
+ // set error on each request of changeset, if commit failed
209
+ const changesetResults = odataContext.applicationData.results[odataContext.id]
210
+ const failedRequests = changesetResults.reduce((obj, resultEntry) => {
211
+ const requestId = resultEntry.req._.odataReq.getOdataRequestId()
212
+ const { error, statusCode } = normalizeError(e, resultEntry.req)
213
+ obj[requestId] = Object.assign(error, { statusCode })
214
+ return obj
215
+ }, {})
216
+
217
+ done(e, { failedRequests })
212
218
  }
213
219
  })
214
220
 
@@ -233,37 +239,33 @@ class OData {
233
239
  /**
234
240
  * Process request.
235
241
  *
236
- * @param req
237
- * @param res
242
+ * @param {http.IncomingMessage} req
243
+ * @param {http.ServerResponse} res
238
244
  * @private
239
245
  */
240
246
  // REVISIT: Remove this when we replaced Okra
241
247
  process(req, res) {
248
+ const headers = req.headers
249
+ const acceptHeader = headers && headers.accept
250
+
242
251
  // default to combination [...];IEEE754Compatible=true;ExponentialDecimals=true if one is omitted
243
- if (req.headers && req.headers.accept && req.headers.accept.startsWith('application/json')) {
244
- if (
245
- req.headers.accept.includes('IEEE754Compatible=true') &&
246
- !req.headers.accept.includes('ExponentialDecimals')
247
- ) {
252
+ if (acceptHeader && acceptHeader.startsWith('application/json')) {
253
+ if (acceptHeader.includes('IEEE754Compatible=true') && !acceptHeader.includes('ExponentialDecimals')) {
248
254
  req.headers.accept += ';ExponentialDecimals=true'
249
- } else if (
250
- req.headers.accept.includes('ExponentialDecimals=true') &&
251
- !req.headers.accept.includes('IEEE754Compatible')
252
- ) {
255
+ } else if (acceptHeader.includes('ExponentialDecimals=true') && !acceptHeader.includes('IEEE754Compatible')) {
253
256
  req.headers.accept += ';IEEE754Compatible=true'
254
257
  }
255
258
 
259
+ const contentType = headers['content-type']
260
+
256
261
  // add IEEE754Compatible=true if !strict_numbers
257
262
  if (
258
263
  !cds.env.features.strict_numbers &&
259
- req.headers['content-type'] &&
260
- req.headers['content-type'].includes('application/json') &&
261
- !req.headers['content-type'].includes('IEEE754Compatible')
264
+ contentType &&
265
+ contentType.includes('application/json') &&
266
+ !contentType.includes('IEEE754Compatible')
262
267
  ) {
263
- req.headers['content-type'] = req.headers['content-type'].replace(
264
- 'application/json',
265
- 'application/json;IEEE754Compatible=true'
266
- )
268
+ req.headers['content-type'] = contentType.replace('application/json', 'application/json;IEEE754Compatible=true')
267
269
  }
268
270
  }
269
271
 
@@ -206,7 +206,7 @@ class ODataRequest extends cds.Request {
206
206
  })
207
207
 
208
208
  // req.attr
209
- const attr = { identityZone: this.user.tenant }
209
+ const attr = { identityZone: this.tenant }
210
210
  Object.defineProperty(this, 'attr', {
211
211
  get() {
212
212
  if (!cds._deprecationWarningForAttr) {
@@ -9,7 +9,7 @@ const { getSapMessages } = require('../../../../common/error/frontend')
9
9
  const { validateResourcePath } = require('../utils/request')
10
10
  const { isReturnMinimal } = require('../utils/handlerUtils')
11
11
  const readAfterWrite = require('../utils/readAfterWrite')
12
- const { toODataResult, postProcess } = require('../utils/result')
12
+ const { toODataResult, postProcess, postProcessMinimal } = require('../utils/result')
13
13
  const { mergeJson } = require('../../../services/utils/compareJson')
14
14
 
15
15
  /**
@@ -44,6 +44,8 @@ const create = service => {
44
44
  }
45
45
 
46
46
  postProcess(req, odataRes, service, result)
47
+ } else {
48
+ postProcessMinimal(req, result)
47
49
  }
48
50
 
49
51
  if (changeset) {
@@ -93,6 +93,11 @@ const getErrorHandler = (crashOnError = true, srv) => {
93
93
  req = odataReq.getIncomingRequest()
94
94
  }
95
95
 
96
+ if (err.getRootCause && typeof err.getRootCause === 'function') {
97
+ // > an OKRA error
98
+ err = _betterOkraError(err)
99
+ }
100
+
96
101
  // invoke srv.on('error', function (err, req) { ... }) here in special situations
97
102
  // REVISIT: if for compat reasons, remove once cds^5.1
98
103
  if (srv._handlers._error) {
@@ -107,11 +112,6 @@ const getErrorHandler = (crashOnError = true, srv) => {
107
112
  }
108
113
  }
109
114
 
110
- if (err.getRootCause && typeof err.getRootCause === 'function') {
111
- // > an OKRA error
112
- err = _betterOkraError(err)
113
- }
114
-
115
115
  // add content id if not generated by okra ("~...")
116
116
  const contentId = odataReq.getOdataRequestId()
117
117
  if (contentId && !contentId.match(/^~/)) err['@Core.ContentID'] = contentId
@@ -36,7 +36,7 @@ const metadata = service => {
36
36
  try {
37
37
  const req = odataReq.getIncomingRequest()
38
38
 
39
- const tenant = req.user && req.user.tenant
39
+ const tenant = req.tenant
40
40
  // REVISIT: can we take locale from user, or is there some odata special wrt metadata?
41
41
  const locale = odataRes.getContract().getLocale()
42
42
 
@@ -1,6 +1,7 @@
1
1
  const cds = require('../../../../cds')
2
2
  const { SELECT } = cds.ql
3
3
  const ODataRequest = require('../ODataRequest')
4
+ const { rewriteExpandAsterisk } = require('../../../../common/utils/rewriteAsterisks')
4
5
 
5
6
  const {
6
7
  QueryOptions,
@@ -78,7 +79,7 @@ const _getCount = async (tx, readReq) => {
78
79
 
79
80
  // Copy CQN including from and where and changing columns
80
81
  const select = SELECT.from(readReq.query.SELECT.from)
81
- select.SELECT.columns = [{ func: 'count', args: [{ val: '1' }], as: '_counted_' }]
82
+ select.SELECT.columns = [{ func: 'count', args: [{ val: '1' }], as: '$count' }]
82
83
 
83
84
  if (readReq.query.SELECT.where) select.SELECT.where = readReq.query.SELECT.where
84
85
  if (readReq.query.SELECT.search) select.SELECT.search = readReq.query.SELECT.search
@@ -92,13 +93,13 @@ const _getCount = async (tx, readReq) => {
92
93
 
93
94
  // Define new CQN
94
95
  req.query = select
96
+ // todo check limit
97
+ const result = await tx.dispatch(req)
95
98
 
96
- let result = await tx.dispatch(req)
99
+ const count = (result[0] && (result[0].$count || result[0]._counted_)) || 0
97
100
 
98
101
  // Transform into scalar result
99
- result = result[0] && result[0]._counted_ ? result[0]._counted_ : 0
100
-
101
- return toODataResult(result)
102
+ return toODataResult(count)
102
103
  }
103
104
 
104
105
  /**
@@ -318,6 +319,7 @@ const _readStream = async (tx, req, segments) => {
318
319
  if (
319
320
  headers &&
320
321
  headers.accept &&
322
+ contentType &&
321
323
  !headers.accept.includes('*/*') &&
322
324
  !headers.accept.includes(contentType) &&
323
325
  !headers.accept.includes(contentType.split('/')[0] + '/*')
@@ -327,7 +329,7 @@ const _readStream = async (tx, req, segments) => {
327
329
 
328
330
  if (contentType) streamObj['*@odata.mediaContentType'] = contentType
329
331
  if (contentDisposition) {
330
- req._.odataRes.setHeader('Content-Disposition', `attachment; filename="${contentDisposition}"`)
332
+ req._.odataRes.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(contentDisposition)}"`)
331
333
  }
332
334
  return streamObj
333
335
  }
@@ -381,6 +383,7 @@ const _readAndTransform = (tx, req, odataReq) => {
381
383
  break
382
384
  }
383
385
  }
386
+
384
387
  return _readStream(tx, req, segments)
385
388
  }
386
389
 
@@ -431,11 +434,13 @@ const _getTarget = (ref, target, definitions) => {
431
434
  }
432
435
 
433
436
  const _getRestrictedExpand = (columns, target, definitions) => {
434
- if (!columns || !target) return
437
+ if (!columns || !target || columns === '*') return
435
438
 
436
439
  const annotation = target['@Capabilities.ExpandRestrictions.NonExpandableProperties']
437
440
  const restrictions = annotation && annotation.map(element => element['='])
438
441
 
442
+ rewriteExpandAsterisk(columns, target)
443
+
439
444
  for (const col of columns) {
440
445
  if (col.expand) {
441
446
  if (restrictions && restrictions.length !== 0) {
@@ -473,6 +478,7 @@ const read = service => {
473
478
  return next(e)
474
479
  }
475
480
 
481
+ // REVISIT: this should be in common/generic/auth.js with the rest of the access control stuff
476
482
  const restricted = _getRestrictedExpand(
477
483
  req.query.SELECT && req.query.SELECT.columns,
478
484
  req.target,
@@ -9,12 +9,18 @@ const {
9
9
  const { getSapMessages } = require('../../../../common/error/frontend')
10
10
  const { validateResourcePath } = require('../utils/request')
11
11
  const { isReturnMinimal } = require('../utils/handlerUtils')
12
- const { foreignKeyPropagations } = require('../../../../common/utils/foreignKeyPropagations')
13
12
  const readAfterWrite = require('../utils/readAfterWrite')
14
- const { toODataResult, postProcess } = require('../utils/result')
13
+ const { toODataResult, postProcess, postProcessMinimal } = require('../utils/result')
15
14
  const { hasOmitValuesPreference } = require('../utils/omitValues')
16
15
  const { mergeJson } = require('../../../services/utils/compareJson')
17
16
 
17
+ /*
18
+ const { isStreaming } = require('../utils/stream')
19
+ const { findCsnTargetFor } = require('../../../../common/utils/csn')
20
+ const { isActiveEntityRequested, removeIsActiveEntityRecursively } = require('../../../../fiori/utils/where')
21
+ const { ensureDraftsSuffix } = require('../../../../fiori/utils/handler')
22
+ */
23
+
18
24
  const _isUpsertAllowed = target => {
19
25
  return !(cds.env.runtime && cds.env.runtime.allow_upsert === false) && !(target && target._isDraftEnabled)
20
26
  }
@@ -36,53 +42,41 @@ const _infoForeignKeyInParent = (req, odataReq, odataRes, tx) => {
36
42
  }
37
43
 
38
44
  const navID = typeof nav === 'string' ? nav : nav.id
39
- const elementNav = tx.model.definitions[parent].elements[navID]
45
+ const navElement = tx.model.definitions[parent].elements[navID]
40
46
 
41
47
  // not a containment
42
- if (!elementNav['@odata.contained']) {
48
+ if (!navElement['@odata.contained']) {
43
49
  return info
44
50
  }
45
51
 
46
52
  const where = req.query.INSERT.into.ref[0].where
47
- return { parent, nav, where }
48
- }
49
-
50
- const _getParentKey = (parentKeyObj, parentKey, childKey, req) => {
51
- let parentKeyVal, parentUpdateRequired
52
-
53
- if (parentKeyObj.length !== 0 && parentKeyObj[0][parentKey] !== null) {
54
- parentKeyVal = parentKeyObj[0][parentKey]
55
- } else if (req.target.keys[childKey].type === 'cds.UUID') {
56
- parentUpdateRequired = true
57
- parentKeyVal = cds.utils.uuid()
58
- } else {
59
- throw new Error('Only keys of type UUID can be generated: ' + childKey)
60
- }
61
-
62
- return { parentKeyVal, parentUpdateRequired }
53
+ return { parent, navElement, where }
63
54
  }
64
55
 
65
56
  const _create = async (req, odataReq, odataRes, tx) => {
66
57
  let result
67
58
 
68
- const { parent, nav, where } = _infoForeignKeyInParent(req, odataReq, odataRes, tx)
69
- if (parent && nav && where) {
70
- const onKeys = foreignKeyPropagations(tx.model.definitions[parent].elements[nav])
71
- const parentKeys = onKeys.map(key => key.parentFieldName)
59
+ const { parent, navElement, where } = _infoForeignKeyInParent(req, odataReq, odataRes, tx)
60
+ if (parent && navElement && where) {
61
+ const onKeys = navElement._foreignKeys
62
+ const parentKeys = onKeys.filter(key => key.parentElement).map(key => key.parentElement.name)
72
63
  const parentKeyObj = await tx.run(SELECT.from(parent).columns(parentKeys).where(where))
73
64
 
74
65
  const parentUpdateObj = {}
75
66
  onKeys.forEach(key => {
76
- const { parentKeyVal, parentUpdateRequired } = _getParentKey(
77
- parentKeyObj,
78
- key.parentFieldName,
79
- key.childFieldName,
80
- req
81
- )
82
- odataReq.getBody()[key.childFieldName] = parentKeyVal
67
+ let parentKeyVal, parentUpdateRequired
68
+ if (parentKeyObj.length !== 0 && parentKeyObj[0][key.parentElement.name] !== null) {
69
+ parentKeyVal = parentKeyObj[0][key.parentElement.name]
70
+ } else if (key.childElement.type === 'cds.UUID' && key.childElement.key) {
71
+ parentUpdateRequired = true
72
+ parentKeyVal = cds.utils.uuid()
73
+ } else {
74
+ throw new Error('Only keys of type UUID can be generated: ' + key.childFieldName)
75
+ }
76
+ odataReq.getBody()[key.childElement.name] = parentKeyVal
83
77
 
84
78
  if (parentUpdateRequired) {
85
- parentUpdateObj[key.parentFieldName] = parentKeyVal
79
+ parentUpdateObj[key.parentElement.name] = parentKeyVal
86
80
  }
87
81
  })
88
82
 
@@ -107,7 +101,7 @@ const _updateThenCreate = async (req, odataReq, odataRes, tx) => {
107
101
  try {
108
102
  result = await tx.dispatch(req)
109
103
  } catch (e) {
110
- if (e.code === 404 && _isUpsertAllowed(req.target)) {
104
+ if ((e.code === 404 || e.status === 404 || e.statusCode === 404) && _isUpsertAllowed(req.target)) {
111
105
  // REVISIT: remove error (and child?) from tx.context? -> would require a unique req.id
112
106
  ;[result, req] = await _create(req, odataReq, odataRes, tx)
113
107
  } else {
@@ -127,6 +121,58 @@ const _readAfterWriteAndVirtuals = async (req, service, result) => {
127
121
  const _shouldReadPreviousResult = req =>
128
122
  req.event === 'UPDATE' && !isReturnMinimal(req) && hasOmitValuesPreference(req.headers.prefer, 'defaults')
129
123
 
124
+ /*
125
+ const _getEntity = (segments, model) => {
126
+ let entityName, namespace
127
+ const previous = segments[segments.length - 2]
128
+ if (previous.getKind() === 'ENTITY') {
129
+ entityName = previous.getEntitySet().getName()
130
+ namespace = previous.getEdmType().getFullQualifiedName().namespace
131
+ } else if (previous.getKind() === 'NAVIGATION.TO.ONE') {
132
+ entityName = previous.getTarget().getName()
133
+ namespace = previous.getTarget().getEntityType().getFullQualifiedName().namespace
134
+ }
135
+
136
+ if (entityName) {
137
+ return findCsnTargetFor(entityName, model, namespace)
138
+ }
139
+ }
140
+
141
+ const _getMediaType = entity => {
142
+ if (entity._hasPersistenceSkip) return
143
+
144
+ return Object.values(entity.elements).find(ele => ele['@Core.IsMediaType'])
145
+ }
146
+
147
+ const _getMediaTypeCQN = (mediaType, contentType, entity, req) => {
148
+ const where = req.query.UPDATE.entity.ref[0].where
149
+ const isActive = isActiveEntityRequested(where)
150
+ const data = {}
151
+ data[mediaType.name] = contentType
152
+ const cqn = UPDATE(entity).set(data)
153
+ cqn.UPDATE.where = removeIsActiveEntityRecursively(where)
154
+ if (!isActive) {
155
+ cqn.UPDATE.entity = ensureDraftsSuffix(entity.name)
156
+ }
157
+
158
+ return cqn
159
+ }
160
+
161
+ const _handleMediaType = async (odataReq, model, tx, req) => {
162
+ const segments = odataReq.getUriInfo().getPathSegments()
163
+ const contentType = odataReq._inRequest.headers['content-type']
164
+ if (isStreaming(segments) && contentType) {
165
+ const entity = _getEntity(segments, model)
166
+ if (entity && !entity['@cds.persistence.skip']) {
167
+ const mediaType = _getMediaType(entity)
168
+ if (mediaType) {
169
+ await tx.run(_getMediaTypeCQN(mediaType, contentType, entity, req))
170
+ }
171
+ }
172
+ }
173
+ }
174
+ */
175
+
130
176
  /**
131
177
  * The handler that will be registered with odata-v4.
132
178
  *
@@ -155,8 +201,10 @@ const update = service => {
155
201
 
156
202
  let result, err, commit
157
203
  try {
158
- let previousResult
204
+ // // REVISIT: should be handled somewhere else
205
+ // await _handleMediaType(odataReq, service.model, tx, req)
159
206
 
207
+ let previousResult
160
208
  if (_shouldReadPreviousResult(req)) {
161
209
  previousResult = await _readAfterWriteAndVirtuals(req, service, result)
162
210
  }
@@ -171,6 +219,8 @@ const update = service => {
171
219
  }
172
220
 
173
221
  postProcess(req, odataRes, service, result, previousResult)
222
+ } else {
223
+ postProcessMinimal(req, result)
174
224
  }
175
225
 
176
226
  if (changeset) {
@@ -42,10 +42,16 @@ class ExpressionToCQN {
42
42
  case EdmPrimitiveTypeKind.Double:
43
43
  return { val: parseFloat(value) }
44
44
  case EdmPrimitiveTypeKind.DateTimeOffset: {
45
- let val = new Date(value).toISOString()
46
- // cut off ms if cds.DateTime
47
- if (expression._cdsType === 'cds.DateTime') val = val.replace(/\.\d\d\dZ$/, 'Z')
48
- return { val }
45
+ try {
46
+ let val = new Date(value).toISOString()
47
+ // cut off ms if cds.DateTime
48
+ if (expression._cdsType === 'cds.DateTime') val = val.replace(/\.\d\d\dZ$/, 'Z')
49
+ return { val }
50
+ } catch (e) {
51
+ throw Object.assign(new Error(`The type 'Edm.DateTimeOffset' is not compatible with '${value}'`), {
52
+ status: 400
53
+ })
54
+ }
49
55
  }
50
56
  default:
51
57
  return { val: value }
@@ -195,17 +195,23 @@ const applyToCQN = (transformations, entity, model) => {
195
195
  _handleTransformation(transformation, entity, res)
196
196
  }
197
197
  for (const item of transformation.getGroupByItems()) {
198
- if (item.getPathSegments()[0].getKind() === 'COMPLEX.PROPERTY') {
198
+ const pathSegment = item.getPathSegments().length > 0 && item.getPathSegments()[0]
199
+ if (!pathSegment) {
200
+ throw getFeatureNotSupportedError(
201
+ 'Transformation "groupby" with query option $apply does not support this request'
202
+ )
203
+ }
204
+ if (pathSegment.getKind() === 'COMPLEX.PROPERTY') {
199
205
  throw getFeatureNotSupportedError(
200
206
  'Transformation "groupby" with query option $apply does not support complex properties'
201
207
  )
202
208
  // TODO support annotations Groupable
203
209
  // Odata spec: http://docs.oasis-open.org/odata/odata-data-aggregation-ext/v4.0/cs01/odata-data-aggregation-ext-v4.0-cs01.html#_Toc378326318
204
210
  // res.groupBy.push(_complexProperty(item.getPathSegments()))
205
- } else if (item.getPathSegments()[0].getProperty()) {
211
+ } else if (pathSegment.getProperty()) {
206
212
  const name = item.getPathSegments()[0].getProperty().getName()
207
213
  res.groupBy.push(name)
208
- } else if (item.getPathSegments()[0].getNavigationProperty()) {
214
+ } else if (pathSegment.getNavigationProperty()) {
209
215
  res.groupBy.push(_createNavGroupBy(item.getPathSegments()))
210
216
  }
211
217
  }
@@ -98,7 +98,7 @@ const _getInnerSelect = expandItem => {
98
98
  * @returns {Array}
99
99
  * @private
100
100
  */
101
- const _getSelectedElements = (expandItem, targetType, relatedEntity) => {
101
+ const _getSelectedElements = (expandItem, targetType, relatedEntity, options) => {
102
102
  if (cds.env.effective.odata.proxies || cds.env.effective.odata.xrefs) {
103
103
  // proxy target?
104
104
  let proxy = true
@@ -113,7 +113,9 @@ const _getSelectedElements = (expandItem, targetType, relatedEntity) => {
113
113
  let innerSelectItems = _getInnerSelect(expandItem)
114
114
 
115
115
  if (innerSelectItems.length === 0 || innerSelectItems.some(item => item.isAll())) {
116
- return _getColumnsFromTargetType(targetType, relatedEntity, true)
116
+ // REVISIT: Remove once we clean up our draft handling
117
+ if (options && options.rewriteAsterisks) return _getColumnsFromTargetType(targetType, relatedEntity, true)
118
+ return ['*']
117
119
  }
118
120
 
119
121
  // remove navigations from select clause
@@ -166,7 +168,7 @@ const _filter = (item, expression) => {
166
168
  item.where = SELECT.from('a').where(expressionToCQN.parse(expression)).SELECT.where
167
169
  }
168
170
 
169
- const _getItemCQN = (model, name, navigationProperty, expandItem) => {
171
+ const _getItemCQN = (model, name, navigationProperty, expandItem, options) => {
170
172
  _notSupported(expandItem)
171
173
 
172
174
  const targetType = navigationProperty.getEntityType()
@@ -176,7 +178,7 @@ const _getItemCQN = (model, name, navigationProperty, expandItem) => {
176
178
  const relatedEntity = findCsnTargetFor(entityName, model, namespace)
177
179
  const item = {
178
180
  ref: name, // ['structured', 'nested_', nestedAssocToOne] if expand on structured
179
- expand: _getSelectedElements(expandItem, targetType, relatedEntity)
181
+ expand: _getSelectedElements(expandItem, targetType, relatedEntity, options)
180
182
  }
181
183
 
182
184
  item.expand.push(..._getInnerExpandItems(model, expandItem, targetType))
@@ -222,7 +224,7 @@ const _name = expandItem =>
222
224
  * @param type
223
225
  * @returns {Array}
224
226
  */
225
- const expandToCQN = (model, expandItems, type) => {
227
+ const expandToCQN = (model, expandItems, type, options) => {
226
228
  const allElements = []
227
229
  const isAll = expandItems.some(item => item.isAll())
228
230
 
@@ -230,7 +232,7 @@ const expandToCQN = (model, expandItems, type) => {
230
232
  const expandItem = _getExpandItem(isAll, expandItems, name)
231
233
 
232
234
  if (isAll || expandItem) {
233
- allElements.push(_getItemCQN(model, [name], navigationProperty, expandItem))
235
+ allElements.push(_getItemCQN(model, [name], navigationProperty, expandItem, options))
234
236
  }
235
237
  }
236
238
 
@@ -1,7 +1,5 @@
1
1
  const cds = require('../../../../cds')
2
2
 
3
- const { _newReadToCQN } = require('../../../../odata/readToCqn')
4
-
5
3
  const {
6
4
  Components: { DATA_CREATE_HANDLER, DATA_DELETE_HANDLER, DATA_READ_HANDLER, DATA_UPDATE_HANDLER }
7
5
  } = require('../okra/odata-server')
@@ -32,7 +30,7 @@ module.exports = (component, service, target, data, odataReq, upsert) => {
32
30
  case DATA_DELETE_HANDLER:
33
31
  return deleteToCQN(service, odataReq)
34
32
  case DATA_READ_HANDLER:
35
- return odata2cqn ? _newReadToCQN(service, target, odataReq) : readToCQN(service, target, odataReq)
33
+ return odata2cqn ? cds.odata.parse(odataReq, { service }) : readToCQN(service, target, odataReq)
36
34
  case DATA_UPDATE_HANDLER:
37
35
  return updateToCQN(service, data, odataReq)
38
36
  case 'BOUND.ACTION':
@@ -2,9 +2,8 @@ const cds = require('../../../../cds')
2
2
  const { SELECT } = cds.ql
3
3
 
4
4
  const QueryOptions = require('../okra/odata-server').QueryOptions
5
- const { getColumns } = require('../../../services/utils/columns')
6
5
  const { isNavigation, isPathSupported } = require('./selectHelper')
7
- const { isViewWithParams, validationQuery } = require('./selectHelper')
6
+ const { isViewWithParams, getValidationQuery } = require('./selectHelper')
8
7
  const { ensureUnlocalized } = require('../../../../fiori/utils/handler')
9
8
  const ExpressionToCQN = require('./ExpressionToCQN')
10
9
  const orderByToCQN = require('./orderByToCQN')
@@ -16,6 +15,7 @@ const { resolveStructuredName } = require('../utils/handlerUtils')
16
15
  const { isStreaming } = require('../utils/stream')
17
16
  const { convertUrlPathToCqn, getAllKeys } = require('./utils')
18
17
  const { getMaxPageSize } = require('../../../../common/utils/page')
18
+ const { isAsteriskColumn } = require('../../../../common/utils/rewriteAsterisks')
19
19
 
20
20
  const {
21
21
  COUNT,
@@ -200,8 +200,10 @@ const _cleanupForApply = (apply, cqn) => {
200
200
  const selectColumns = cqn.SELECT.columns.map(c => c.as || (c.ref && c.ref[c.ref.length - 1]))
201
201
  if (cqn.SELECT.orderBy) {
202
202
  // include path expressions
203
- const newOrderBy = cqn.SELECT.orderBy.filter(o => _containsSelectedColumn(o, selectColumns))
204
- cqn.SELECT.orderBy = newOrderBy
203
+ if (!cqn.SELECT.columns.some(c => isAsteriskColumn(c))) {
204
+ const newOrderBy = cqn.SELECT.orderBy.filter(o => _containsSelectedColumn(o, selectColumns))
205
+ cqn.SELECT.orderBy = newOrderBy
206
+ }
205
207
  }
206
208
 
207
209
  if (!cqn.SELECT.orderBy || !cqn.SELECT.orderBy.length) {
@@ -229,16 +231,16 @@ const _checkViewWithParamCall = (isView, segments, kind, name) => {
229
231
  }
230
232
  }
231
233
 
232
- const enhanceCqnForNavigation = (segments, isView, cqn, service, SELECT, kind) => {
234
+ const addValidationQueryIfRequired = (segments, isView, cqn, service, kind) => {
233
235
  if (isNavigation(segments) && !isView && (kind === NAVIGATION_TO_MANY || kind === NAVIGATION_TO_ONE)) {
234
- cqn._validationQuery = validationQuery(segments, service.model, SELECT)
236
+ cqn._validationQuery = getValidationQuery(cqn.SELECT.from.ref, service.model)
235
237
  cqn._validationQuery.__navToManyWithKeys =
236
238
  kind === NAVIGATION_TO_ONE && segments[segments.length - 1].getKeyPredicates().length !== 0
237
239
  }
238
240
  }
239
241
 
240
242
  const _addKeysToSelectIfNoStreaming = (entity, select, streaming) => {
241
- // might also be signleton w/o keys
243
+ // might also be singleton w/o keys
242
244
  if (!streaming && entity.keys) {
243
245
  for (const k of Object.values(entity.keys)) {
244
246
  // REVISIT: !select.includes(k.name) needed?
@@ -370,9 +372,7 @@ const readToCQN = (service, target, odataReq) => {
370
372
  }
371
373
 
372
374
  if (select.length === 0) {
373
- select.push(
374
- ...getColumns(entity, { onlyNames: true, removeIgnore: true, filterDraft: false }).map(col => ({ ref: [col] }))
375
- )
375
+ select.push('*')
376
376
  }
377
377
 
378
378
  if (expand.length) {
@@ -388,7 +388,7 @@ const readToCQN = (service, target, odataReq) => {
388
388
 
389
389
  // keep target as input because of localized view
390
390
  const cqn = SELECT.from(isView ? _convertUrlPathToViewCqn(segments) : convertUrlPathToCqn(segments, service), select)
391
- enhanceCqnForNavigation(segments, isView, cqn, service, SELECT, kind)
391
+ addValidationQueryIfRequired(segments, isView, cqn, service, kind)
392
392
 
393
393
  if (Object.keys(apply).length) {
394
394
  _extendCqnWithApply(cqn, apply, entity)
@@ -409,6 +409,8 @@ const readToCQN = (service, target, odataReq) => {
409
409
  }
410
410
 
411
411
  _cleanupForApply(apply, cqn)
412
+ // just like in new parser
413
+ if (cqn.SELECT.columns.length === 1 && cqn.SELECT.columns[0] === '*') delete cqn.SELECT.columns
412
414
  return cqn
413
415
  }
414
416