@sap/cds 5.7.3 → 5.8.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 (151) hide show
  1. package/CHANGELOG.md +111 -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/minify.js +1 -1
  13. package/lib/compile/resolve.js +1 -1
  14. package/lib/compile/to/srvinfo.js +1 -1
  15. package/lib/core/classes.js +21 -1
  16. package/lib/env/index.js +3 -2
  17. package/lib/env/requires.js +4 -0
  18. package/lib/i18n/localize.js +5 -8
  19. package/lib/index.js +1 -0
  20. package/lib/log/errors.js +1 -1
  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/handlers/action.js +11 -38
  32. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +12 -5
  33. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +7 -4
  34. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +24 -3
  35. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +43 -38
  36. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +1 -1
  37. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +11 -5
  38. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +13 -7
  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/UriHelper.js +7 -6
  47. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriTokenizer.js +5 -8
  48. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +19 -47
  49. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +4 -11
  50. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/ResourceJsonDeserializer.js +7 -1
  51. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/CommandExecutor.js +0 -3
  52. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/ConditionalRequestControlCommand.js +0 -1
  53. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ContextURLFactory.js +2 -2
  54. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ErrorJsonSerializer.js +2 -0
  55. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/TrustedResourceJsonSerializer.js +2 -5
  56. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +6 -6
  57. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
  58. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +18 -5
  59. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +41 -17
  60. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +1 -17
  61. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +80 -21
  62. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +47 -10
  63. package/libx/_runtime/cds-services/adapter/rest/Rest.js +22 -1
  64. package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +8 -3
  65. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +3 -0
  66. package/libx/_runtime/cds-services/services/Service.js +1 -1
  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/update.js +39 -34
  76. package/libx/_runtime/common/error/frontend.js +19 -5
  77. package/libx/_runtime/common/generic/auth.js +20 -85
  78. package/libx/_runtime/common/generic/crud.js +22 -1
  79. package/libx/_runtime/common/i18n/messages.properties +2 -1
  80. package/libx/_runtime/common/utils/cqn.js +2 -6
  81. package/libx/_runtime/common/utils/cqn2cqn4sql.js +95 -122
  82. package/libx/_runtime/common/utils/csn.js +15 -4
  83. package/libx/_runtime/common/utils/foreignKeyPropagations.js +18 -1
  84. package/libx/_runtime/common/utils/keys.js +2 -1
  85. package/libx/_runtime/common/utils/path.js +1 -1
  86. package/libx/_runtime/common/utils/resolveView.js +12 -4
  87. package/libx/_runtime/common/utils/rewriteAsterisks.js +27 -13
  88. package/libx/_runtime/common/utils/search2cqn4sql.js +11 -6
  89. package/libx/_runtime/common/utils/structured.js +11 -5
  90. package/libx/_runtime/common/utils/vcap.js +27 -10
  91. package/libx/_runtime/db/data-conversion/post-processing.js +42 -35
  92. package/libx/_runtime/db/expand/expand-v2.js +21 -12
  93. package/libx/_runtime/db/expand/expandCQNToJoin.js +57 -29
  94. package/libx/_runtime/db/expand/index.js +3 -0
  95. package/libx/_runtime/db/generic/create.js +0 -10
  96. package/libx/_runtime/db/generic/index.js +3 -0
  97. package/libx/_runtime/db/generic/read.js +2 -24
  98. package/libx/_runtime/db/generic/rewrite.js +1 -3
  99. package/libx/_runtime/db/generic/update.js +1 -1
  100. package/libx/_runtime/db/query/delete.js +10 -4
  101. package/libx/_runtime/db/query/insert.js +3 -4
  102. package/libx/_runtime/db/query/read.js +4 -1
  103. package/libx/_runtime/db/query/update.js +5 -5
  104. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +9 -2
  105. package/libx/_runtime/db/sql-builder/FunctionBuilder.js +3 -0
  106. package/libx/_runtime/db/sql-builder/SelectBuilder.js +7 -3
  107. package/libx/_runtime/db/sql-builder/index.js +3 -0
  108. package/libx/_runtime/db/utils/columns.js +5 -2
  109. package/libx/_runtime/db/utils/deep.js +6 -8
  110. package/libx/_runtime/db/utils/generateAliases.js +56 -6
  111. package/libx/_runtime/fiori/generic/before.js +73 -49
  112. package/libx/_runtime/fiori/generic/edit.js +14 -18
  113. package/libx/_runtime/fiori/generic/patch.js +8 -11
  114. package/libx/_runtime/fiori/generic/read.js +22 -20
  115. package/libx/_runtime/fiori/generic/readOverDraft.js +1 -4
  116. package/libx/_runtime/fiori/utils/handler.js +1 -11
  117. package/libx/_runtime/hana/Service.js +1 -1
  118. package/libx/_runtime/hana/conversion.js +12 -1
  119. package/libx/_runtime/hana/execute.js +31 -16
  120. package/libx/_runtime/hana/localized.js +1 -1
  121. package/libx/_runtime/hana/search.js +3 -3
  122. package/libx/_runtime/hana/search2cqn4sql.js +23 -25
  123. package/libx/_runtime/hana/searchToContains.js +1 -1
  124. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +4 -2
  125. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +0 -1
  126. package/libx/_runtime/messaging/enterprise-messaging.js +1 -0
  127. package/libx/_runtime/messaging/file-based.js +3 -1
  128. package/libx/_runtime/messaging/service.js +16 -7
  129. package/libx/_runtime/remote/utils/client.js +37 -20
  130. package/libx/_runtime/remote/utils/data.js +53 -12
  131. package/libx/_runtime/sqlite/Service.js +1 -1
  132. package/libx/_runtime/sqlite/conversion.js +10 -0
  133. package/libx/_runtime/sqlite/localized.js +1 -1
  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 +50 -22
  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
