@sap/cds 5.7.5 → 5.8.0

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 (141) hide show
  1. package/CHANGELOG.md +72 -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/ql/SELECT.js +2 -2
  21. package/lib/req/cds-context.js +1 -1
  22. package/lib/req/context.js +1 -1
  23. package/lib/serve/Transaction.js +9 -5
  24. package/lib/serve/index.js +13 -21
  25. package/lib/utils/tests.js +90 -66
  26. package/libx/_runtime/audit/generic/personal/modification.js +0 -8
  27. package/libx/_runtime/auth/index.js +7 -6
  28. package/libx/_runtime/auth/strategies/dwc.js +43 -0
  29. package/libx/_runtime/auth/utils.js +24 -0
  30. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +11 -32
  31. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +12 -5
  32. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +7 -4
  33. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +24 -3
  34. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +43 -38
  35. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +1 -1
  36. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +11 -5
  37. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/boundToCQN.js +1 -2
  38. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/deleteToCQN.js +0 -1
  39. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -2
  40. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/orderByToCQN.js +9 -0
  41. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +17 -30
  42. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +12 -1
  43. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/edm/AbstractEdmStructuredType.js +2 -1
  44. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriHelper.js +7 -6
  45. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriTokenizer.js +2 -5
  46. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +19 -47
  47. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +4 -11
  48. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/ResourceJsonDeserializer.js +7 -1
  49. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/CommandExecutor.js +0 -3
  50. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/ConditionalRequestControlCommand.js +0 -1
  51. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ContextURLFactory.js +1 -1
  52. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/TrustedResourceJsonSerializer.js +2 -5
  53. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +6 -6
  54. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
  55. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -4
  56. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +41 -17
  57. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +1 -17
  58. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +60 -18
  59. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +7 -5
  60. package/libx/_runtime/cds-services/adapter/rest/Rest.js +22 -1
  61. package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +8 -3
  62. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +3 -0
  63. package/libx/_runtime/cds-services/services/utils/columns.js +5 -1
  64. package/libx/_runtime/cds-services/services/utils/compareJson.js +15 -16
  65. package/libx/_runtime/cds-services/services/utils/differ.js +2 -8
  66. package/libx/_runtime/common/aspects/Association.js +16 -0
  67. package/libx/_runtime/common/composition/data.js +28 -37
  68. package/libx/_runtime/common/composition/delete.js +107 -58
  69. package/libx/_runtime/common/composition/index.js +2 -1
  70. package/libx/_runtime/common/composition/insert.js +13 -13
  71. package/libx/_runtime/common/composition/update.js +39 -34
  72. package/libx/_runtime/common/error/frontend.js +17 -2
  73. package/libx/_runtime/common/generic/auth.js +20 -85
  74. package/libx/_runtime/common/generic/crud.js +22 -1
  75. package/libx/_runtime/common/i18n/messages.properties +2 -1
  76. package/libx/_runtime/common/utils/cqn.js +2 -6
  77. package/libx/_runtime/common/utils/cqn2cqn4sql.js +95 -122
  78. package/libx/_runtime/common/utils/csn.js +14 -3
  79. package/libx/_runtime/common/utils/foreignKeyPropagations.js +18 -1
  80. package/libx/_runtime/common/utils/keys.js +2 -1
  81. package/libx/_runtime/common/utils/path.js +1 -1
  82. package/libx/_runtime/common/utils/resolveView.js +12 -4
  83. package/libx/_runtime/common/utils/rewriteAsterisks.js +27 -13
  84. package/libx/_runtime/common/utils/search2cqn4sql.js +11 -6
  85. package/libx/_runtime/common/utils/vcap.js +27 -10
  86. package/libx/_runtime/db/data-conversion/post-processing.js +20 -13
  87. package/libx/_runtime/db/expand/expand-v2.js +21 -12
  88. package/libx/_runtime/db/expand/expandCQNToJoin.js +8 -6
  89. package/libx/_runtime/db/expand/index.js +3 -0
  90. package/libx/_runtime/db/generic/create.js +0 -10
  91. package/libx/_runtime/db/generic/index.js +3 -0
  92. package/libx/_runtime/db/generic/read.js +2 -24
  93. package/libx/_runtime/db/generic/rewrite.js +1 -3
  94. package/libx/_runtime/db/generic/update.js +1 -1
  95. package/libx/_runtime/db/query/delete.js +10 -4
  96. package/libx/_runtime/db/query/insert.js +3 -3
  97. package/libx/_runtime/db/query/read.js +4 -1
  98. package/libx/_runtime/db/query/update.js +5 -5
  99. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +9 -2
  100. package/libx/_runtime/db/sql-builder/FunctionBuilder.js +3 -0
  101. package/libx/_runtime/db/sql-builder/index.js +3 -0
  102. package/libx/_runtime/db/utils/columns.js +5 -2
  103. package/libx/_runtime/db/utils/deep.js +6 -8
  104. package/libx/_runtime/db/utils/generateAliases.js +56 -6
  105. package/libx/_runtime/fiori/generic/before.js +73 -49
  106. package/libx/_runtime/fiori/generic/edit.js +14 -18
  107. package/libx/_runtime/fiori/generic/patch.js +8 -11
  108. package/libx/_runtime/fiori/generic/read.js +19 -16
  109. package/libx/_runtime/fiori/generic/readOverDraft.js +1 -4
  110. package/libx/_runtime/hana/Service.js +1 -1
  111. package/libx/_runtime/hana/conversion.js +10 -0
  112. package/libx/_runtime/hana/execute.js +33 -16
  113. package/libx/_runtime/hana/search.js +3 -3
  114. package/libx/_runtime/hana/search2cqn4sql.js +22 -21
  115. package/libx/_runtime/hana/searchToContains.js +1 -1
  116. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +1 -1
  117. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +0 -1
  118. package/libx/_runtime/messaging/enterprise-messaging.js +1 -0
  119. package/libx/_runtime/messaging/file-based.js +3 -1
  120. package/libx/_runtime/messaging/service.js +4 -1
  121. package/libx/_runtime/remote/utils/client.js +33 -20
  122. package/libx/_runtime/remote/utils/data.js +52 -11
  123. package/libx/_runtime/sqlite/Service.js +1 -1
  124. package/libx/_runtime/sqlite/conversion.js +10 -0
  125. package/libx/_runtime/types/api.js +2 -2
  126. package/libx/gql/resolvers/parse/ast/enrich.js +1 -0
  127. package/libx/odata/afterburner.js +29 -6
  128. package/libx/odata/cqn2odata.js +9 -0
  129. package/libx/odata/grammar.pegjs +49 -21
  130. package/libx/odata/index.js +2 -2
  131. package/libx/odata/parser.js +1 -1
  132. package/libx/odata/utils.js +2 -2
  133. package/libx/rest/RestAdapter.js +29 -1
  134. package/libx/rest/middleware/auth.js +1 -3
  135. package/libx/rest/middleware/parse.js +1 -0
  136. package/package.json +1 -1
  137. package/server.js +1 -1
  138. package/bin/deploy/to-hana/logger.js +0 -27
  139. package/bin/deploy/to-hana/runCommand.js +0 -113
  140. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/selectHelper.js +0 -37
  141. package/libx/_runtime/common/utils/auth.js +0 -16
