@sap/cds 5.7.5 → 5.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/app/fiori/routes.js +1 -1
  3. package/bin/deploy/to-hana/cfUtil.js +251 -138
  4. package/bin/deploy/to-hana/gitUtil.js +55 -0
  5. package/bin/deploy/to-hana/hana.js +92 -93
  6. package/bin/deploy/to-hana/hdiDeployUtil.js +42 -27
  7. package/bin/deploy/to-hana/index.js +14 -13
  8. package/bin/mtx/in-cds.js +1 -0
  9. package/bin/serve.js +1 -1
  10. package/bin/version.js +1 -0
  11. package/lib/compile/cdsc.js +0 -6
  12. package/lib/compile/resolve.js +1 -1
  13. package/lib/compile/to/srvinfo.js +1 -1
  14. package/lib/core/classes.js +21 -1
  15. package/lib/env/index.js +3 -2
  16. package/lib/env/requires.js +4 -0
  17. package/lib/i18n/localize.js +5 -8
  18. package/lib/index.js +1 -0
  19. package/lib/log/errors.js +1 -1
  20. package/lib/log/format/kibana.js +3 -3
  21. package/lib/ql/SELECT.js +2 -2
  22. package/lib/req/cds-context.js +1 -1
  23. package/lib/req/context.js +1 -1
  24. package/lib/serve/Transaction.js +9 -5
  25. package/lib/serve/index.js +13 -21
  26. package/lib/utils/tests.js +90 -66
  27. package/libx/_runtime/audit/generic/personal/modification.js +0 -8
  28. package/libx/_runtime/auth/index.js +7 -6
  29. package/libx/_runtime/auth/strategies/dwc.js +43 -0
  30. package/libx/_runtime/auth/utils.js +24 -0
  31. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -3
  32. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +11 -32
  33. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +12 -5
  34. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +7 -4
  35. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +24 -3
  36. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +43 -38
  37. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +1 -1
  38. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +11 -5
  39. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/boundToCQN.js +1 -2
  40. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/deleteToCQN.js +0 -1
  41. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -2
  42. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/orderByToCQN.js +9 -0
  43. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +17 -30
  44. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +12 -1
  45. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/edm/AbstractEdmStructuredType.js +2 -1
  46. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/ResourcePathParser.js +23 -2
  47. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriHelper.js +7 -6
  48. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriTokenizer.js +2 -5
  49. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +19 -47
  50. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +4 -11
  51. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/ResourceJsonDeserializer.js +7 -1
  52. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/CommandExecutor.js +0 -3
  53. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/ConditionalRequestControlCommand.js +0 -1
  54. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ContextURLFactory.js +2 -2
  55. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ErrorJsonSerializer.js +2 -0
  56. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/TrustedResourceJsonSerializer.js +2 -5
  57. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +6 -6
  58. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
  59. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -4
  60. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +41 -17
  61. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +1 -17
  62. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +60 -18
  63. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +47 -10
  64. package/libx/_runtime/cds-services/adapter/rest/Rest.js +22 -1
  65. package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +8 -3
  66. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +3 -0
  67. package/libx/_runtime/cds-services/services/utils/columns.js +5 -1
  68. package/libx/_runtime/cds-services/services/utils/compareJson.js +15 -16
  69. package/libx/_runtime/cds-services/services/utils/differ.js +2 -8
  70. package/libx/_runtime/common/aspects/Association.js +16 -0
  71. package/libx/_runtime/common/composition/data.js +28 -37
  72. package/libx/_runtime/common/composition/delete.js +107 -58
  73. package/libx/_runtime/common/composition/index.js +3 -3
  74. package/libx/_runtime/common/composition/insert.js +14 -27
  75. package/libx/_runtime/common/composition/tree.js +1 -1
  76. package/libx/_runtime/common/composition/update.js +39 -34
  77. package/libx/_runtime/common/error/frontend.js +19 -5
  78. package/libx/_runtime/common/generic/auth.js +20 -85
  79. package/libx/_runtime/common/generic/crud.js +22 -1
  80. package/libx/_runtime/common/i18n/messages.properties +2 -1
  81. package/libx/_runtime/common/utils/cqn.js +2 -6
  82. package/libx/_runtime/common/utils/cqn2cqn4sql.js +95 -122
  83. package/libx/_runtime/common/utils/csn.js +29 -6
  84. package/libx/_runtime/common/utils/foreignKeyPropagations.js +21 -1
  85. package/libx/_runtime/common/utils/keys.js +2 -1
  86. package/libx/_runtime/common/utils/path.js +1 -1
  87. package/libx/_runtime/common/utils/resolveView.js +12 -4
  88. package/libx/_runtime/common/utils/rewriteAsterisks.js +27 -13
  89. package/libx/_runtime/common/utils/search2cqn4sql.js +11 -6
  90. package/libx/_runtime/common/utils/structured.js +10 -4
  91. package/libx/_runtime/common/utils/vcap.js +27 -10
  92. package/libx/_runtime/db/data-conversion/post-processing.js +20 -13
  93. package/libx/_runtime/db/expand/expand-v2.js +21 -12
  94. package/libx/_runtime/db/expand/expandCQNToJoin.js +67 -26
  95. package/libx/_runtime/db/expand/index.js +3 -0
  96. package/libx/_runtime/db/generic/create.js +0 -10
  97. package/libx/_runtime/db/generic/index.js +3 -0
  98. package/libx/_runtime/db/generic/read.js +2 -24
  99. package/libx/_runtime/db/generic/rewrite.js +1 -3
  100. package/libx/_runtime/db/generic/update.js +1 -1
  101. package/libx/_runtime/db/query/delete.js +10 -4
  102. package/libx/_runtime/db/query/insert.js +3 -4
  103. package/libx/_runtime/db/query/read.js +4 -1
  104. package/libx/_runtime/db/query/update.js +5 -5
  105. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +9 -2
  106. package/libx/_runtime/db/sql-builder/FunctionBuilder.js +3 -0
  107. package/libx/_runtime/db/sql-builder/SelectBuilder.js +7 -3
  108. package/libx/_runtime/db/sql-builder/index.js +3 -0
  109. package/libx/_runtime/db/utils/columns.js +5 -2
  110. package/libx/_runtime/db/utils/deep.js +16 -14
  111. package/libx/_runtime/db/utils/generateAliases.js +56 -6
  112. package/libx/_runtime/fiori/generic/before.js +73 -49
  113. package/libx/_runtime/fiori/generic/edit.js +14 -18
  114. package/libx/_runtime/fiori/generic/patch.js +8 -11
  115. package/libx/_runtime/fiori/generic/read.js +20 -19
  116. package/libx/_runtime/fiori/generic/readOverDraft.js +1 -4
  117. package/libx/_runtime/fiori/utils/handler.js +1 -11
  118. package/libx/_runtime/hana/Service.js +1 -1
  119. package/libx/_runtime/hana/conversion.js +12 -1
  120. package/libx/_runtime/hana/dynatrace.js +11 -5
  121. package/libx/_runtime/hana/execute.js +132 -19
  122. package/libx/_runtime/hana/search.js +3 -3
  123. package/libx/_runtime/hana/search2cqn4sql.js +23 -25
  124. package/libx/_runtime/hana/searchToContains.js +1 -1
  125. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +1 -1
  126. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +0 -1
  127. package/libx/_runtime/messaging/enterprise-messaging.js +1 -0
  128. package/libx/_runtime/messaging/file-based.js +3 -1
  129. package/libx/_runtime/messaging/service.js +4 -1
  130. package/libx/_runtime/remote/utils/client.js +41 -24
  131. package/libx/_runtime/remote/utils/data.js +54 -12
  132. package/libx/_runtime/sqlite/Service.js +1 -1
  133. package/libx/_runtime/sqlite/conversion.js +10 -0
  134. package/libx/_runtime/types/api.js +2 -2
  135. package/libx/gql/resolvers/crud/update.js +8 -5
  136. package/libx/gql/resolvers/parse/ast/enrich.js +1 -0
  137. package/libx/odata/afterburner.js +29 -6
  138. package/libx/odata/cqn2odata.js +9 -0
  139. package/libx/odata/grammar.pegjs +49 -21
  140. package/libx/odata/index.js +2 -2
  141. package/libx/odata/parser.js +1 -1
  142. package/libx/odata/utils.js +2 -2
  143. package/libx/rest/RestAdapter.js +29 -1
  144. package/libx/rest/middleware/auth.js +1 -3
  145. package/libx/rest/middleware/parse.js +1 -0
  146. package/package.json +1 -1
  147. package/server.js +1 -1
  148. package/bin/deploy/to-hana/logger.js +0 -27
  149. package/bin/deploy/to-hana/runCommand.js +0 -113
  150. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/selectHelper.js +0 -37
  151. package/libx/_runtime/common/utils/auth.js +0 -16
