@sap/cds 5.7.2 → 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 (148) hide show
  1. package/CHANGELOG.md +108 -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 -42
  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 +18 -8
  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 +7 -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 +21 -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 +1 -1
  54. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/TrustedResourceJsonSerializer.js +2 -5
  55. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +6 -6
  56. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
  57. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +18 -5
  58. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +41 -17
  59. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +1 -17
  60. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +80 -21
  61. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +7 -5
  62. package/libx/_runtime/cds-services/adapter/rest/Rest.js +22 -1
  63. package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +8 -3
  64. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +3 -0
  65. package/libx/_runtime/cds-services/services/Service.js +1 -1
  66. package/libx/_runtime/cds-services/services/utils/columns.js +5 -1
  67. package/libx/_runtime/cds-services/services/utils/compareJson.js +15 -16
  68. package/libx/_runtime/cds-services/services/utils/differ.js +2 -8
  69. package/libx/_runtime/common/aspects/Association.js +16 -0
  70. package/libx/_runtime/common/composition/data.js +28 -37
  71. package/libx/_runtime/common/composition/delete.js +107 -58
  72. package/libx/_runtime/common/composition/index.js +2 -1
  73. package/libx/_runtime/common/composition/insert.js +13 -13
  74. package/libx/_runtime/common/composition/update.js +39 -34
  75. package/libx/_runtime/common/error/frontend.js +17 -2
  76. package/libx/_runtime/common/generic/auth.js +20 -85
  77. package/libx/_runtime/common/generic/crud.js +22 -1
  78. package/libx/_runtime/common/i18n/messages.properties +3 -0
  79. package/libx/_runtime/common/utils/cqn.js +2 -6
  80. package/libx/_runtime/common/utils/cqn2cqn4sql.js +97 -123
  81. package/libx/_runtime/common/utils/csn.js +14 -3
  82. package/libx/_runtime/common/utils/foreignKeyPropagations.js +18 -1
  83. package/libx/_runtime/common/utils/keys.js +2 -1
  84. package/libx/_runtime/common/utils/path.js +1 -1
  85. package/libx/_runtime/common/utils/resolveView.js +12 -4
  86. package/libx/_runtime/common/utils/rewriteAsterisks.js +27 -13
  87. package/libx/_runtime/common/utils/search2cqn4sql.js +11 -6
  88. package/libx/_runtime/common/utils/structured.js +1 -1
  89. package/libx/_runtime/common/utils/vcap.js +27 -10
  90. package/libx/_runtime/db/data-conversion/post-processing.js +42 -35
  91. package/libx/_runtime/db/expand/expand-v2.js +21 -12
  92. package/libx/_runtime/db/expand/expandCQNToJoin.js +27 -29
  93. package/libx/_runtime/db/expand/index.js +3 -0
  94. package/libx/_runtime/db/generic/create.js +0 -10
  95. package/libx/_runtime/db/generic/index.js +3 -0
  96. package/libx/_runtime/db/generic/read.js +2 -24
  97. package/libx/_runtime/db/generic/rewrite.js +1 -3
  98. package/libx/_runtime/db/generic/update.js +1 -1
  99. package/libx/_runtime/db/query/delete.js +10 -4
  100. package/libx/_runtime/db/query/insert.js +3 -3
  101. package/libx/_runtime/db/query/read.js +15 -8
  102. package/libx/_runtime/db/query/update.js +5 -5
  103. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +9 -2
  104. package/libx/_runtime/db/sql-builder/FunctionBuilder.js +3 -0
  105. package/libx/_runtime/db/sql-builder/index.js +3 -0
  106. package/libx/_runtime/db/utils/columns.js +5 -2
  107. package/libx/_runtime/db/utils/deep.js +6 -8
  108. package/libx/_runtime/db/utils/generateAliases.js +56 -6
  109. package/libx/_runtime/fiori/generic/before.js +73 -49
  110. package/libx/_runtime/fiori/generic/edit.js +14 -18
  111. package/libx/_runtime/fiori/generic/patch.js +8 -11
  112. package/libx/_runtime/fiori/generic/read.js +22 -17
  113. package/libx/_runtime/fiori/generic/readOverDraft.js +1 -4
  114. package/libx/_runtime/hana/Service.js +1 -1
  115. package/libx/_runtime/hana/conversion.js +10 -0
  116. package/libx/_runtime/hana/execute.js +33 -16
  117. package/libx/_runtime/hana/localized.js +1 -1
  118. package/libx/_runtime/hana/search.js +3 -3
  119. package/libx/_runtime/hana/search2cqn4sql.js +22 -21
  120. package/libx/_runtime/hana/searchToContains.js +1 -1
  121. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +4 -2
  122. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +0 -1
  123. package/libx/_runtime/messaging/enterprise-messaging.js +1 -0
  124. package/libx/_runtime/messaging/file-based.js +3 -1
  125. package/libx/_runtime/messaging/message-queuing-utils/options-messaging.js +1 -0
  126. package/libx/_runtime/messaging/service.js +16 -7
  127. package/libx/_runtime/remote/utils/client.js +33 -20
  128. package/libx/_runtime/remote/utils/data.js +53 -12
  129. package/libx/_runtime/sqlite/Service.js +1 -1
  130. package/libx/_runtime/sqlite/conversion.js +10 -0
  131. package/libx/_runtime/sqlite/localized.js +1 -1
  132. package/libx/_runtime/types/api.js +2 -2
  133. package/libx/gql/resolvers/parse/ast/enrich.js +1 -0
  134. package/libx/odata/afterburner.js +29 -6
  135. package/libx/odata/cqn2odata.js +9 -0
  136. package/libx/odata/grammar.pegjs +101 -45
  137. package/libx/odata/index.js +7 -1
  138. package/libx/odata/parser.js +1 -1
  139. package/libx/odata/utils.js +2 -2
  140. package/libx/rest/RestAdapter.js +29 -1
  141. package/libx/rest/middleware/auth.js +1 -3
  142. package/libx/rest/middleware/parse.js +1 -0
  143. package/package.json +1 -1
  144. package/server.js +1 -1
  145. package/bin/deploy/to-hana/logger.js +0 -27
  146. package/bin/deploy/to-hana/runCommand.js +0 -113
  147. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/selectHelper.js +0 -37
  148. package/libx/_runtime/common/utils/auth.js +0 -16
