@sap/cds 6.7.2 → 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 (93) hide show
  1. package/CHANGELOG.md +41 -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/handlers/action.js +1 -1
  63. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
  64. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +2 -3
  65. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -1
  66. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/batch/BatchProcessor.js +2 -0
  67. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +2 -1
  68. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +10 -3
  69. package/libx/_runtime/cds-services/services/Service.js +2 -7
  70. package/libx/_runtime/cds-services/services/utils/differ.js +1 -1
  71. package/libx/_runtime/common/aspects/any.js +1 -1
  72. package/libx/_runtime/common/generic/auth/utils.js +30 -41
  73. package/libx/_runtime/common/i18n/messages.properties +1 -1
  74. package/libx/_runtime/db/expand/expandCQNToJoin.js +19 -17
  75. package/libx/_runtime/db/expand/rawToExpanded.js +3 -5
  76. package/libx/_runtime/db/utils/generateAliases.js +1 -1
  77. package/libx/_runtime/fiori/generic/activate.js +1 -1
  78. package/libx/_runtime/fiori/generic/before.js +18 -19
  79. package/libx/_runtime/fiori/generic/prepare.js +1 -1
  80. package/libx/_runtime/fiori/generic/read.js +1 -1
  81. package/libx/_runtime/fiori/lean-draft.js +8 -6
  82. package/libx/_runtime/fiori/utils/handler.js +0 -6
  83. package/libx/_runtime/hana/customBuilder/CustomFunctionBuilder.js +1 -1
  84. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +0 -5
  85. package/libx/_runtime/hana/pool.js +26 -18
  86. package/libx/_runtime/hana/search2Contains.js +1 -1
  87. package/libx/_runtime/hana/search2cqn4sql.js +26 -18
  88. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +23 -16
  89. package/libx/_runtime/messaging/outbox/utils.js +6 -1
  90. package/libx/_runtime/remote/Service.js +64 -38
  91. package/libx/_runtime/remote/utils/client.js +13 -9
  92. package/libx/rest/middleware/read.js +2 -1
  93. 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
@@ -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) {
@@ -162,7 +162,8 @@ cds.ApplicationService.prototype.handle = async function (req) {
162
162
  ])
163
163
  )
164
164
  const inProcessByUser = draft?.DraftAdministrativeData?.InProcessByUser
165
- 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])
166
167
  const deletes = [run(DELETE.from({ ref: query.DELETE.from.ref }))]
167
168
  if (draft)
168
169
  deletes.push(
@@ -203,7 +204,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
203
204
  )
204
205
  if (!res) req.reject(404)
205
206
  if (res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
206
- req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
207
+ req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [res.DraftAdministrativeData?.InProcessByUser])
207
208
  const DraftAdministrativeData_DraftUUID = res.DraftAdministrativeData_DraftUUID
208
209
  delete res.DraftAdministrativeData_DraftUUID
209
210
  delete res.DraftAdministrativeData
@@ -263,7 +264,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
263
264
  )
264
265
  if (!res) req.reject(404)
265
266
  if (res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
266
- req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
267
+ req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [res.DraftAdministrativeData?.InProcessByUser])
267
268
  await UPDATE('DRAFT.DraftAdministrativeData')
268
269
  .data({
269
270
  InProcessByUser: req.user.id,
@@ -843,7 +844,7 @@ async function onNew(req) {
843
844
  )
844
845
  if (!rootData) req.reject(404)
845
846
  if (rootData.DraftAdministrativeData?.InProcessByUser !== req.user.id)
846
- req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
847
+ req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [rootData.DraftAdministrativeData.InProcessByUser])
847
848
  DraftUUID = rootData.DraftAdministrativeData_DraftUUID
848
849
  }
849
850
  const timestamp = cds.context.timestamp.toISOString() // REVISIT: toISOString should be done on db layer
@@ -997,7 +998,7 @@ async function onCancel(req) {
997
998
  const draft = await this.run(draftDelete)
998
999
  if (draftParams.IsActiveEntity === false && !draft) req.reject(404)
999
1000
  if (draft && draft.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
1000
- req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
1001
+ req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [draft.DraftAdministrativeData?.InProcessByUser])
1001
1002
  const deletes = !draft ? [] : [this.run(DELETE.from({ ref: req.query.DELETE.from.ref }))]
1002
1003
  if (draft && req.target['@Common.DraftRoot.ActivationAction'])
1003
1004
  // only for draft root
@@ -1027,7 +1028,8 @@ async function onPrepare(req) {
1027
1028
  Object.defineProperty(draftQuery, '_draftParams', { value: draftParams, enumerable: false })
1028
1029
  const data = await this.run(draftQuery)
1029
1030
  if (!data) req.reject(404)
1030
- 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])
1031
1033
  delete data.DraftAdministrativeData
1032
1034
  return { ...data, IsActiveEntity: false }
1033
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 })
@@ -17,6 +17,7 @@ function multiTenantServiceManager() {
17
17
  if (e.code === 'MODULE_NOT_FOUND') return null
18
18
  else throw e
19
19
  }
20
+
20
21
  const oldIm =
21
22
  cds.requires.multitenancy?.['old-instance-manager'] ??
22
23
  cds.env.requires?.['cds.xt.DeploymentService']?.['old-instance-manager']