@@ -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
  })
@@ -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) {
@@ -779,13 +777,16 @@ const _getDraftDoc = (req, draftName, draftWhere) => {
779
777
  return draftDocs
780
778
  }
781
779
 
782
- const _getOrderByEnrichedColumns = (orderBy, columns) => {
780
+ const _getOrderByEnrichedColumns = (orderBy, columns, entity) => {
783
781
  const enrichedCol = []
784
782
  if (orderBy && orderBy.length > 1) {
785
783
  const colNames = columns.map(el => el.ref[el.ref.length - 1])
786
784
  // REVISIT: GET Books?$select=title&$expand=NotBooks($select=pages)&$orderby=NotBooks/title - what's then?
787
785
  for (const el of orderBy) {
788
- 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])) {
789
790
  enrichedCol.push({ ref: [...el.ref] })
790
791
  }
791
792
  }
@@ -807,11 +808,11 @@ const _replaceDraftAlias = where => {
807
808
 
808
809
  const _poorMansAlias4 = xpr => '_' + xpr.ref.join('_') + '_'
809
810
 
810
- const _getUnionCQN = (req, draftName, columns, subSelect, draftWhere, model) => {
811
+ const _getUnionCQN = (req, draftName, columns, subSelect, draftWhere, model, entity) => {
811
812
  const draftActiveWhere = _getWhereForActive(draftWhere)
812
813
  const activeDocs = getEnrichedCQN(SELECT.from(req.target), req.query.SELECT, draftActiveWhere, undefined, false)
813
814
  activeDocs.where(_getWhereWithAppendedDraftRestrictions([], req))
814
- convertWhereExists(activeDocs, model, {})
815
+ convertWhereExists(activeDocs.SELECT, model, {})
815
816
 
816
817
  // @restrict.where not applicable for drafts (I can ALWAYS read mine)
817
818
  _replaceDraftAlias(draftWhere)
@@ -838,7 +839,7 @@ const _getUnionCQN = (req, draftName, columns, subSelect, draftWhere, model) =>
838
839
  return union.columns({ func: 'sum', args: [{ ref: ['$count'] }], as: '$count' })
839
840
  }
840
841
 
841
- const enrichedColumns = _getOrderByEnrichedColumns(req.query.SELECT.orderBy, columns)
842
+ const enrichedColumns = _getOrderByEnrichedColumns(req.query.SELECT.orderBy, columns, entity)
842
843
 
843
844
  for (const col of enrichedColumns) {
844
845
  // if we have columns for outer order by that may also be needed for joins, we need to duplicate them
@@ -915,12 +916,14 @@ const _excludeActiveDraftExists = (req, draftWhere, columns, model) => {
915
916
  ])
916
917
  .where(_inProcessByUserWhere(req.user.id))
917
918
 
919
+ const targetName = ensureNoDraftsSuffix(req.target.name)
918
920
  for (const key of _getTargetKeys(req)) {
919
- subSelect.where([{ ref: [ensureNoDraftsSuffix(req.target.name), key] }, '=', { ref: [draftName, key] }])
921
+ subSelect.where([{ ref: [targetName, key] }, '=', { ref: [draftName, key] }])
920
922
  }
921
923
 
924
+ const entity = model.definitions[targetName]
922
925
  draftWhere = removeIsActiveEntityRecursively(draftWhere)
923
- const cqn = _getUnionCQN(req, draftName, columns, subSelect, draftWhere, model)
926
+ const cqn = _getUnionCQN(req, draftName, columns, subSelect, draftWhere, model, entity)
924
927
  cqn.SELECT.from.as = name
925
928
 
926
929
  if (cqn.SELECT.orderBy) {
@@ -960,13 +963,16 @@ const _validatedWithSiblingInProcess = (req, draftWhere, draftParameters, column
960
963
  )
961
964
  return _excludeActiveDraftExists(req, draftWhere, columns, model)
962
965
  if (
966
+ draftInProcessByUser &&
963
967
  draftInProcessByUser.op === '!=' &&
964
968
  _isValidWithDraftLocked(isActiveEntity, siblingIsActive, draftInProcessByUser)
965
969
  ) {
966
970
  return _activeWithDraftInProcess(req, draftWhere, columns, req.user.id)
967
- } else if (_isValidWithDraftTimeout(isActiveEntity, siblingIsActive, draftInProcessByUser)) {
971
+ } else if (draftInProcessByUser && _isValidWithDraftTimeout(isActiveEntity, siblingIsActive, draftInProcessByUser)) {
968
972
  return _activeWithDraftInProcess(req, draftWhere, columns, null)
969
973
  }
974
+
975
+ //
970
976
  }
971
977
 
972
978
  const _validatedDraftOfWhichIAmOwner = (req, draftWhere, draftParameters, columns) =>
@@ -1215,16 +1221,13 @@ const _handler = async function (req) {
1215
1221
  // handle localized here as it was previously handled for req.target
1216
1222
  req.target = _getLocalizedEntity(this.model, req.target, req.user) || req.target
1217
1223
 
1218
- // REVISIT
1219
- delete req.query._validationQuery
1220
-
1221
1224
  const originalFrom = _copyCQNPartial(req.query.SELECT.from)
1222
1225
 
1223
1226
  // REVISIT DRAFT HANDLING: cqn2cqn4sql must not be called here
1224
- const sqlQuery = cqn2cqn4sql(req.query, this.model, { draft: true })
1227
+ const query4sql = cqn2cqn4sql(req.query, this.model, { _4fiori: true })
1225
1228
 
1226
1229
  // do not clone with Object.assign as that would skip all non-enumerable properties
1227
- const reqClone = { __proto__: req, query: _copyCQNPartial(sqlQuery) }
1230
+ const reqClone = { __proto__: req, query: _copyCQNPartial(query4sql) }
1228
1231
 
1229
1232
  // ensure draft restrictions are copied to new query
1230
1233
  reqClone.query._draftRestrictions = req.query._draftRestrictions
@@ -1234,6 +1237,7 @@ const _handler = async function (req) {
1234
1237
  reqClone.query._streaming = true
1235
1238
  return cds.tx(req).run(reqClone.query)
1236
1239
  }
1240
+
1237
1241
  let cqnScenario
1238
1242
 
1239
1243
  // to replace scenario CQNs for queries with $apply SELECT chain (new odata2cqn parser)
@@ -1264,16 +1268,13 @@ const _handler = async function (req) {
1264
1268
  )
1265
1269
 
1266
1270
  _adaptSubSelects(cqnScenario.cqn, cqnScenario.scenario)
1267
-
1268
1271
  _adaptAnnotationAliases(cqnScenario.cqn)
1269
1272
 
1270
1273
  // unlocalize for db and after handlers as it was before
1271
1274
  req.target = this.model.definitions[ensureUnlocalized(req.target.name)]
1272
1275
 
1273
1276
  const result = await cds.tx(req).send({ query: cqnScenario.cqn, target: req.target })
1274
-
1275
1277
  const resultAsArray = Array.isArray(result) ? result : result ? [result] : []
1276
-
1277
1278
  removeDraftUUIDIfNecessary(resultAsArray, req)
1278
1279
 
1279
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
  }
@@ -7,11 +7,12 @@ try {
7
7
  }
8
8
 
9
9
  const isDynatraceEnabled = () => {
10
- return dynatrace.sdk !== undefined
10
+ return dynatrace.sdk !== undefined && !process.env.CDS_SKIP_DYNATRACE
11
11
  }
12
12
 
13
13
  const _dynatraceResultCallback = function (tracer, cb) {
14
- return function (err, results, fields) {
14
+ return function (err, ...args) {
15
+ const results = args.shift()
15
16
  if (err) {
16
17
  tracer.error(err)
17
18
  } else {
@@ -19,7 +20,7 @@ const _dynatraceResultCallback = function (tracer, cb) {
19
20
  rowsReturned: (results && results.length) || results
20
21
  })
21
22
  }
22
- tracer.end(cb, err, results, fields)
23
+ tracer.end(cb, err, results, ...args)
23
24
  }
24
25
  }
25
26
 
@@ -73,9 +74,14 @@ const dynatraceClient = (client, credentials, tenant) => {
73
74
  // hana-client does not like decorating.
74
75
  // because of that, we need to override the fn and pass the original fn for execution
75
76
  const originalExecFn = client.exec
76
- const originalPrepareFn = client.prepare
77
77
  client.exec = _execUsingDynatrace(client, originalExecFn, dbInfo)
78
- client.prepare = _preparedStmtUsingDynatrace(client, originalPrepareFn, dbInfo)
78
+ const originalPrepareFn = client.prepare
79
+ if (client.name === '@sap/hana-client') {
80
+ // client.prepare = ... doesn't work for hana-client
81
+ Object.defineProperty(client, 'prepare', { value: _preparedStmtUsingDynatrace(client, originalPrepareFn, dbInfo) })
82
+ } else {
83
+ client.prepare = _preparedStmtUsingDynatrace(client, originalPrepareFn, dbInfo)
84
+ }
79
85
 
80
86
  return client
81
87
  }
@@ -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,32 +48,145 @@ 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
+
69
+ function _isProcedureCall(sql) {
70
+ return sql.trim().match(/^call \s*"{0,1}\w*/i)
71
+ }
72
+
73
+ function _getProcedureName(sql) {
74
+ const match = sql.trim().match(/^call \s*"{0,1}(\w*)/i)
75
+ return match && match[1]
76
+ }
77
+
78
+ function _hdbGetResultForProcedure(rows, args, outParameters) {
79
+ // on hdb, rows already contains results for scalar params
80
+ const result = rows || {}
81
+ // merge table output params into scalar params
82
+ if (args && args.length && outParameters) {
83
+ const params = outParameters.filter(md => !(md.PARAMETER_NAME in rows))
84
+ for (let i = 0; i < args.length; i++) {
85
+ result[params[i].PARAMETER_NAME] = args[i]
86
+ }
87
+ }
88
+ return result
89
+ }
90
+
91
+ function _hcGetResultForProcedure(stmt, resultSet, outParameters) {
92
+ const result = {}
93
+ // build result from scalar params
94
+ const paramInfo = stmt.getParameterInfo()
95
+ if (paramInfo.some(p => p.direction > 1)) {
96
+ for (let i = 0; i < paramInfo.length; i++) {
97
+ if (paramInfo[i].direction > 1) {
98
+ result[paramInfo[i].name] = stmt.getParameterValue(i)
99
+ }
100
+ }
101
+ }
102
+ // merge table output params into scalar params
103
+ if (outParameters && outParameters.length) {
104
+ const params = outParameters.filter(md => !(md.PARAMETER_NAME in result))
105
+ let i = 0
106
+ while (resultSet.next()) {
107
+ result[params[i].PARAMETER_NAME] = [resultSet.getValues()]
108
+ resultSet.nextResult()
109
+ i++
110
+ }
111
+ }
112
+ return result
113
+ }
114
+
115
+ function _getProcedureMetadata(procedureName, dbc) {
116
+ return new Promise((resolve, reject) => {
117
+ dbc.exec(
118
+ `SELECT PARAMETER_NAME FROM SYS.PROCEDURE_PARAMETERS WHERE SCHEMA_NAME = CURRENT_SCHEMA AND PROCEDURE_NAME = '${procedureName}' AND PARAMETER_TYPE IN ('OUT', 'INOUT') ORDER BY POSITION`,
119
+ (err, res) => {
120
+ if (err) reject(err)
121
+ else resolve(res)
122
+ }
123
+ )
124
+ })
125
+ }
126
+
51
127
  function _executeAsPreparedStatement(dbc, sql, values, reject, resolve) {
52
- dbc.prepare(sql, function (err, stmt) {
128
+ dbc.prepare(sql, async function (err, stmt) {
53
129
  if (err) {
54
130
  err.query = sql
55
131
  if (values) err.values = SANITIZE_VALUES ? ['***'] : values
56
132
  return reject(err)
57
133
  }
58
134
 
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)))
135
+ // convert binary strings to buffers
136
+ if (cds.env.hana.base64_to_buffer !== false && _hasValues(values)) {
137
+ const binaries = _getBinaries(stmt)
138
+ if (binaries.length) {
139
+ const vals = Array.isArray(values[0]) ? values : [values]
140
+ for (const i of binaries) {
141
+ for (const row of vals) {
142
+ if (row[i] && typeof row[i] === 'string' && row[i].match(BASE64)) {
143
+ row[i] = Buffer.from(row[i], 'base64')
71
144
  }
72
145
  }
73
146
  }
74
147
  }
75
148
  }
76
149
 
150
+ if (cds.env.features.new_call_prodecure) {
151
+ // procedure call metadata
152
+ let outParameters
153
+ const isProcedureCall = _isProcedureCall(sql)
154
+ if (isProcedureCall) {
155
+ try {
156
+ const procedureName = _getProcedureName(sql)
157
+ outParameters = await _getProcedureMetadata(procedureName, dbc)
158
+ } catch (e) {
159
+ LOG._warn && LOG.warn('Unable to fetch procedure metadata due to error:', e)
160
+ }
161
+ }
162
+
163
+ // on @sap/hana-client, we need to use execQuery in case of calling procedures
164
+ stmt[isProcedureCall && dbc.name !== 'hdb' ? 'execQuery' : 'exec'](values, function (err, rows, ...args) {
165
+ if (err) {
166
+ stmt.drop(() => {})
167
+ err.query = sql
168
+ if (values) err.values = SANITIZE_VALUES ? ['***'] : values
169
+ return reject(err)
170
+ }
171
+
172
+ let result
173
+ if (isProcedureCall) {
174
+ result =
175
+ dbc.name === 'hdb'
176
+ ? _hdbGetResultForProcedure(rows, args, outParameters)
177
+ : _hcGetResultForProcedure(stmt, rows, outParameters)
178
+ } else {
179
+ result = rows
180
+ }
181
+
182
+ stmt.drop(() => {})
183
+
184
+ resolve(result)
185
+ })
186
+
187
+ return
188
+ }
189
+
77
190
  stmt.exec(values, function (err, rows, procedureReturn) {
78
191
  if (err) {
79
192
  stmt.drop(() => {})
@@ -108,15 +221,15 @@ function _executeSimpleSQL(dbc, sql, values) {
108
221
  values = Object.values(values)
109
222
  }
110
223
  // ensure that stored procedure with parameters is always executed as prepared
111
- if (_hasValues(sql, values) || sql.match(/^call.*?\?.*$/i)) {
224
+ if (_hasValues(values) || _isProcedureCall(sql)) {
112
225
  _executeAsPreparedStatement(dbc, sql, values, reject, resolve)
113
226
  } else {
114
- dbc.exec(sql, function (err, result, procedureReturn) {
227
+ dbc.exec(sql, function (err, result) {
115
228
  if (err) {
116
229
  err.query = sql
117
230
  return reject(err)
118
231
  }
119
- resolve(procedureReturn || result)
232
+ resolve(result)
120
233
  })
121
234
  }
122
235
  })
@@ -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
@@ -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
  }