@sap/cds 6.7.1 → 6.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/CHANGELOG.md +50 -1
  2. package/_i18n/i18n.properties +9 -6
  3. package/_i18n/i18n_ar.properties +6 -6
  4. package/_i18n/i18n_cs.properties +6 -6
  5. package/_i18n/i18n_da.properties +6 -6
  6. package/_i18n/i18n_de.properties +6 -6
  7. package/_i18n/i18n_en.properties +6 -6
  8. package/_i18n/i18n_es.properties +6 -6
  9. package/_i18n/i18n_fi.properties +6 -6
  10. package/_i18n/i18n_fr.properties +6 -6
  11. package/_i18n/i18n_hu.properties +6 -6
  12. package/_i18n/i18n_it.properties +6 -6
  13. package/_i18n/i18n_ja.properties +6 -6
  14. package/_i18n/i18n_ko.properties +6 -6
  15. package/_i18n/i18n_ms.properties +6 -6
  16. package/_i18n/i18n_nl.properties +6 -6
  17. package/_i18n/i18n_no.properties +6 -6
  18. package/_i18n/i18n_pl.properties +6 -6
  19. package/_i18n/i18n_pt.properties +6 -6
  20. package/_i18n/i18n_ro.properties +6 -6
  21. package/_i18n/i18n_ru.properties +6 -6
  22. package/_i18n/i18n_sv.properties +6 -6
  23. package/_i18n/i18n_th.properties +6 -6
  24. package/_i18n/i18n_tr.properties +8 -8
  25. package/_i18n/i18n_zh_CN.properties +3 -3
  26. package/_i18n/i18n_zh_TW.properties +6 -6
  27. package/apis/core.d.ts +30 -31
  28. package/apis/csn.d.ts +1 -1
  29. package/apis/ql.d.ts +69 -39
  30. package/apis/serve.d.ts +4 -3
  31. package/apis/services.d.ts +20 -7
  32. package/bin/build/buildTaskEngine.js +1 -1
  33. package/bin/build/index.js +1 -1
  34. package/bin/build/provider/buildTaskProviderInternal.js +9 -6
  35. package/bin/build/provider/hana/index.js +11 -4
  36. package/bin/build/provider/mtx-sidecar/index.js +3 -3
  37. package/bin/build/provider/nodejs/index.js +23 -0
  38. package/bin/version.js +3 -2
  39. package/common.cds +3 -2
  40. package/lib/auth/index.js +3 -0
  41. package/lib/auth/mocked-users.js +13 -0
  42. package/lib/compile/etc/_localized.js +3 -0
  43. package/lib/core/entities.js +7 -3
  44. package/lib/dbs/cds-deploy.js +36 -12
  45. package/lib/env/cds-env.js +47 -14
  46. package/lib/env/cds-requires.js +16 -7
  47. package/lib/env/defaults.js +2 -2
  48. package/lib/env/schemas/cds-rc.json +1 -8
  49. package/lib/index.js +1 -1
  50. package/lib/ql/STREAM.js +89 -0
  51. package/lib/ql/cds-ql.js +2 -1
  52. package/lib/req/request.js +5 -2
  53. package/lib/req/user.js +1 -1
  54. package/lib/srv/middlewares/index.js +9 -7
  55. package/lib/srv/middlewares/trace.js +6 -5
  56. package/lib/srv/srv-api.js +1 -0
  57. package/lib/utils/cds-utils.js +1 -1
  58. package/lib/utils/tar.js +30 -31
  59. package/libx/_runtime/audit/Service.js +96 -37
  60. package/libx/_runtime/audit/generic/personal/utils.js +26 -13
  61. package/libx/_runtime/audit/utils/v2.js +21 -22
  62. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +1 -1
  63. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
  64. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
  65. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +7 -6
  66. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -1
  67. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/batch/BatchProcessor.js +2 -0
  68. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +2 -1
  69. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +10 -3
  70. package/libx/_runtime/cds-services/services/Service.js +2 -7
  71. package/libx/_runtime/cds-services/services/utils/differ.js +1 -1
  72. package/libx/_runtime/common/aspects/any.js +1 -1
  73. package/libx/_runtime/common/generic/auth/utils.js +30 -41
  74. package/libx/_runtime/common/generic/input.js +14 -8
  75. package/libx/_runtime/common/i18n/messages.properties +1 -1
  76. package/libx/_runtime/db/expand/expandCQNToJoin.js +19 -17
  77. package/libx/_runtime/db/expand/rawToExpanded.js +3 -5
  78. package/libx/_runtime/db/utils/generateAliases.js +1 -1
  79. package/libx/_runtime/fiori/generic/activate.js +1 -1
  80. package/libx/_runtime/fiori/generic/before.js +18 -19
  81. package/libx/_runtime/fiori/generic/prepare.js +1 -1
  82. package/libx/_runtime/fiori/generic/read.js +1 -1
  83. package/libx/_runtime/fiori/lean-draft.js +27 -21
  84. package/libx/_runtime/fiori/utils/handler.js +0 -6
  85. package/libx/_runtime/hana/customBuilder/CustomFunctionBuilder.js +1 -1
  86. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +0 -5
  87. package/libx/_runtime/hana/pool.js +26 -18
  88. package/libx/_runtime/hana/search2Contains.js +1 -1
  89. package/libx/_runtime/hana/search2cqn4sql.js +26 -18
  90. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +23 -16
  91. package/libx/_runtime/messaging/outbox/utils.js +6 -1
  92. package/libx/_runtime/remote/Service.js +64 -38
  93. package/libx/_runtime/remote/utils/client.js +13 -9
  94. package/libx/rest/middleware/read.js +2 -1
  95. package/package.json +1 -1
