@cap-js/db-service 1.5.1 → 1.6.0

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,17 @@ 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 uniqueSubqueryAlias = inferred.joinTree.addNextAvailableTableAlias(
920
+ getLastStringSegment(q.SELECT.from.ref[q.SELECT.from.ref.length - 1]),
921
+ originalQuery.outerQueries,
922
+ )
923
+ Object.defineProperty(q.SELECT.from, 'uniqueSubqueryAlias', { value: uniqueSubqueryAlias })
924
+ }
949
925
  }
950
926
 
951
927
  /**
@@ -998,7 +974,11 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
998
974
  * @returns {boolean} true if the element is a managed association and the model is flat
999
975
  */
1000
976
  function isManagedAssocInFlatMode(e) {
1001
- return model.meta.transformation === 'odata' && e.isAssociation && e.keys
977
+ return (
978
+ (model.meta.transformation === 'odata' || model.meta.unfolded?.some(u => u === 'assocs')) &&
979
+ e.isAssociation &&
980
+ e.keys
981
+ )
1002
982
  }
1003
983
  }
1004
984
 
@@ -1051,25 +1031,32 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1051
1031
  if (!column) return column
1052
1032
  if (column.val || column.func || column.SELECT) return [column]
1053
1033
 
1034
+ const structsAreUnfoldedAlready = model.meta.unfolded?.some(u => u === 'structs')
1054
1035
  let { baseName, columnAlias, tableAlias } = names
1055
1036
  const { exclude, replace } = excludeAndReplace || {}
1056
1037
  const { $refLinks, flatName, isJoinRelevant } = column
1057
1038
  let leafAssoc
1058
1039
  let element = $refLinks ? $refLinks[$refLinks.length - 1].definition : column
1059
1040
  if (isWildcard && element.type === 'cds.LargeBinary') return []
1060
- if (element.on) return [] // unmanaged doesn't make it into columns
1041
+ if (element.on && !element.keys) return [] // unmanaged doesn't make it into columns
1061
1042
  else if (element.virtual === true) return []
1062
1043
  else if (!isJoinRelevant && flatName) baseName = flatName
1063
1044
  else if (isJoinRelevant) {
1064
1045
  const leaf = column.$refLinks[column.$refLinks.length - 1]
1065
1046
  leafAssoc = [...column.$refLinks].reverse().find(link => link.definition.isAssociation)
1066
- const { foreignKeys } = leafAssoc.definition
1067
- if (foreignKeys && leaf.alias in foreignKeys) {
1047
+ let elements
1048
+ //> REVISIT: remove once UCSN is standard (no more .foreignKeys)
1049
+ elements = leafAssoc.definition.elements || leafAssoc.definition.foreignKeys
1050
+ if (elements && leaf.alias in elements) {
1068
1051
  element = leafAssoc.definition
1069
1052
  baseName = getFullName(leafAssoc.definition)
1070
1053
  columnAlias = column.ref.slice(0, -1).map(idOnly).join('_')
1071
1054
  } else baseName = getFullName(column.$refLinks[column.$refLinks.length - 1].definition)
1072
- } else baseName = baseName ? `${baseName}_${element.name}` : getFullName(element)
1055
+ } else if (!baseName && structsAreUnfoldedAlready) {
1056
+ baseName = element.name // name is already fully constructed
1057
+ } else {
1058
+ baseName = baseName ? `${baseName}_${element.name}` : getFullName(element)
1059
+ }
1073
1060
 
1074
1061
  // now we have the name of the to be expanded column
1075
1062
  // it could be a structure, an association or a scalar
@@ -1289,7 +1276,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1289
1276
  transformedTokenStream.push({ list: [] })
1290
1277
  }
1291
1278
  } else {
1292
- transformedTokenStream.push({ list: getTransformedTokenStream(token.list) })
1279
+ const { list } = token
1280
+ if (list.every(e => e.val)) // no need for transformation
1281
+ transformedTokenStream.push({ list })
1282
+ else transformedTokenStream.push({ list: getTransformedTokenStream(list, $baseLink) })
1293
1283
  }