@@ -93,8 +94,10 @@ async function credentials4(tenant, db) {
93
94
  return new Promise((resolve, reject) => {
94
95
  db._instance_manager.get(tenant, (err, res) => {
95
96
  if (err) return reject(err)
96
- if (!res)
97
+ if (!res) {
97
98
  return reject(Object.assign(new Error(`There is no instance for tenant "${tenant}"`), { statusCode: 404 }))
99
+ }
100
+
98
101
  resolve(res.credentials)
99
102
  })
100
103
  })
@@ -102,15 +105,9 @@ async function credentials4(tenant, db) {
102
105
 
103
106
  function factory4(creds, tenant) {
104
107
  return {
105
- create: function () {
106
- return hana.__connect(creds, tenant)
107
- },
108
- destroy: function (client) {
109
- return hana.__disconnect(client)
110
- },
111
- validate: function (client) {
112
- return hana.__isConnected(client)
113
- }
108
+ create: () => hana.__connect(creds, tenant),
109
+ destroy: client => hana.__disconnect(client),
110
+ validate: client => hana.__isConnected(client)
114
111
  }
115
112
  }
116
113
 
@@ -121,7 +118,6 @@ const defaultConfig = { min: 0, max: 100, testOnBorrow: true, fifo: false }
121
118
 
122
119
  const _getPoolConfig = function () {
123
120
  const { pool: poolConfig } = cds.env.requires.db
124
-
125
121
  const mergedConfig = Object.assign({}, defaultConfig, poolConfig)
126
122
 
127
123
  // defaults
@@ -154,15 +150,19 @@ const _getMassagedCreds = function (creds) {
154
150
  if (!('ca' in creds) && creds.certificate) {
155
151
  creds.ca = creds.certificate
156
152
  }
153
+
157
154
  if ('encrypt' in creds && !('useTLS' in creds)) {
158
155
  creds.useTLS = creds.encrypt
159
156
  }
157
+
160
158
  if ('hostname_in_certificate' in creds && !('sslHostNameInCertificate' in creds)) {
161
159
  creds.sslHostNameInCertificate = creds.hostname_in_certificate
162
160
  }
161
+
163
162
  if ('validate_certificate' in creds && !('sslValidateCertificate' in creds)) {
164
163
  creds.sslValidateCertificate = creds.validate_certificate
165
164
  }
165
+
166
166
  return creds
167
167
  }
168
168
 
@@ -185,9 +185,11 @@ async function pool4(tenant, db) {
185
185
  )
186
186
 
187
187
  /*
188
- * The error listener for "factoryCreateError" is registered in order to find out failed connection attempts.
189
- * If it fails due to invalid credentials, we delete the current pool from the pools map and overwrite the pool factory create function.
190
- * Background is that generic-pool will continue to try to open a connection by calling the factory create function until the "acquireTimeoutMillis" is reached.
188
+ * The error listener for "factoryCreateError" is registered to find out failed connection attempts.
189
+ * If it fails due to invalid credentials, we delete the current pool from the pools map and overwrite the
190
+ * pool factory create function.
191
+ * Background is that generic-pool will continue to try to open a connection by calling the factory create
192
+ * function until the "acquireTimeoutMillis" is reached.
191
193
  * This ends up in many connection attempts for one request even though the credentials are invalid.
192
194
  * Because of the deletion in the map, subsequent requests will fetch the credentials again.
193
195
  */
@@ -218,6 +220,7 @@ async function pool4(tenant, db) {
218
220
  })
219
221
  )
220
222
  }
223
+
221
224
  if ('then' in pools.get(tenant)) {
222
225
  pools.set(tenant, await pools.get(tenant))
223
226
  }
@@ -240,12 +243,17 @@ async function resilientAcquire(pool, attempts = 1) {
240
243
  attempt++
241
244
  }
242
245
  }
246
+
243
247
  if (client) return client
248
+
244
249
  const { borrowed, pending, size, available, max } = pool
250
+ const message =
251
+ 'Acquiring client from pool timed out. Please review your system setup, transaction handling, and pool configuration. ' +
252
+ `Pool State: borrowed: ${borrowed}, pending: ${pending}, size: ${size}, available: ${available}, max: ${max}`
245
253
  err = getError(
246
254
  Object.assign(err, {
247
255
  statusCode: 503,
248
- message: `Acquiring client from pool timed out. Please review your system setup, transaction handling, and pool configuration. Pool State: borrowed: ${borrowed}, pending: ${pending}, size: ${size}, available: ${available}, max: ${max}`
256
+ message
249
257
  })
250
258
  )
251
259
  err._attempts = attempt
@@ -262,9 +270,9 @@ module.exports = {
262
270
  client._pool = pool
263
271
  return client
264
272
  },
265
- release: client => {
266
- return client._pool.release(client)
267
- },
273
+
274
+ release: client => client._pool.release(client),
275
+
268
276
  drain: async tenant => {
269
277
  const pool = pools.get(tenant)
270
278
  if (!pool) return
@@ -62,7 +62,7 @@ const search2Contains = (cqnSearchPhrase, columns) => {
62
62
  const isContainsPredicateSupported = (query, entity, columns2Search) => {
63
63
  const cqnSearchPhrase = query.SELECT.search
64
64
 
65
- if (cqnSearchPhrase && cqnSearchPhrase[0] && cqnSearchPhrase[0].val === ' ') return false
65
+ if (cqnSearchPhrase?.[0]?.val === ' ') return false
66
66
 
67
67
  // REVISIT: In the future, to further optimize search queries, you might
68
68
  // want to remove the following condition(s).