@@ -10,7 +10,6 @@ const {
10
10
  } = require('../utils/handler')
11
11
  const { getKeysCondition } = require('../utils/where')
12
12
  const { getColumns } = require('../../cds-services/services/utils/columns')
13
-
14
13
  const { DRAFT_COLUMNS_CQN } = require('../../common/constants/draft')
15
14
 
16
15
  const _getSelectCQN = (model, { data, target: { name } }, keysCondition, checkUser = true) => {
@@ -24,6 +23,7 @@ const _getSelectCQN = (model, { data, target: { name } }, keysCondition, checkUs
24
23
  ),
25
24
  ...DRAFT_COLUMNS_CQN
26
25
  ]
26
+
27
27
  if (checkUser) {
28
28
  columns.push({ ref: ['DRAFT.DraftAdministrativeData', 'inProcessByUser'], as: 'draftAdmin_inProcessByUser' })
29
29
  }
@@ -42,14 +42,16 @@ const _getSelectCQN = (model, { data, target: { name } }, keysCondition, checkUs
42
42
 
43
43
  const _getUpdateDraftCQN = ({ query, target: { name } }, keysCondition) => {
44
44
  const set = {}
45
+
45
46
  for (const entry in query.UPDATE.data) {
46
47
  if (entry === 'DraftAdministrativeData_DraftUUID') {
47
48
  continue
48
49
  }
50
+
49
51
  set[entry] = query.UPDATE.data[entry]
50
52
  }
51
- if (set.IsActiveEntity) set.IsActiveEntity = false
52
53
 
54
+ if (set.IsActiveEntity) set.IsActiveEntity = false
53
55
  return UPDATE(ensureDraftsSuffix(name)).data(set).where(keysCondition)
54
56
  }
55
57
 
@@ -65,9 +67,7 @@ const _handler = async function (req) {
65
67
  if (req.data.IsActiveEntity === 'true') req.reject(400, 'Patch can only be applied to a draft entity')
66
68
 
67
69
  const keysCondition = getKeysCondition(req.target, req.data)
68
-
69
70
  const dbtx = cds.tx(req)
70
-
71
71
  let result = await dbtx.run(_getSelectCQN(this.model, req, keysCondition))
72
72
 
73
73
  // Potential timeout scenario supported
@@ -80,19 +80,16 @@ const _handler = async function (req) {
80
80
  const updateDraftAdminCQN = getUpdateDraftAdminCQN(req, result[0].DraftAdministrativeData_DraftUUID)
81
81
 
82
82
  await Promise.all([dbtx.run(updateDraftCQN), dbtx.run(updateDraftAdminCQN)])
83
-
84
83
  result = await dbtx.run(_getSelectCQN(this.model, req, keysCondition, false))
85
- if (result.length === 0) {
86
- req.reject(404)
87
- }
88
-
84
+ if (result.length === 0) req.reject(404)
89
85
  removeDraftUUIDIfNecessary(result[0], req)
90
-
91
86
  return result[0]
92
87
  }
93
88
 
94
89
  module.exports = cds.service.impl(function () {
95
- for (const entity of Object.values(this.entities).filter(e => e._isDraftEnabled)) {
90
+ const entities = Object.values(this.entities).filter(e => e._isDraftEnabled)
91
+
92
+ for (const entity of entities) {
96
93
  this.on('PATCH', entity, _handler)
97
94
  }
98
95
  })
@@ -50,7 +50,8 @@ const _getWhereWithAppendedDraftRestrictions = (where = [], req, scenarioAlias,
50
50
  })
51
51
 
52
52
  if (where.length) where.push('and')
53
- where.push(...xpr)
53
+ // restriction might contain or clause -> use xpr for grouping
54
+ xpr.includes('or') ? where.push({ xpr }) : where.push(...xpr)
54
55
  } else {
55
56
  // > restriction inherited from parent via autoexposure
56
57
  // find inner most sub select if available and append restriction to where clause
@@ -778,13 +779,16 @@ const _getDraftDoc = (req, draftName, draftWhere) => {
778
779
  return draftDocs
779
780
  }
780
781
 
781
- const _getOrderByEnrichedColumns = (orderBy, columns) => {
782
+ const _getOrderByEnrichedColumns = (orderBy, columns, entity) => {
782
783
  const enrichedCol = []
783
784
  if (orderBy && orderBy.length > 1) {
784
785
  const colNames = columns.map(el => el.ref[el.ref.length - 1])
785
786
  // REVISIT: GET Books?$select=title&$expand=NotBooks($select=pages)&$orderby=NotBooks/title - what's then?
786
787
  for (const el of orderBy) {
787
- if (!DRAFT_COLUMNS.includes(el.ref[el.ref.length - 1]) && !colNames.includes(el.ref[el.ref.length - 1])) {
788
+ // For associations we need to 'materialise' the resulting field, otherwise we cannot access it in an outer SELECT.
789
+ if (entity && entity.elements[el.ref[0]] && entity.elements[el.ref[0]].isAssociation) {
790
+ enrichedCol.push({ ref: [...el.ref], as: _poorMansAlias4(el) })
791
+ } else if (!DRAFT_COLUMNS.includes(el.ref[el.ref.length - 1]) && !colNames.includes(el.ref[el.ref.length - 1])) {
788
792
  enrichedCol.push({ ref: [...el.ref] })
789
793
  }
790
794
  }
@@ -806,11 +810,11 @@ const _replaceDraftAlias = where => {
806
810
 
807
811
  const _poorMansAlias4 = xpr => '_' + xpr.ref.join('_') + '_'
808
812
 
809
- const _getUnionCQN = (req, draftName, columns, subSelect, draftWhere, model) => {
813
+ const _getUnionCQN = (req, draftName, columns, subSelect, draftWhere, model, entity) => {
810
814
  const draftActiveWhere = _getWhereForActive(draftWhere)
811
815
  const activeDocs = getEnrichedCQN(SELECT.from(req.target), req.query.SELECT, draftActiveWhere, undefined, false)
812
816
  activeDocs.where(_getWhereWithAppendedDraftRestrictions([], req))
813
- convertWhereExists(activeDocs, model, {})
817
+ convertWhereExists(activeDocs.SELECT, model, {})
814
818
 
815
819
  // @restrict.where not applicable for drafts (I can ALWAYS read mine)
816
820
  _replaceDraftAlias(draftWhere)
@@ -818,6 +822,7 @@ const _getUnionCQN = (req, draftName, columns, subSelect, draftWhere, model) =>
818
822
 
819
823
  const union = SELECT.from({ SET: { op: 'union', all: true, args: [draftDocs, activeDocs] } })
820
824
  if (req.query.SELECT.count) union.SELECT.count = true
825
+ if (req.query.SELECT.__countAggregated) union.SELECT.__countAggregated = true
821
826
 
822
827
  if (req.query.SELECT.from.as) {
823
828
  draftDocs.SELECT.from.as = req.query.SELECT.from.as
@@ -836,7 +841,7 @@ const _getUnionCQN = (req, draftName, columns, subSelect, draftWhere, model) =>
836
841
  return union.columns({ func: 'sum', args: [{ ref: ['$count'] }], as: '$count' })
837
842
  }
838
843
 
839
- const enrichedColumns = _getOrderByEnrichedColumns(req.query.SELECT.orderBy, columns)
844
+ const enrichedColumns = _getOrderByEnrichedColumns(req.query.SELECT.orderBy, columns, entity)
840
845
 
841
846
  for (const col of enrichedColumns) {
842
847
  // if we have columns for outer order by that may also be needed for joins, we need to duplicate them
@@ -913,12 +918,14 @@ const _excludeActiveDraftExists = (req, draftWhere, columns, model) => {
913
918
  ])
914
919
  .where(_inProcessByUserWhere(req.user.id))
915
920
 
921
+ const targetName = ensureNoDraftsSuffix(req.target.name)
916
922
  for (const key of _getTargetKeys(req)) {
917
- subSelect.where([{ ref: [ensureNoDraftsSuffix(req.target.name), key] }, '=', { ref: [draftName, key] }])
923
+ subSelect.where([{ ref: [targetName, key] }, '=', { ref: [draftName, key] }])
918
924
  }
919
925
 
926
+ const entity = model.definitions[targetName]
920
927
  draftWhere = removeIsActiveEntityRecursively(draftWhere)
921
- const cqn = _getUnionCQN(req, draftName, columns, subSelect, draftWhere, model)
928
+ const cqn = _getUnionCQN(req, draftName, columns, subSelect, draftWhere, model, entity)
922
929
  cqn.SELECT.from.as = name
923
930
 
924
931
  if (cqn.SELECT.orderBy) {
@@ -958,13 +965,16 @@ const _validatedWithSiblingInProcess = (req, draftWhere, draftParameters, column
958
965
  )
959
966
  return _excludeActiveDraftExists(req, draftWhere, columns, model)
960
967
  if (
968
+ draftInProcessByUser &&
961
969
  draftInProcessByUser.op === '!=' &&
962
970
  _isValidWithDraftLocked(isActiveEntity, siblingIsActive, draftInProcessByUser)
963
971
  ) {
964
972
  return _activeWithDraftInProcess(req, draftWhere, columns, req.user.id)
965
- } else if (_isValidWithDraftTimeout(isActiveEntity, siblingIsActive, draftInProcessByUser)) {
973
+ } else if (draftInProcessByUser && _isValidWithDraftTimeout(isActiveEntity, siblingIsActive, draftInProcessByUser)) {
966
974
  return _activeWithDraftInProcess(req, draftWhere, columns, null)
967
975
  }
976
+
977
+ //
968
978
  }
969
979
 
970
980
  const _validatedDraftOfWhichIAmOwner = (req, draftWhere, draftParameters, columns) =>
@@ -1213,16 +1223,13 @@ const _handler = async function (req) {
1213
1223
  // handle localized here as it was previously handled for req.target
1214
1224
  req.target = _getLocalizedEntity(this.model, req.target, req.user) || req.target
1215
1225
 
1216
- // REVISIT
1217
- delete req.query._validationQuery
1218
-
1219
1226
  const originalFrom = _copyCQNPartial(req.query.SELECT.from)
1220
1227
 
1221
1228
  // REVISIT DRAFT HANDLING: cqn2cqn4sql must not be called here
1222
- const sqlQuery = cqn2cqn4sql(req.query, this.model, { draft: true })
1229
+ const query4sql = cqn2cqn4sql(req.query, this.model, { _4fiori: true })
1223
1230
 
1224
1231
  // do not clone with Object.assign as that would skip all non-enumerable properties
1225
- const reqClone = { __proto__: req, query: _copyCQNPartial(sqlQuery) }
1232
+ const reqClone = { __proto__: req, query: _copyCQNPartial(query4sql) }
1226
1233
 
1227
1234
  // ensure draft restrictions are copied to new query
1228
1235
  reqClone.query._draftRestrictions = req.query._draftRestrictions
@@ -1232,6 +1239,7 @@ const _handler = async function (req) {
1232
1239
  reqClone.query._streaming = true
1233
1240
  return cds.tx(req).run(reqClone.query)
1234
1241
  }
1242
+
1235
1243
  let cqnScenario
1236
1244
 
1237
1245
  // to replace scenario CQNs for queries with $apply SELECT chain (new odata2cqn parser)
@@ -1262,16 +1270,13 @@ const _handler = async function (req) {
1262
1270
  )
1263
1271
 
1264
1272
  _adaptSubSelects(cqnScenario.cqn, cqnScenario.scenario)
1265
-
1266
1273
  _adaptAnnotationAliases(cqnScenario.cqn)
1267
1274
 
1268
1275
  // unlocalize for db and after handlers as it was before
1269
1276
  req.target = this.model.definitions[ensureUnlocalized(req.target.name)]
1270
1277
 
1271
1278
  const result = await cds.tx(req).send({ query: cqnScenario.cqn, target: req.target })
1272
-
1273
1279
  const resultAsArray = Array.isArray(result) ? result : result ? [result] : []
1274
-
1275
1280
  removeDraftUUIDIfNecessary(resultAsArray, req)
1276
1281
 
1277
1282
  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
@@ -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
@@ -47,6 +49,14 @@ const HANA_TYPE_CONVERSION_MAP = new Map([
47
49
  ['cds.LargeString', convertToString]
48
50
  ])
49
51
 
52
+ if (cds.env.features.bigjs) {
53
+ const Big = require('big.js')
54
+ const convertToBig = value => new Big(value)
55
+
56
+ HANA_TYPE_CONVERSION_MAP.set('cds.Integer64', convertToBig)
57
+ HANA_TYPE_CONVERSION_MAP.set('cds.Decimal', convertToBig)
58
+ }
59
+
50
60
  module.exports = {
51
61
  HANA_TYPE_CONVERSION_MAP
52
62
  }
@@ -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,26 @@ 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
+ 25: 'CLOB',
55
+ 26: 'NCLOB',
56
+ 27: 'BLOB'
57
+ }
58
+
59
+ function _getBinaries(stmt) {
60
+ // hdb vs. @sap/hana-client
61
+ const parameters = stmt.parameterMetadata || stmt.getParameterInfo()
62
+ const typeKey = stmt.parameterMetadata ? 'dataType' : 'nativeType'
63
+ return parameters.reduce((acc, cur, i) => {
64
+ if (BINARY_TYPES[cur[typeKey]]) acc.push(i)
65
+ return acc
66
+ }, [])
67
+ }
68
+
69
+ const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}={2})$/
70
+
51
71
  function _executeAsPreparedStatement(dbc, sql, values, reject, resolve) {
52
72
  dbc.prepare(sql, function (err, stmt) {
53
73
  if (err) {
@@ -56,18 +76,15 @@ function _executeAsPreparedStatement(dbc, sql, values, reject, resolve) {
56
76
  return reject(err)
57
77
  }
58
78
 
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)))
79
+ // convert binary strings to buffers ()
80
+ if (cds.env.hana.base64_to_buffer !== false && _hasValues(values)) {
81
+ const binaries = _getBinaries(stmt)
82
+ if (binaries.length) {
83
+ const vals = Array.isArray(values[0]) ? values : [values]
84
+ for (const i of binaries) {
85
+ for (const row of vals) {
86
+ if (row[i] && typeof row[i] === 'string' && row[i].match(BASE64)) {
87
+ row[i] = Buffer.from(row[i], 'base64')
71
88
  }
72
89
  }
73
90
  }
@@ -108,7 +125,7 @@ function _executeSimpleSQL(dbc, sql, values) {
108
125
  values = Object.values(values)
109
126
  }
110
127
  // ensure that stored procedure with parameters is always executed as prepared
111
- if (_hasValues(sql, values) || sql.match(/^call.*?\?.*$/i)) {
128
+ if (_hasValues(values) || sql.match(/^call.*?\?.*$/i)) {
112
129
  _executeAsPreparedStatement(dbc, sql, values, reject, resolve)
113
130
  } else {
114
131
  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,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
@@ -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) {
@@ -22,5 +22,6 @@ module.exports = options => {
22
22
  password: options.credentials.amqp10.auth.basic.password
23
23
  }
24
24
  }
25
+ if (options.amqp) amqp.amqp = options.amqp
25
26
  return amqp
26
27
  }
@@ -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) {