@sap/cds 5.9.1 → 5.9.4

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 (45) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/lib/compile/etc/_localized.js +3 -2
  3. package/lib/compile/for/drafts.js +1 -1
  4. package/lib/connect/bindings.js +1 -1
  5. package/lib/connect/index.js +2 -3
  6. package/lib/env/requires.js +1 -1
  7. package/lib/index.js +2 -1
  8. package/lib/serve/Service-methods.js +28 -1
  9. package/lib/serve/adapters.js +6 -6
  10. package/lib/serve/factory.js +14 -9
  11. package/lib/serve/index.js +4 -3
  12. package/libx/_runtime/auth/index.js +16 -1
  13. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +6 -1
  14. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/BatchRequestListBuilder.js +3 -1
  15. package/libx/_runtime/cds-services/adapter/odata-v4/utils/dispatcherUtils.js +11 -6
  16. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +1 -3
  17. package/libx/_runtime/cds-services/services/Service.js +1 -1
  18. package/libx/_runtime/common/aspects/utils.js +8 -2
  19. package/libx/_runtime/common/composition/data.js +22 -13
  20. package/libx/_runtime/common/composition/delete.js +14 -12
  21. package/libx/_runtime/common/generic/auth/expand.js +1 -0
  22. package/libx/_runtime/common/generic/auth/restrict.js +3 -1
  23. package/libx/_runtime/{cds-services/services/utils → common/generic/auth}/restrictions.js +8 -1
  24. package/libx/_runtime/common/generic/input.js +1 -0
  25. package/libx/_runtime/common/generic/put.js +1 -0
  26. package/libx/_runtime/common/utils/cqn.js +5 -10
  27. package/libx/_runtime/common/utils/cqn2cqn4sql.js +37 -63
  28. package/libx/_runtime/common/utils/foreignKeyPropagations.js +28 -8
  29. package/libx/_runtime/common/utils/path.js +3 -3
  30. package/libx/_runtime/common/utils/require.js +2 -1
  31. package/libx/_runtime/common/utils/resolveView.js +3 -0
  32. package/libx/_runtime/common/utils/structured.js +6 -1
  33. package/libx/_runtime/db/Service.js +10 -0
  34. package/libx/_runtime/db/expand/expand-v2.js +13 -5
  35. package/libx/_runtime/db/expand/expandCQNToJoin.js +56 -26
  36. package/libx/_runtime/db/utils/generateAliases.js +9 -0
  37. package/libx/_runtime/extensibility/uiflex/handler/transformWRITE.js +1 -0
  38. package/libx/_runtime/fiori/generic/read.js +83 -31
  39. package/libx/_runtime/fiori/generic/readOverDraft.js +31 -19
  40. package/libx/_runtime/fiori/utils/handler.js +3 -0
  41. package/libx/_runtime/fiori/utils/where.js +38 -25
  42. package/libx/_runtime/hana/execute.js +18 -1
  43. package/libx/_runtime/hana/search2cqn4sql.js +4 -1
  44. package/libx/_runtime/sqlite/convertAssocToOneManaged.js +19 -12
  45. package/package.json +1 -1
