@sap/cds 5.7.5 → 5.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/app/fiori/routes.js +1 -1
  3. package/bin/deploy/to-hana/cfUtil.js +251 -138
  4. package/bin/deploy/to-hana/gitUtil.js +55 -0
  5. package/bin/deploy/to-hana/hana.js +92 -93
  6. package/bin/deploy/to-hana/hdiDeployUtil.js +42 -27
  7. package/bin/deploy/to-hana/index.js +14 -13
  8. package/bin/mtx/in-cds.js +1 -0
  9. package/bin/serve.js +1 -1
  10. package/bin/version.js +1 -0
  11. package/lib/compile/cdsc.js +0 -6
  12. package/lib/compile/resolve.js +1 -1
  13. package/lib/compile/to/srvinfo.js +1 -1
  14. package/lib/core/classes.js +21 -1
  15. package/lib/env/index.js +3 -2
  16. package/lib/env/requires.js +4 -0
  17. package/lib/i18n/localize.js +5 -8
  18. package/lib/index.js +1 -0
  19. package/lib/log/errors.js +1 -1
  20. package/lib/ql/SELECT.js +2 -2
  21. package/lib/req/cds-context.js +1 -1
  22. package/lib/req/context.js +1 -1
  23. package/lib/serve/Transaction.js +9 -5
  24. package/lib/serve/index.js +13 -21
  25. package/lib/utils/tests.js +90 -66
  26. package/libx/_runtime/audit/generic/personal/modification.js +0 -8
  27. package/libx/_runtime/auth/index.js +7 -6
  28. package/libx/_runtime/auth/strategies/dwc.js +43 -0
  29. package/libx/_runtime/auth/utils.js +24 -0
  30. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +11 -32
  31. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +12 -5
  32. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +7 -4
  33. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +24 -3
  34. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +43 -38
  35. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +1 -1
  36. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +11 -5
  37. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/boundToCQN.js +1 -2
  38. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/deleteToCQN.js +0 -1
  39. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -2
  40. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/orderByToCQN.js +9 -0
  41. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +17 -30
  42. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +12 -1
  43. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/edm/AbstractEdmStructuredType.js +2 -1
  44. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriHelper.js +7 -6
  45. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriTokenizer.js +2 -5
  46. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +19 -47
  47. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +4 -11
  48. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/ResourceJsonDeserializer.js +7 -1
  49. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/CommandExecutor.js +0 -3
  50. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/ConditionalRequestControlCommand.js +0 -1
  51. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ContextURLFactory.js +1 -1
  52. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/TrustedResourceJsonSerializer.js +2 -5
  53. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +6 -6
  54. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
  55. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -4
  56. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +41 -17
  57. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +1 -17
  58. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +60 -18
  59. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +7 -5
  60. package/libx/_runtime/cds-services/adapter/rest/Rest.js +22 -1
  61. package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +8 -3
  62. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +3 -0
  63. package/libx/_runtime/cds-services/services/utils/columns.js +5 -1
  64. package/libx/_runtime/cds-services/services/utils/compareJson.js +15 -16
  65. package/libx/_runtime/cds-services/services/utils/differ.js +2 -8
  66. package/libx/_runtime/common/aspects/Association.js +16 -0
  67. package/libx/_runtime/common/composition/data.js +28 -37
  68. package/libx/_runtime/common/composition/delete.js +107 -58
  69. package/libx/_runtime/common/composition/index.js +2 -1
  70. package/libx/_runtime/common/composition/insert.js +13 -13
  71. package/libx/_runtime/common/composition/update.js +39 -34
  72. package/libx/_runtime/common/error/frontend.js +17 -2
  73. package/libx/_runtime/common/generic/auth.js +20 -85
  74. package/libx/_runtime/common/generic/crud.js +22 -1
  75. package/libx/_runtime/common/i18n/messages.properties +2 -1
  76. package/libx/_runtime/common/utils/cqn.js +2 -6
  77. package/libx/_runtime/common/utils/cqn2cqn4sql.js +95 -122
  78. package/libx/_runtime/common/utils/csn.js +14 -3
  79. package/libx/_runtime/common/utils/foreignKeyPropagations.js +18 -1
  80. package/libx/_runtime/common/utils/keys.js +2 -1
  81. package/libx/_runtime/common/utils/path.js +1 -1
  82. package/libx/_runtime/common/utils/resolveView.js +12 -4
  83. package/libx/_runtime/common/utils/rewriteAsterisks.js +27 -13
  84. package/libx/_runtime/common/utils/search2cqn4sql.js +11 -6
  85. package/libx/_runtime/common/utils/vcap.js +27 -10
  86. package/libx/_runtime/db/data-conversion/post-processing.js +20 -13
  87. package/libx/_runtime/db/expand/expand-v2.js +21 -12
  88. package/libx/_runtime/db/expand/expandCQNToJoin.js +8 -6
  89. package/libx/_runtime/db/expand/index.js +3 -0
  90. package/libx/_runtime/db/generic/create.js +0 -10
  91. package/libx/_runtime/db/generic/index.js +3 -0
  92. package/libx/_runtime/db/generic/read.js +2 -24
  93. package/libx/_runtime/db/generic/rewrite.js +1 -3
  94. package/libx/_runtime/db/generic/update.js +1 -1
  95. package/libx/_runtime/db/query/delete.js +10 -4
  96. package/libx/_runtime/db/query/insert.js +3 -3
  97. package/libx/_runtime/db/query/read.js +4 -1
  98. package/libx/_runtime/db/query/update.js +5 -5
  99. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +9 -2
  100. package/libx/_runtime/db/sql-builder/FunctionBuilder.js +3 -0
  101. package/libx/_runtime/db/sql-builder/index.js +3 -0
  102. package/libx/_runtime/db/utils/columns.js +5 -2
  103. package/libx/_runtime/db/utils/deep.js +6 -8
  104. package/libx/_runtime/db/utils/generateAliases.js +56 -6
  105. package/libx/_runtime/fiori/generic/before.js +73 -49
  106. package/libx/_runtime/fiori/generic/edit.js +14 -18
  107. package/libx/_runtime/fiori/generic/patch.js +8 -11
  108. package/libx/_runtime/fiori/generic/read.js +19 -16
  109. package/libx/_runtime/fiori/generic/readOverDraft.js +1 -4
  110. package/libx/_runtime/hana/Service.js +1 -1
  111. package/libx/_runtime/hana/conversion.js +10 -0
  112. package/libx/_runtime/hana/execute.js +33 -16
  113. package/libx/_runtime/hana/search.js +3 -3
  114. package/libx/_runtime/hana/search2cqn4sql.js +22 -21
  115. package/libx/_runtime/hana/searchToContains.js +1 -1
  116. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +1 -1
  117. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +0 -1
  118. package/libx/_runtime/messaging/enterprise-messaging.js +1 -0
  119. package/libx/_runtime/messaging/file-based.js +3 -1
  120. package/libx/_runtime/messaging/service.js +4 -1
  121. package/libx/_runtime/remote/utils/client.js +33 -20
  122. package/libx/_runtime/remote/utils/data.js +52 -11
  123. package/libx/_runtime/sqlite/Service.js +1 -1
  124. package/libx/_runtime/sqlite/conversion.js +10 -0
  125. package/libx/_runtime/types/api.js +2 -2
  126. package/libx/gql/resolvers/parse/ast/enrich.js +1 -0
  127. package/libx/odata/afterburner.js +29 -6
  128. package/libx/odata/cqn2odata.js +9 -0
  129. package/libx/odata/grammar.pegjs +49 -21
  130. package/libx/odata/index.js +2 -2
  131. package/libx/odata/parser.js +1 -1
  132. package/libx/odata/utils.js +2 -2
  133. package/libx/rest/RestAdapter.js +29 -1
  134. package/libx/rest/middleware/auth.js +1 -3
  135. package/libx/rest/middleware/parse.js +1 -0
  136. package/package.json +1 -1
  137. package/server.js +1 -1
  138. package/bin/deploy/to-hana/logger.js +0 -27
  139. package/bin/deploy/to-hana/runCommand.js +0 -113
  140. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/selectHelper.js +0 -37
  141. package/libx/_runtime/common/utils/auth.js +0 -16