@@ -40,54 +40,51 @@ const _getCurrentSubClause = (next, restrict) => {
40
40
  const escaped = next[0].replace(/\$/g, '\\$').replace(/\./g, '\\.')
41
41
  const re1 = new RegExp(`([\\w\\.']*)\\s*=\\s*(${escaped})|(${escaped})\\s*=\\s*([\\w\\.']*)`)
42
42
  const re2 = new RegExp(`([\\w\\.']*)\\s*in\\s*(${escaped})|(${escaped})\\s*in\\s*([\\w\\.']*)`)
43
- const clause = restrict.where.match(re1) || restrict.where.match(re2)
43
+ const re3 = new RegExp(`(${escaped})\\s*is\\s*null`)
44
+ const re4 = new RegExp(`(${escaped})\\s*is\\s*not\\s*null`)
45
+ const clause =
46
+ restrict.where.match(re3) || restrict.where.match(re4) || restrict.where.match(re1) || restrict.where.match(re2)
44
47
 
45
48
  if (clause) return clause
46
49
 
47
50
  // NOTE: arrayed attr with "=" as operator is some kind of legacy case
48
- throw new Error('user attribute array must be used with operator "=" or "in"')
51
+ throw new Error('user attribute array must be used with operator "=", "in", "is null", or "is not null"')
49
52
  }
50
53
 