@@ -1,6 +1,7 @@
1
1
  const { computeColumnsToBeSearched } = require('../cds-services/services/utils/columns')
2
2
  const searchToLike = require('../common/utils/searchToLike')
3
3
  const { isContainsPredicateSupported, searchToContains } = require('./searchToContains')
4
+ const { addAliasToExpression } = require('../db/utils/generateAliases')
4
5
 
5
6
  /**
6
7
  * Computes a CQN expression for a search query.
@@ -33,8 +34,7 @@ const search2cqn4sql = (query, entity, options) => {
33
34
  // suppress the localize handler from redirecting the query's target to the localized view
34
35
  Object.defineProperty(query, '_suppressLocalization', { value: true })
35
36
 
36
- // do not join if subquery exists - already done in there
37
- if (resolveLocalizedDataAtRuntime && !query.SELECT.from.SELECT) {
37
+ if (resolveLocalizedDataAtRuntime) {
38
38
  const onCondition = entity._relations[localizedAssociation.name].join(localizedAssociation.target, entity.name)
39
39
 
40
40
  // replace $user_locale placeholder with the user locale or the HANA session context
@@ -46,22 +46,27 @@ const search2cqn4sql = (query, entity, options) => {
46
46
  query.join(localizedEntityName).on(onCondition)
47
47
 
48
48
  // prevent SQL ambiguity error for columns with the same name
49
- columnsToBeSearched = _addAliasToColumns(query, entity, columnsToBeSearched)
49
+ columnsToBeSearched = _addAliasToQuery(query, entity, columnsToBeSearched)
50
50
  } // else --> resolve localized texts via localized view (default)
51
51
 
52
52
  const useContains = isContainsPredicateSupported(query)
53
53
  let expression
54
54
 
55
55
  if (useContains) {
56
- const funcCols = columnsToBeSearched.filter(col => col.func)
57
- const refCols = columnsToBeSearched.filter(col => !col.func)
58
- expression = [searchToContains(cqnSearchPhrase, refCols)]
59
- if (funcCols.length) expression.push('or', ...searchToLike(cqnSearchPhrase, funcCols))
56
+ const namedColumns = columnsToBeSearched.filter(col => !col.func)
57
+ expression = searchToContains(cqnSearchPhrase, namedColumns)
58
+
59
+ // func columns handling
60
+ if (columnsToBeSearched.length > namedColumns.length) {
61
+ const funcColumns = columnsToBeSearched.filter(col => col.func)
62
+ expression = [expression, 'or', ...searchToLike(cqnSearchPhrase, funcColumns)]
63
+ }
60
64
  } else {
61
65
  // No CONTAINS optimization possible. The search implementation for localized
62
66
  // texts falls back to the LIKE predicate.
63
67
  expression = searchToLike(cqnSearchPhrase, columnsToBeSearched)
64
68
  }
69
+
65
70
  // REVISIT: find out here if where or having must be used
66
71
  query._aggregated || /* if new parser */ query.SELECT.groupBy ? query.having(expression) : query.where(expression)
67
72
  return query
@@ -75,27 +80,23 @@ const _getLocalizedAssociation = entity => {
75
80
  // The inner join modifies the original SELECT ... FROM query and adds ambiguity,
76
81
  // therefore add the table/entity name (as a preceding element) to the columns ref
77
82
  // to prevent a SQL ambiguity error.
78
- const _addAliasToColumns = (query, entity, columnsToBeSearched) => {
83
+ const _addAliasToQuery = (query, entity, columnsToBeSearched) => {
84
+ const SELECT = query.SELECT
79
85
  const localizedEntityName = _getLocalizedAssociation(entity).target
80
86
  const elements = entity.elements
81
87
  const entityName = entity.name
82
- const _addAliasToColumn = (entityName, localizedEntityName, elements) => column => {
83
- const columnRef = column.ref
84
- if (!columnRef) return column
88
+ const getEntityName = columnRef => {
85
89
  const columnName = columnRef[0]
86
- const localizedElement = elements[columnName].localized
90
+ const localizedElement = !!elements[columnName].localized
87
91
  const targetEntityName = localizedElement ? localizedEntityName : entityName
88
- return { ref: [targetEntityName, columnName] }
89
- }
90
-
91
- query.SELECT.columns = query.SELECT.columns.map(_addAliasToColumn(entityName, localizedEntityName, elements))
92
- const columns = columnsToBeSearched.map(_addAliasToColumn(entityName, localizedEntityName, elements))
93
-
94
- if (query.SELECT.groupBy) {
95
- query.SELECT.groupBy = query.SELECT.groupBy.map(_addAliasToColumn(entityName, localizedEntityName, elements))
92
+ return targetEntityName
96
93
  }
97
94
 
98
- return columns
95
+ SELECT.columns = addAliasToExpression(SELECT.columns, getEntityName)
96
+ columnsToBeSearched = addAliasToExpression(columnsToBeSearched, getEntityName)
97
+ SELECT.groupBy = addAliasToExpression(SELECT.groupBy, getEntityName)
98
+ SELECT.where = addAliasToExpression(SELECT.where, getEntityName)
99
+ return columnsToBeSearched
99
100
  }
100
101
 
101
102
  module.exports = search2cqn4sql
@@ -25,7 +25,7 @@
25
25
  * @returns {import("../types/api").searchContainsExp} The `CONTAINS` CQN expression
26
26
  */
27
27
  const searchToContains = (cqnSearchPhrase, columns) => {
28
- // serialize CQN search phrase
28
+ // serialize the CQN search phrase
29
29
  const searchString = cqnSearchPhrase.reduce((searchStringAccumulator, currentValue) => {
30
30
  // Multiple search terms separated by an space are automatically
31
31
  // interpreted as an AND operator. Therefore, it is not mandatory
@@ -61,7 +61,7 @@ class AMQPWebhookMessaging extends MessagingService {
61
61
  // In case of AMQP and Solace, the `failed` callback must be called
62
62
  // with an error, otherwise there are problems with the redelivery count.
63
63
  failed(new Error('processing failed'))
64
- LOG._error && LOG.error(e)
64
+ LOG.error('ERROR occured in asynchronous event processing:', e)
65
65
  }
66
66
  })
67
67
  }
@@ -65,7 +65,6 @@ class EndpointRegistry {
65
65
  const other = authInfo
66
66
  ? {
67
67
  _: { req: { authInfo, headers: {}, query: {} } }, // for messaging to retrieve subdomain
68
- user: new cds.User.Privileged(),
69
68
  tenant: tenantId
70
69
  }
71
70
  : {}
@@ -20,6 +20,7 @@ const _checkAppURL = appURL => {
20
20
  throw new Error(
21
21
  'Enterprise Messaging: You need to provide an HTTPS endpoint to your application.\n\nHint: You can set the application URI in environment variable `VCAP_APPLICATION.application_uris[0]`. This is needed because incoming messages are delivered through HTTP via webhooks.\nExample: `{ ..., "VCAP_APPLICATION": { "application_uris": ["my-app.com"] } }`\nIn case you want to use Enterprise Messaging in shared (that means single-tenant) mode, you can use kind `enterprise-messaging-amqp`.'
22
22
  )
23
+
23
24
  if (appURL.startsWith('https://localhost'))
24
25
  throw new Error(
25
26
  'The endpoint of your application is local and cannot be reached from Enterprise Messaging.\n\nHint: For local development you can set up a tunnel to your local endpoint and enter its public https endpoint in `VCAP_APPLICATION.application_uris[0]`.\nIn case you want to use Enterprise Messaging in shared (that means single-tenant) mode, you can use kind `enterprise-messaging-amqp`.'
@@ -53,7 +53,9 @@ class FileBasedMessaging extends MessagingService {
53
53
  if (this.subscribedTopics.has(topic)) {
54
54
  const event = this.subscribedTopics.get(topic)
55
55
  if (!event) return
56
- super.emit({ event, ...json, inbound: true }).catch(e => LOG._debug && LOG.debug(e))
56
+ super
57
+ .emit({ event, ...json, inbound: true })
58
+ .catch(e => LOG.error('ERROR occured in asynchronous event processing:', e))
57
59
  } else other.push(each + '\n')
58
60
  }
59
61
  } catch (e) {
@@ -75,7 +75,8 @@ class MessagingService extends OutboxService {
75
75
  }
76
76
 
77
77
  emit(event, data, headers) {
78
- const msg = event instanceof cds.Event ? event : new cds.Event(this.message4(event, data, headers))
78
+ const _msg = typeof event === 'object' ? event : { event, data, headers }
79
+ const msg = _msg instanceof cds.Event ? _msg : new cds.Event(this.message4(_msg))
79
80
  return super.emit(msg)
80
81
  }
81
82
 
@@ -108,6 +109,8 @@ class MessagingService extends OutboxService {
108
109
 
109
110
  message4(msg) {
110
111
  const _msg = { ...msg }
112
+ if (msg.inbound && !cds.context)
113
+ _msg.user = msg.tenant ? new cds.User.Privileged({ tenant: msg.tenant }) : new cds.User.Privileged()
111
114
  _msg.event = _warnAndStripTopicPrefix(_msg.event)
112
115
  if (!_msg.headers) _msg.headers = {}
113
116
  if (!_msg.inbound) {
@@ -5,7 +5,7 @@ const SANITIZE_VALUES = process.env.NODE_ENV === 'production' && cds.env.log.san
5
5
 
6
6
  const cdsLocale = require('../../../../lib/req/locale')
7
7
 
8
- const { convertV2ResponseData, deepSanitize } = require('./data')
8
+ const { convertV2ResponseData, deepSanitize, convertV2PayloadData } = require('./data')
9
9
 
10
10
  let _cloudSdkCore
11
11
 
@@ -30,6 +30,7 @@ const _executeHttpRequest = async ({ requestConfig, destination, destinationOpti
30
30
  const destinationName = typeof destination === 'string' && destination
31
31
  if (destinationName) {
32
32
  destination = await getDestination(destinationName, resolveDestinationOptions(destinationOptions, jwt))
33
+ if (!destination) throw new Error(`Cannot resolve destination "${destinationName}"`)
33
34
  } else if (destination.forwardAuthToken) {
34
35
  destination = {
35
36
  ...destination,
@@ -166,10 +167,10 @@ const _purgeODataV2 = (data, target, reqHeaders) => {
166
167
  if (typeof data !== 'object' || !data.d) return data
167
168
 
168
169
  data = data.d
169
- const contentType = reqHeaders['content-type']
170
- const ieee754Compatible = contentType && contentType.includes('IEEE754Compatible=true')
170
+ const ieee754Compatible = reqHeaders.accept && reqHeaders.accept.includes('IEEE754Compatible=true')
171
+ const exponentialDecimals = ieee754Compatible && reqHeaders.accept.includes('ExponentialDecimals=true')
171
172
  const purgedResponse = data.results || data
172
- const convertedResponse = convertV2ResponseData(purgedResponse, target, ieee754Compatible)
173
+ const convertedResponse = convertV2ResponseData(purgedResponse, target, ieee754Compatible, exponentialDecimals)
173
174
  return _normalizeMetadata(/^__/, data, convertedResponse)
174
175
  }
175
176
 
@@ -321,32 +322,37 @@ const getJwt = req => {
321
322
  return null
322
323
  }
323
324
 
324
- const _cqnToReqOptions = (query, kind, model) => {
325
+ const _cqnToReqOptions = (query, kind, model, target) => {
325
326
  const queryObject = cds.odata.urlify(query, { kind, model })
326
- return {
327
+ const reqOptions = {
327
328
  method: queryObject.method,
328
329
  url: encodeURI(
329
330
  queryObject.path
330
331
  // ugly workaround for Okra not allowing spaces in ( x eq 1 )
331
332
  .replace(/\( /g, '(')
332
333
  .replace(/ \)/g, ')')
333
- ),
334
- data: queryObject.body
334
+ )
335
+ }
336
+ if (queryObject.method !== 'GET' && queryObject.method !== 'HEAD') {
337
+ reqOptions.data = kind === 'odata-v2' ? convertV2PayloadData(queryObject.body, target) : queryObject.body
335
338
  }
339
+ return reqOptions
336
340
  }
337
341
 
338
- const _stringToReqOptions = (query, data) => {
342
+ const _stringToReqOptions = (query, data, target) => {
339
343
  const cleanQuery = query.trim()
340
344
  const blankIndex = cleanQuery.substring(0, 8).indexOf(' ')
341
345
  const reqOptions = {
342
346
  method: cleanQuery.substring(0, blankIndex).toUpperCase(),
343
347
  url: encodeURI(formatPath(cleanQuery.substring(blankIndex, cleanQuery.length).trim()))
344
348
  }
345
- if (data && reqOptions.method !== 'GET' && reqOptions.method !== 'HEAD') reqOptions.data = data
349
+ if (data && reqOptions.method !== 'GET' && reqOptions.method !== 'HEAD') {
350
+ reqOptions.data = this.kind === 'odata-v2' ? Object.assign({}, convertV2PayloadData(data, target)) : data
351
+ }
346
352
  return reqOptions
347
353
  }
348
354
 
349
- const _pathToReqOptions = (method, path, data) => {
355
+ const _pathToReqOptions = (method, path, data, target) => {
350
356
  let url = path
351
357
  if (!url.startsWith('/')) {
352
358
  // extract entity name and instance identifier (either in "()" or after "/") from fully qualified path
@@ -358,7 +364,9 @@ const _pathToReqOptions = (method, path, data) => {
358
364
  url = url.replace(/^\/\//, '/')
359
365
  }
360
366
  const reqOptions = { method, url }
361
- if (data && reqOptions.method !== 'GET' && reqOptions.method !== 'HEAD') reqOptions.data = data
367
+ if (data && reqOptions.method !== 'GET' && reqOptions.method !== 'HEAD') {
368
+ reqOptions.data = this.kind === 'odata-v2' ? Object.assign({}, convertV2PayloadData(data, target)) : data
369
+ }
362
370
  return reqOptions
363
371
  }
364
372
 
@@ -367,13 +375,14 @@ const _hasHeader = (headers, header) =>
367
375
  .map(k => k.toLowerCase())
368
376
  .includes(header)
369
377
 
378
+ // eslint-disable-next-line complexity
370
379
  const getReqOptions = (req, query, service) => {
371
380
  const reqOptions =
372
381
  typeof query === 'object'
373
- ? _cqnToReqOptions(query, service.kind, service.model)
382
+ ? _cqnToReqOptions(query, service.kind, service.model, req.target)
374
383
  : typeof query === 'string'
375
- ? _stringToReqOptions(query, req.data)
376
- : _pathToReqOptions(req.method, req.path, req.data)
384
+ ? _stringToReqOptions(query, req.data, req.target)
385
+ : _pathToReqOptions(req.method, req.path, req.data, req.target)
377
386
 
378
387
  reqOptions.headers = { accept: 'application/json,text/plain' }
379
388
  reqOptions.timeout = service.requestTimeout
@@ -387,6 +396,12 @@ const getReqOptions = (req, query, service) => {
387
396
  if (locale) reqOptions.headers['accept-language'] = locale
388
397
  }
389
398
 
399
+ // forward all dwc-* headers
400
+ if (service.options.forward_dwc_headers) {
401
+ const originalHeaders = (req.context && req.context._ && req.context._.req && req.context._.req.headers) || {}
402
+ for (const k in originalHeaders) if (k.match(/^dwc-/)) reqOptions.headers[k] = originalHeaders[k]
403
+ }
404
+
390
405
  if (reqOptions.data && reqOptions.method !== 'GET' && reqOptions.method !== 'HEAD') {
391
406
  reqOptions.headers['content-type'] = 'application/json'
392
407
  reqOptions.headers['content-length'] = Buffer.byteLength(JSON.stringify(reqOptions.data))
@@ -394,11 +409,9 @@ const getReqOptions = (req, query, service) => {
394
409
  reqOptions.url = formatPath(reqOptions.url)
395
410
 
396
411
  // batch envelope if needed
397
- if (
398
- KINDS_SUPPORTING_BATCH[service.kind] &&
399
- reqOptions.method === 'GET' &&
400
- reqOptions.url.length > ((cds.env.remote && cds.env.remote.max_get_url_length) || 1028)
401
- ) {
412
+ const maxGetUrlLength =
413
+ service.options.max_get_url_length || (cds.env.remote && cds.env.remote.max_get_url_length) || 1028
414
+ if (KINDS_SUPPORTING_BATCH[service.kind] && reqOptions.method === 'GET' && reqOptions.url.length > maxGetUrlLength) {
402
415
  reqOptions._autoBatch = true
403
416
  reqOptions.data = [
404
417
  '--batch1',
@@ -1,3 +1,5 @@
1
+ const { big } = require('@sap/cds-foss')
2
+
1
3
  // Code adopted from @sap/cds-odata-v2-adapter-proxy
2
4
  // https://www.w3.org/TR/xmlschema11-2/#nt-duDTFrag
3
5
  const DurationRegex = /^P(?:(\d)Y)?(?:(\d{1,2})M)?(?:(\d{1,2})D)?T(?:(\d{1,2})H)?(?:(\d{2})M)?(?:(\d{2}(?:\.\d+)?)S)?$/i
@@ -20,15 +22,16 @@ const DataTypeOData = {
20
22
  Time: 'cds.TimeOfDay'
21
23
  }
22
24
 
23
- const _convertData = (data, target, ieee754Compatible) => {
25
+ const _convertData = (data, target, convertValueFn) => {
26
+ const _convertRecordFn = _getConvertRecordFn(target, convertValueFn)
24
27
  if (Array.isArray(data)) {
25
- return data.map(record => _getConvertRecordFn(target, ieee754Compatible)(record))
28
+ return data.map(_convertRecordFn)
26
29
  }
27
30
 
28
- return _getConvertRecordFn(target, ieee754Compatible)(data)
31
+ return _convertRecordFn(data)
29
32
  }
30
33
 
31
- const _getConvertRecordFn = (target, ieee754Compatible) => record => {
34
+ const _getConvertRecordFn = (target, convertValueFn) => record => {
32
35
  for (const key in record) {
33
36
  if (key === '__metadata') continue
34
37
 
@@ -36,24 +39,26 @@ const _getConvertRecordFn = (target, ieee754Compatible) => record => {
36
39
  if (!element) continue
37
40
 
38
41
  const recordValue = record[key]
39
- const type = _elementType(element)
40
42
  const value = (recordValue && recordValue.results) || recordValue
41
43
 
42
44
  if (value && (element.isAssociation || Array.isArray(value))) {
43
- record[key] = _convertData(value, element._target, ieee754Compatible)
45
+ record[key] = _convertData(value, element._target, convertValueFn)
44
46
  } else {
45
- record[key] = _convertValue(value, type, ieee754Compatible)
47
+ record[key] = convertValueFn(value, element)
46
48
  }
47
49
  }
48
50
 
49
51
  return record
50
52
  }
51
53
 
52
- const _convertValue = (value, type, ieee754Compatible) => {
54
+ // eslint-disable-next-line complexity
55
+ const _convertValue = (ieee754Compatible, exponentialDecimals) => (value, element) => {
53
56
  if (value == null) {
54
57
  return value
55
58
  }
56
59
 
60
+ const type = _elementType(element)
61
+
57
62
  if (['cds.Boolean'].includes(type)) {
58
63
  if (value === 'true') {
59
64
  value = true
@@ -63,7 +68,14 @@ const _convertValue = (value, type, ieee754Compatible) => {
63
68
  } else if (['cds.Integer'].includes(type)) {
64
69
  value = parseInt(value, 10)
65
70
  } else if (['cds.Decimal', 'cds.Integer64', 'cds.DecimalFloat'].includes(type)) {
66
- value = ieee754Compatible ? `${value}` : parseFloat(value)
71
+ const bigValue = big(value)
72
+ if (ieee754Compatible) {
73
+ // TODO test with arrayed => element.items.scale?
74
+ value = exponentialDecimals ? bigValue.toExponential(element.scale) : bigValue.toFixed(element.scale)
75
+ } else {
76
+ // OData V2 does not even mention ieee754Compatible, but V4 requires JSON number if ieee754Compatible=false
77
+ value = bigValue.toNumber()
78
+ }
67
79
  } else if (['cds.Double'].includes(type)) {
68
80
  value = parseFloat(value)
69
81
  } else if (['cds.Time'].includes(type)) {
@@ -90,6 +102,29 @@ const _convertValue = (value, type, ieee754Compatible) => {
90
102
  return value
91
103
  }
92
104
 
105
+ const _convertPayloadValue = (value, element) => {
106
+ const type = _elementType(element)
107
+
108
+ // see https://www.odata.org/documentation/odata-version-2-0/json-format/
109
+ if (value == null) return value
110
+ switch (type) {
111
+ case 'cds.Date':
112
+ case 'cds.DateTime':
113
+ return `/Date(${new Date(value).getTime()})/`
114
+ case 'cds.Binary':
115
+ case 'cds.LargeBinary':
116
+ return Buffer.isBuffer(value) ? value.toString('base64') : value
117
+ case 'cds.Timestamp':
118
+ // According to OData V2 spec, and as cds.DateTime => (V2) Edm.DateTimeOffset => cds.Timestamp,
119
+ // cds.Timestamp should be converted into Edm.DateTimeOffset literal form `datetimeoffset'${new Date(value).toISOString()}'`
120
+ // However, odata-v2-proxy forwards it literaly as `datetimeoffset'...'` which is rejected by okra.
121
+ // Note that OData V2 spec example also does not contain 'datetimeoffset' predicate.
122
+ return new Date(value).toISOString()
123
+ default:
124
+ return value
125
+ }
126
+ }
127
+
93
128
  const _calculateTicksOffsetSum = text => {
94
129
  return (text.replace(/\s/g, '').match(/[+-]?([0-9]+)/g) || []).reduce((sum, value, index) => {
95
130
  return sum + parseFloat(value) * (index === 0 ? 1 : 60 * 1000) // ticks are milliseconds (0), offset are minutes (1)
@@ -115,9 +150,14 @@ const _elementType = element => {
115
150
  return type
116
151
  }
117
152
 
118
- const convertV2ResponseData = (data, target, ieee754Compatible) => {
153
+ const convertV2ResponseData = (data, target, ieee754Compatible, exponentialDecimals) => {
154
+ if (!target || !target.elements) return data
155
+ return _convertData(data, target, _convertValue(ieee754Compatible, exponentialDecimals))
156
+ }
157
+
158
+ const convertV2PayloadData = (data, target) => {
119
159
  if (!target || !target.elements) return data
120
- return _convertData(data, target, ieee754Compatible)
160
+ return _convertData(data, target, _convertPayloadValue)
121
161
  }
122
162
 
123
163
  const deepSanitize = arg => {
@@ -132,5 +172,6 @@ const deepSanitize = arg => {
132
172
 
133
173
  module.exports = {
134
174
  convertV2ResponseData,
175
+ convertV2PayloadData,
135
176
  deepSanitize
136
177
  }
@@ -39,7 +39,7 @@ module.exports = class SQLiteDatabase extends DatabaseService {
39
39
  this._insert = this._queries.insert(execute.insert)
40
40
  this._read = this._queries.read(execute.select, execute.stream)
41
41
  this._update = this._queries.update(execute.update, execute.select)
42
- this._delete = this._queries.delete(execute.delete)
42
+ this._delete = this._queries.delete(execute.delete, execute.update)
43
43
  this._run = this._queries.run(this._insert, this._read, this._update, this._delete, execute.cqn, execute.sql)
44
44
 
45
45
  this.dbcs = new Map()
@@ -1,3 +1,5 @@
1
+ const cds = require('../cds')
2
+
1
3
  const convertToBoolean = boolean => {
2
4
  if (boolean === null) return null
3
5
 
@@ -41,4 +43,12 @@ const SQLITE_TYPE_CONVERSION_MAP = new Map([
41
43
  ['cds.Timestamp', convertToISOTime]
42
44
  ])
43
45
 
46
+ if (cds.env.features.bigjs) {
47
+ const Big = require('big.js')
48
+ const convertToBig = value => new Big(value)
49
+
50
+ SQLITE_TYPE_CONVERSION_MAP.set('cds.Integer64', convertToBig)
51
+ SQLITE_TYPE_CONVERSION_MAP.set('cds.Decimal', convertToBig)
52
+ }
53
+
44
54
  module.exports = { SQLITE_TYPE_CONVERSION_MAP }
@@ -94,14 +94,14 @@
94
94
 
95
95
  /**
96
96
  * @typedef {object} search2cqnOptions
97
- * @property {ColumnRefs} [columns] The columns to
98
- * be searched
97
+ * @property {ColumnRefs} [columns] The columns to be searched
99
98
  * @property {string} locale The user locale
100
99
  */
101
100
 
102
101
  /**
103
102
  * @typedef {object} cqn2cqn4sqlOptions
104
103
  * @property {boolean} [suppressSearch=false] Indicates whether the search handler is called.
104
+ * @property {boolean} [_4fiori]
105
105
  * @property {import('../db/Service')} [service]
106
106
  */
107
107
 
@@ -45,6 +45,7 @@ const traverseField = (info, field) => {
45
45
  const traverseFieldNodes = (info, fieldNodes) => fieldNodes.map(fieldNode => traverseField(info, fieldNode))
46
46
 
47
47
  module.exports = info => {
48
+ // REVISIT: JSON.parse(JSON.stringify(obj)) breaks buffers
48
49
  const deepClonedFieldNodes = JSON.parse(JSON.stringify(info.fieldNodes))
49
50
  traverseFieldNodes(info, deepClonedFieldNodes)
50
51
  return deepClonedFieldNodes
@@ -3,12 +3,23 @@ const cds = require('../_runtime/cds')
3
3
  const { where2obj } = require('../_runtime/common/utils/cqn')
4
4
  const { findCsnTargetFor } = require('../_runtime/common/utils/csn')
5
5
 
6
+ const _addKeysDeep = (keys, keysCollector) => {
7
+ for (const keyName in keys) {
8
+ const key = keys[keyName]
9
+ if (key.type === 'cds.Association' || key['@odata.foreignKey4'] === 'up_') continue
10
+ if ('elements' in key) {
11
+ _addKeysDeep(key.elements, keysCollector)
12
+ continue
13
+ }
14
+ keysCollector.push(keyName)
15
+ }
16
+ }
17
+
6
18
  function _keysOf(entity) {
7
- return entity && entity.keys
8
- ? Object.keys(entity.keys).filter(
9
- k => entity.elements[k].type !== 'cds.Association' && entity.elements[k]['@odata.foreignKey4'] !== 'up_'
10
- )
11
- : []
19
+ if (!entity || !entity.keys) return
20
+ const keysCollector = []
21
+ _addKeysDeep(entity.keys, keysCollector)
22
+ return keysCollector
12
23
  }
13
24
 
14
25
  function _getDefinition(definition, name, namespace) {
@@ -49,7 +60,7 @@ function _processSegments(cqn, model, namespace) {
49
60
  let one
50
61
  for (let i = 0; i < ref.length; i++) {
51
62
  const seg = ref[i].id || ref[i]
52
- const params = ref[i].where && where2obj(ref[i].where)
63
+ let params = ref[i].where && where2obj(ref[i].where)
53
64
 
54
65
  if (incompleteKeys) {
55
66
  // > key
@@ -91,6 +102,9 @@ function _processSegments(cqn, model, namespace) {
91
102
  if (ref[i].where) {
92
103
  keyCount += addRefToWhereIfNecessary(ref[i].where, current)
93
104
  _resolveAliasInWhere(ref[i].where, current)
105
+ // in case of Foo(1), params will be {} (before addRefToWhereIfNecessary was called)
106
+ if (!Object.keys(params).length) params = where2obj(ref[i].where)
107
+ _checkAllKeysProvided(params, current)
94
108
  }
95
109
  } else if ({ action: 1, function: 1 }[current.kind]) {
96
110
  // > action or function
@@ -146,6 +160,15 @@ function _processSegments(cqn, model, namespace) {
146
160
 
147
161
  const _resolveFrom = from => (from.SELECT ? _resolveFrom(from.SELECT.from) : from)
148
162
 
163
+ const _checkAllKeysProvided = (params, entity) => {
164
+ const keysOfEntity = _keysOf(entity)
165
+ if (!keysOfEntity) return
166
+ for (const keyOfEntity of keysOfEntity) {
167
+ if (!(keyOfEntity in params))
168
+ throw Object.assign(new Error(`Key "${keyOfEntity}" is missing for entity "${entity.name}"`), { status: 400 })
169
+ }
170
+ }
171
+
149
172
  function _4service(service) {
150
173
  const { namespace, model } = service
151
174
 
@@ -357,6 +357,15 @@ function $orderBy(orderBy) {
357
357
  if (hasValidProps(cur, 'ref')) {
358
358
  res.push(cur.ref.join('/'))
359
359
  }
360
+
361
+ if (hasValidProps(cur, 'func', 'sort')) {
362
+ res.push(`${cur.func}(${_args(cur.args)})` + ' ' + cur.sort)
363
+ continue
364
+ }
365
+
366
+ if (hasValidProps(cur, 'func')) {
367
+ res.push(`${cur.func}(${_args(cur.args)})`)
368
+ }
360
369
  }
361
370
 
362
371
  return '$orderby=' + res.join(',')