@sap/cds 6.7.2 → 6.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/CHANGELOG.md +53 -1
  2. package/README.md +1 -0
  3. package/_i18n/i18n.properties +9 -6
  4. package/_i18n/i18n_ar.properties +6 -6
  5. package/_i18n/i18n_cs.properties +6 -6
  6. package/_i18n/i18n_da.properties +6 -6
  7. package/_i18n/i18n_de.properties +6 -6
  8. package/_i18n/i18n_en.properties +6 -6
  9. package/_i18n/i18n_es.properties +6 -6
  10. package/_i18n/i18n_fi.properties +6 -6
  11. package/_i18n/i18n_fr.properties +6 -6
  12. package/_i18n/i18n_hu.properties +6 -6
  13. package/_i18n/i18n_it.properties +6 -6
  14. package/_i18n/i18n_ja.properties +6 -6
  15. package/_i18n/i18n_ko.properties +6 -6
  16. package/_i18n/i18n_ms.properties +6 -6
  17. package/_i18n/i18n_nl.properties +6 -6
  18. package/_i18n/i18n_no.properties +6 -6
  19. package/_i18n/i18n_pl.properties +6 -6
  20. package/_i18n/i18n_pt.properties +6 -6
  21. package/_i18n/i18n_ro.properties +6 -6
  22. package/_i18n/i18n_ru.properties +6 -6
  23. package/_i18n/i18n_sv.properties +6 -6
  24. package/_i18n/i18n_th.properties +6 -6
  25. package/_i18n/i18n_tr.properties +8 -8
  26. package/_i18n/i18n_zh_CN.properties +3 -3
  27. package/_i18n/i18n_zh_TW.properties +6 -6
  28. package/apis/core.d.ts +30 -31
  29. package/apis/csn.d.ts +1 -1
  30. package/apis/ql.d.ts +69 -39
  31. package/apis/serve.d.ts +4 -3
  32. package/apis/services.d.ts +20 -7
  33. package/bin/build/buildTaskEngine.js +1 -1
  34. package/bin/build/index.js +1 -1
  35. package/bin/build/provider/buildTaskProviderInternal.js +9 -6
  36. package/bin/build/provider/hana/index.js +11 -4
  37. package/bin/build/provider/mtx-extension/index.js +13 -1
  38. package/bin/build/provider/mtx-sidecar/index.js +3 -3
  39. package/bin/build/provider/nodejs/index.js +23 -0
  40. package/bin/plugins.js +2 -1
  41. package/bin/version.js +3 -2
  42. package/common.cds +3 -2
  43. package/lib/auth/index.js +3 -0
  44. package/lib/auth/mocked-users.js +13 -0
  45. package/lib/compile/etc/_localized.js +3 -0
  46. package/lib/compile/for/lean_drafts.js +0 -1
  47. package/lib/core/entities.js +7 -3
  48. package/lib/dbs/cds-deploy.js +36 -12
  49. package/lib/env/cds-env.js +47 -14
  50. package/lib/env/cds-requires.js +16 -7
  51. package/lib/env/defaults.js +2 -2
  52. package/lib/env/schemas/cds-rc.json +1 -8
  53. package/lib/index.js +1 -1
  54. package/lib/ql/STREAM.js +89 -0
  55. package/lib/ql/cds-ql.js +2 -1
  56. package/lib/req/request.js +6 -2
  57. package/lib/req/user.js +1 -1
  58. package/lib/srv/middlewares/index.js +9 -7
  59. package/lib/srv/middlewares/trace.js +6 -5
  60. package/lib/srv/srv-api.js +1 -0
  61. package/lib/utils/cds-utils.js +1 -1
  62. package/lib/utils/tar.js +30 -31
  63. package/libx/_runtime/audit/Service.js +96 -37
  64. package/libx/_runtime/audit/generic/personal/utils.js +26 -13
  65. package/libx/_runtime/audit/utils/v2.js +21 -22
  66. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
  67. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
  68. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +2 -0
  69. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +2 -3
  70. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -1
  71. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/batch/BatchProcessor.js +2 -0
  72. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +2 -1
  73. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +10 -3
  74. package/libx/_runtime/cds-services/services/Service.js +2 -7
  75. package/libx/_runtime/cds-services/services/utils/differ.js +1 -1
  76. package/libx/_runtime/cds-services/util/assert.js +28 -5
  77. package/libx/_runtime/common/aspects/any.js +4 -1
  78. package/libx/_runtime/common/generic/auth/utils.js +30 -41
  79. package/libx/_runtime/common/generic/crud.js +1 -1
  80. package/libx/_runtime/common/i18n/messages.properties +1 -1
  81. package/libx/_runtime/common/utils/generateOnCond.js +18 -22
  82. package/libx/_runtime/db/expand/expandCQNToJoin.js +49 -41
  83. package/libx/_runtime/db/expand/rawToExpanded.js +3 -5
  84. package/libx/_runtime/db/generic/rewrite.js +3 -0
  85. package/libx/_runtime/db/utils/generateAliases.js +1 -1
  86. package/libx/_runtime/fiori/generic/activate.js +1 -1
  87. package/libx/_runtime/fiori/generic/before.js +18 -19
  88. package/libx/_runtime/fiori/generic/prepare.js +1 -1
  89. package/libx/_runtime/fiori/generic/read.js +1 -1
  90. package/libx/_runtime/fiori/lean-draft.js +87 -53
  91. package/libx/_runtime/fiori/utils/handler.js +0 -6
  92. package/libx/_runtime/hana/customBuilder/CustomFunctionBuilder.js +1 -1
  93. package/libx/_runtime/hana/customBuilder/CustomReferenceBuilder.js +2 -1
  94. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +0 -5
  95. package/libx/_runtime/hana/execute.js +18 -11
  96. package/libx/_runtime/hana/pool.js +26 -18
  97. package/libx/_runtime/hana/search2Contains.js +1 -1
  98. package/libx/_runtime/hana/search2cqn4sql.js +26 -18
  99. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +23 -16
  100. package/libx/_runtime/messaging/outbox/utils.js +6 -1
  101. package/libx/_runtime/remote/Service.js +83 -48
  102. package/libx/_runtime/remote/utils/client.js +17 -19
  103. package/libx/_runtime/sqlite/execute.js +2 -0
  104. package/libx/rest/middleware/read.js +2 -1
  105. package/libx/rest/middleware/update.js +1 -1
  106. package/package.json +1 -1