@@ -12,9 +12,7 @@ const { DRAFT_COLUMNS_ADMIN } = require('../../common/constants/draft')
12
12
 
13
13
  // copied from adapter/odata-v4/utils/context-object
14
14
  const _getTargetEntityName = (service, pathSegments) => {
15
- if (isCustomOperation(pathSegments, false)) {
16
- return undefined
17
- }
15
+ if (isCustomOperation(pathSegments, false)) return
18
16
 
19
17
  let navSegmentName
20
18
  let entityName = `${service.name}.${pathSegments[0].getEntitySet().getName()}`
@@ -36,12 +34,11 @@ const _getTargetEntityName = (service, pathSegments) => {
36
34
  * @returns {object}
37
35
  * @private
38
36
  */
39
- const _getParent = (service, req) => {
37
+ const _getParent = (req, service) => {
40
38
  // REVISIT: get rid of getUriInfo
41
39
  if (!req.getUriInfo) return
42
40
 
43
41
  const segments = req.getUriInfo().getPathSegments()
44
-
45
42
  if (segments.length === 1) return
46
43
 
47
44
  const parent = {
@@ -50,6 +47,7 @@ const _getParent = (service, req) => {
50
47
 
51
48
  const parentKeyPredicates = segments[segments.length - 2].getKeyPredicates()
52
49
  let keyPredicateName, keyPredicateText
50
+
53
51
  for (const keyPredicate of parentKeyPredicates) {
54
52
  keyPredicateName = keyPredicate.getEdmRef().getName()
55
53
  keyPredicateText = keyPredicate.getText()
@@ -65,36 +63,28 @@ const _getParent = (service, req) => {
65
63
  return parent
66
64
  }
67
65
 
68
- const _validateDraft = (draftResult, req) => {
69
- if (!draftResult || draftResult.length === 0) {
70
- req.reject(404)
71
- }
72
-
66
+ const _validateDraft = (req, draftResult, isBoundAction) => {
67
+ if (!draftResult || draftResult.length === 0) req.reject(404)
73
68
  const draftAdminData = draftResult[0]
74
69
 
75
- // the same user that locked the entity can always delete it
76
- if (draftAdminData.InProcessByUser === req.user.id) {
77
- return
78
- }
70
+ // the same user that locked the entity can always delete/update it
71
+ if (draftAdminData.InProcessByUser === req.user.id) return
79
72
 
80
- // proceed with the delete action only if it was initiated by a different user
81
- // than the one who locked the entity and the configured drafts cancellation
73
+ // proceed with the delete/update action only if it was initiated by a different
74
+ // user than the one who locked the entity and the configured drafts cancellation
82
75
  // timeout timer has expired
83
76
  if (draftIsLocked(draftAdminData.LastChangeDateTime)) {
84
77
  req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
85
78
  }
79
+
80
+ // At this point, the request user ID isn't the owner of the draft.
81
+ if (isBoundAction) req.reject(403)
86
82
  }
87
83
 
88
84
  const _addDraftDataToContext = (req, result) => {
89
- _validateDraft(result, req)
90
-
91
- if (req.rejected) {
92
- return
93
- }
94
-
95
- if (!req._draftMetadata) {
96
- req._draftMetadata = {}
97
- }
85
+ if (!result || result.length === 0) return
86
+ if (req.rejected) return
87
+ if (!req._draftMetadata) req._draftMetadata = {}
98
88
 
99
89
  DRAFT_COLUMNS_ADMIN.forEach(column => {
100
90
  if (column in result[0]) req._draftMetadata[column] = result[0][column]
@@ -103,11 +93,7 @@ const _addDraftDataToContext = (req, result) => {
103
93
  req.data.DraftAdministrativeData_DraftUUID = result[0].DraftUUID
104
94
  }
105
95
 
106
- const _prefixDraftColumns = () => {
107
- return DRAFT_COLUMNS_ADMIN.map(col => {
108
- return { ref: ['DRAFT_DraftAdministrativeData', col] }
109
- })
110
- }
96
+ const _prefixDraftColumns = () => DRAFT_COLUMNS_ADMIN.map(col => ({ ref: ['DRAFT_DraftAdministrativeData', col] }))
111
97
 
112
98
  const _getSelectDraftDataCqn = (entityName, where) => {
113
99
  return SELECT.from(ensureDraftsSuffix(entityName), _prefixDraftColumns())
@@ -116,27 +102,41 @@ const _getSelectDraftDataCqn = (entityName, where) => {
116
102
  .where(where)
117
103
  }
118
104
 
119
- const _addDraftDataFromExistingDraft = async (req, service) => {
120
- const parent = _getParent(service, req)
121
- let result
122
-
123
- if (parent && parent.IsActiveEntity === 'false') {
124
- const parentWhere = [{ ref: [parent.keyName] }, '=', { val: parent.keyValue }]
125
- result = await cds.tx(req).run(_getSelectDraftDataCqn(parent.entityName, parentWhere))
126
- _addDraftDataToContext(req, result)
127
- return result
105
+ const _getDraftDataFromExistingDraft = async (req, service, parent = _getParent(req, service)) => {
106
+ if (parent) {
107
+ if (parent.IsActiveEntity === 'false') {
108
+ const parentWhere = [{ ref: [parent.keyName] }, '=', { val: parent.keyValue }]
109
+ const query = _getSelectDraftDataCqn(parent.entityName, parentWhere)
110
+ const result = await cds.tx(req).run(query)
111
+ return result
112
+ }
113
+
114
+ return []
128
115
  }
129
116
 
130
- if (!parent) {
131
- const rootWhere = getKeysCondition(req.target, req.data)
132
- result = await cds.tx(req).run(_getSelectDraftDataCqn(ensureNoDraftsSuffix(req.target.name), rootWhere))
133
- if (result && result.length > 0) {
117
+ const rootWhere = getKeysCondition(req.target, req.data)
118
+ const query = _getSelectDraftDataCqn(ensureNoDraftsSuffix(req.target.name), rootWhere)
119
+ const result = await cds.tx(req).run(query)
120
+ return result
121
+ }
122
+
123
+ const _addDraftDataFromExistingDraft = async (req, service) => {
124
+ const parent = _getParent(req, service)
125
+ const result = await _getDraftDataFromExistingDraft(req, service, parent)
126
+
127
+ if (parent) {
128
+ if (parent.IsActiveEntity === 'false') {
129
+ _validateDraft(req, result)
134
130
  _addDraftDataToContext(req, result)
131
+ return result
135
132
  }
136
- return result
133
+
134
+ return []
137
135
  }
138
136
 
139
- return []
137
+ if (result && result.length > 0) _validateDraft(req, result)
138
+ _addDraftDataToContext(req, result)
139
+ return result
140
140
  }
141
141
 
142
142
  const _addGeneratedDraftUUID = async req => {
@@ -156,11 +156,12 @@ const _new = async function (req) {
156
156
  if (isNavigationToMany(req)) {
157
157
  const result = await _addDraftDataFromExistingDraft(req, this)
158
158
 
159
- // in order to fix strange case where active subitems are created in draft case
159
+ // in order to fix corner case where active subitems are created in draft case
160
160
  if (result.length === 0) req.reject(404)
161
- } else {
162
- _addGeneratedDraftUUID(req)
161
+ return
163
162
  }
163
+
164
+ _addGeneratedDraftUUID(req)
164
165
  }
165
166
 
166
167
  /**
@@ -173,7 +174,7 @@ const _patchUpdate = async function (req) {
173
174
 
174
175
  const result = await _addDraftDataFromExistingDraft(req, this)
175
176
 
176
- // means that draft not exists
177
+ // means that the draft does not exists
177
178
  if (result.length === 0) req.reject(404)
178
179
  }
179
180
 
@@ -186,6 +187,28 @@ const _deleteCancel = async function (req) {
186
187
  await _addDraftDataFromExistingDraft(req, this)
187
188
  }
188
189
 
190
+ const _validateDraftBoundAction = async function (req, srv) {
191
+ const result = await _getDraftDataFromExistingDraft(req, srv)
192
+ const isBoundAction = true
193
+ if (result && result.length > 0) _validateDraft(req, result, isBoundAction)
194
+ }
195
+
196
+ const _registerBoundActionHandlers = function (entityName, actions) {
197
+ if (!actions) return
198
+
199
+ const boundActions = Object.values(actions).filter(
200
+ action =>
201
+ action.kind === 'action' &&
202
+ action.name !== 'draftPrepare' &&
203
+ action.name !== 'draftEdit' &&
204
+ action.name !== 'draftActivate'
205
+ )
206
+
207
+ for (const action of boundActions) {
208
+ this.before(action.name, entityName, req => _validateDraftBoundAction(req, this))
209
+ }
210
+ }
211
+
189
212
  module.exports = cds.service.impl(function () {
190
213
  _new._initial = true
191
214
  _patchUpdate._initial = true
@@ -196,5 +219,6 @@ module.exports = cds.service.impl(function () {
196
219
  this.before('NEW', entity, _new)
197
220
  this.before(['PATCH', 'UPDATE'], entity, _patchUpdate)
198
221
  this.before(['DELETE', 'CANCEL'], entity, _deleteCancel)
222
+ _registerBoundActionHandlers.call(this, entity.name, entity.actions)
199
223
  }
200
224
  })
@@ -56,21 +56,18 @@ const _getLockWhere = (where, columnsMap) => {
56
56
  return lockWhere
57
57
  }
58
58
 
59
- const _select = async (CQNs, req, dbtx) => {
60
- let allResults
61
-
59
+ const _select = async (lockRecordCQN, draftExistsCQN, selectCQNs, req, dbtx) => {
62
60
  try {
63
- allResults = await Promise.all(CQNs.map(CQN => dbtx.run(CQN)))
64
- } catch (err) {
65
- // resource busy and NOWAIT (WAIT 0) specified (heuristic error handling method)
66
- if (err.query.includes('FOR UPDATE')) {
67
- req.reject(409, 'DRAFT_ALREADY_EXISTS')
68
- }
69
-
70
- req.reject(err)
61
+ await dbtx.run(lockRecordCQN)
62
+ } catch (e) {
63
+ const drafts = await dbtx.run(draftExistsCQN)
64
+ if (drafts.length) req.reject(409, 'DRAFT_ALREADY_EXISTS')
65
+ req.reject(409, 'ENTITY_LOCKED')
71
66
  }
72
-
73
- return allResults
67
+ const promisesResults = await Promise.allSettled([dbtx.run(draftExistsCQN), ...selectCQNs.map(cqn => dbtx.run(cqn))])
68
+ const firstRejected = promisesResults.find(r => r.status === 'rejected')
69
+ if (firstRejected) req.reject(firstRejected.reason)
70
+ return promisesResults.map(r => r.value)
74
71
  }
75
72
 
76
73
  /**
@@ -127,11 +124,9 @@ const _handler = async function (req) {
127
124
  }
128
125
  }
129
126
 
130
- const lockAndSelectCQNs = [lockRecordCQN, draftExistsCQN, ...selectCQNs]
131
-
132
127
  const dbtx = cds.tx(req)
133
128
  // REVISIT: Use service.read with expand **
134
- const [, draftExists, ...results] = await _select(lockAndSelectCQNs, req, dbtx)
129
+ const [draftExists, ...results] = await _select(lockRecordCQN, draftExistsCQN, [...selectCQNs], req, dbtx)
135
130
 
136
131
  if (!results[0].length) {
137
132
  req.reject(404)
@@ -169,12 +164,13 @@ const _handler = async function (req) {
169
164
 
170
165
  await Promise.all(insertCQNs.map(CQN => dbtx.run(CQN)))
171
166
  setStatusCodeAndHeader(req._.odataRes, rootWhere, req.target.name.replace(`${this.name}.`, ''), false)
172
-
173
167
  return Object.assign({}, results[0][0], { HasDraftEntity: false, HasActiveEntity: true, IsActiveEntity: false })
174
168
  }
175
169
 
176
170
  module.exports = cds.service.impl(function () {
177
- for (const entity of Object.values(this.entities).filter(e => e._isDraftEnabled)) {
171
+ const entities = Object.values(this.entities).filter(e => e._isDraftEnabled)
172
+
173
+ for (const entity of entities) {
178
174
  this.on('EDIT', entity, _handler)
179
175
  }
180
176
  })
@@ -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
  })
@@ -779,13 +779,16 @@ const _getDraftDoc = (req, draftName, draftWhere) => {
779
779
  return draftDocs
780
780
  }
781
781
 
782
- const _getOrderByEnrichedColumns = (orderBy, columns) => {
782
+ const _getOrderByEnrichedColumns = (orderBy, columns, entity) => {
783
783
  const enrichedCol = []
784
784
  if (orderBy && orderBy.length > 1) {
785
785
  const colNames = columns.map(el => el.ref[el.ref.length - 1])
786
786
  // REVISIT: GET Books?$select=title&$expand=NotBooks($select=pages)&$orderby=NotBooks/title - what's then?
787
787
  for (const el of orderBy) {
788
- 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])) {
789
792
  enrichedCol.push({ ref: [...el.ref] })
790
793
  }
791
794
  }
@@ -807,11 +810,11 @@ const _replaceDraftAlias = where => {
807
810
 
808
811
  const _poorMansAlias4 = xpr => '_' + xpr.ref.join('_') + '_'
809
812
 
810
- const _getUnionCQN = (req, draftName, columns, subSelect, draftWhere, model) => {
813
+ const _getUnionCQN = (req, draftName, columns, subSelect, draftWhere, model, entity) => {
811
814
  const draftActiveWhere = _getWhereForActive(draftWhere)
812
815
  const activeDocs = getEnrichedCQN(SELECT.from(req.target), req.query.SELECT, draftActiveWhere, undefined, false)
813
816
  activeDocs.where(_getWhereWithAppendedDraftRestrictions([], req))
814
- convertWhereExists(activeDocs, model, {})
817
+ convertWhereExists(activeDocs.SELECT, model, {})
815
818
 
816
819
  // @restrict.where not applicable for drafts (I can ALWAYS read mine)
817
820
  _replaceDraftAlias(draftWhere)
@@ -838,7 +841,7 @@ const _getUnionCQN = (req, draftName, columns, subSelect, draftWhere, model) =>
838
841
  return union.columns({ func: 'sum', args: [{ ref: ['$count'] }], as: '$count' })
839
842
  }
840
843
 
841
- const enrichedColumns = _getOrderByEnrichedColumns(req.query.SELECT.orderBy, columns)
844
+ const enrichedColumns = _getOrderByEnrichedColumns(req.query.SELECT.orderBy, columns, entity)
842
845
 
843
846
  for (const col of enrichedColumns) {
844
847
  // if we have columns for outer order by that may also be needed for joins, we need to duplicate them
@@ -915,12 +918,14 @@ const _excludeActiveDraftExists = (req, draftWhere, columns, model) => {
915
918
  ])
916
919
  .where(_inProcessByUserWhere(req.user.id))
917
920
 
921
+ const targetName = ensureNoDraftsSuffix(req.target.name)
918
922
  for (const key of _getTargetKeys(req)) {
919
- subSelect.where([{ ref: [ensureNoDraftsSuffix(req.target.name), key] }, '=', { ref: [draftName, key] }])
923
+ subSelect.where([{ ref: [targetName, key] }, '=', { ref: [draftName, key] }])
920
924
  }
921
925
 
926
+ const entity = model.definitions[targetName]
922
927
  draftWhere = removeIsActiveEntityRecursively(draftWhere)
923
- const cqn = _getUnionCQN(req, draftName, columns, subSelect, draftWhere, model)
928
+ const cqn = _getUnionCQN(req, draftName, columns, subSelect, draftWhere, model, entity)
924
929
  cqn.SELECT.from.as = name
925
930
 
926
931
  if (cqn.SELECT.orderBy) {
@@ -960,13 +965,16 @@ const _validatedWithSiblingInProcess = (req, draftWhere, draftParameters, column
960
965
  )
961
966
  return _excludeActiveDraftExists(req, draftWhere, columns, model)
962
967
  if (
968
+ draftInProcessByUser &&
963
969
  draftInProcessByUser.op === '!=' &&
964
970
  _isValidWithDraftLocked(isActiveEntity, siblingIsActive, draftInProcessByUser)
965
971
  ) {
966
972
  return _activeWithDraftInProcess(req, draftWhere, columns, req.user.id)
967
- } else if (_isValidWithDraftTimeout(isActiveEntity, siblingIsActive, draftInProcessByUser)) {
973
+ } else if (draftInProcessByUser && _isValidWithDraftTimeout(isActiveEntity, siblingIsActive, draftInProcessByUser)) {
968
974
  return _activeWithDraftInProcess(req, draftWhere, columns, null)
969
975
  }
976
+
977
+ //
970
978
  }
971
979
 
972
980
  const _validatedDraftOfWhichIAmOwner = (req, draftWhere, draftParameters, columns) =>
@@ -1215,16 +1223,13 @@ const _handler = async function (req) {
1215
1223
  // handle localized here as it was previously handled for req.target
1216
1224
  req.target = _getLocalizedEntity(this.model, req.target, req.user) || req.target
1217
1225
 
1218
- // REVISIT
1219
- delete req.query._validationQuery
1220
-
1221
1226
  const originalFrom = _copyCQNPartial(req.query.SELECT.from)
1222
1227
 
1223
1228
  // REVISIT DRAFT HANDLING: cqn2cqn4sql must not be called here
1224
- const sqlQuery = cqn2cqn4sql(req.query, this.model, { draft: true })
1229
+ const query4sql = cqn2cqn4sql(req.query, this.model, { _4fiori: true })
1225
1230
 
1226
1231
  // do not clone with Object.assign as that would skip all non-enumerable properties
1227
- const reqClone = { __proto__: req, query: _copyCQNPartial(sqlQuery) }
1232
+ const reqClone = { __proto__: req, query: _copyCQNPartial(query4sql) }
1228
1233
 
1229
1234
  // ensure draft restrictions are copied to new query
1230
1235
  reqClone.query._draftRestrictions = req.query._draftRestrictions
@@ -1234,6 +1239,7 @@ const _handler = async function (req) {
1234
1239
  reqClone.query._streaming = true
1235
1240
  return cds.tx(req).run(reqClone.query)
1236
1241
  }
1242
+
1237
1243
  let cqnScenario
1238
1244
 
1239
1245
  // to replace scenario CQNs for queries with $apply SELECT chain (new odata2cqn parser)
@@ -1264,16 +1270,13 @@ const _handler = async function (req) {
1264
1270
  )
1265
1271
 
1266
1272
  _adaptSubSelects(cqnScenario.cqn, cqnScenario.scenario)
1267
-
1268
1273
  _adaptAnnotationAliases(cqnScenario.cqn)
1269
1274
 
1270
1275
  // unlocalize for db and after handlers as it was before
1271
1276
  req.target = this.model.definitions[ensureUnlocalized(req.target.name)]
1272
1277
 
1273
1278
  const result = await cds.tx(req).send({ query: cqnScenario.cqn, target: req.target })
1274
-
1275
1279
  const resultAsArray = Array.isArray(result) ? result : result ? [result] : []
1276
-
1277
1280
  removeDraftUUIDIfNecessary(resultAsArray, req)
1278
1281
 
1279
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) {
@@ -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