51
- const _processUserAttr = (next, restrict, user, attr) => {
54
+ const _isNull = (userAttrs, attr) =>
55
+ userAttrs[attr] == null || (Array.isArray(userAttrs[attr]) && userAttrs[attr].length === 0)
56
+ const _isNotNull = (userAttrs, attr) =>
57
+ userAttrs[attr] != null && Array.isArray(userAttrs[attr]) && userAttrs[attr].length > 0
58
+
59
+ const _processUserAttr = (next, restrict, userAttrs, attr) => {
52
60
  const clause = _getCurrentSubClause(next, restrict)
53
61
  const valOrRef = clause[1] || clause[4]
54
62
 
55
- if (clause[0].match(/ in /)) {
56
- if (!user[attr] || user[attr].length === 0) {
63
+ if (clause[0].match(/ is\s*null/)) {
64
+ restrict.where = restrict.where.replace(clause[0], _isNull(userAttrs, attr) ? '1 = 1' : '1 = 2')
65
+ } else if (clause[0].match(/ is\s*not\s*null/)) {
66
+ restrict.where = restrict.where.replace(clause[0], _isNotNull(userAttrs, attr) ? '1 = 1' : '1 = 2')
67
+ } else {
68
+ if (_isNull(userAttrs, attr)) {
57
69
  restrict.where = restrict.where.replace(clause[0], '1 = 2')
58
- } else if (user[attr].length === 1) {
59
- restrict.where = restrict.where.replace(clause[0], `${valOrRef} = '${user[attr][0]}'`)
70
+ } else if (clause[0].match(/ in /)) {
71
+ if (userAttrs[attr].length === 1) {
72
+ restrict.where = restrict.where.replace(clause[0], `${valOrRef} = '${userAttrs[attr][0]}'`)
73
+ } else {
74
+ restrict.where = restrict.where.replace(
75
+ clause[0],
76
+ `${valOrRef} in (${userAttrs[attr].map(ele => `'${ele}'`).join(', ')})`
77
+ )
78
+ }
79
+ } else if (valOrRef.startsWith("'") && userAttrs[attr].includes(valOrRef.split("'")[1])) {
80
+ restrict.where = restrict.where.replace(clause[0], `${valOrRef} = ${valOrRef}`)
60
81
  } else {
61
82
  restrict.where = restrict.where.replace(
62
83
  clause[0],
63
- `${valOrRef} in (${user[attr].map(ele => `'${ele}'`).join(', ')})`
84
+ `(${userAttrs[attr].map(ele => `${valOrRef} = '${ele}'`).join(' or ')})`
64
85
  )
65
86
  }
66
- } else if (valOrRef.startsWith("'") && user[attr].includes(valOrRef.split("'")[1])) {
67
- restrict.where = restrict.where.replace(clause[0], `${valOrRef} = ${valOrRef}`)
68
- } else {
69
- restrict.where = restrict.where.replace(
70
- clause[0],
71
- `(${user[attr].map(ele => `${valOrRef} = '${ele}'`).join(' or ')})`
72
- )
73
- }
74
- }
75
-
76
- const _getShortcut = (attrs, attr) => {
77
- // undefined
78
- if (attrs[attr] === undefined) {
79
- return '1 = 2'
80
87
  }
81
-
82
- // $UNRESTRICTED
83
- if (
84
- (typeof attrs[attr] === 'string' && attrs[attr].match(/\$UNRESTRICTED/i)) ||
85
- (Array.isArray(attrs[attr]) && attrs[attr].some(a => a.match(/\$UNRESTRICTED/i)))
86
- ) {
87
- return '1 = 1'
88
- }
89
-
90
- return null
91
88
  }
92
89
 
93
90
  /*
@@ -120,15 +117,7 @@ const resolveUserAttrs = (restrict, req) => {
120
117
  let attr = parts.shift()
121
118
 
122
119
  while (attr) {
123
- const shortcut = _getShortcut(attrs, attr)
124
- if (shortcut) {
125
- const clause = _getCurrentSubClause(next, restrict)
126
- restrict.where = restrict.where.replace(clause[0], shortcut)
127
- skip = true
128
- break
129
- }
130
-
131
- if (Array.isArray(attrs[attr])) {
120
+ if (attrs[attr] === undefined || Array.isArray(attrs[attr])) {
132
121
  _processUserAttr(next, restrict, attrs, attr)
133
122
  skip = true
134
123
  break
@@ -47,8 +47,12 @@ const _isStreamingProperty = (elements, row, property) =>
47
47
  element => element['@Core.MediaType'] && element['@Core.MediaType']['='] === property && row[element.name]
48
48
  )
49
49
 
50
- const _getMediaTypeValue = req =>
51
- !req.http?.req?.headers?.['content-type'].match(/json|multipart/i) && req.http?.req?.headers?.['content-type']
50
+ const _getMediaTypeValue = () => {
51
+ const ctx = cds.context
52
+ return (
53
+ !ctx?.http?.req?.headers?.['content-type']?.match(/json|multipart/i) && ctx?.http?.req?.headers?.['content-type']
54
+ )
55
+ }
52
56
 
53
57
  const _preProcessAssertTarget = (assocInfo, assertMap) => {
54
58
  const { element: assoc, row } = assocInfo
@@ -145,9 +149,12 @@ const _processCategory = (req, category, value, elementInfo, assertMap) => {
145
149
  }
146
150
 
147
151
  // set media type from content-type header if streaming
148
- const isStreaming = _isStreamingProperty(element.parent.elements, row, key)
149
- const mtValue = _getMediaTypeValue(req)
150
- if (category === 'stream' && isStreaming && mtValue) row[key] = mtValue
152
+ if (category === 'stream') {
153
+ if (_isStreamingProperty(element.parent.elements, row, key)) {
154
+ const mtValue = _getMediaTypeValue()
155
+ if (mtValue) row[key] = mtValue
156
+ }
157
+ }
151
158
  }
152
159
 
153
160
  const _getProcessorFn = (req, errors, assertMap) => {
@@ -219,7 +226,7 @@ const _callError = (req, errors) => {
219
226
  for (const error of errors) req.error(error)
220
227
  }
221
228
 
222
- const _getBoundAction = req => req.target.actions?.[req.context?.event]
229
+ const _getBoundAction = req => req.target.actions?.[req._?.event || req.context?.event]
223
230
  const _getBoundActionBindingParameter = action => action['@cds.odata.bindingparameter.name'] || 'in'
224
231
 
225
232
  async function commonGenericInput(req) {
@@ -248,9 +255,8 @@ async function commonGenericInput(req) {
248
255
 
249
256
  if (boundAction) {
250
257
  const pathSegment = _getBoundActionBindingParameter(boundAction)
251
- const keys = req._ && req._.params && req._.params[0]
252
258
  if (pathSegment) pathOptions.pathSegmentsInfo.push(pathSegment)
253
-
259
+ const keys = req._?.params?.[0]
254
260
  if (keys && 'IsActiveEntity' in keys) {
255
261
  pathOptions.draftKeys = { IsActiveEntity: keys.IsActiveEntity }
256
262
  }
@@ -85,7 +85,7 @@ CRUD_VIA_NAVIGATION_NOT_SUPPORTED=CRUD via navigations is not yet supported
85
85
 
86
86
  # draft
87
87
  DRAFT_ALREADY_EXISTS=A draft for this entity already exists
88
- DRAFT_LOCKED_BY_ANOTHER_USER=The entity is locked by another user
88
+ DRAFT_LOCKED_BY_ANOTHER_USER=The entity is locked by user "{0}"
89
89
  DRAFT_MODIFICATION_ONLY_VIA_ROOT=A draft can only be modified via its root entity
90
90
 
91
91
  # singleton
@@ -66,7 +66,6 @@ class JoinCQNFromExpanded {
66
66
 
67
67
  // Get first level of expanding regarding to many and all to one if not part of a nested to many expand.
68
68
  this._createJoinCQNFromExpanded(this._SELECT, [])
69
-
70
69
  return this
71
70
  }
72
71
 
@@ -117,10 +116,10 @@ class JoinCQNFromExpanded {
117
116
  */
118
117
  _createJoinCQNFromExpanded(SELECT, toManyTree, defaultLanguage) {
119
118
  const joinArgs = SELECT.from.args
120
- const isJoinOfTwoSelects = joinArgs && joinArgs.every(a => a.SELECT)
119
+ const isJoinOfTwoSelects = joinArgs?.every(a => a.SELECT)
121
120
 
122
121
  const unionTableRef = this._getUnionTable(SELECT)
123
- const unionTable = unionTableRef && unionTableRef.table
122
+ const unionTable = unionTableRef?.table
124
123
  const tableAlias = this._getTableAlias(SELECT, toManyTree)
125
124
 
126
125
  const readToOneCQN = this._getReadToOneCQN(SELECT, isJoinOfTwoSelects ? 'filterExpand' : tableAlias)
@@ -178,7 +177,8 @@ class JoinCQNFromExpanded {
178
177
  * @private
179
178
  */
180
179
  _getTableAlias(SELECT, toManyTree) {
181
- return this._createAlias(toManyTree.length === 0 ? this._getRef(SELECT).table : toManyTree.join(':'))
180
+ const ref = this._getRef(SELECT)
181
+ return this._createAlias(toManyTree.length === 0 ? ref.table : toManyTree.join(':'), ref.as)
182
182
  }
183
183
 
184
184
  _getRef(SELECT) {
@@ -212,15 +212,21 @@ class JoinCQNFromExpanded {
212
212
  * Create an alias from value.
213
213
  *
214
214
  * @param {string} value
215
+ * @param {string} [alias]
215
216
  * @returns {string}
216
217
  * @private
217
218
  */
218
- _createAlias(value) {
219
+ _createAlias(value, alias) {
219
220
  if (!this._aliases) {
220
221
  this._aliases = {}
221
222
  }
222
223
 
223
224
  if (!this._aliases[value]) {
225
+ if (alias) {
226
+ this._aliases[value] = alias
227
+ return alias
228
+ }
229
+
224
230
  const aliasNum = Object.keys(this._aliases).length
225
231
 
226
232
  if (aliasNum < 26) {
@@ -319,6 +325,7 @@ class JoinCQNFromExpanded {
319
325
  list: element.list.map(element => this._checkOrderByWhereElementRecursive(cqn, element, tableAlias))
320
326
  })
321
327
  }
328
+
322
329
  return this._checkOrderByWhereElementRecursive(cqn, element, tableAlias)
323
330
  }
324
331
 
@@ -333,9 +340,7 @@ class JoinCQNFromExpanded {
333
340
  */
334
341
  _adaptWhereOrderBy(cqn, tableAlias) {
335
342
  if (cqn.where) {
336
- cqn.where = cqn.where.map(element => {
337
- return this._adaptWhereElement(element, cqn, tableAlias)
338
- })
343
+ cqn.where = cqn.where.map(element => this._adaptWhereElement(element, cqn, tableAlias))
339
344
  }
340
345
 
341
346
  if (cqn.having) {
@@ -343,15 +348,11 @@ class JoinCQNFromExpanded {
343
348
  }
344
349
 
345
350
  if (cqn.orderBy) {
346
- cqn.orderBy = cqn.orderBy.map(element => {
347
- return this._checkOrderByWhereElementRecursive(cqn, element, tableAlias)
348
- })
351
+ cqn.orderBy = cqn.orderBy.map(element => this._checkOrderByWhereElementRecursive(cqn, element, tableAlias))
349
352
  }
350
353
 
351
354
  if (cqn.groupBy) {
352
- cqn.groupBy = cqn.groupBy.map(element => {
353
- return this._checkOrderByWhereElementRecursive(cqn, element, tableAlias)
354
- })
355
+ cqn.groupBy = cqn.groupBy.map(element => this._checkOrderByWhereElementRecursive(cqn, element, tableAlias))
355
356
  }
356
357
 
357
358
  return cqn
@@ -393,7 +394,7 @@ class JoinCQNFromExpanded {
393
394
  element.xpr = element.xpr.map(nestedElement => {
394
395
  return this._checkOrderByWhereElementRecursive(cqn, nestedElement, tableAlias)
395
396
  })
396
- } else if (element.SELECT && element.SELECT.where) {
397
+ } else if (element.SELECT?.where) {
397
398
  element = {
398
399
  SELECT: Object.assign({}, element.SELECT, {
399
400
  where: this._adaptWhereSELECT(this._getRef(cqn), element.SELECT.where, tableAlias)
@@ -416,6 +417,7 @@ class JoinCQNFromExpanded {
416
417
  if (element.xpr) {
417
418
  return { xpr: this._adaptWhereSELECT(aliasedTable, element.xpr, tableAlias) }
418
419
  }
420
+
419
421
  return this._elementAliasNeedsReplacement(element, aliasedTable)
420
422
  ? Object.assign({}, element, { ref: [tableAlias, element.ref[1]] })
421
423
  : element
@@ -847,7 +849,7 @@ class JoinCQNFromExpanded {
847
849
  continue
848
850
  }
849
851
 
850
- if (arg.SELECT && arg.SELECT.columns.some(column => column[IDENTIFIER])) {
852
+ if (arg.SELECT?.columns.some(column => column[IDENTIFIER])) {
851
853
  return arg.SELECT.columns
852
854
  }
853
855
 
@@ -1434,7 +1436,7 @@ class JoinCQNFromExpanded {
1434
1436
  }
1435
1437
 
1436
1438
  _getValueFromEntry(entry, parentAlias, key, struct) {
1437
- let value = entry[key] || entry[key.toUpperCase()]
1439
+ let value = entry[key] ?? entry[key.toUpperCase()]
1438
1440
  if (value === undefined) {
1439
1441
  value = entry[`${parentAlias}_${key}`] || entry[`${parentAlias}_${key}`.toUpperCase()]
1440
1442
  }
@@ -158,9 +158,7 @@ class RawToExpanded {
158
158
  }
159
159
 
160
160
  // No property holds any value. A to null must have failed.
161
- if (isEntityNull) {
162
- return
163
- }
161
+ if (isEntityNull) return
164
162
 
165
163
  return row
166
164
  }
@@ -175,10 +173,10 @@ class RawToExpanded {
175
173
  */
176
174
  _isNull(isEntityNull, value, key) {
177
175
  if (isEntityNull === undefined) {
178
- return value === null || value === undefined || key === 'IsActiveEntity'
176
+ return value == null || key === 'IsActiveEntity'
179
177
  }
180
178
 
181
- return isEntityNull === true && (value === null || value === undefined || key === 'IsActiveEntity')
179
+ return isEntityNull === true && (value == null || key === 'IsActiveEntity')
182
180
  }
183
181
 
184
182
  /**
@@ -120,7 +120,7 @@ const _addAliasToElement = (expr, alias) => {
120
120
  alias = alias(expr.ref)
121
121
  }
122
122
 
123
- return { ref: [alias, ...expr.ref] }
123
+ return { ...expr, ref: [alias, ...expr.ref] }
124
124
  }
125
125
 
126
126
  if (expr.list) {
@@ -122,7 +122,7 @@ const fioriGenericActivate = async function (req) {
122
122
  if (!draftData) req.reject(404)
123
123
  if (adminData.InProcessByUser !== req.user.id) {
124
124
  // REVISIT: security log?
125
- req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
125
+ req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [adminData.InProcessByUser])
126
126
  }
127
127
 
128
128
  /*
@@ -4,7 +4,7 @@ const { SELECT } = cds.ql
4
4
 
5
5
  const { isNavigationToMany } = require('../utils/req')
6
6
  const { getKeysCondition, removeIsActiveEntityRecursively } = require('../utils/where')
7
- const { isDraftActivateAction, ensureNoDraftsSuffix, ensureDraftsSuffix, draftIsLocked } = require('../utils/handler')
7
+ const { ensureNoDraftsSuffix, ensureDraftsSuffix, draftIsLocked } = require('../utils/handler')
8
8
 
9
9
  const { DRAFT_COLUMNS_ADMIN_MAP } = require('../../common/constants/draft')
10
10
  const { deepCopyArray } = require('../../common/utils/copy')
@@ -32,7 +32,7 @@ const _validateDraft = (req, draftResult, isBoundAction) => {
32
32
  // user than the one who locked the entity and the configured drafts cancellation
33
33
  // timeout timer has expired
34
34
  if (draftIsLocked(draftAdminData.LastChangeDateTime)) {
35
- req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
35
+ req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [draftAdminData.CreatedByUser])
36
36
  }
37
37
 
38
38
  // At this point, the request user ID isn't the owner of the draft.
@@ -60,16 +60,19 @@ const _getSelectDraftDataCqn = (entityName, where) => {
60
60
 
61
61
  const _getRoot = req => {
62
62
  if (!req.query) return
63
+
63
64
  const refObj = req.query.SELECT?.from || req.query.UPDATE?.entity || req.query.INSERT?.into || req.query.DELETE?.from
65
+ const ref0 = refObj.ref[0]
66
+
64
67
  const root = {
65
- entityName: ensureDraftsSuffix(refObj.ref[0].id),
66
- where: removeIsActiveEntityRecursively(deepCopyArray(refObj.ref[0].where))
68
+ entityName: ensureDraftsSuffix(ref0.id),
69
+ where: removeIsActiveEntityRecursively(deepCopyArray(ref0.where))
67
70
  }
68
71
 
69
- for (const item of refObj.ref[0].where) {
72
+ for (const item of ref0.where) {
70
73
  if (item.ref && item.ref[item.ref.length - 1] === 'IsActiveEntity') {
71
- const index = refObj.ref[0].where.indexOf(item)
72
- root.IsActiveEntity = refObj.ref[0].where[index + 2].val
74
+ const index = ref0.where.indexOf(item)
75
+ root.IsActiveEntity = ref0.where[index + 2].val
73
76
  break
74
77
  }
75
78
  }
@@ -115,14 +118,12 @@ const _addDraftDataFromExistingDraft = async req => {
115
118
  * Generic Handler for before NEW requests.
116
119
  */
117
120
  const _new = async function (req) {
118
- if (isDraftActivateAction(req)) return // REVISIT: How can NEW be draftActivate???
119
-
120
121
  if (isNavigationToMany(req)) {
121
- // REVISIT: How can NEW be a navigation to many?
122
122
  const result = await _addDraftDataFromExistingDraft(req)
123
123
 
124
124
  // in order to fix corner case where active subitems are created in draft case
125
125
  if (result.length === 0) req.reject(404)
126
+
126
127
  return
127
128
  }
128
129
 
@@ -134,19 +135,17 @@ const _new = async function (req) {
134
135
  /**
135
136
  * Generic Handler for before PATCH and UPDATE requests.
136
137
  */
137
- const _patchUpdate = async function (req) {
138
- if (isDraftActivateAction(req)) return
139
-
138
+ const _patch = async function (req) {
140
139
  const result = await _addDraftDataFromExistingDraft(req)
141
140
 
142
- // means that the draft does not exists
141
+ // no result means that the draft does not exist
143
142
  if (result.length === 0) req.reject(404)
144
143
  }
145
144
 
146
145
  /**
147
146
  * Generic Handler for before DELETE and CANCEL requests.
148
147
  */
149
- const _deleteCancel = async function (req) {
148
+ const _cancel = async function (req) {
150
149
  await _addDraftDataFromExistingDraft(req)
151
150
  }
152
151
 
@@ -181,12 +180,12 @@ const _registerBoundActionHandlers = function (entityName, actions) {
181
180
  }
182
181
 
183
182
  _new._initial = true
184
- _patchUpdate._initial = true
185
- _deleteCancel._initial = true
183
+ _patch._initial = true
184
+ _cancel._initial = true
186
185
 
187
186
  module.exports = cds.service.impl((srv, entity) => {
188
187
  srv.before('NEW', entity, _new)
189
- srv.before(['PATCH', 'UPDATE'], entity, _patchUpdate)
190
- srv.before(['DELETE', 'CANCEL'], entity, _deleteCancel)
188
+ srv.before('PATCH', entity, _patch)
189
+ srv.before('CANCEL', entity, _cancel)
191
190
  _registerBoundActionHandlers.call(srv, entity.name, entity.actions)
192
191
  })
@@ -39,7 +39,7 @@ const fioriGenericPrepare = async function (req) {
39
39
  if (!result) req.reject(404)
40
40
  if (result.draftAdmin_inProcessByUser !== req.user.id) {
41
41
  // REVISIT: security log?
42
- req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
42
+ req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [result.draftAdmin_inProcessByUser])
43
43
  }
44
44
  delete result.draftAdmin_inProcessByUser
45
45
  return result
@@ -787,7 +787,7 @@ const _getOrderByEnrichedColumns = (orderBy, columns, entity) => {
787
787
  const enrichedCol = []
788
788
 
789
789
  if (orderBy && orderBy.length > 1) {
790
- const colNames = columns.map(el => el.ref[el.ref.length - 1])
790
+ const colNames = columns.filter(el => el.ref).map(el => el.ref[el.ref.length - 1])
791
791
 
792
792
  // REVISIT: GET Books?$select=title&$expand=NotBooks($select=pages)&$orderby=NotBooks/title - what's then?
793
793
  for (const el of orderBy) {
@@ -25,6 +25,16 @@ const DRAFT_ADMIN_ELEMENTS = [
25
25
  'DraftIsProcessedByMe'
26
26
  ]
27
27
 
28
+ /// It's important to wait for the completion of all promises, otherwise a rollback might happen too soon
29
+ const _promiseAll = async array => {
30
+ const results = await Promise.allSettled(array)
31
+ const e = results.find(r => r.status === 'rejected')
32
+ if (e) throw e.reason
33
+ return results.map(r => r.value)
34
+ }
35
+
36
+ const _isCount = query => query.SELECT.columns?.length === 1 && query.SELECT.columns[0].func === 'count'
37
+
28
38
  const _inProcessByUserXpr = lockShiftedNow => ({
29
39
  xpr: [
30
40
  'case',
@@ -42,14 +52,6 @@ const _inProcessByUserXpr = lockShiftedNow => ({
42
52
  cast: { type: 'cds.String' }
43
53
  })
44
54
 
45
- /// It's important to wait for the completion of all promises, otherwise a rollback might happen too soon
46
- const _promiseAll = async array => {
47
- const results = await Promise.allSettled(array)
48
- const e = results.find(r => r.status === 'rejected')
49
- if (e) throw e.reason
50
- return results.map(r => r.value)
51
- }
52
-
53
55
  const _lock = {
54
56
  get shiftedNow() {
55
57
  return new Date(Math.max(0, Date.now() - DRAFT_CANCEL_TIMEOUT_IN_MIN() * 60 * 1000)).toISOString()
@@ -160,7 +162,8 @@ cds.ApplicationService.prototype.handle = async function (req) {
160
162
  ])
161
163
  )
162
164
  const inProcessByUser = draft?.DraftAdministrativeData?.InProcessByUser
163
- if (inProcessByUser && inProcessByUser !== cds.context.user.id) req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
165
+ if (inProcessByUser && inProcessByUser !== cds.context.user.id)
166
+ req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [inProcessByUser])
164
167
  const deletes = [run(DELETE.from({ ref: query.DELETE.from.ref }))]
165
168
  if (draft)
166
169
  deletes.push(
@@ -201,21 +204,22 @@ cds.ApplicationService.prototype.handle = async function (req) {
201
204
  )
202
205
  if (!res) req.reject(404)
203
206
  if (res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
204
- req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
207
+ req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [res.DraftAdministrativeData?.InProcessByUser])
205
208
  const DraftAdministrativeData_DraftUUID = res.DraftAdministrativeData_DraftUUID
206
209
  delete res.DraftAdministrativeData_DraftUUID
207
210
  delete res.DraftAdministrativeData
208
211
  const HasActiveEntity = res.HasActiveEntity
209
212
  delete res.HasActiveEntity
210
213
  delete res.IsActiveEntity
214
+ // First run the handlers as they might need access to DraftAdministrativeData or the draft entities
215
+ const result = await run(
216
+ HasActiveEntity ? UPDATE(req.target).data(res).where(targetWhere) : INSERT.into(req.target).entries(res)
217
+ )
211
218
  await _promiseAll([
212
219
  run(DELETE.from(targetDraft).where(targetWhere)),
213
220
  DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: DraftAdministrativeData_DraftUUID })
214
221
  ])
215
222
 
216
- const result = await run(
217
- HasActiveEntity ? UPDATE(req.target).data(res).where(targetWhere) : INSERT.into(req.target).entries(res)
218
- )
219
223
  req?._?.odataRes?.setStatusCode(201)
220
224
 
221
225
  return Object.assign(result, { IsActiveEntity: true })
@@ -239,7 +243,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
239
243
 
240
244
  if (req.event === 'PATCH') {
241
245
  if (draftParams.IsActiveEntity) req.reject(501)
242
- if (req.event === 'PATCH' && !('IsActiveEntity' in draftParams)) {
246
+ if (!('IsActiveEntity' in draftParams)) {
243
247
  const res = await run(
244
248
  SELECT.one.from({ ref: req.UPDATE.entity.ref }).columns('DraftAdministrativeData_DraftUUID')
245
249
  )
@@ -248,7 +252,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
248
252
  const result = await handle(_req)
249
253
  return result
250
254
  }
251
- if (req.event === 'PATCH' && draftParams.IsActiveEntity === false) {
255
+ if (draftParams.IsActiveEntity === false) {
252
256
  LOG.debug('patch draft')
253
257
  if (req.target?.name.endsWith('DraftAdministrativeData')) req.reject(405)
254
258
  const draftsRef = _redirectRefToDrafts(query.UPDATE.entity.ref, this.model)
@@ -260,7 +264,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
260
264
  )
261
265
  if (!res) req.reject(404)
262
266
  if (res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
263
- req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
267
+ req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [res.DraftAdministrativeData?.InProcessByUser])
264
268
  await UPDATE('DRAFT.DraftAdministrativeData')
265
269
  .data({
266
270
  InProcessByUser: req.user.id,
@@ -345,6 +349,7 @@ const Read = {
345
349
  onlyActives: async function (run, query, { ignoreDrafts } = {}) {
346
350
  LOG.debug('List Editing Status: Only Active')
347
351
  // DraftAdministrativeData is only accessible via drafts
352
+ if (_isCount(query)) return run(query)
348
353
  if (query._target.name.endsWith('.DraftAdministrativeData')) return run(query._drafts)
349
354
  if (!query._target._isDraftEnabled) return run(query)
350
355
  if (!query.SELECT.groupBy && query.SELECT.columns && !query.SELECT.columns.some(c => c === '*')) {
@@ -428,7 +433,7 @@ const Read = {
428
433
  LOG.debug('List Editing Status: All')
429
434
  query._drafts.SELECT.count = false
430
435
  query._drafts.SELECT.limit = undefined // We need all entries for the keys to properly select actives (count)
431
- const isCount = query._drafts.SELECT.columns?.[0]?.func === 'count'
436
+ const isCount = _isCount(query._drafts)
432
437
  if (isCount) {
433
438
  const keys = Object_keys(query._target.keys).filter(k => k !== 'IsActiveEntity')
434
439
  query._drafts.SELECT.columns = keys.map(k => ({ ref: [k] }))
@@ -839,7 +844,7 @@ async function onNew(req) {
839
844
  )
840
845
  if (!rootData) req.reject(404)
841
846
  if (rootData.DraftAdministrativeData?.InProcessByUser !== req.user.id)
842
- req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
847
+ req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [rootData.DraftAdministrativeData.InProcessByUser])
843
848
  DraftUUID = rootData.DraftAdministrativeData_DraftUUID
844
849
  }
845
850
  const timestamp = cds.context.timestamp.toISOString() // REVISIT: toISOString should be done on db layer
@@ -863,7 +868,7 @@ async function onNew(req) {
863
868
  .where({ DraftUUID })
864
869
 
865
870
  const _assignDraftData = (obj, target) => {
866
- const newObj = Object.assign({ DraftAdministrativeData_DraftUUID: DraftUUID, HasActiveEntity: false }, obj)
871
+ const newObj = Object.assign({}, obj, { DraftAdministrativeData_DraftUUID: DraftUUID, HasActiveEntity: false })
867
872
  if (!target) return newObj
868
873
 
869
874
  // Also support deep insertions
@@ -993,7 +998,7 @@ async function onCancel(req) {
993
998
  const draft = await this.run(draftDelete)
994
999
  if (draftParams.IsActiveEntity === false && !draft) req.reject(404)
995
1000
  if (draft && draft.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
996
- req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
1001
+ req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [draft.DraftAdministrativeData?.InProcessByUser])
997
1002
  const deletes = !draft ? [] : [this.run(DELETE.from({ ref: req.query.DELETE.from.ref }))]
998
1003
  if (draft && req.target['@Common.DraftRoot.ActivationAction'])
999
1004
  // only for draft root
@@ -1023,7 +1028,8 @@ async function onPrepare(req) {
1023
1028
  Object.defineProperty(draftQuery, '_draftParams', { value: draftParams, enumerable: false })
1024
1029
  const data = await this.run(draftQuery)
1025
1030
  if (!data) req.reject(404)
1026
- if (data.DraftAdministrativeData?.InProcessByUser !== req.user.id) req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
1031
+ if (data.DraftAdministrativeData?.InProcessByUser !== req.user.id)
1032
+ req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [data.DraftAdministrativeData?.InProcessByUser])
1027
1033
  delete data.DraftAdministrativeData
1028
1034
  return { ...data, IsActiveEntity: false }
1029
1035
  }
@@ -188,11 +188,6 @@ const removeDraftUUIDIfNecessary = req =>
188
188
  ? () => {}
189
189
  : result => delete result.DraftAdministrativeData_DraftUUID
190
190
 
191
- const isDraftActivateAction = req => {
192
- // REVISIT: get rid of getUrlObject
193
- if (req.getUrlObject) return req.getUrlObject().pathname.endsWith('draftActivate')
194
- }
195
-
196
191
  const addColumnAlias = (columns, alias) => {
197
192
  if (!alias) {
198
193
  return columns
@@ -258,7 +253,6 @@ module.exports = {
258
253
  getUpdateDraftAdminCQN,
259
254
  getEnrichedCQN,
260
255
  removeDraftUUIDIfNecessary,
261
- isDraftActivateAction,
262
256
  ensureDraftsSuffix,
263
257
  ensureNoDraftsSuffix,
264
258
  ensureUnlocalized,
@@ -21,7 +21,7 @@ class CustomFunctionBuilder extends FunctionBuilder {
21
21
 
22
22
  _handleContains(args) {
23
23
  // fuzzy search has three arguments, must not be converted to like expressions
24
- if (args.length > 2 || this._options.$searchUsingContains) {
24
+ if (args.length > 2 || this._obj.searchUsingContains) {
25
25
  this._outputObj.sql.push('CONTAINS')
26
26
  this._addFunctionArgs(args, true)
27
27
  return
@@ -4,11 +4,6 @@ const LOG = cds.log('hana|db|sql')
4
4
  const SelectBuilder = require('../../db/sql-builder').SelectBuilder
5
5
 
6
6
  class CustomSelectBuilder extends SelectBuilder {
7
- constructor(obj, options, csn) {
8
- super(obj, options, csn)
9
- // $searchUsingContains property is set in the sub SELECT of the query, optimized search case
10
- if (this._obj?._$searchUsingContains) this._options.$searchUsingContains = this._obj._$searchUsingContains
11
- }
12
7
  get FunctionBuilder() {
13
8
  const FunctionBuilder = require('./CustomFunctionBuilder')
14
9
  Object.defineProperty(this, 'FunctionBuilder', { value: FunctionBuilder })