@@ -19,9 +19,18 @@ const { deleteCondition, readAndDeleteKeywords, removeIsActiveEntityRecursively
19
19
  const { getColumns } = require('../../cds-services/services/utils/columns')
20
20
  const { adaptStreamCQN } = require('../../cds-services/adapter/odata-v4/utils/stream')
21
21
 
22
+ const _findSubselect = where => {
23
+ return where.find((e, i) => {
24
+ if (e.xpr) {
25
+ return _findSubselect(e.xpr)
26
+ }
27
+ return e.SELECT && where[i - 1] === 'exists'
28
+ })
29
+ }
30
+
22
31
  const _findRootSubSelectFor = query => {
23
32
  if (query.SELECT.where) {
24
- const subSelect = query.SELECT.where.find((e, i) => e.SELECT && query.SELECT.where[i - 1] === 'exists')
33
+ const subSelect = _findSubselect(query.SELECT.where)
25
34
  return subSelect ? _findRootSubSelectFor(subSelect) : query
26
35
  }
27
36
  return query
@@ -422,8 +431,8 @@ const _allActive = (req, columns, model) => {
422
431
  cqn.leftJoin(ensureDraftsSuffix(table.ref[0]) + ' as drafts').on(`${table.as}.${ids[0]} = drafts.${ids[0]}`)
423
432
 
424
433
  for (let i = 1; i < ids.length; i++) {
425
- // REVISIT: this is extremely expensive as it repeatedly invokes the compiler's cds.parse.expr -> better extend plain CQN yourself here
426
- cqn.and(`${table.as}.${ids[i]} = drafts.${ids[i]}`)
434
+ // this 'and' belongs to the join condition and is not a where and
435
+ cqn.and({ ref: [table.as, ids[i]] }, '=', { ref: ['drafts', ids[i]] })
427
436
  }
428
437
  }
429
438
 
@@ -557,27 +566,38 @@ const _alignAliasForUnion = (table, as, select) => {
557
566
  return select
558
567
  }
559
568
 
560
- const _findJoinInQuery = (query, parentAlias) => {
561
- const targetAlias = query.SELECT.from.as
562
- const isTargetRef = el => targetAlias && el.ref && el.ref.length > 1 && el.ref[0] === targetAlias
563
- if (query.SELECT && query.SELECT.where)
564
- return query.SELECT.where.reduce((links, el, idx, where) => {
565
- if (el.ref && el.ref[0] === parentAlias && el.ref[el.ref.length - 1] !== 'IsActiveEntity') {
566
- if (where[idx - 1] && where[idx - 1] === '=' && isTargetRef(where[idx - 2])) {
567
- if (links.length) links.push('and')
568
- links.push(el, '=', where[idx - 2])
569
- }
570
- if (where[idx + 1] && where[idx + 1] === '=' && isTargetRef(where[idx + 2])) {
571
- if (links.length) links.push('and')
572
- links.push(el, '=', where[idx + 2])
573
- }
569
+ const isTargetRef = (el, targetAlias) => targetAlias && el.ref && el.ref.length > 1 && el.ref[0] === targetAlias
570
+
571
+ const _joinFromWhere = (where, parentAlias, targetAlias) => {
572
+ return where.reduce((links, el, idx, where) => {
573
+ if (el.xpr) {
574
+ const result = _joinFromWhere(el.xpr, parentAlias, targetAlias)
575
+ if (result.length) {
576
+ if (links.length) links.push('and')
577
+ links.push(...result)
574
578
  }
575
579
  return links
576
- }, [])
577
- return []
580
+ }
581
+
582
+ if (el.ref && el.ref[0] === parentAlias && el.ref[el.ref.length - 1] !== 'IsActiveEntity') {
583
+ if (where[idx - 1] && where[idx - 1] === '=' && isTargetRef(where[idx - 2], targetAlias)) {
584
+ if (links.length) links.push('and')
585
+ links.push(el, '=', where[idx - 2])
586
+ }
587
+ if (where[idx + 1] && where[idx + 1] === '=' && isTargetRef(where[idx + 2], targetAlias)) {
588
+ if (links.length) links.push('and')
589
+ links.push(el, '=', where[idx + 2])
590
+ }
591
+ }
592
+ return links
593
+ }, [])
578
594
  }
579
595
 
580
- const _isFiltered = where => where.some(element => !(element in ['(', ')']))
596
+ const _findJoinInQuery = (query, parentAlias) => {
597
+ const targetAlias = query.SELECT.from.as
598
+ if (query.SELECT && query.SELECT.where) return _joinFromWhere(query.SELECT.where, parentAlias, targetAlias)
599
+ return []
600
+ }
581
601
 
582
602
  const _isDraftField = element => element.ref && element.ref.length > 1 && element.ref[0] === 'DraftAdministrativeData'
583
603
 
@@ -597,6 +617,10 @@ const _isLogicalFunction = (where, index) => {
597
617
  const _getWhereForActive = where => {
598
618
  const activeWhere = []
599
619
  for (let i = 0; i < where.length; i++) {
620
+ if (where[i].xpr) {
621
+ activeWhere.push({ xpr: _getWhereForActive(where[i].xpr) })
622
+ continue
623
+ }
600
624
  if (_isDraftField(where[i])) {
601
625
  activeWhere.push({ val: null })
602
626
  } else if (_functionContainsDraftField(where[i])) {
@@ -721,10 +745,20 @@ const _getSiblingScenario = (req, columns, model, siblingIndex, nav) => {
721
745
  const draftAdminAlias = _isDraftAdminScenario(req) && req.query.SELECT.from.as
722
746
  const params = [...req.params].reverse()
723
747
  const _getSiblingQueryFromWhere = (query, queryIndex, parentQuery) => {
724
- if (query.SELECT && query.SELECT.where) {
725
- const indexExists = query.SELECT.where.indexOf('exists')
726
- if (indexExists > -1 && queryIndex > 0) {
727
- return _getSiblingQueryFromWhere(query.SELECT.where[indexExists + 1], queryIndex - 1, query)
748
+ if (query.SELECT && query.SELECT.where && queryIndex > 0) {
749
+ for (let i = 0; i < query.SELECT.where.length; i++) {
750
+ if (query.SELECT.where[i].xpr && queryIndex > 0) {
751
+ const sibilingQueryFromWhere = _getSiblingQueryFromWhere(
752
+ { SELECT: { where: query.SELECT.where[i].xpr } },
753
+ queryIndex - 1,
754
+ query
755
+ )
756
+ if (sibilingQueryFromWhere) return sibilingQueryFromWhere
757
+ }
758
+
759
+ if (query.SELECT.where[i] === 'exists' && queryIndex > 0) {
760
+ return _getSiblingQueryFromWhere(query.SELECT.where[i + 1], queryIndex - 1, query)
761
+ }
728
762
  }
729
763
  }
730
764
  const target = { name: query.SELECT.from.ref[0].id || query.SELECT.from.ref[0], as: query.SELECT.from.as }
@@ -741,9 +775,16 @@ const _getSiblingScenario = (req, columns, model, siblingIndex, nav) => {
741
775
  return _getSiblingQueryFromWhere(req.query, siblingIndex)
742
776
  }
743
777
 
744
- const _mergeSiblingIntoCQN = (cqn, { cqn: siblingCQN }, siblingIndex) => {
745
- const _replaceWhereExists = (query, _siblingIndex) => {
746
- if (query.SELECT && query.SELECT.where) {
778
+ const _replaceWhereExists = (query, _siblingIndex, siblingCQN) => {
779
+ if (query.SELECT && query.SELECT.where) {
780
+ for (let i = 0; i < query.SELECT.where.length; i++) {
781
+ const whereElement = query.SELECT.where[i]
782
+ if (whereElement.xpr) {
783
+ const res = _replaceWhereExists({ SELECT: { where: whereElement.xpr } }, _siblingIndex, siblingCQN)
784
+ if (res) return res
785
+ continue
786
+ }
787
+
747
788
  const indexExists = query.SELECT.where.indexOf('exists')
748
789
  if (indexExists > -1) {
749
790
  if (_siblingIndex > 0) return _replaceWhereExists(query.SELECT.where[indexExists + 1], _siblingIndex - 1)
@@ -751,7 +792,10 @@ const _mergeSiblingIntoCQN = (cqn, { cqn: siblingCQN }, siblingIndex) => {
751
792
  }
752
793
  }
753
794
  }
754
- return _replaceWhereExists(cqn, siblingIndex)
795
+ }
796
+
797
+ const _mergeSiblingIntoCQN = (cqn, { cqn: siblingCQN }, siblingIndex) => {
798
+ return _replaceWhereExists(cqn, siblingIndex, siblingCQN)
755
799
  }
756
800
 
757
801
  const _getDraftDoc = (req, draftName, draftWhere) => {
@@ -796,6 +840,11 @@ const _getOrderByEnrichedColumns = (orderBy, columns, entity) => {
796
840
 
797
841
  const _replaceDraftAlias = where => {
798
842
  where.forEach(element => {
843
+ if (element.xpr) {
844
+ _replaceDraftAlias(element.xpr)
845
+ return
846
+ }
847
+
799
848
  if (_isDraftField(element)) {
800
849
  element.ref[0] = 'filterAdmin'
801
850
  }
@@ -981,7 +1030,10 @@ const _validatedDraftOfWhichIAmOwner = (req, draftWhere, draftParameters, column
981
1030
  _isValidDraftOfWhichIAmOwner(draftParameters.isActiveEntity) && _draftOfWhichIAmOwner(req, draftWhere, columns)
982
1031
 
983
1032
  const _draftInSubSelect = (where, req) => {
984
- return where.some(({ SELECT }) => {
1033
+ return where.some(({ SELECT, xpr }) => {
1034
+ if (xpr) {
1035
+ return _draftInSubSelect(xpr, req)
1036
+ }
985
1037
  if (SELECT && SELECT.where) {
986
1038
  const isActiveEntity = readAndDeleteKeywords(['IsActiveEntity'], SELECT.where, false)
987
1039
  if (isActiveEntity) {
@@ -1022,7 +1074,7 @@ const _generateCQN = (originalFrom, req, columns, model) => {
1022
1074
  return _draftAdminTable(req)
1023
1075
  }
1024
1076
 
1025
- if (!req.query.SELECT.where || !_isFiltered(req.query.SELECT.where)) {
1077
+ if (!req.query.SELECT.where) {
1026
1078
  return _allActive(req, columns, model)
1027
1079
  }
1028
1080
 
@@ -1032,7 +1084,7 @@ const _generateCQN = (originalFrom, req, columns, model) => {
1032
1084
  if (
1033
1085
  draftParameters.isActiveEntity &&
1034
1086
  _isTrue(draftParameters.isActiveEntity.value.val) &&
1035
- !draftParameters.siblingIsActive &&
1087
+ !(draftParameters.siblingIsActive && draftParameters.siblingIsActive.value.val === null) &&
1036
1088
  !draftParameters.hasDraftEntity
1037
1089
  ) {
1038
1090
  return _allActive(req, columns, model)
@@ -5,18 +5,14 @@ const { readAndDeleteKeywords } = require('../utils/where')
5
5
  const { cqn2cqn4sql } = require('../../common/utils/cqn2cqn4sql')
6
6
  const { isActiveEntityRequested } = require('../../../_runtime/fiori/utils/where')
7
7
 
8
- const _modifyCQN = (cqnDraft, where, context) => {
9
- const whereDraft = [...where]
10
- const result = readAndDeleteKeywords(['IsActiveEntity'], whereDraft)
11
- cqnDraft.where(whereDraft)
12
-
13
- if (result && result.value.val === false) {
14
- const fromRef = cqnDraft.SELECT.from.ref
15
- cqnDraft.SELECT.from.ref[fromRef.length - 1] = ensureDraftsSuffix(fromRef[fromRef.length - 1])
16
- }
8
+ const _modifyWhere = (where, context) => {
9
+ for (let i = 0; i < where.length; i++) {
10
+ const element = where[i]
17
11
 
18
- for (let i = 0; i < cqnDraft.SELECT.where.length; i++) {
19
- const element = cqnDraft.SELECT.where[i]
12
+ if (element.xpr) {
13
+ _modifyWhere(element.xpr, context)
14
+ continue
15
+ }
20
16
 
21
17
  if (element.SELECT) {
22
18
  const subCqnDraft = SELECT.from(
@@ -27,12 +23,25 @@ const _modifyCQN = (cqnDraft, where, context) => {
27
23
  [1]
28
24
  )
29
25
 
30
- cqnDraft.SELECT.where[i] = subCqnDraft
26
+ where[i] = subCqnDraft
31
27
  _modifyCQN(subCqnDraft, element.SELECT.where, context)
32
28
  }
33
29
  }
34
30
  }
35
31
 
32
+ const _modifyCQN = (cqnDraft, where, context) => {
33
+ const whereDraft = [...where]
34
+ const result = readAndDeleteKeywords(['IsActiveEntity'], whereDraft)
35
+ cqnDraft.where(whereDraft)
36
+
37
+ if (result && result.value.val === false) {
38
+ const fromRef = cqnDraft.SELECT.from.ref
39
+ cqnDraft.SELECT.from.ref[fromRef.length - 1] = ensureDraftsSuffix(fromRef[fromRef.length - 1])
40
+ }
41
+
42
+ _modifyWhere(cqnDraft.SELECT.where, context)
43
+ }
44
+
36
45
  const _hasNavToNonDraftEnclosedAssoc = (pathSegments, definitions, excludeAssoc) => {
37
46
  if (pathSegments.length < 2) return false
38
47
  const entity = definitions[pathSegments[0]]
@@ -67,19 +76,22 @@ const _shouldReadOverDraft = (req, definitions) => {
67
76
  const rootEntityName = typeof firstFromRef === 'string' ? firstFromRef : firstFromRef.id
68
77
  const rootEntity = definitions[rootEntityName]
69
78
 
70
- // read over the draft only if the root entity is draft-enabled and
71
- // the navigation starts with a draft entity (IsActiveEntity=false)
72
- if (!!rootEntity._isDraftEnabled && isActiveEntityRequested(firstFromRef.where)) return false
79
+ // read over the draft only if the root entity is draft-enabled
80
+ if (!rootEntity._isDraftEnabled) return false
73
81
 
74
- // read over the draft if the navigation target is an association and
75
- // only if it isn't annotated with @odata.draft.enclosed
76
- const pathSegments = fromRef.map(path => (typeof path === 'string' ? path : path.id))
82
+ // read over the draft only if the navigation starts from a draft entity, e.g.,
83
+ // /Books(ID=1, IsActiveEntity=false)
84
+ if (isActiveEntityRequested(firstFromRef.where)) return false
77
85
 
86
+ const pathSegments = fromRef.map(path => (typeof path === 'string' ? path : path.id))
78
87
  const excludeAssoc = assoc => {
79
88
  if (assoc.name === 'DraftAdministrativeData' || assoc.name === 'SiblingEntity') return true
80
89
  return false
81
90
  }
82
91
 
92
+ // Read over the draft only if:
93
+ // - the navigation target is an association and
94
+ // - isn't annotated with the @odata.draft.enclosed annotation
83
95
  return _hasNavToNonDraftEnclosedAssoc(pathSegments, definitions, excludeAssoc)
84
96
  }
85
97
 
@@ -109,7 +121,7 @@ const _readOverDraftHandler = async function (req, next) {
109
121
 
110
122
  const hasDraftEntity = hasDraft(definitions, sqlQuery)
111
123
 
112
- if (hasDraftEntity && sqlQuery.SELECT.where && sqlQuery.SELECT.where.length > 0) {
124
+ if (hasDraftEntity && sqlQuery.SELECT.where && sqlQuery.SELECT.where.length) {
113
125
  let cqnDraft = SELECT.from({
114
126
  ref: [...sqlQuery.SELECT.from.ref],
115
127
  as: sqlQuery.SELECT.from.as
@@ -93,6 +93,9 @@ const getUpdateDraftAdminCQN = ({ user }, draftUUID) => {
93
93
  const _addAlias = (where, tableName) => {
94
94
  // copy where
95
95
  return where.map(element => {
96
+ if (element.xpr) {
97
+ return { xpr: _addAlias(element.xpr, tableName) }
98
+ }
96
99
  if (element.ref && element.ref.length === 1) {
97
100
  // and copy ref
98
101
  return { ref: [tableName, element.ref[0]] }
@@ -57,7 +57,9 @@ const _removeIsActiveEntityCondition = where => {
57
57
  } else if (where[i] === 'and' && where[i + 1] === '(' && _isActiveEntity(where[i + 2])) {
58
58
  i = i + 6
59
59
  } else if (where[i].xpr) {
60
- newWhere.push({ xpr: _removeIsActiveEntityCondition(where[i].xpr) })
60
+ if (where[i].xpr.length) {
61
+ newWhere.push({ xpr: _removeIsActiveEntityCondition(where[i].xpr) })
62
+ }
61
63
  i++
62
64
  } else {
63
65
  newWhere.push(where[i])
@@ -65,7 +67,7 @@ const _removeIsActiveEntityCondition = where => {
65
67
  }
66
68
  }
67
69
 
68
- if (newWhere[0] === 'and') {
70
+ if (newWhere[0] === 'and' || newWhere[0] === 'or') {
69
71
  newWhere.splice(0, 1)
70
72
  } else if (newWhere[0] === '(' && newWhere[1] === 'and') {
71
73
  newWhere.splice(0, 2)
@@ -90,32 +92,35 @@ const deleteCondition = (index, whereCondition, isXpr = false) => {
90
92
  }
91
93
 
92
94
  const readAndDeleteKeywords = (keywords, whereCondition, toDelete = true) => {
93
- let index = whereCondition.findIndex(({ xpr }) => xpr)
94
- if (index !== -1) {
95
- const result = readAndDeleteKeywords(keywords, whereCondition[index].xpr, toDelete)
96
- if (result) {
97
- if (whereCondition[index].xpr.length === 0) {
98
- deleteCondition(index, whereCondition, true)
95
+ let index = -1
96
+ for (let i = 0; i < whereCondition.length; i++) {
97
+ const entry = whereCondition[i]
98
+ if (entry.xpr) {
99
+ const result = readAndDeleteKeywords(keywords, entry.xpr, toDelete)
100
+ if (result) {
101
+ if (entry.xpr.length === 0) {
102
+ deleteCondition(i, whereCondition, true)
103
+ }
104
+ return result
105
+ }
106
+ } else if (entry.ref) {
107
+ const refLastIndex = entry.ref.length - 1
108
+
109
+ if (keywords.length === 1) {
110
+ if (entry.ref[refLastIndex] === keywords[0]) {
111
+ index = i
112
+ break
113
+ }
99
114
  }
100
- return result
101
- }
102
- }
103
-
104
- index = whereCondition.findIndex(({ ref }) => {
105
- if (!ref) {
106
- return false
107
- }
108
-
109
- const refLastIndex = ref.length - 1
110
-
111
- if (keywords.length === 1) {
112
- return ref[refLastIndex] === keywords[0]
113
- }
114
115
 
115
- if (keywords.length === 2 && ref.length >= 2) {
116
- return ref[refLastIndex - 1] === keywords[0] && ref[refLastIndex] === keywords[1]
116
+ if (keywords.length === 2 && entry.ref.length >= 2) {
117
+ if (entry.ref[refLastIndex - 1] === keywords[0] && entry.ref[refLastIndex] === keywords[1]) {
118
+ index = i
119
+ break
120
+ }
121
+ }
117
122
  }
118
- })
123
+ }
119
124
 
120
125
  if (index === -1) {
121
126
  return
@@ -135,6 +140,10 @@ const readAndDeleteKeywords = (keywords, whereCondition, toDelete = true) => {
135
140
 
136
141
  const removeIsActiveEntityRecursively = where => {
137
142
  for (const entry of where) {
143
+ if (entry.xpr) {
144
+ entry.xpr = removeIsActiveEntityRecursively(entry.xpr)
145
+ continue
146
+ }
138
147
  if (entry.SELECT && entry.SELECT.where && entry.SELECT.from.ref && !entry.SELECT.from.ref[0].endsWith('_drafts')) {
139
148
  entry.SELECT.where = removeIsActiveEntityRecursively(entry.SELECT.where)
140
149
 
@@ -153,6 +162,10 @@ const isActiveEntityRequested = where => {
153
162
  let i = 0
154
163
 
155
164
  while (where[i]) {
165
+ if (where[i].xpr) {
166
+ const isRequested = isActiveEntityRequested(where.xpr)
167
+ if (isRequested) return true
168
+ }
156
169
  if (
157
170
  where[i].ref &&
158
171
  where[i].ref[where[i].ref.length - 1] === 'IsActiveEntity' &&
@@ -280,11 +280,28 @@ function executeInsertCQN(model, dbc, query, user, locale, txTimestamp) {
280
280
  }
281
281
 
282
282
  return _executeSimpleSQL(dbc, sql, values).then(affectedRows => {
283
+ const entriesOrRows = query.INSERT.entries || query.INSERT.rows
284
+ const affectedRowsCount = Array.isArray(affectedRows)
285
+ ? affectedRows.reduce((sum, rows) => sum + rows, 0)
286
+ : affectedRows
287
+ if (entriesOrRows && entriesOrRows.length !== affectedRowsCount) {
288
+ LOG._warn &&
289
+ LOG.warn(
290
+ `INSERT input deviates from affected rows (input: ${entriesOrRows.length}, affectedRows: ${affectedRowsCount})`,
291
+ {
292
+ sql,
293
+ args: values && values.length,
294
+ values,
295
+ query
296
+ }
297
+ )
298
+ throw new Error('Possible data loss by INSERT into HANA db. Please, update a corresponding HANA driver.')
299
+ }
283
300
  // InsertResult needs an object per row with its values
284
301
  // query.INSERT.values -> one row
285
302
  if (query.INSERT.values) return [{ affectedRows: 1, values: [values] }]
286
303
  // query.INSERT.entries or .rows -> multiple rows
287
- if (query.INSERT.entries || query.INSERT.rows) return values.map(v => ({ affectedRows: 1, values: v }))
304
+ if (entriesOrRows) return values.map(v => ({ affectedRows: 1, values: v }))
288
305
  // INSERT into SELECT
289
306
  return [{ affectedRows }]
290
307
  })
@@ -37,8 +37,11 @@ const search2cqn4sql = (query, entity, options) => {
37
37
  if (resolveLocalizedDataAtRuntime) {
38
38
  const onCondition = entity._relations[localizedAssociation.name].join(localizedAssociation.target, entity.name)
39
39
 
40
+ // REVISIT this is dirty but works for now
40
41
  // replace $user_locale placeholder with the user locale or the HANA session context
41
- onCondition[onCondition.length - 2] = { val: locale || "SESSION_CONTEXT('LOCALE')" }
42
+ if (onCondition[0].xpr) {
43
+ onCondition[0].xpr[onCondition[0].xpr.length - 1] = { val: locale || "SESSION_CONTEXT('LOCALE')" }
44
+ } else onCondition[onCondition.length - 2] = { val: locale || "SESSION_CONTEXT('LOCALE')" }
42
45
 
43
46
  // inner join the target table with the _texts table (the _texts table contains
44
47
  // the translated texts)
@@ -17,29 +17,36 @@ const _getConvertibleEntries = req => {
17
17
  return [...orders, ...groups, ...filters, ...havings]
18
18
  }
19
19
 
20
- // REVISIT once sql can handle structured keys properly, this handler should not be required anymore
21
- const _handler = function (req) {
22
- // do simple checks upfront and exit early
23
- if (!req.query || typeof req.query === 'string') return
24
- if (!req.query.SELECT.orderBy && !req.query.SELECT.groupBy && !req.query.SELECT.where && !req.query.SELECT.having) {
25
- return
26
- }
27
-
28
- if (!req.target || !req.target.elements) return
20
+ const _convert = (refEntries, req) => {
21
+ for (const refEntry of refEntries) {
22
+ if (refEntry.xpr) {
23
+ _convert(refEntry.xpr, req)
24
+ continue
25
+ }
29
26
 
30
- for (const refEntry of _getConvertibleEntries(req)) {
31
27
  if (!refEntry.ref || refEntry.ref.length < 2) {
32
28
  // only check refs in format {ref: ['assoc', 'id']}
33
29
  continue
34
30
  }
35
-
36
31
  const element = req.target.elements[refEntry.ref[0]]
37
32
  if (!element || !element.is2one) return
38
-
39
33
  _convertRefForAssocToOneManaged(element, refEntry)
40
34
  }
41
35
  }
42
36
 
37
+ // REVISIT once sql can handle structured keys properly, this handler should not be required anymore
38
+ const _handler = function (req) {
39
+ // do simple checks upfront and exit early
40
+ if (!(req.query && req.query.SELECT) || typeof req.query === 'string') return
41
+ if (!req.query.SELECT.orderBy && !req.query.SELECT.groupBy && !req.query.SELECT.where && !req.query.SELECT.having) {
42
+ return
43
+ }
44
+
45
+ if (!req.target || !req.target.elements) return
46
+
47
+ _convert(_getConvertibleEntries(req), req)
48
+ }
49
+
43
50
  _handler._initial = true
44
51
 
45
52
  module.exports = _handler
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "5.9.1",
3
+ "version": "5.9.4",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [