@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/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
- if (!inferred.STREAM?.from && inferred.STREAM?.into) {
58
- transformedQuery = transformStreamQuery()
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
- const transformedProp = { __proto__: queryProp } // IMPORTANT: don't lose anything you might not know of
60
+ const transformedProp = { __proto__: queryProp } // IMPORTANT: don't lose anything you might not know of
64
61
 
65
- // Transform the existing where, prepend table aliases, and so on...
66
- if (where) {
67
- transformedProp.where = getTransformedTokenStream(where)
68
- }
62
+ // Transform the existing where, prepend table aliases, and so on...
63
+ if (where) {
64
+ transformedProp.where = getTransformedTokenStream(where)
65
+ }
69
66
 
70
- // Transform the from clause: association path steps turn into `WHERE EXISTS` subqueries.
71
- // The already transformed `where` clause is then glued together with the resulting subqueries.
72
- const { transformedWhere, transformedFrom } = getTransformedFrom(from || entity, transformedProp.where)
73
- const queryNeedsJoins = inferred.joinTree && !inferred.joinTree.isInitial
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
- if (inferred.SELECT) {
76
- transformedQuery = transformSelectQuery(queryProp, transformedFrom, transformedWhere, transformedQuery)
77
- } else {
78
- if (from) {
79
- transformedProp.from = transformedFrom
80
- } else if (!queryNeedsJoins) {
81
- transformedProp.entity = transformedFrom
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
- if (transformedWhere?.length > 0) {
85
- transformedProp.where = transformedWhere
86
- }
81
+ if (transformedWhere?.length > 0) {
82
+ transformedProp.where = transformedWhere
83
+ }
87
84
 
88
- transformedQuery[kind] = transformedProp
85
+ transformedQuery[kind] = transformedProp
89
86
 
90
- if (inferred.UPDATE?.with) {
91
- Object.entries(inferred.UPDATE.with).forEach(([key, val]) => {
92
- const transformed = getTransformedTokenStream([val])
93
- inferred.UPDATE.with[key] = transformed[0]
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
- if (queryNeedsJoins) {
99
- if (inferred.UPDATE || inferred.DELETE) {
100
- const prop = inferred.UPDATE ? 'UPDATE' : 'DELETE'
101
- const subquery = {
102
- SELECT: {
103
- from: { ...transformedFrom },
104
- columns: [], // primary keys of the query target will be added later
105
- where: [...transformedProp.where],
106
- },
107
- }
108
- // The alias of the original query is now the alias for the subquery
109
- // so that potential references in the where clause to the alias match.
110
- // Hence, replace the alias of the original query with the next
111
- // available alias, so that each alias is unique.
112
- const uniqueSubqueryAlias = getNextAvailableTableAlias(transformedFrom.as)
113
- transformedFrom.as = uniqueSubqueryAlias
114
-
115
- // calculate the primary keys of the target entity, there is always exactly
116
- // one query source for UPDATE / DELETE
117
- const queryTarget = Object.values(originalQuery.sources)[0]
118
- const keys = Object.values(queryTarget.elements).filter(e => e.key === true)
119
- const primaryKey = { list: [] }
120
- keys.forEach(k => {
121
- // cqn4sql will add the table alias to the column later, no need to add it here
122
- subquery.SELECT.columns.push({ ref: [k.name] })
123
-
124
- // add the alias of the main query to the list of primary key references
125
- primaryKey.list.push({ ref: [transformedFrom.as, k.name] })
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
- const transformedSubquery = cqn4sql(subquery)
125
+ const transformedSubquery = cqn4sql(subquery)
129
126
 
130
- // replace where condition of original query with the transformed subquery
131
- // correlate UPDATE / DELETE query with subquery by primary key matches
132
- transformedQuery[prop].where = [primaryKey, 'in', transformedSubquery]
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
- if (prop === 'UPDATE') transformedQuery.UPDATE.entity = transformedFrom
135
- else transformedQuery.DELETE.from = transformedFrom
136
- } else {
137
- transformedQuery[kind].from = translateAssocsToJoins(transformedQuery[kind].from)
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.ref[0] === tableAlias ? 1 : 0).join('_')
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 model.meta.transformation === 'odata' && e.isAssociation && e.keys
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
- const { foreignKeys } = leafAssoc.definition
1067
- if (foreignKeys && leaf.alias in foreignKeys) {
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 = baseName ? `${baseName}_${element.name}` : getFullName(element)
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
- transformedTokenStream.push({ list: getTransformedTokenStream(token.list) })
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
- //REVISIT: Early exit here? We kndow we cant compare the structs, however we do not know exactly why
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
- const pathNotFoundErr = []
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
- pathNotFoundErr.push(`Path "${lhs_csnPath.slice(1).join('.')}" not found in "${rhsPath}"`)
1444
- continue
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.kind === 'element')
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 || rhsLeafArt?.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
- if (node.parent.kind === 'entity') return name
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
  }
@@ -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
- if (keys[key].type === 'cds.UUID' && !data[key] && event === 'CREATE') {
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
  }
@@ -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
@@ -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 || originalQuery.STREAM) {
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]?.elements ? d.elements[r.id || r] : d.elements[r.id || r]?._target
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._isAssociation)
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._target?.elements
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 in e.foreignKeys))
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._target?.elements
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?.elements
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 `element`
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} element if this is an association, the next step must be a foreign key of the 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(element) {
658
- if (!inNestedProjection && !inCalcElement && element.target) {
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 && element.on)
661
+ if (!inExists && assoc.on)
663
662
  throw new Error(
664
- `"${element.name}" in path "${column.ref
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 && element.foreignKeys && !(nextStep in element.foreignKeys))
670
- throw new Error(`Only foreign keys of "${element.name}" can be accessed in infix filter`)
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._target.elements
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
@@ -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 foreignKeys = node.$refLink?.definition.foreignKeys
205
- if (node.$refLink && (!foreignKeys || !(child.$refLink.alias in foreignKeys)))
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.5.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.1.1"
32
+ "@sap/cds": ">=7.6"
33
33
  },
34
34
  "license": "SEE LICENSE",
35
35
  "devDependencies": {