@sap/cds 5.9.0 → 5.9.3
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.
- package/CHANGELOG.md +35 -0
- package/app/fiori/routes.js +15 -8
- package/lib/compile/cdsc.js +1 -19
- package/lib/compile/etc/_localized.js +5 -4
- package/lib/compile/for/drafts.js +1 -1
- package/lib/compile/for/java.js +1 -1
- package/lib/compile/for/nodejs.js +1 -1
- package/lib/compile/for/odata.js +1 -1
- package/lib/connect/bindings.js +1 -1
- package/lib/connect/index.js +2 -3
- package/lib/env/requires.js +1 -1
- package/lib/index.js +2 -1
- package/lib/serve/Service-methods.js +28 -1
- package/lib/serve/adapters.js +6 -6
- package/lib/serve/factory.js +14 -9
- package/lib/serve/index.js +4 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +6 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/BatchRequestListBuilder.js +3 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/dispatcherUtils.js +13 -7
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +1 -3
- package/libx/_runtime/cds-services/services/Service.js +1 -1
- package/libx/_runtime/common/composition/data.js +22 -13
- package/libx/_runtime/common/composition/delete.js +14 -12
- package/libx/_runtime/common/generic/auth/restrict.js +3 -1
- package/libx/_runtime/{cds-services/services/utils → common/generic/auth}/restrictions.js +8 -1
- package/libx/_runtime/common/generic/input.js +1 -0
- package/libx/_runtime/common/generic/put.js +1 -0
- package/libx/_runtime/common/utils/cqn.js +5 -10
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +39 -65
- package/libx/_runtime/common/utils/foreignKeyPropagations.js +28 -8
- package/libx/_runtime/common/utils/path.js +3 -3
- package/libx/_runtime/common/utils/resolveView.js +3 -0
- package/libx/_runtime/common/utils/structured.js +6 -1
- package/libx/_runtime/db/Service.js +10 -0
- package/libx/_runtime/db/data-conversion/post-processing.js +5 -0
- package/libx/_runtime/db/expand/expand-v2.js +13 -5
- package/libx/_runtime/db/expand/expandCQNToJoin.js +56 -26
- package/libx/_runtime/db/sql-builder/FunctionBuilder.js +8 -8
- package/libx/_runtime/db/utils/generateAliases.js +9 -0
- package/libx/_runtime/extensibility/uiflex/handler/transformWRITE.js +1 -0
- package/libx/_runtime/fiori/generic/read.js +83 -31
- package/libx/_runtime/fiori/generic/readOverDraft.js +31 -19
- package/libx/_runtime/fiori/utils/handler.js +3 -0
- package/libx/_runtime/fiori/utils/where.js +38 -25
- package/libx/_runtime/hana/execute.js +18 -1
- package/libx/_runtime/hana/search2cqn4sql.js +4 -1
- package/libx/_runtime/remote/utils/client.js +4 -6
- package/libx/_runtime/sqlite/convertAssocToOneManaged.js +19 -12
- package/libx/odata/cqn2odata.js +24 -27
- package/package.json +2 -2
|
@@ -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 =
|
|
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
|
-
//
|
|
426
|
-
cqn.and(
|
|
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
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
|
745
|
-
|
|
746
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
|
71
|
-
|
|
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
|
|
75
|
-
//
|
|
76
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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 (
|
|
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[
|
|
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)
|
|
@@ -339,12 +339,10 @@ const _cqnToReqOptions = (query, kind, model, target) => {
|
|
|
339
339
|
const queryObject = cds.odata.urlify(query, { kind, model })
|
|
340
340
|
const reqOptions = {
|
|
341
341
|
method: queryObject.method,
|
|
342
|
-
url:
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
.replace(/ \)/g, ')')
|
|
347
|
-
)
|
|
342
|
+
url: queryObject.path
|
|
343
|
+
// ugly workaround for Okra not allowing spaces in ( x eq 1 )
|
|
344
|
+
.replace(/\( /g, '(')
|
|
345
|
+
.replace(/ \)/g, ')')
|
|
348
346
|
}
|
|
349
347
|
if (queryObject.method !== 'GET' && queryObject.method !== 'HEAD') {
|
|
350
348
|
reqOptions.data = kind === 'odata-v2' ? convertV2PayloadData(queryObject.body, target) : queryObject.body
|
|
@@ -17,29 +17,36 @@ const _getConvertibleEntries = req => {
|
|
|
17
17
|
return [...orders, ...groups, ...filters, ...havings]
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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/libx/odata/cqn2odata.js
CHANGED
|
@@ -83,14 +83,10 @@ function _args(args) {
|
|
|
83
83
|
|
|
84
84
|
if (hasValidProps(cur, 'func', 'args')) {
|
|
85
85
|
res.push(`${cur.func}(${_args(cur.args)})`)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (hasValidProps(cur, '
|
|
89
|
-
res.push(cur
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (hasValidProps(cur, 'val')) {
|
|
93
|
-
res.push(formatVal(cur.val))
|
|
86
|
+
} else if (hasValidProps(cur, 'ref')) {
|
|
87
|
+
res.push(_format(cur))
|
|
88
|
+
} else if (hasValidProps(cur, 'val')) {
|
|
89
|
+
res.push(_format(cur))
|
|
94
90
|
}
|
|
95
91
|
}
|
|
96
92
|
|
|
@@ -98,13 +94,13 @@ function _args(args) {
|
|
|
98
94
|
}
|
|
99
95
|
|
|
100
96
|
const _in = (column, /* in */ collection, target, kind, isLambda) => {
|
|
101
|
-
const ref =
|
|
97
|
+
const ref = _format(column, null, target, kind, isLambda)
|
|
102
98
|
// { val: [ 1, 2, 3 ] } or { list: [ { val: 1}, { val: 2}, { val: 3} ] }
|
|
103
99
|
const values = collection.val || collection.list
|
|
104
100
|
if (values && values.length) {
|
|
105
101
|
// REVISIT: what about OData `in` operator?
|
|
106
|
-
const expressions = values.map(value => `${ref}
|
|
107
|
-
return expressions.join('
|
|
102
|
+
const expressions = values.map(value => `${ref}%20eq%20${_format(value, ref, target, kind, isLambda)}`)
|
|
103
|
+
return expressions.join('%20or%20')
|
|
108
104
|
}
|
|
109
105
|
}
|
|
110
106
|
|
|
@@ -119,9 +115,10 @@ const _odataV2Func = (func, args) => {
|
|
|
119
115
|
}
|
|
120
116
|
|
|
121
117
|
const _format = (cur, element, target, kind, isLambda) => {
|
|
122
|
-
if (typeof cur !== 'object') return formatVal(cur, element, target, kind)
|
|
123
|
-
if (hasValidProps(cur, 'ref'))
|
|
124
|
-
|
|
118
|
+
if (typeof cur !== 'object') return encodeURIComponent(formatVal(cur, element, target, kind))
|
|
119
|
+
if (hasValidProps(cur, 'ref'))
|
|
120
|
+
return encodeURIComponent(isLambda ? [LAMBDA_VARIABLE, ...cur.ref].join('/') : cur.ref.join('/'))
|
|
121
|
+
if (hasValidProps(cur, 'val')) return encodeURIComponent(formatVal(cur.val, element, target, kind))
|
|
125
122
|
if (hasValidProps(cur, 'xpr')) return `(${_xpr(cur.xpr, target, kind, isLambda)})`
|
|
126
123
|
// REVISIT: How to detect the types for all functions?
|
|
127
124
|
if (hasValidProps(cur, 'func', 'args')) {
|
|
@@ -156,7 +153,7 @@ function _xpr(expr, target, kind, isLambda) {
|
|
|
156
153
|
} else if (isOrIsNotValue) {
|
|
157
154
|
// REVISIT: "is" only used for null values?
|
|
158
155
|
const operator = isOrIsNotValue[1] /* 'is not' */ ? 'ne' : 'eq'
|
|
159
|
-
res.push(...[operator,
|
|
156
|
+
res.push(...[operator, _format({ val: isOrIsNotValue[2] })])
|
|
160
157
|
} else if (cur === 'between') {
|
|
161
158
|
// ref gt low.val and ref lt high.val
|
|
162
159
|
const between = [expr[i - 1], 'gt', expr[i + 1], 'and', expr[i - 1], 'lt', expr[i + 3]]
|
|
@@ -188,7 +185,7 @@ function _xpr(expr, target, kind, isLambda) {
|
|
|
188
185
|
}
|
|
189
186
|
}
|
|
190
187
|
|
|
191
|
-
return res.join('
|
|
188
|
+
return res.join('%20')
|
|
192
189
|
}
|
|
193
190
|
|
|
194
191
|
const _keysOfWhere = (where, kind, target) => {
|
|
@@ -202,11 +199,11 @@ const _keysOfWhere = (where, kind, target) => {
|
|
|
202
199
|
const res = []
|
|
203
200
|
for (const cur of where) {
|
|
204
201
|
if (hasValidProps(cur, 'ref')) {
|
|
205
|
-
res.push(cur
|
|
202
|
+
res.push(_format(cur))
|
|
206
203
|
} else if (hasValidProps(cur, 'val')) {
|
|
207
204
|
// find previous ref
|
|
208
205
|
const element = res[res.length - 2]
|
|
209
|
-
res.push(
|
|
206
|
+
res.push(_format(cur, element, target, kind))
|
|
210
207
|
} else if (cur === 'and') {
|
|
211
208
|
res.push(',')
|
|
212
209
|
} else {
|
|
@@ -267,15 +264,15 @@ const _parseColumnsV2 = (columns, prefix = []) => {
|
|
|
267
264
|
|
|
268
265
|
if (hasValidProps(column, 'expand')) {
|
|
269
266
|
const parsed = _parseColumnsV2(column.expand, [refName])
|
|
270
|
-
expand.push(refName, ...parsed.expand)
|
|
267
|
+
expand.push(encodeURIComponent(refName), ...parsed.expand)
|
|
271
268
|
select.push(...parsed.select)
|
|
272
269
|
} else {
|
|
273
|
-
select.push(refName)
|
|
270
|
+
select.push(encodeURIComponent(refName))
|
|
274
271
|
}
|
|
275
272
|
}
|
|
276
273
|
|
|
277
274
|
if (column === '*') {
|
|
278
|
-
select.push(`${prefix.join('/')}/*`)
|
|
275
|
+
select.push(encodeURIComponent(`${prefix.join('/')}/*`))
|
|
279
276
|
}
|
|
280
277
|
}
|
|
281
278
|
|
|
@@ -288,7 +285,7 @@ const _parseColumns = columns => {
|
|
|
288
285
|
|
|
289
286
|
for (const column of columns) {
|
|
290
287
|
if (hasValidProps(column, 'ref')) {
|
|
291
|
-
let refName = column
|
|
288
|
+
let refName = _format(column)
|
|
292
289
|
if (hasValidProps(column, 'expand')) {
|
|
293
290
|
// REVISIT: incomplete, see test Foo?$expand=invoices($count=true;$expand=item($search="some"))
|
|
294
291
|
if (!columns.some(c => !c.expand)) select.push(refName)
|
|
@@ -350,16 +347,16 @@ function $orderBy(orderBy) {
|
|
|
350
347
|
|
|
351
348
|
for (const cur of orderBy) {
|
|
352
349
|
if (hasValidProps(cur, 'ref', 'sort')) {
|
|
353
|
-
res.push(cur
|
|
350
|
+
res.push(_format(cur) + '%20' + cur.sort)
|
|
354
351
|
continue
|
|
355
352
|
}
|
|
356
353
|
|
|
357
354
|
if (hasValidProps(cur, 'ref')) {
|
|
358
|
-
res.push(cur
|
|
355
|
+
res.push(_format(cur))
|
|
359
356
|
}
|
|
360
357
|
|
|
361
358
|
if (hasValidProps(cur, 'func', 'sort')) {
|
|
362
|
-
res.push(`${cur.func}(${_args(cur.args)})` + '
|
|
359
|
+
res.push(`${cur.func}(${_args(cur.args)})` + '%20' + cur.sort)
|
|
363
360
|
continue
|
|
364
361
|
}
|
|
365
362
|
|
|
@@ -382,7 +379,7 @@ function parseSearch(search) {
|
|
|
382
379
|
|
|
383
380
|
if (hasValidProps(cur, 'val')) {
|
|
384
381
|
// search term must not be formatted
|
|
385
|
-
res.push(`"${cur.val}"`)
|
|
382
|
+
res.push(`"${encodeURIComponent(cur.val)}"`)
|
|
386
383
|
}
|
|
387
384
|
|
|
388
385
|
if (typeof cur === 'string') {
|
|
@@ -398,7 +395,7 @@ function parseSearch(search) {
|
|
|
398
395
|
}
|
|
399
396
|
|
|
400
397
|
function $search(search, kind) {
|
|
401
|
-
const expr = parseSearch(search).join('
|
|
398
|
+
const expr = parseSearch(search).join('%20').replace('(%20', '(').replace('%20)', ')')
|
|
402
399
|
|
|
403
400
|
if (expr) {
|
|
404
401
|
// odata-v2 may support custom query option "search"
|