@@ -3,10 +3,8 @@ const { SELECT } = cds.ql
3
3
 
4
4
  const { cqn2cqn4sql, convertWhereExists } = require('../../common/utils/cqn2cqn4sql')
5
5
  const { getElementDeep } = require('../../common/utils/csn')
6
-
7
6
  const { DRAFT_COLUMNS, DRAFT_COLUMNS_MAP, SCENARIO } = require('../../common/constants/draft')
8
7
  const {
9
- adaptStreamCQN,
10
8
  addColumnAlias,
11
9
  draftIsLocked,
12
10
  ensureDraftsSuffix,
@@ -18,8 +16,8 @@ const {
18
16
  filterKeys
19
17
  } = require('../utils/handler')
20
18
  const { deleteCondition, readAndDeleteKeywords, removeIsActiveEntityRecursively } = require('../utils/where')
21
-
22
19
  const { getColumns } = require('../../cds-services/services/utils/columns')
20
+ const { adaptStreamCQN } = require('../../cds-services/adapter/odata-v4/utils/stream')
23
21
 
24
22
  const _findRootSubSelectFor = query => {
25
23
  if (query.SELECT.where) {
@@ -50,7 +48,8 @@ const _getWhereWithAppendedDraftRestrictions = (where = [], req, scenarioAlias,
50
48
  })
51
49
 
52
50
  if (where.length) where.push('and')
53
- where.push(...xpr)
51
+ // restriction might contain or clause -> use xpr for grouping
52
+ xpr.includes('or') ? where.push({ xpr }) : where.push(...xpr)
54
53
  } else {
55
54
  // > restriction inherited from parent via autoexposure
56
55
  // find inner most sub select if available and append restriction to where clause
@@ -778,13 +777,16 @@ const _getDraftDoc = (req, draftName, draftWhere) => {
778
777
  return draftDocs
779
778
  }
780
779
 
781
- const _getOrderByEnrichedColumns = (orderBy, columns) => {
780
+ const _getOrderByEnrichedColumns = (orderBy, columns, entity) => {
782
781
  const enrichedCol = []
783
782
  if (orderBy && orderBy.length > 1) {
784
783
  const colNames = columns.map(el => el.ref[el.ref.length - 1])
785
784
  // REVISIT: GET Books?$select=title&$expand=NotBooks($select=pages)&$orderby=NotBooks/title - what's then?
786
785
  for (const el of orderBy) {
787
- if (!DRAFT_COLUMNS.includes(el.ref[el.ref.length - 1]) && !colNames.includes(el.ref[el.ref.length - 1])) {
786
+ // For associations we need to 'materialise' the resulting field, otherwise we cannot access it in an outer SELECT.
787
+ if (entity && entity.elements[el.ref[0]] && entity.elements[el.ref[0]].isAssociation) {
788
+ enrichedCol.push({ ref: [...el.ref], as: _poorMansAlias4(el) })
789
+ } else if (!DRAFT_COLUMNS.includes(el.ref[el.ref.length - 1]) && !colNames.includes(el.ref[el.ref.length - 1])) {
788
790
  enrichedCol.push({ ref: [...el.ref] })
789
791
  }
790
792
  }
@@ -806,11 +808,11 @@ const _replaceDraftAlias = where => {
806
808
 
807
809
  const _poorMansAlias4 = xpr => '_' + xpr.ref.join('_') + '_'
808
810
 
809
- const _getUnionCQN = (req, draftName, columns, subSelect, draftWhere, model) => {
811
+ const _getUnionCQN = (req, draftName, columns, subSelect, draftWhere, model, entity) => {
810
812
  const draftActiveWhere = _getWhereForActive(draftWhere)
811
813
  const activeDocs = getEnrichedCQN(SELECT.from(req.target), req.query.SELECT, draftActiveWhere, undefined, false)
812
814
  activeDocs.where(_getWhereWithAppendedDraftRestrictions([], req))
813
- convertWhereExists(activeDocs, model, {})
815
+ convertWhereExists(activeDocs.SELECT, model, {})
814
816
 
815
817
  // @restrict.where not applicable for drafts (I can ALWAYS read mine)
816
818
  _replaceDraftAlias(draftWhere)
@@ -837,7 +839,7 @@ const _getUnionCQN = (req, draftName, columns, subSelect, draftWhere, model) =>
837
839
  return union.columns({ func: 'sum', args: [{ ref: ['$count'] }], as: '$count' })
838
840
  }
839
841
 
840
- const enrichedColumns = _getOrderByEnrichedColumns(req.query.SELECT.orderBy, columns)
842
+ const enrichedColumns = _getOrderByEnrichedColumns(req.query.SELECT.orderBy, columns, entity)
841
843
 
842
844
  for (const col of enrichedColumns) {
843
845
  // if we have columns for outer order by that may also be needed for joins, we need to duplicate them
@@ -914,12 +916,14 @@ const _excludeActiveDraftExists = (req, draftWhere, columns, model) => {
914
916
  ])
915
917
  .where(_inProcessByUserWhere(req.user.id))
916
918
 
919
+ const targetName = ensureNoDraftsSuffix(req.target.name)
917
920
  for (const key of _getTargetKeys(req)) {
918
- subSelect.where([{ ref: [ensureNoDraftsSuffix(req.target.name), key] }, '=', { ref: [draftName, key] }])
921
+ subSelect.where([{ ref: [targetName, key] }, '=', { ref: [draftName, key] }])
919
922
  }
920
923
 
924
+ const entity = model.definitions[targetName]
921
925
  draftWhere = removeIsActiveEntityRecursively(draftWhere)
922
- const cqn = _getUnionCQN(req, draftName, columns, subSelect, draftWhere, model)
926
+ const cqn = _getUnionCQN(req, draftName, columns, subSelect, draftWhere, model, entity)
923
927
  cqn.SELECT.from.as = name
924
928
 
925
929
  if (cqn.SELECT.orderBy) {
@@ -959,13 +963,16 @@ const _validatedWithSiblingInProcess = (req, draftWhere, draftParameters, column
959
963
  )
960
964
  return _excludeActiveDraftExists(req, draftWhere, columns, model)
961
965
  if (
966
+ draftInProcessByUser &&
962
967
  draftInProcessByUser.op === '!=' &&
963
968
  _isValidWithDraftLocked(isActiveEntity, siblingIsActive, draftInProcessByUser)
964
969
  ) {
965
970
  return _activeWithDraftInProcess(req, draftWhere, columns, req.user.id)
966
- } else if (_isValidWithDraftTimeout(isActiveEntity, siblingIsActive, draftInProcessByUser)) {
971
+ } else if (draftInProcessByUser && _isValidWithDraftTimeout(isActiveEntity, siblingIsActive, draftInProcessByUser)) {
967
972
  return _activeWithDraftInProcess(req, draftWhere, columns, null)
968
973
  }
974
+
975
+ //
969
976
  }
970
977
 
971
978
  const _validatedDraftOfWhichIAmOwner = (req, draftWhere, draftParameters, columns) =>
@@ -1214,16 +1221,13 @@ const _handler = async function (req) {
1214
1221
  // handle localized here as it was previously handled for req.target
1215
1222
  req.target = _getLocalizedEntity(this.model, req.target, req.user) || req.target
1216
1223
 
1217
- // REVISIT
1218
- delete req.query._validationQuery
1219
-
1220
1224
  const originalFrom = _copyCQNPartial(req.query.SELECT.from)
1221
1225
 
1222
1226
  // REVISIT DRAFT HANDLING: cqn2cqn4sql must not be called here
1223
- const sqlQuery = cqn2cqn4sql(req.query, this.model, { draft: true })
1227
+ const query4sql = cqn2cqn4sql(req.query, this.model, { _4fiori: true })
1224
1228
 
1225
1229
  // do not clone with Object.assign as that would skip all non-enumerable properties
1226
- const reqClone = { __proto__: req, query: _copyCQNPartial(sqlQuery) }
1230
+ const reqClone = { __proto__: req, query: _copyCQNPartial(query4sql) }
1227
1231
 
1228
1232
  // ensure draft restrictions are copied to new query
1229
1233
  reqClone.query._draftRestrictions = req.query._draftRestrictions
@@ -1233,6 +1237,7 @@ const _handler = async function (req) {
1233
1237
  reqClone.query._streaming = true
1234
1238
  return cds.tx(req).run(reqClone.query)
1235
1239
  }
1240
+
1236
1241
  let cqnScenario
1237
1242
 
1238
1243
  // to replace scenario CQNs for queries with $apply SELECT chain (new odata2cqn parser)
@@ -1263,16 +1268,13 @@ const _handler = async function (req) {
1263
1268
  )
1264
1269
 
1265
1270
  _adaptSubSelects(cqnScenario.cqn, cqnScenario.scenario)
1266
-
1267
1271
  _adaptAnnotationAliases(cqnScenario.cqn)
1268
1272
 
1269
1273
  // unlocalize for db and after handlers as it was before
1270
1274
  req.target = this.model.definitions[ensureUnlocalized(req.target.name)]
1271
1275
 
1272
1276
  const result = await cds.tx(req).send({ query: cqnScenario.cqn, target: req.target })
1273
-
1274
1277
  const resultAsArray = Array.isArray(result) ? result : result ? [result] : []
1275
-
1276
1278
  removeDraftUUIDIfNecessary(resultAsArray, req)
1277
1279
 
1278
1280
  if (cqnScenario.scenario === SCENARIO.DRAFT_ADMIN) {
@@ -45,7 +45,7 @@ const _handler = async function (req) {
45
45
  }
46
46
 
47
47
  // REVISIT DRAFT HANDLING: cqn2cqn4sql must not be called here
48
- const sqlQuery = cqn2cqn4sql(req.query, this.model, { draft: true })
48
+ const sqlQuery = cqn2cqn4sql(req.query, this.model, { _4fiori: true })
49
49
  if (req.query._streaming) {
50
50
  sqlQuery._streaming = true
51
51
  }
@@ -53,9 +53,6 @@ const _handler = async function (req) {
53
53
  const hasDraftEntity = hasDraft(this.model.definitions, sqlQuery)
54
54
 
55
55
  if (hasDraftEntity && sqlQuery.SELECT.where && sqlQuery.SELECT.where.length !== 0) {
56
- // REVISIT
57
- delete req.query._validationQuery
58
-
59
56
  let cqnDraft = SELECT.from({
60
57
  ref: [...sqlQuery.SELECT.from.ref],
61
58
  as: sqlQuery.SELECT.from.as
@@ -1,6 +1,5 @@
1
1
  const cds = require('../../cds')
2
2
  const { UPDATE, SELECT } = cds.ql
3
- const { removeIsActiveEntityRecursively, isActiveEntityRequested } = require('./where')
4
3
  const { getColumns } = require('../../cds-services/services/utils/columns')
5
4
  const { ensureNoDraftsSuffix, ensureDraftsSuffix, ensureUnlocalized } = require('../../common/utils/draft')
6
5
  const getTemplate = require('../../common/utils/template')
@@ -126,7 +125,7 @@ const getEnrichedCQN = (cqn, select, draftWhere, scenarioAlias, addLimitOrder =
126
125
  }
127
126
 
128
127
  if (select.distinct) {
129
- cqn.distinct()
128
+ cqn.SELECT.distinct = true
130
129
  }
131
130
 
132
131
  const alias = (select.from && select.from.as) || scenarioAlias
@@ -249,14 +248,6 @@ const replaceRefWithDraft = ref => {
249
248
  ref[0] = ensureDraftsSuffix(ref[0])
250
249
  }
251
250
 
252
- const adaptStreamCQN = cqn => {
253
- if (isActiveEntityRequested(cqn.SELECT.where)) {
254
- cqn.SELECT.where = removeIsActiveEntityRecursively(cqn.SELECT.where)
255
- } else {
256
- replaceRefWithDraft(cqn.SELECT.from.ref)
257
- }
258
- }
259
-
260
251
  const draftIsLocked = lastChangedAt => {
261
252
  // default timeout timer is 15 minutes
262
253
  const DRAFT_CANCEL_TIMEOUT_IN_MS = ((cds.env.drafts && cds.env.drafts.cancellationTimeout) || 15) * 60 * 1000
@@ -289,7 +280,6 @@ module.exports = {
289
280
  hasDraft,
290
281
  proxifyToNoDraftsName,
291
282
  addColumnAlias,
292
- adaptStreamCQN,
293
283
  replaceRefWithDraft,
294
284
  getKeyProperty,
295
285
  filterKeys,
@@ -41,7 +41,7 @@ class HanaDatabase extends DatabaseService {
41
41
  this._insert = this._queries.insert(execute.insert)
42
42
  this._read = this._queries.read(execute.select, execute.stream)
43
43
  this._update = this._queries.update(execute.update, execute.select)
44
- this._delete = this._queries.delete(execute.delete)
44
+ this._delete = this._queries.delete(execute.delete, execute.update)
45
45
  this._run = this._queries.run(this._insert, this._read, this._update, this._delete, execute.cqn, execute.sql)
46
46
  }
47
47
 
@@ -1,3 +1,5 @@
1
+ const cds = require('../cds')
2
+
1
3
  const convertToBoolean = boolean => {
2
4
  if (boolean === null) {
3
5
  return null
@@ -44,9 +46,18 @@ const HANA_TYPE_CONVERSION_MAP = new Map([
44
46
  ['cds.Integer64', convertInt64ToString],
45
47
  ['cds.DateTime', convertToISONoMillis],
46
48
  ['cds.Timestamp', convertToISO],
47
- ['cds.LargeString', convertToString]
49
+ ['cds.LargeString', convertToString],
50
+ ['cds.hana.CLOB', convertToString]
48
51
  ])
49
52
 
53
+ if (cds.env.features.bigjs) {
54
+ const Big = require('big.js')
55
+ const convertToBig = value => new Big(value)
56
+
57
+ HANA_TYPE_CONVERSION_MAP.set('cds.Integer64', convertToBig)
58
+ HANA_TYPE_CONVERSION_MAP.set('cds.Decimal', convertToBig)
59
+ }
60
+
50
61
  module.exports = {
51
62
  HANA_TYPE_CONVERSION_MAP
52
63
  }
@@ -1,3 +1,6 @@
1
+ const cds = require('../cds')
2
+ const LOG = cds.log('hana|db|sql')
3
+
1
4
  const { HANA_TYPE_CONVERSION_MAP } = require('./conversion')
2
5
  const CustomBuilder = require('./customBuilder')
3
6
  const { sqlFactory } = require('../db/sql-builder/')
@@ -30,9 +33,6 @@ function _cqnToSQL(model, query, user, locale, txTimestamp) {
30
33
  )
31
34
  }
32
35
 
33
- const cds = require('../cds')
34
- const LOG = cds.log('hana|db|sql')
35
-
36
36
  const SANITIZE_VALUES = process.env.NODE_ENV === 'production' && cds.env.log.sanitize_values !== false
37
37
 
38
38
  function _getOutputParameters(stmt) {
@@ -48,6 +48,24 @@ function _getOutputParameters(stmt) {
48
48
  return Object.keys(result).length > 0 ? result : undefined
49
49
  }
50
50
 
51
+ const BINARY_TYPES = {
52
+ 12: 'BINARY',
53
+ 13: 'VARBINARY',
54
+ 27: 'BLOB'
55
+ }
56
+
57
+ function _getBinaries(stmt) {
58
+ // hdb vs. @sap/hana-client
59
+ const parameters = stmt.parameterMetadata || stmt.getParameterInfo()
60
+ const typeKey = stmt.parameterMetadata ? 'dataType' : 'nativeType'
61
+ return parameters.reduce((acc, cur, i) => {
62
+ if (BINARY_TYPES[cur[typeKey]]) acc.push(i)
63
+ return acc
64
+ }, [])
65
+ }
66
+
67
+ const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}={2})$/
68
+
51
69
  function _executeAsPreparedStatement(dbc, sql, values, reject, resolve) {
52
70
  dbc.prepare(sql, function (err, stmt) {
53
71
  if (err) {
@@ -56,18 +74,15 @@ function _executeAsPreparedStatement(dbc, sql, values, reject, resolve) {
56
74
  return reject(err)
57
75
  }
58
76
 
59
- // REVISIT: adjust binary values on hdb
60
- if (_hasValues(values) && dbc.name === 'hdb' && stmt.parameterMetadata) {
61
- const vals = Array.isArray(values[0]) ? values : [values]
62
- for (const row of vals) {
63
- for (let i = 0; i < stmt.parameterMetadata.length; i++) {
64
- /*
65
- * BINARY: 12
66
- * VARBINARY: 13
67
- */
68
- if (stmt.parameterMetadata[i].dataType === 12 || stmt.parameterMetadata[i].dataType === 13) {
69
- if (row[i] && !Buffer.isBuffer(row[i])) {
70
- row[i] = Buffer.from(row[i].match(/.{1,2}/g).map(val => parseInt(val, 16)))
77
+ // convert binary strings to buffers ()
78
+ if (cds.env.hana.base64_to_buffer !== false && _hasValues(values)) {
79
+ const binaries = _getBinaries(stmt)
80
+ if (binaries.length) {
81
+ const vals = Array.isArray(values[0]) ? values : [values]
82
+ for (const i of binaries) {
83
+ for (const row of vals) {
84
+ if (row[i] && typeof row[i] === 'string' && row[i].match(BASE64)) {
85
+ row[i] = Buffer.from(row[i], 'base64')
71
86
  }
72
87
  }
73
88
  }
@@ -108,7 +123,7 @@ function _executeSimpleSQL(dbc, sql, values) {
108
123
  values = Object.values(values)
109
124
  }
110
125
  // ensure that stored procedure with parameters is always executed as prepared
111
- if (_hasValues(sql, values) || sql.match(/^call.*?\?.*$/i)) {
126
+ if (_hasValues(values) || sql.match(/^call.*?\?.*$/i)) {
112
127
  _executeAsPreparedStatement(dbc, sql, values, reject, resolve)
113
128
  } else {
114
129
  dbc.exec(sql, function (err, result, procedureReturn) {
@@ -31,7 +31,7 @@ const localizedHandler = function (req) {
31
31
  // suppress localization in "select for update"
32
32
  if (query.SELECT.forUpdate) return
33
33
 
34
- redirect(query.SELECT, getLocalize(req.locale, this.model))
34
+ redirect(query, getLocalize(req.locale, this.model))
35
35
  }
36
36
 
37
37
  localizedHandler._initial = true
@@ -11,9 +11,9 @@ function searchHandler(req) {
11
11
  // REVISIT: remove feature toggle optimized_search after grace period
12
12
  // inject the search2cqn4sql module into the rewrite handler only when
13
13
  // the optimized search feature toggle is turned on
14
- if (!cds.env.features.optimized_search) return
15
-
16
- _setSearchOptions(req.query, { search2cqn4sql, locale: req.locale })
14
+ if (cds.env.features.optimized_search) {
15
+ _setSearchOptions(req.query, { search2cqn4sql, locale: req.locale })
16
+ }
17
17
  }
18
18
 
19
19
  // handlers marked with `._initial = true` run in sequence
@@ -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,56 +46,54 @@ 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
68
73
  }
69
74
 
70
- const _getLocalizedAssociation = entity => {
71
- const associations = entity.associations
72
- return associations && associations.localized
73
- }
75
+ const _getLocalizedAssociation = entity => entity.associations && entity.associations.localized
74
76
 
75
77
  // The inner join modifies the original SELECT ... FROM query and adds ambiguity,
76
78
  // therefore add the table/entity name (as a preceding element) to the columns ref
77
79
  // to prevent a SQL ambiguity error.
78
- const _addAliasToColumns = (query, entity, columnsToBeSearched) => {
80
+ const _addAliasToQuery = (query, entity, columnsToBeSearched) => {
81
+ const SELECT = query.SELECT
79
82
  const localizedEntityName = _getLocalizedAssociation(entity).target
80
83
  const elements = entity.elements
81
84
  const entityName = entity.name
82
- const _addAliasToColumn = (entityName, localizedEntityName, elements) => column => {
83
- const columnRef = column.ref
84
- if (!columnRef) return column
85
+ const getEntityName = columnRef => {
85
86
  const columnName = columnRef[0]
86
- const localizedElement = elements[columnName].localized
87
+ const localizedElement = !!elements[columnName].localized
87
88
  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))
89
+ return targetEntityName
96
90
  }
97
91
 
98
- return columns
92
+ SELECT.columns = addAliasToExpression(SELECT.columns, getEntityName)
93
+ columnsToBeSearched = addAliasToExpression(columnsToBeSearched, getEntityName)
94
+ SELECT.groupBy = addAliasToExpression(SELECT.groupBy, getEntityName)
95
+ SELECT.where = addAliasToExpression(SELECT.where, getEntityName)
96
+ return columnsToBeSearched
99
97
  }
100
98
 
101
99
  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
@@ -58,8 +58,10 @@ class AMQPWebhookMessaging extends MessagingService {
58
58
  await super.emit(msg)
59
59
  done()
60
60
  } catch (e) {
61
- failed()
62
- LOG._error && LOG.error(e)
61
+ // In case of AMQP and Solace, the `failed` callback must be called
62
+ // with an error, otherwise there are problems with the redelivery count.
63
+ failed(new Error('processing failed'))
64
+ LOG.error('ERROR occured in asynchronous event processing:', e)
63
65
  }
64
66
  })
65
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) {
@@ -26,17 +26,24 @@ class MessagingService extends OutboxService {
26
26
  this.subscribedTopics = new Map()
27
27
  // Only for one central `messaging` service, otherwise all technical services would register themselves
28
28
  if (this.name === 'messaging') {
29
+ this._registeredServices = new Map()
29
30
  // listen for all subscriptions to declared events of remote, i.e. connected services
30
31
  cds.on('subscribe', (srv, event) => {
31
32
  const declared = srv.events[event]
32
33
  if (declared && srv.name in cds.requires && !srv.mocked) {
33
34
  // we register self-handlers for declared events, which are supposed
34
35
  // to be calles by subclasses calling this.dispatch on incoming events
36
+ let registeredEvents = this._registeredServices.get(srv.name)
37
+ if (!registeredEvents) {
38
+ registeredEvents = new Set()
39
+ this._registeredServices.set(srv.name, registeredEvents)
40
+ }
41
+ if (registeredEvents.has(event)) return
42
+ registeredEvents.add(event)
35
43
  const topic = _topic(declared)
36
- this.on(topic, async (msg, next) => {
44
+ this.on(topic, msg => {
37
45
  const { data, headers } = msg
38
- await srv.tx(msg).emit({ event, data, headers, __proto__: msg })
39
- return next()
46
+ return srv.tx(msg).emit({ event, data, headers, __proto__: msg })
40
47
  })
41
48
  }
42
49
  })
@@ -48,10 +55,9 @@ class MessagingService extends OutboxService {
48
55
  // calls to srv.emit are forwarded to this.emit, which is expected to
49
56
  // be overwritten by subclasses to write events to message channel
50
57
  const topic = _topic(declared)
51
- srv.on(event, async (msg, next) => {
58
+ srv.on(event, msg => {
52
59
  const { data, headers } = msg
53
- await this.tx(msg).emit({ event: topic, data, headers })
54
- return next()
60
+ return this.tx(msg).emit({ event: topic, data, headers })
55
61
  })
56
62
  }
57
63
  })
@@ -69,7 +75,8 @@ class MessagingService extends OutboxService {
69
75
  }
70
76
 
71
77
  emit(event, data, headers) {
72
- 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))
73
80
  return super.emit(msg)
74
81
  }
75
82
 
@@ -102,6 +109,8 @@ class MessagingService extends OutboxService {
102
109
 
103
110
  message4(msg) {
104
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()
105
114
  _msg.event = _warnAndStripTopicPrefix(_msg.event)
106
115
  if (!_msg.headers) _msg.headers = {}
107
116
  if (!_msg.inbound) {