@cap-js/db-service 1.5.1 → 1.6.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.
- package/CHANGELOG.md +35 -0
- package/lib/SQLService.js +130 -82
- package/lib/common/DatabaseService.js +6 -14
- package/lib/common/session-context.js +14 -0
- package/lib/cqn2sql.js +165 -96
- package/lib/cqn4sql.js +110 -125
- package/lib/fill-in-keys.js +6 -1
- package/lib/infer/cqn.d.ts +0 -3
- package/lib/infer/index.js +36 -23
- package/lib/infer/join-tree.js +5 -6
- package/package.json +2 -2
package/lib/cqn4sql.js
CHANGED
|
@@ -54,88 +54,84 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
54
54
|
transformedQuery = transformQueryForInsertUpsert(kind)
|
|
55
55
|
} else {
|
|
56
56
|
const queryProp = inferred[kind]
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
} else {
|
|
60
|
-
const { entity, where } = queryProp
|
|
61
|
-
const from = queryProp.from
|
|
57
|
+
const { entity, where } = queryProp
|
|
58
|
+
const from = queryProp.from
|
|
62
59
|
|
|
63
|
-
|
|
60
|
+
const transformedProp = { __proto__: queryProp } // IMPORTANT: don't lose anything you might not know of
|
|
64
61
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
// Transform the existing where, prepend table aliases, and so on...
|
|
63
|
+
if (where) {
|
|
64
|
+
transformedProp.where = getTransformedTokenStream(where)
|
|
65
|
+
}
|
|
69
66
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
// Transform the from clause: association path steps turn into `WHERE EXISTS` subqueries.
|
|
68
|
+
// The already transformed `where` clause is then glued together with the resulting subqueries.
|
|
69
|
+
const { transformedWhere, transformedFrom } = getTransformedFrom(from || entity, transformedProp.where)
|
|
70
|
+
const queryNeedsJoins = inferred.joinTree && !inferred.joinTree.isInitial
|
|
74
71
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
72
|
+
if (inferred.SELECT) {
|
|
73
|
+
transformedQuery = transformSelectQuery(queryProp, transformedFrom, transformedWhere, transformedQuery)
|
|
74
|
+
} else {
|
|
75
|
+
if (from) {
|
|
76
|
+
transformedProp.from = transformedFrom
|
|
77
|
+
} else if (!queryNeedsJoins) {
|
|
78
|
+
transformedProp.entity = transformedFrom
|
|
79
|
+
}
|
|
83
80
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
if (transformedWhere?.length > 0) {
|
|
82
|
+
transformedProp.where = transformedWhere
|
|
83
|
+
}
|
|
87
84
|
|
|
88
|
-
|
|
85
|
+
transformedQuery[kind] = transformedProp
|
|
89
86
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
87
|
+
if (inferred.UPDATE?.with) {
|
|
88
|
+
Object.entries(inferred.UPDATE.with).forEach(([key, val]) => {
|
|
89
|
+
const transformed = getTransformedTokenStream([val])
|
|
90
|
+
inferred.UPDATE.with[key] = transformed[0]
|
|
91
|
+
})
|
|
96
92
|
}
|
|
93
|
+
}
|
|
97
94
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
95
|
+
if (queryNeedsJoins) {
|
|
96
|
+
if (inferred.UPDATE || inferred.DELETE) {
|
|
97
|
+
const prop = inferred.UPDATE ? 'UPDATE' : 'DELETE'
|
|
98
|
+
const subquery = {
|
|
99
|
+
SELECT: {
|
|
100
|
+
from: { ...transformedFrom },
|
|
101
|
+
columns: [], // primary keys of the query target will be added later
|
|
102
|
+
where: [...transformedProp.where],
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
// The alias of the original query is now the alias for the subquery
|
|
106
|
+
// so that potential references in the where clause to the alias match.
|
|
107
|
+
// Hence, replace the alias of the original query with the next
|
|
108
|
+
// available alias, so that each alias is unique.
|
|
109
|
+
const uniqueSubqueryAlias = getNextAvailableTableAlias(transformedFrom.as)
|
|
110
|
+
transformedFrom.as = uniqueSubqueryAlias
|
|
111
|
+
|
|
112
|
+
// calculate the primary keys of the target entity, there is always exactly
|
|
113
|
+
// one query source for UPDATE / DELETE
|
|
114
|
+
const queryTarget = Object.values(originalQuery.sources)[0]
|
|
115
|
+
const keys = Object.values(queryTarget.elements).filter(e => e.key === true)
|
|
116
|
+
const primaryKey = { list: [] }
|
|
117
|
+
keys.forEach(k => {
|
|
118
|
+
// cqn4sql will add the table alias to the column later, no need to add it here
|
|
119
|
+
subquery.SELECT.columns.push({ ref: [k.name] })
|
|
120
|
+
|
|
121
|
+
// add the alias of the main query to the list of primary key references
|
|
122
|
+
primaryKey.list.push({ ref: [transformedFrom.as, k.name] })
|
|
123
|
+
})
|
|
127
124
|
|
|
128
|
-
|
|
125
|
+
const transformedSubquery = cqn4sql(subquery)
|
|
129
126
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
127
|
+
// replace where condition of original query with the transformed subquery
|
|
128
|
+
// correlate UPDATE / DELETE query with subquery by primary key matches
|
|
129
|
+
transformedQuery[prop].where = [primaryKey, 'in', transformedSubquery]
|
|
133
130
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
131
|
+
if (prop === 'UPDATE') transformedQuery.UPDATE.entity = transformedFrom
|
|
132
|
+
else transformedQuery.DELETE.from = transformedFrom
|
|
133
|
+
} else {
|
|
134
|
+
transformedQuery[kind].from = translateAssocsToJoins(transformedQuery[kind].from)
|
|
139
135
|
}
|
|
140
136
|
}
|
|
141
137
|
}
|
|
@@ -207,29 +203,6 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
207
203
|
return transformedQuery
|
|
208
204
|
}
|
|
209
205
|
|
|
210
|
-
/**
|
|
211
|
-
* Transforms a stream query, replacing the `where` and `into` clauses after processing.
|
|
212
|
-
*
|
|
213
|
-
* @param {object} inferred - The inferred object containing the STREAM query.
|
|
214
|
-
* @param {object} transformedQuery - The query object to be transformed.
|
|
215
|
-
*
|
|
216
|
-
* @returns {object} - The transformed query with updated STREAM clauses.
|
|
217
|
-
*/
|
|
218
|
-
function transformStreamQuery() {
|
|
219
|
-
const { into, where } = inferred.STREAM
|
|
220
|
-
const transformedProp = { __proto__: inferred.STREAM }
|
|
221
|
-
if (where) {
|
|
222
|
-
transformedProp.where = getTransformedTokenStream(where)
|
|
223
|
-
}
|
|
224
|
-
const { transformedWhere, transformedFrom } = getTransformedFrom(into, transformedProp.where)
|
|
225
|
-
if (transformedWhere?.length > 0) {
|
|
226
|
-
transformedProp.where = transformedWhere
|
|
227
|
-
}
|
|
228
|
-
transformedProp.into = transformedFrom
|
|
229
|
-
transformedQuery.STREAM = transformedProp
|
|
230
|
-
return transformedQuery
|
|
231
|
-
}
|
|
232
|
-
|
|
233
206
|
/**
|
|
234
207
|
* Transforms a search expression to a WHERE clause for a SELECT operation.
|
|
235
208
|
*
|
|
@@ -415,13 +388,6 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
415
388
|
return transformedColumns
|
|
416
389
|
|
|
417
390
|
function handleSubquery(col) {
|
|
418
|
-
if (!col.SELECT.from.as) {
|
|
419
|
-
const uniqueSubqueryAlias = inferred.joinTree.addNextAvailableTableAlias(
|
|
420
|
-
getLastStringSegment(col.SELECT.from.ref[col.SELECT.from.ref.length - 1]),
|
|
421
|
-
originalQuery.outerQueries,
|
|
422
|
-
)
|
|
423
|
-
Object.defineProperty(col.SELECT.from, 'uniqueSubqueryAlias', { value: uniqueSubqueryAlias })
|
|
424
|
-
}
|
|
425
391
|
transformedColumns.push(() => {
|
|
426
392
|
const res = transformSubquery(col)
|
|
427
393
|
if (col.as) res.as = col.as
|
|
@@ -469,7 +435,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
469
435
|
}
|
|
470
436
|
|
|
471
437
|
let columnAlias = col.as || (col.isJoinRelevant ? col.flatName : null)
|
|
472
|
-
const refNavigation = col.ref.slice(col
|
|
438
|
+
const refNavigation = col.ref.slice(col.$refLinks[0].definition.kind !== 'element' ? 1 : 0).join('_')
|
|
473
439
|
if (!columnAlias && col.flatName && col.flatName !== refNavigation) columnAlias = refNavigation
|
|
474
440
|
|
|
475
441
|
if (col.$refLinks.some(link => link.definition._target?.['@cds.persistence.skip'] === true)) return
|
|
@@ -945,7 +911,18 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
945
911
|
Object.defineProperty(q, 'outerQueries', { value: outerQueries })
|
|
946
912
|
}
|
|
947
913
|
if (isLocalized(inferred.target)) q.SELECT.localized = true
|
|
914
|
+
if (q.SELECT.from.ref && !q.SELECT.from.as) assignUniqueSubqueryAlias()
|
|
948
915
|
return cqn4sql(q, model)
|
|
916
|
+
|
|
917
|
+
function assignUniqueSubqueryAlias() {
|
|
918
|
+
if (q.SELECT.from.uniqueSubqueryAlias) return
|
|
919
|
+
const last = q.SELECT.from.ref.at(-1)
|
|
920
|
+
const uniqueSubqueryAlias = inferred.joinTree.addNextAvailableTableAlias(
|
|
921
|
+
getLastStringSegment(last.id||last),
|
|
922
|
+
originalQuery.outerQueries,
|
|
923
|
+
)
|
|
924
|
+
Object.defineProperty(q.SELECT.from, 'uniqueSubqueryAlias', { value: uniqueSubqueryAlias })
|
|
925
|
+
}
|
|
949
926
|
}
|
|
950
927
|
|
|
951
928
|
/**
|
|
@@ -998,7 +975,11 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
998
975
|
* @returns {boolean} true if the element is a managed association and the model is flat
|
|
999
976
|
*/
|
|
1000
977
|
function isManagedAssocInFlatMode(e) {
|
|
1001
|
-
return
|
|
978
|
+
return (
|
|
979
|
+
(model.meta.transformation === 'odata' || model.meta.unfolded?.some(u => u === 'assocs')) &&
|
|
980
|
+
e.isAssociation &&
|
|
981
|
+
e.keys
|
|
982
|
+
)
|
|
1002
983
|
}
|
|
1003
984
|
}
|
|
1004
985
|
|
|
@@ -1051,25 +1032,32 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1051
1032
|
if (!column) return column
|
|
1052
1033
|
if (column.val || column.func || column.SELECT) return [column]
|
|
1053
1034
|
|
|
1035
|
+
const structsAreUnfoldedAlready = model.meta.unfolded?.some(u => u === 'structs')
|
|
1054
1036
|
let { baseName, columnAlias, tableAlias } = names
|
|
1055
1037
|
const { exclude, replace } = excludeAndReplace || {}
|
|
1056
1038
|
const { $refLinks, flatName, isJoinRelevant } = column
|
|
1057
1039
|
let leafAssoc
|
|
1058
1040
|
let element = $refLinks ? $refLinks[$refLinks.length - 1].definition : column
|
|
1059
1041
|
if (isWildcard && element.type === 'cds.LargeBinary') return []
|
|
1060
|
-
if (element.on) return [] // unmanaged doesn't make it into columns
|
|
1042
|
+
if (element.on && !element.keys) return [] // unmanaged doesn't make it into columns
|
|
1061
1043
|
else if (element.virtual === true) return []
|
|
1062
1044
|
else if (!isJoinRelevant && flatName) baseName = flatName
|
|
1063
1045
|
else if (isJoinRelevant) {
|
|
1064
1046
|
const leaf = column.$refLinks[column.$refLinks.length - 1]
|
|
1065
1047
|
leafAssoc = [...column.$refLinks].reverse().find(link => link.definition.isAssociation)
|
|
1066
|
-
|
|
1067
|
-
|
|
1048
|
+
let elements
|
|
1049
|
+
//> REVISIT: remove once UCSN is standard (no more .foreignKeys)
|
|
1050
|
+
elements = leafAssoc.definition.elements || leafAssoc.definition.foreignKeys
|
|
1051
|
+
if (elements && leaf.alias in elements) {
|
|
1068
1052
|
element = leafAssoc.definition
|
|
1069
1053
|
baseName = getFullName(leafAssoc.definition)
|
|
1070
1054
|
columnAlias = column.ref.slice(0, -1).map(idOnly).join('_')
|
|
1071
1055
|
} else baseName = getFullName(column.$refLinks[column.$refLinks.length - 1].definition)
|
|
1072
|
-
} else baseName
|
|
1056
|
+
} else if (!baseName && structsAreUnfoldedAlready) {
|
|
1057
|
+
baseName = element.name // name is already fully constructed
|
|
1058
|
+
} else {
|
|
1059
|
+
baseName = baseName ? `${baseName}_${element.name}` : getFullName(element)
|
|
1060
|
+
}
|
|
1073
1061
|
|
|
1074
1062
|
// now we have the name of the to be expanded column
|
|
1075
1063
|
// it could be a structure, an association or a scalar
|
|
@@ -1289,7 +1277,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1289
1277
|
transformedTokenStream.push({ list: [] })
|
|
1290
1278
|
}
|
|
1291
1279
|
} else {
|
|
1292
|
-
|
|
1280
|
+
const { list } = token
|
|
1281
|
+
if (list.every(e => e.val)) // no need for transformation
|
|
1282
|
+
transformedTokenStream.push({ list })
|
|
1283
|
+
else transformedTokenStream.push({ list: getTransformedTokenStream(list, $baseLink) })
|
|
1293
1284
|
}
|
|
1294
1285
|
} else if (tokenStream.length === 1 && token.val && $baseLink) {
|
|
1295
1286
|
// infix filter - OData variant w/o mentioning key --> flatten out and compare each leaf to token.val
|
|
@@ -1418,19 +1409,16 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1418
1409
|
|
|
1419
1410
|
if (flatRhs) {
|
|
1420
1411
|
const flatLhs = flattenWithBaseName(token)
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
// --> calculate error message or exit early? See test "proper error if structures cannot be compared / too many elements on lhs"
|
|
1424
|
-
if (flatRhs.length !== flatLhs.length)
|
|
1425
|
-
// make sure we can compare both structures
|
|
1412
|
+
// make sure we can compare both structures
|
|
1413
|
+
if (flatRhs.length !== flatLhs.length) {
|
|
1426
1414
|
throw new Error(
|
|
1427
1415
|
`Can't compare "${definition.name}" with "${
|
|
1428
1416
|
value.$refLinks[value.$refLinks.length - 1].definition.name
|
|
1429
1417
|
}": the operands must have the same structure`,
|
|
1430
1418
|
)
|
|
1431
|
-
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1432
1421
|
const boolOp = notEqOps.some(([f, s]) => operator[0] === f && operator[1] === s) ? 'or' : 'and'
|
|
1433
|
-
const rhsPath = value.ref.join('.') // original path of the comparison, used in error message
|
|
1434
1422
|
while (flatLhs.length > 0) {
|
|
1435
1423
|
// retrieve and remove one flat element from LHS and search for it in RHS (remove it there too)
|
|
1436
1424
|
const { ref, _csnPath: lhs_csnPath } = flatLhs.shift()
|
|
@@ -1439,20 +1427,16 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1439
1427
|
// all following steps must also be part of lhs
|
|
1440
1428
|
return lhs_csnPath.slice(1).every((val, i) => val === rhs_csnPath[i + 1]) // first step is name of struct -> ignore
|
|
1441
1429
|
})
|
|
1430
|
+
// not found in rhs --> exit
|
|
1442
1431
|
if (indexOfElementOnRhs === -1) {
|
|
1443
|
-
|
|
1444
|
-
|
|
1432
|
+
const lhsPath = token.ref.join('.')
|
|
1433
|
+
const rhsPath = value.ref.join('.')
|
|
1434
|
+
throw new Error(`Can't compare "${lhsPath}" with "${rhsPath}": the operands must have the same structure`)
|
|
1445
1435
|
}
|
|
1446
1436
|
const rhs = flatRhs.splice(indexOfElementOnRhs, 1)[0] // remove the element also from RHS
|
|
1447
1437
|
result.push({ ref }, ...operator, rhs)
|
|
1448
1438
|
if (flatLhs.length > 0) result.push(boolOp)
|
|
1449
1439
|
}
|
|
1450
|
-
if (flatRhs.length) {
|
|
1451
|
-
// if we still have elements in flatRhs -> those were not found in lhs
|
|
1452
|
-
const lhsPath = token.ref.join('.') // original path of the comparison, used in error message
|
|
1453
|
-
flatRhs.forEach(t => pathNotFoundErr.push(`Path "${t._csnPath.slice(1).join('.')}" not found in "${lhsPath}"`))
|
|
1454
|
-
throw new Error(`Can't compare "${lhsPath}" with "${rhsPath}": ${pathNotFoundErr.join(', ')}`)
|
|
1455
|
-
}
|
|
1456
1440
|
} else {
|
|
1457
1441
|
// compare with value
|
|
1458
1442
|
const flatLhs = flattenWithBaseName(token)
|
|
@@ -1647,7 +1631,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1647
1631
|
* @returns {boolean}
|
|
1648
1632
|
*/
|
|
1649
1633
|
function isStructured(elt) {
|
|
1650
|
-
return Boolean(elt?.elements && elt.
|
|
1634
|
+
return Boolean(elt?.kind !== 'entity' && elt?.elements && !elt.isAssociation)
|
|
1651
1635
|
}
|
|
1652
1636
|
|
|
1653
1637
|
/**
|
|
@@ -1774,7 +1758,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1774
1758
|
else {
|
|
1775
1759
|
const lhsLeafArt = lhs.ref && lhs.$refLinks[lhs.$refLinks.length - 1].definition
|
|
1776
1760
|
const rhsLeafArt = rhs.ref && rhs.$refLinks[rhs.$refLinks.length - 1].definition
|
|
1777
|
-
if (lhsLeafArt?.target
|
|
1761
|
+
if ((lhsLeafArt?.target && rhsLeafArt?.target) || (lhsLeafArt?.elements && rhsLeafArt?.elements)) {
|
|
1778
1762
|
if (rhs.$refLinks[0].definition !== assocRefLink.definition) {
|
|
1779
1763
|
rhs.ref.unshift(targetSideRefLink.alias)
|
|
1780
1764
|
rhs.$refLinks.unshift(targetSideRefLink)
|
|
@@ -2020,7 +2004,8 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
2020
2004
|
* @returns the flat name of the element
|
|
2021
2005
|
*/
|
|
2022
2006
|
function getFullName(node, name = node.name) {
|
|
2023
|
-
|
|
2007
|
+
// REVISIT: this is an unfortunate implementation
|
|
2008
|
+
if (!node.parent || node.parent.kind === 'entity') return name
|
|
2024
2009
|
|
|
2025
2010
|
return getFullName(node.parent, `${node.parent.name}_${name}`)
|
|
2026
2011
|
}
|
package/lib/fill-in-keys.js
CHANGED
|
@@ -8,7 +8,12 @@ const generateUUIDandPropagateKeys = (target, data, event) => {
|
|
|
8
8
|
if (!data) return
|
|
9
9
|
const keys = target.keys
|
|
10
10
|
for (const key in keys) {
|
|
11
|
-
|
|
11
|
+
const keyElement = keys[key]
|
|
12
|
+
if (
|
|
13
|
+
keyElement.type === 'cds.UUID' &&
|
|
14
|
+
!data[key] && event === 'CREATE' &&
|
|
15
|
+
!keyElement.parent.elements[keyElement._foreignKey4]?._isAssociationStrict
|
|
16
|
+
) {
|
|
12
17
|
data[key] = cds.utils.uuid()
|
|
13
18
|
}
|
|
14
19
|
}
|
package/lib/infer/cqn.d.ts
CHANGED
|
@@ -9,9 +9,6 @@ export type SELECT = cqn.SELECT & linkedQuery
|
|
|
9
9
|
export type INSERT = cqn.INSERT & linkedQuery
|
|
10
10
|
export type UPSERT = cqn.UPSERT & linkedQuery
|
|
11
11
|
export type UPDATE = cqn.UPDATE & linkedQuery
|
|
12
|
-
export type STREAM = {
|
|
13
|
-
STREAM: { into: cqn.name | ref; data: ReadableStream<Buffer>; from: cqn.source; column?: ref; where: predicate }
|
|
14
|
-
} & linkedQuery
|
|
15
12
|
export type DELETE = cqn.DELETE & linkedQuery
|
|
16
13
|
export type CREATE = cqn.CREATE & linkedQuery
|
|
17
14
|
export type DROP = cqn.DROP & linkedQuery
|
package/lib/infer/index.js
CHANGED
|
@@ -36,8 +36,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
36
36
|
inferred.UPDATE ||
|
|
37
37
|
inferred.DELETE ||
|
|
38
38
|
inferred.CREATE ||
|
|
39
|
-
inferred.DROP
|
|
40
|
-
inferred.STREAM
|
|
39
|
+
inferred.DROP
|
|
41
40
|
|
|
42
41
|
// cache for already processed calculated elements
|
|
43
42
|
const alreadySeenCalcElements = new Set()
|
|
@@ -58,7 +57,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
58
57
|
writable: true,
|
|
59
58
|
},
|
|
60
59
|
})
|
|
61
|
-
if (originalQuery.SELECT || originalQuery.DELETE || originalQuery.UPDATE
|
|
60
|
+
if (originalQuery.SELECT || originalQuery.DELETE || originalQuery.UPDATE) {
|
|
62
61
|
const $combinedElements = inferCombinedElements()
|
|
63
62
|
/**
|
|
64
63
|
* TODO: this function is currently only called on DELETE's
|
|
@@ -100,12 +99,12 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
100
99
|
if (!target) throw new Error(`"${first}" not found in the definitions of your model`)
|
|
101
100
|
if (ref.length > 1) {
|
|
102
101
|
target = from.ref.slice(1).reduce((d, r) => {
|
|
103
|
-
const next = d.elements[r.id || r]?.
|
|
102
|
+
const next = d.elements[r.id || r]?._target || d.elements[r.id || r]
|
|
104
103
|
if (!next) throw new Error(`No association “${r.id || r}” in ${d.kind} “${d.name}”`)
|
|
105
104
|
return next
|
|
106
105
|
}, target)
|
|
107
106
|
}
|
|
108
|
-
if (target.kind !== 'entity' && !target.
|
|
107
|
+
if (target.kind !== 'entity' && !target.isAssociation)
|
|
109
108
|
throw new Error('Query source must be a an entity or an association')
|
|
110
109
|
|
|
111
110
|
attachRefLinksToArg(from) // REVISIT: remove
|
|
@@ -154,9 +153,10 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
154
153
|
* @returns {void} This function does not return a value; it mutates the 'arg' object directly.
|
|
155
154
|
*/
|
|
156
155
|
function attachRefLinksToArg(arg, $baseLink = null, expandOrExists = false) {
|
|
157
|
-
const { ref, xpr, args } = arg
|
|
156
|
+
const { ref, xpr, args, list } = arg
|
|
158
157
|
if (xpr) xpr.forEach(t => attachRefLinksToArg(t, $baseLink, expandOrExists))
|
|
159
158
|
if (args) args.forEach(arg => attachRefLinksToArg(arg, $baseLink, expandOrExists))
|
|
159
|
+
if (list) list.forEach(arg => attachRefLinksToArg(arg, $baseLink, expandOrExists))
|
|
160
160
|
if (!ref) return
|
|
161
161
|
init$refLinks(arg)
|
|
162
162
|
ref.forEach((step, i) => {
|
|
@@ -166,7 +166,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
166
166
|
// we need to search for first step in ´model.definitions[infixAlias]`
|
|
167
167
|
if ($baseLink) {
|
|
168
168
|
const { definition } = $baseLink
|
|
169
|
-
const elements = definition.elements || definition.
|
|
169
|
+
const elements = definition._target?.elements || definition.elements
|
|
170
170
|
const e = elements?.[id] || cds.error`"${id}" not found in the elements of "${definition.name}"`
|
|
171
171
|
if (e.target) {
|
|
172
172
|
// only fk access in infix filter
|
|
@@ -177,7 +177,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
177
177
|
`"${e.name}" in path "${arg.ref.map(idOnly).join('.')}" must not be an unmanaged association`,
|
|
178
178
|
)
|
|
179
179
|
// no non-fk traversal in infix filter
|
|
180
|
-
if (!expandOrExists && nextStep && !(nextStep
|
|
180
|
+
if (!expandOrExists && nextStep && !isForeignKeyOf(nextStep, e))
|
|
181
181
|
throw new Error(`Only foreign keys of "${e.name}" can be accessed in infix filter`)
|
|
182
182
|
}
|
|
183
183
|
arg.$refLinks.push({ definition: e, target: definition })
|
|
@@ -460,12 +460,11 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
460
460
|
|
|
461
461
|
function inferQueryElement(column, insertIntoQueryElements = true, $baseLink = null, context) {
|
|
462
462
|
const { inExists, inExpr, inNestedProjection, inCalcElement, baseColumn } = context || {}
|
|
463
|
-
if (column.param) return // parameter references are only resolved into values on execution e.g. :val, :1 or ?
|
|
463
|
+
if (column.param || column.SELECT) return // parameter references are only resolved into values on execution e.g. :val, :1 or ?
|
|
464
464
|
if (column.args) column.args.forEach(arg => inferQueryElement(arg, false, $baseLink, context)) // e.g. function in expression
|
|
465
465
|
if (column.list) column.list.forEach(arg => inferQueryElement(arg, false, $baseLink, context))
|
|
466
466
|
if (column.xpr)
|
|
467
467
|
column.xpr.forEach(token => inferQueryElement(token, false, $baseLink, { ...context, inExpr: true })) // e.g. function in expression
|
|
468
|
-
if (column.SELECT) return
|
|
469
468
|
|
|
470
469
|
if (!column.ref) {
|
|
471
470
|
if (column.expand) queryElements[column.as] = resolveExpand(column)
|
|
@@ -496,7 +495,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
496
495
|
nameSegments.push(id)
|
|
497
496
|
} else if ($baseLink) {
|
|
498
497
|
const { definition, target } = $baseLink
|
|
499
|
-
const elements = definition.elements || definition.
|
|
498
|
+
const elements = definition._target?.elements || definition.elements
|
|
500
499
|
if (elements && id in elements) {
|
|
501
500
|
const element = elements[id]
|
|
502
501
|
rejectNonFkAccess(element)
|
|
@@ -531,7 +530,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
531
530
|
}
|
|
532
531
|
} else {
|
|
533
532
|
const { definition } = column.$refLinks[i - 1]
|
|
534
|
-
const elements = definition.elements || definition._target
|
|
533
|
+
const elements = definition._target?.elements || definition.elements //> go for assoc._target first, instead of assoc as struct
|
|
535
534
|
const element = elements?.[id]
|
|
536
535
|
|
|
537
536
|
if (firstStepIsSelf && element?.isAssociation) {
|
|
@@ -649,25 +648,25 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
649
648
|
}
|
|
650
649
|
|
|
651
650
|
/**
|
|
652
|
-
* Check if the next step in the ref is foreign key of `
|
|
651
|
+
* Check if the next step in the ref is foreign key of `assoc`
|
|
653
652
|
* if not, an error is thrown.
|
|
654
|
-
*
|
|
655
|
-
* @param {CSN.Element}
|
|
653
|
+
*
|
|
654
|
+
* @param {CSN.Element} assoc if this is an association, the next step must be a foreign key of the element.
|
|
656
655
|
*/
|
|
657
|
-
function rejectNonFkAccess(
|
|
658
|
-
if (!inNestedProjection && !inCalcElement &&
|
|
656
|
+
function rejectNonFkAccess(assoc) {
|
|
657
|
+
if (!inNestedProjection && !inCalcElement && assoc.target) {
|
|
659
658
|
// only fk access in infix filter
|
|
660
659
|
const nextStep = column.ref[i + 1]?.id || column.ref[i + 1]
|
|
661
660
|
// no unmanaged assoc in infix filter path
|
|
662
|
-
if (!inExists &&
|
|
661
|
+
if (!inExists && assoc.on)
|
|
663
662
|
throw new Error(
|
|
664
|
-
`"${
|
|
663
|
+
`"${assoc.name}" in path "${column.ref
|
|
665
664
|
.map(idOnly)
|
|
666
665
|
.join('.')}" must not be an unmanaged association`
|
|
667
666
|
)
|
|
668
|
-
// no non-fk traversal in infix filter
|
|
669
|
-
if (nextStep &&
|
|
670
|
-
throw new Error(`Only foreign keys of "${
|
|
667
|
+
// no non-fk traversal in infix filter in non-exists path
|
|
668
|
+
if (nextStep && !assoc.on && !isForeignKeyOf(nextStep, assoc))
|
|
669
|
+
throw new Error(`Only foreign keys of "${assoc.name}" can be accessed in infix filter`)
|
|
671
670
|
}
|
|
672
671
|
}
|
|
673
672
|
})
|
|
@@ -724,7 +723,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
724
723
|
if (inlineCol === '*') {
|
|
725
724
|
const wildCardElements = {}
|
|
726
725
|
// either the `.elements´ of the struct or the `.elements` of the assoc target
|
|
727
|
-
const leafLinkElements = $leafLink.definition.elements || $leafLink.definition.
|
|
726
|
+
const leafLinkElements = $leafLink.definition._target?.elements || $leafLink.definition.elements
|
|
728
727
|
Object.entries(leafLinkElements).forEach(([k, v]) => {
|
|
729
728
|
const name = namePrefix ? `${namePrefix}_${k}` : k
|
|
730
729
|
// if overwritten/excluded omit from wildcard elements
|
|
@@ -1131,6 +1130,20 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1131
1130
|
}, '')
|
|
1132
1131
|
}
|
|
1133
1132
|
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Returns true if e is a foreign key of assoc.
|
|
1135
|
+
* this function is also compatible with unfolded csn (UCSN),
|
|
1136
|
+
* where association do not have foreign keys anymore.
|
|
1137
|
+
*
|
|
1138
|
+
* @param {*} e
|
|
1139
|
+
* @param {*} assoc
|
|
1140
|
+
* @returns
|
|
1141
|
+
*/
|
|
1142
|
+
function isForeignKeyOf(e, assoc) {
|
|
1143
|
+
if(!assoc.isAssociation) return false
|
|
1144
|
+
if(assoc.foreignKeys) return e in assoc.foreignKeys
|
|
1145
|
+
return assoc.elements && e in assoc.elements
|
|
1146
|
+
}
|
|
1134
1147
|
const idOnly = ref => ref.id || ref
|
|
1135
1148
|
|
|
1136
1149
|
module.exports = infer
|
package/lib/infer/join-tree.js
CHANGED
|
@@ -101,9 +101,6 @@ class JoinTree {
|
|
|
101
101
|
Object.entries(sources).forEach(entry => {
|
|
102
102
|
const alias = this.addNextAvailableTableAlias(entry[0])
|
|
103
103
|
this._roots.set(alias, new Root(entry))
|
|
104
|
-
if (entry[1].sources)
|
|
105
|
-
// respect outer aliases
|
|
106
|
-
this.addAliasesOfSubqueryInFrom(entry[1].sources)
|
|
107
104
|
})
|
|
108
105
|
}
|
|
109
106
|
|
|
@@ -200,9 +197,11 @@ class JoinTree {
|
|
|
200
197
|
}
|
|
201
198
|
child.$refLink.alias = this.addNextAvailableTableAlias($refLink.alias, outerQueries)
|
|
202
199
|
}
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
200
|
+
//> REVISIT: remove fallback once UCSN is standard
|
|
201
|
+
const elements =
|
|
202
|
+
node.$refLink?.definition.isAssociation &&
|
|
203
|
+
(node.$refLink.definition.elements || node.$refLink.definition.foreignKeys)
|
|
204
|
+
if (node.$refLink && (!elements || !(child.$refLink.alias in elements)))
|
|
206
205
|
// foreign key access
|
|
207
206
|
node.$refLink.onlyForeignKeyAccess = false
|
|
208
207
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/db-service",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"description": "CDS base database service",
|
|
5
5
|
"homepage": "https://github.com/cap-js/cds-dbs/tree/main/db-service#cds-base-database-service",
|
|
6
6
|
"repository": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"test": "jest --silent"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
|
-
"@sap/cds": ">=7.
|
|
32
|
+
"@sap/cds": ">=7.6"
|
|
33
33
|
},
|
|
34
34
|
"license": "SEE LICENSE",
|
|
35
35
|
"devDependencies": {
|