1294
1284
  } else if (tokenStream.length === 1 && token.val && $baseLink) {
1295
1285
  // infix filter - OData variant w/o mentioning key --> flatten out and compare each leaf to token.val
@@ -1418,19 +1408,16 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1418
1408
 
1419
1409
  if (flatRhs) {
1420
1410
  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
1411
+ // make sure we can compare both structures
1412
+ if (flatRhs.length !== flatLhs.length) {
1426
1413
  throw new Error(
1427
1414
  `Can't compare "${definition.name}" with "${
1428
1415
  value.$refLinks[value.$refLinks.length - 1].definition.name
1429
1416
  }": the operands must have the same structure`,
1430
1417
  )
1431
- const pathNotFoundErr = []
1418
+ }
1419
+
1432
1420
  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
1421
  while (flatLhs.length > 0) {
1435
1422
  // retrieve and remove one flat element from LHS and search for it in RHS (remove it there too)
1436
1423
  const { ref, _csnPath: lhs_csnPath } = flatLhs.shift()
@@ -1439,20 +1426,16 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1439
1426
  // all following steps must also be part of lhs
1440
1427
  return lhs_csnPath.slice(1).every((val, i) => val === rhs_csnPath[i + 1]) // first step is name of struct -> ignore
1441
1428
  })
1429
+ // not found in rhs --> exit
1442
1430
  if (indexOfElementOnRhs === -1) {
1443
- pathNotFoundErr.push(`Path "${lhs_csnPath.slice(1).join('.')}" not found in "${rhsPath}"`)
1444
- continue
1431
+ const lhsPath = token.ref.join('.')
1432
+ const rhsPath = value.ref.join('.')
1433
+ throw new Error(`Can't compare "${lhsPath}" with "${rhsPath}": the operands must have the same structure`)
1445
1434
  }
1446
1435
  const rhs = flatRhs.splice(indexOfElementOnRhs, 1)[0] // remove the element also from RHS
1447
1436
  result.push({ ref }, ...operator, rhs)
1448
1437
  if (flatLhs.length > 0) result.push(boolOp)
1449
1438
  }
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
1439
  } else {
1457
1440
  // compare with value
1458
1441
  const flatLhs = flattenWithBaseName(token)
@@ -1647,7 +1630,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1647
1630
  * @returns {boolean}
1648
1631
  */
1649
1632
  function isStructured(elt) {
1650
- return Boolean(elt?.elements && elt.kind === 'element')
1633
+ return Boolean(elt?.kind !== 'entity' && elt?.elements && !elt.isAssociation)
1651
1634
  }
1652
1635
 
1653
1636
  /**
@@ -1774,7 +1757,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1774
1757
  else {
1775
1758
  const lhsLeafArt = lhs.ref && lhs.$refLinks[lhs.$refLinks.length - 1].definition
1776
1759
  const rhsLeafArt = rhs.ref && rhs.$refLinks[rhs.$refLinks.length - 1].definition
1777
- if (lhsLeafArt?.target || rhsLeafArt?.target) {
1760
+ if ((lhsLeafArt?.target && rhsLeafArt?.target) || (lhsLeafArt?.elements && rhsLeafArt?.elements)) {
1778
1761
  if (rhs.$refLinks[0].definition !== assocRefLink.definition) {
1779
1762
  rhs.ref.unshift(targetSideRefLink.alias)
1780
1763
  rhs.$refLinks.unshift(targetSideRefLink)
@@ -2020,7 +2003,8 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
2020
2003
  * @returns the flat name of the element
2021
2004
  */
2022
2005
  function getFullName(node, name = node.name) {
2023
- if (node.parent.kind === 'entity') return name
2006
+ // REVISIT: this is an unfortunate implementation
2007
+ if (!node.parent || node.parent.kind === 'entity') return name
2024
2008
 
2025
2009
  return getFullName(node.parent, `${node.parent.name}_${name}`)
2026
2010
  }
@@ -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.0",
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": {