@@ -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,12 +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)
121
-
119
+ const isJoinOfTwoSelects = joinArgs?.every(a => a.SELECT)
122
120
  const unionTableRef = this._getUnionTable(SELECT)
123
- const unionTable = unionTableRef && unionTableRef.table
121
+ const unionTable = unionTableRef?.table
124
122
  const tableAlias = this._getTableAlias(SELECT, toManyTree)
125
-
126
123
  const readToOneCQN = this._getReadToOneCQN(SELECT, isJoinOfTwoSelects ? 'filterExpand' : tableAlias)
127
124
 
128
125
  if (isJoinOfTwoSelects) {
@@ -134,6 +131,7 @@ class JoinCQNFromExpanded {
134
131
  .forEach(c => {
135
132
  mappings[c.as.replace(prefix, '')] = c.as
136
133
  })
134
+
137
135
  // expand to one
138
136
  const entity = this._csn.definitions[joinArgs[0].SELECT.from.SET.args[1].SELECT.from.ref[0]]
139
137
  this._addImplicitOrderBy(readToOneCQN, entity, tableAlias)
@@ -147,9 +145,7 @@ class JoinCQNFromExpanded {
147
145
  const entity = this._getEntityForTable(table)
148
146
  this._addImplicitOrderBy(readToOneCQN, entity, tableAlias)
149
147
  if (unionTable) readToOneCQN[IS_UNION_DRAFT] = true
150
-
151
148
  readToOneCQN[IS_ACTIVE] = isDraftTree ? this._isDraftTargetActive(table) : true
152
-
153
149
  const givenColumns = readToOneCQN.columns
154
150
  readToOneCQN.columns = []
155
151
  if (entity['@cds.localized'] === false) defaultLanguage = true
@@ -178,7 +174,8 @@ class JoinCQNFromExpanded {
178
174
  * @private
179
175
  */
180
176
  _getTableAlias(SELECT, toManyTree) {
181
- return this._createAlias(toManyTree.length === 0 ? this._getRef(SELECT).table : toManyTree.join(':'))
177
+ const ref = this._getRef(SELECT)
178
+ return this._createAlias(toManyTree.length === 0 ? ref.table : toManyTree.join(':'), ref.as)
182
179
  }
183
180
 
184
181
  _getRef(SELECT) {
@@ -212,15 +209,21 @@ class JoinCQNFromExpanded {
212
209
  * Create an alias from value.
213
210
  *
214
211
  * @param {string} value
212
+ * @param {string} [alias]
215
213
  * @returns {string}
216
214
  * @private
217
215
  */
218
- _createAlias(value) {
216
+ _createAlias(value, alias) {
219
217
  if (!this._aliases) {
220
218
  this._aliases = {}
221
219
  }
222
220
 
223
221
  if (!this._aliases[value]) {
222
+ if (alias) {
223
+ this._aliases[value] = alias
224
+ return alias
225
+ }
226
+
224
227
  const aliasNum = Object.keys(this._aliases).length
225
228
 
226
229
  if (aliasNum < 26) {
@@ -319,6 +322,7 @@ class JoinCQNFromExpanded {
319
322
  list: element.list.map(element => this._checkOrderByWhereElementRecursive(cqn, element, tableAlias))
320
323
  })
321
324
  }
325
+
322
326
  return this._checkOrderByWhereElementRecursive(cqn, element, tableAlias)
323
327
  }
324
328
 
@@ -333,9 +337,7 @@ class JoinCQNFromExpanded {
333
337
  */
334
338
  _adaptWhereOrderBy(cqn, tableAlias) {
335
339
  if (cqn.where) {
336
- cqn.where = cqn.where.map(element => {
337
- return this._adaptWhereElement(element, cqn, tableAlias)
338
- })
340
+ cqn.where = cqn.where.map(element => this._adaptWhereElement(element, cqn, tableAlias))
339
341
  }
340
342
 
341
343
  if (cqn.having) {
@@ -343,15 +345,11 @@ class JoinCQNFromExpanded {
343
345
  }
344
346
 
345
347
  if (cqn.orderBy) {
346
- cqn.orderBy = cqn.orderBy.map(element => {
347
- return this._checkOrderByWhereElementRecursive(cqn, element, tableAlias)
348
- })
348
+ cqn.orderBy = cqn.orderBy.map(element => this._checkOrderByWhereElementRecursive(cqn, element, tableAlias))
349
349
  }
350
350
 
351
351
  if (cqn.groupBy) {
352
- cqn.groupBy = cqn.groupBy.map(element => {
353
- return this._checkOrderByWhereElementRecursive(cqn, element, tableAlias)
354
- })
352
+ cqn.groupBy = cqn.groupBy.map(element => this._checkOrderByWhereElementRecursive(cqn, element, tableAlias))
355
353
  }
356
354
 
357
355
  return cqn
@@ -393,7 +391,7 @@ class JoinCQNFromExpanded {
393
391
  element.xpr = element.xpr.map(nestedElement => {
394
392
  return this._checkOrderByWhereElementRecursive(cqn, nestedElement, tableAlias)
395
393
  })
396
- } else if (element.SELECT && element.SELECT.where) {
394
+ } else if (element.SELECT?.where) {
397
395
  element = {
398
396
  SELECT: Object.assign({}, element.SELECT, {
399
397
  where: this._adaptWhereSELECT(this._getRef(cqn), element.SELECT.where, tableAlias)
@@ -416,6 +414,7 @@ class JoinCQNFromExpanded {
416
414
  if (element.xpr) {
417
415
  return { xpr: this._adaptWhereSELECT(aliasedTable, element.xpr, tableAlias) }
418
416
  }
417
+
419
418
  return this._elementAliasNeedsReplacement(element, aliasedTable)
420
419
  ? Object.assign({}, element, { ref: [tableAlias, element.ref[1]] })
421
420
  : element
@@ -498,11 +497,11 @@ class JoinCQNFromExpanded {
498
497
  _expandedToFlat({ entity, givenColumns, readToOneCQN, tableAlias, toManyTree, defaultLanguage }) {
499
498
  const toManyColumns = []
500
499
  const mappings = this._getMappingObject(toManyTree)
501
-
502
500
  const readToOneCQNCopy = getCqnCopy(readToOneCQN)
503
501
 
504
502
  for (const column of givenColumns) {
505
503
  let navigation
504
+
506
505
  if (column.expand) {
507
506
  navigation = getNavigationIfStruct(entity, tableAlias === column.ref[0] ? column.ref.slice(1) : column.ref)
508
507
  if (this._skip(navigation && navigation._target)) continue
@@ -518,6 +517,7 @@ class JoinCQNFromExpanded {
518
517
  // Expands with to one target can be processed directly
519
518
  const navProp = column.ref[column.ref.length - 1]
520
519
  const navTarget = entity.elements[navProp]
520
+
521
521
  if (
522
522
  entity._isDraftEnabled &&
523
523
  navTarget._isAssociationStrict &&
@@ -527,6 +527,7 @@ class JoinCQNFromExpanded {
527
527
  ) {
528
528
  mappings[navProp] = { [TO_ACTIVE]: true }
529
529
  }
530
+
530
531
  this._addJoinAndElements({
531
532
  column,
532
533
  entity,
@@ -537,7 +538,8 @@ class JoinCQNFromExpanded {
537
538
  })
538
539
  } else {
539
540
  // No expand, directly add the column and its mapping.
540
- readToOneCQN.columns.push(this._addAliasToColumn(column, entity, tableAlias, mappings))
541
+ const columnAliased = this._addAliasToColumn(column, entity, tableAlias, mappings)
542
+ readToOneCQN.columns.push(columnAliased)
541
543
 
542
544
  // REVISIT required for other cqn properties as well?
543
545
  this.adjustOrderBy(readToOneCQN.orderBy, mappings, column, tableAlias)
@@ -554,6 +556,7 @@ class JoinCQNFromExpanded {
554
556
  }
555
557
  }
556
558
  }
559
+
557
560
  // only as second step handle expand to many, or else keys might still be unknown
558
561
  this._toMany({
559
562
  entity,
@@ -656,7 +659,6 @@ class JoinCQNFromExpanded {
656
659
  const extendedToManyTree = toManyTree.concat(column.ref[0] === parentAlias ? column.ref.slice(1) : column.ref)
657
660
  const tableAlias = this._createAlias(extendedToManyTree.join(':'))
658
661
  const target = this._getTarget(entity, column, parentAlias)
659
-
660
662
  const name = column.ref[column.ref.length - 1]
661
663
  const element = name && entity.elements[name]
662
664
 
@@ -763,8 +765,7 @@ class JoinCQNFromExpanded {
763
765
  const givenColumns = column.expand.map(col => {
764
766
  if (
765
767
  activeTableRequired &&
766
- col.ref &&
767
- col.ref.length &&
768
+ col.ref?.length &&
768
769
  (col.ref[0] === 'IsActiveEntity' || col.ref[0] === 'HasActiveEntity')
769
770
  ) {
770
771
  return {
@@ -847,7 +848,7 @@ class JoinCQNFromExpanded {
847
848
  continue
848
849
  }
849
850
 
850
- if (arg.SELECT && arg.SELECT.columns.some(column => column[IDENTIFIER])) {
851
+ if (arg.SELECT?.columns.some(column => column[IDENTIFIER])) {
851
852
  return arg.SELECT.columns
852
853
  }
853
854
 
@@ -972,9 +973,7 @@ class JoinCQNFromExpanded {
972
973
  */
973
974
  _addAliasToColumn(column, entity, tableAlias, mappings) {
974
975
  // No identifier for this row entry or technical column
975
- if (this._isAliasNotNeeded(column)) {
976
- return column
977
- }
976
+ if (this._isAliasNotNeeded(column)) return column
978
977
 
979
978
  if (Array.isArray(column.xpr)) {
980
979
  return this._buildNewAliasColumn(
@@ -993,6 +992,7 @@ class JoinCQNFromExpanded {
993
992
  mappings
994
993
  )
995
994
  }
995
+
996
996
  return this._buildNewAliasColumn(column, entity, tableAlias, mappings)
997
997
  }
998
998
 
@@ -1073,10 +1073,7 @@ class JoinCQNFromExpanded {
1073
1073
  defaultLanguage,
1074
1074
  readToOneCQNCopy
1075
1075
  }) {
1076
- if (toManyColumns.length === 0) {
1077
- return
1078
- }
1079
-
1076
+ if (toManyColumns.length === 0) return
1080
1077
  this._addKeysIfNeeded({ entity, readToOneCQN, tableAlias })
1081
1078
 
1082
1079
  for (const { column, parentAlias } of toManyColumns) {
@@ -1089,6 +1086,7 @@ class JoinCQNFromExpanded {
1089
1086
  parentAlias,
1090
1087
  defaultLanguage
1091
1088
  })
1089
+
1092
1090
  this._createJoinCQNFromExpanded(select, toManyTree.concat([column.ref[column.ref.length - 1]]), defaultLanguage)
1093
1091
  }
1094
1092
  }
@@ -1160,7 +1158,6 @@ class JoinCQNFromExpanded {
1160
1158
  // eslint-disable-next-line complexity
1161
1159
  _buildExpandedCQN({ column, entity, readToOneCQN, toManyTree, mappings, parentAlias, defaultLanguage }) {
1162
1160
  const isUnion = !!readToOneCQN.from.SET
1163
-
1164
1161
  const colRef = parentAlias === column.ref[0] ? column.ref.slice(1) : column.ref.slice(0)
1165
1162
  const element = entity.elements[colRef[0]]
1166
1163
  const colTarget = ensureUnlocalized(element.target)
@@ -1168,24 +1165,21 @@ class JoinCQNFromExpanded {
1168
1165
  defaultLanguage ||
1169
1166
  entity['@cds.localized'] === false ||
1170
1167
  this._csn.definitions[colTarget]['@cds.localized'] === false
1171
-
1172
1168
  const expandActive =
1173
1169
  readToOneCQN[IS_ACTIVE] ||
1174
1170
  (element._isAssociationStrict && !element['@odata.draft.enclosed']) ||
1175
1171
  !this._csn.definitions[colTarget]._isDraftEnabled
1176
-
1177
1172
  const ref = this._refFromRefByExpand(column.ref[0], colTarget, defaultLanguageThis, expandActive)
1178
1173
  const tableAlias = this._createAlias(toManyTree.concat(colRef).join(':'))
1179
1174
  const on = entity._relations[colRef[0]].join(tableAlias, 'filterExpand')
1180
1175
  const filterExpand = this._getFilterExpandCQN(readToOneCQN, on, parentAlias, entity.keys)
1181
1176
  const expandedEntity = this._csn.definitions[colTarget]
1182
1177
  const joinColumns = this._getJoinColumnsFromOnAddToMapping(mappings[colRef[0]], parentAlias, on, entity)
1183
-
1184
1178
  let cqn = {
1185
1179
  from: {
1186
1180
  join: 'inner',
1187
1181
  args: [{ ref: [ref], as: tableAlias }, filterExpand],
1188
- on: on
1182
+ on
1189
1183
  }
1190
1184
  }
1191
1185
 
@@ -1211,16 +1205,17 @@ class JoinCQNFromExpanded {
1211
1205
  }
1212
1206
 
1213
1207
  if (column.limit) throw getError(501, 'Pagination is not supported in expand')
1214
-
1215
1208
  cqn = this._adaptWhereOrderBy(cqn, tableAlias)
1216
1209
 
1217
1210
  if (isUnion) {
1218
1211
  const cols = column.expand.filter(c => !c.expand && !(c.ref[0] in DRAFT_COLUMNS_MAP)).map(c => c.ref[0])
1212
+
1219
1213
  // ensure the join columns are selected
1220
1214
  for (const each of joinColumns) {
1221
1215
  const col = each.ref[each.ref.length - 1]
1222
1216
  if (!cols.includes(col)) cols.push(col)
1223
1217
  }
1218
+
1224
1219
  // ensure the foreign keys are selected in case of expand to one
1225
1220
  for (const each of cqn.columns) {
1226
1221
  if (each.expand) {
@@ -1238,14 +1233,17 @@ class JoinCQNFromExpanded {
1238
1233
  )
1239
1234
  const user = (cds.context && cds.context.user && cds.context.user.id) || 'anonymous'
1240
1235
  const unionFrom = getCQNUnionFrom(cols, ref.replace(/_drafts$/, ''), ref, ks, user)
1236
+
1241
1237
  for (const each of cqn.columns) {
1242
1238
  if (!each.as) continue
1239
+
1243
1240
  // replace val with ref
1244
1241
  if (each.as === 'IsActiveEntity' || each.as === 'HasActiveEntity') {
1245
1242
  delete each.val
1246
1243
  each.ref = [tableAlias, each.as]
1247
1244
  each.as = tableAlias + '_' + each.as
1248
1245
  }
1246
+
1249
1247
  // ensure the cast
1250
1248
  if (
1251
1249
  each.as.match(/IsActiveEntity$/) ||
@@ -1255,6 +1253,7 @@ class JoinCQNFromExpanded {
1255
1253
  each.cast = { type: 'cds.Boolean' }
1256
1254
  }
1257
1255
  }
1256
+
1258
1257
  const cs = cqn.columns
1259
1258
  .filter(c => !c.expand && c.ref && c.ref[0] === tableAlias)
1260
1259
  .map(c => ({ ref: [c.ref[1]] }))
@@ -1278,6 +1277,7 @@ class JoinCQNFromExpanded {
1278
1277
  const sort = element.sort
1279
1278
  if (element.args)
1280
1279
  return { func: element.func, args: this._copyOrderBy(element.args, alias, expandedEntity), sort }
1280
+
1281
1281
  const ref =
1282
1282
  element.ref[0] === alias
1283
1283
  ? [...element.ref]
@@ -1286,6 +1286,7 @@ class JoinCQNFromExpanded {
1286
1286
  : this._isPathExpressionToOne(element.ref, expandedEntity)
1287
1287
  ? [alias, ...element.ref]
1288
1288
  : [alias, element.ref[1]]
1289
+
1289
1290
  return (sort && { ref, sort }) || { ref }
1290
1291
  })
1291
1292
  }
@@ -1304,6 +1305,7 @@ class JoinCQNFromExpanded {
1304
1305
  where: where
1305
1306
  }
1306
1307
  }
1308
+
1307
1309
  return {
1308
1310
  xpr: ['case', 'when', hasDraftQuery, 'IS NOT NULL', 'then', 'true', 'else', 'false', 'end'],
1309
1311
  as: 'HasDraftEntity',
@@ -1368,6 +1370,7 @@ class JoinCQNFromExpanded {
1368
1370
  outerColumns.push(...outerCols)
1369
1371
  continue
1370
1372
  }
1373
+
1371
1374
  if (typeof entry === 'object' && entry.ref && entry.ref[0] === 'filterExpand') {
1372
1375
  columns.push(this._getColumnObjectForFilterExpand(readToOneCQN, parentAlias, entry.ref[1]))
1373
1376
  outerColumns.push({ ref: [entry.ref[1]] })
@@ -1434,7 +1437,7 @@ class JoinCQNFromExpanded {
1434
1437
  }
1435
1438
 
1436
1439
  _getValueFromEntry(entry, parentAlias, key, struct) {
1437
- let value = entry[key] || entry[key.toUpperCase()]
1440
+ let value = entry[key] ?? entry[key.toUpperCase()]
1438
1441
  if (value === undefined) {
1439
1442
  value = entry[`${parentAlias}_${key}`] || entry[`${parentAlias}_${key}`.toUpperCase()]
1440
1443
  }
@@ -1464,6 +1467,7 @@ class JoinCQNFromExpanded {
1464
1467
  }
1465
1468
  struct = current.elements[key.replace(parentAlias + '_', '')]
1466
1469
  }
1470
+
1467
1471
  // build value for spreading (cf. mapping[GET_KEY_VALUE])
1468
1472
  value = []
1469
1473
  for (const k in struct.elements) {
@@ -1480,7 +1484,8 @@ class JoinCQNFromExpanded {
1480
1484
 
1481
1485
  _addColumNames(entity, parentAlias, columnNames) {
1482
1486
  for (const keyName in entity.keys) {
1483
- if (entity.keys[keyName].is2one || entity.keys[keyName].is2many) continue
1487
+ const key = entity.keys[keyName]
1488
+ if (key.is2one || key.is2many) continue
1484
1489
  const columnNameAlt = keyName === 'IsActiveEntity' ? 'IsActiveEntity' : `${parentAlias}_${keyName}`
1485
1490
  if (!columnNames.includes(columnNameAlt)) {
1486
1491
  columnNames.push(columnNameAlt)
@@ -1509,6 +1514,7 @@ class JoinCQNFromExpanded {
1509
1514
  columns.push(...this._getJoinColumnsFromOnAddToMapping(mapping, parentAlias, entry.xpr, entity))
1510
1515
  continue
1511
1516
  }
1517
+
1512
1518
  if (typeof entry === 'object' && entry.ref && entry.ref[0] !== 'filterExpand') {
1513
1519
  const as = entry.ref.join('_')
1514
1520
  columns.push({
@@ -1528,6 +1534,7 @@ class JoinCQNFromExpanded {
1528
1534
 
1529
1535
  for (const key of keyList) {
1530
1536
  const parts = key.split('_')
1537
+
1531
1538
  // For draft-enabled entities, associations may not take over 'IsActiveEntity', e.g.
1532
1539
  // when a draft points to an active entity
1533
1540
  if (parts[parts.length - 1] !== 'IsActiveEntity') {
@@ -1575,7 +1582,6 @@ class JoinCQNFromExpanded {
1575
1582
  this._addMissingJoinElements(columns, joinColumns)
1576
1583
  this._addMissingKeyColumns(columns, tableAlias, keys, isActive, entity)
1577
1584
  this._addMissingParentKeyColumns(columns, 'filterExpand', parentKeys, isActive)
1578
-
1579
1585
  return columns
1580
1586
  }
1581
1587
 
@@ -1604,11 +1610,13 @@ class JoinCQNFromExpanded {
1604
1610
  columns.push(this._createCalculatedBooleanColumn('IsActiveEntity', isActive))
1605
1611
  return
1606
1612
  }
1613
+
1607
1614
  if (isActive) {
1608
1615
  if (columnName === 'HasActiveEntity') {
1609
1616
  columns.push(this._createCalculatedBooleanColumn('HasActiveEntity', false))
1610
1617
  return
1611
1618
  }
1619
+
1612
1620
  if (columnName === 'HasDraftEntity') {
1613
1621
  columns.push(this._getHasDraftEntityXpr(entity, tableAlias))
1614
1622
  return
@@ -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
  /**
@@ -14,6 +14,9 @@ const _isLinked = req => {
14
14
  function handler(req) {
15
15
  if (typeof req.query === 'string') return
16
16
 
17
+ // invoke req.subject before it gets modified
18
+ req.subject
19
+
17
20
  if (!this.model) {
18
21
  // best-effort rewrite of path in from
19
22
  req.query = cqn2cqn4sql(req.query, { definitions: {} }, { service: this })
@@ -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) {