@cap-js/db-service 1.14.1 → 1.15.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/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@
4
4
  - The format is based on [Keep a Changelog](http://keepachangelog.com/).
5
5
  - This project adheres to [Semantic Versioning](http://semver.org/).
6
6
 
7
+ ## [1.15.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.14.1...db-service-v1.15.0) (2024-11-14)
8
+
9
+
10
+ ### Added
11
+
12
+ * enable path expressions in infix filter after `exists` predicate ([#875](https://github.com/cap-js/cds-dbs/issues/875)) ([7e50359](https://github.com/cap-js/cds-dbs/commit/7e5035932ac3bf39f052aa67e1565567e9d6b1ad))
13
+
14
+
15
+ ### Fixed
16
+
17
+ * **`search`:** ignore invalid path expressions inside `@cds.search` ([#849](https://github.com/cap-js/cds-dbs/issues/849)) ([250edd5](https://github.com/cap-js/cds-dbs/commit/250edd5ec9f7ba1d8e40e1330e4b4f9ad9e599b0))
18
+ * nested exists wrapped in xpr ([7e50359](https://github.com/cap-js/cds-dbs/commit/7e5035932ac3bf39f052aa67e1565567e9d6b1ad))
19
+
7
20
  ## [1.14.1](https://github.com/cap-js/cds-dbs/compare/db-service-v1.14.0...db-service-v1.14.1) (2024-10-28)
8
21
 
9
22
 
package/lib/cqn4sql.js CHANGED
@@ -266,10 +266,10 @@ function cqn4sql(originalQuery, model) {
266
266
  const id = localized(r.queryArtifact)
267
267
  args.push({ ref: [r.args ? { id, args: r.args } : id], as: r.alias })
268
268
  }
269
- from = { join: 'left', args, on: [] }
269
+ from = { join: r.join || 'left', args, on: [] }
270
270
  r.children.forEach(c => {
271
271
  from = joinForBranch(from, c)
272
- from = { join: 'left', args: [from], on: [] }
272
+ from = { join: c.join || 'left', args: [from], on: [] }
273
273
  })
274
274
  })
275
275
  return from.args.length > 1 ? from : from.args[0]
@@ -309,7 +309,7 @@ function cqn4sql(originalQuery, model) {
309
309
  }
310
310
  if (node.children) {
311
311
  node.children.forEach(c => {
312
- lhs = { join: 'left', args: [lhs], on: [] }
312
+ lhs = { join: c.join || 'left', args: [lhs], on: [] }
313
313
  lhs = joinForBranch(lhs, c)
314
314
  })
315
315
  }
@@ -2093,11 +2093,6 @@ function cqn4sql(originalQuery, model) {
2093
2093
  const unmanagedOn = onCondFor(inWhere ? next : current, inWhere ? current : next, inWhere)
2094
2094
  on.push(...(customWhere && hasLogicalOr(unmanagedOn) ? [asXpr(unmanagedOn)] : unmanagedOn))
2095
2095
  }
2096
- // infix filter conditions are wrapped in `xpr` when added to the on-condition
2097
- if (customWhere) {
2098
- const filter = getTransformedTokenStream(customWhere, next)
2099
- on.push(...['and', ...(hasLogicalOr(filter) ? [asXpr(filter)] : filter)])
2100
- }
2101
2096
 
2102
2097
  const subquerySource = assocTarget(nextDefinition) || nextDefinition
2103
2098
  const id = localized(subquerySource)
@@ -2115,6 +2110,26 @@ function cqn4sql(originalQuery, model) {
2115
2110
  ],
2116
2111
  where: on,
2117
2112
  }
2113
+ if (next.pathExpressionInsideFilter) {
2114
+ SELECT.where = customWhere
2115
+ const transformedExists = transformSubquery({ SELECT })
2116
+ // infix filter conditions are wrapped in `xpr` when added to the on-condition
2117
+ if (transformedExists.SELECT.where) {
2118
+ on.push(
2119
+ ...[
2120
+ 'and',
2121
+ ...(hasLogicalOr(transformedExists.SELECT.where)
2122
+ ? [asXpr(transformedExists.SELECT.where)]
2123
+ : transformedExists.SELECT.where),
2124
+ ],
2125
+ )
2126
+ }
2127
+ transformedExists.SELECT.where = on
2128
+ return transformedExists.SELECT
2129
+ } else if (customWhere) {
2130
+ const filter = getTransformedTokenStream(customWhere, next)
2131
+ on.push(...['and', ...(hasLogicalOr(filter) ? [asXpr(filter)] : filter)])
2132
+ }
2118
2133
  return SELECT
2119
2134
  }
2120
2135
 
@@ -184,16 +184,13 @@ function infer(originalQuery, model) {
184
184
  if (e.target) {
185
185
  // only fk access in infix filter
186
186
  const nextStep = ref[1]?.id || ref[1]
187
- // no unmanaged assoc in infix filter path
188
- if (!expandOrExists && e.on) {
189
- const err = `Unexpected unmanaged association “${e.name}” in filter expression of “${$baseLink.definition.name}”`
190
- throw new Error(err)
187
+ if (isNonForeignKeyNavigation(e, nextStep)) {
188
+ if (expandOrExists) {
189
+ Object.defineProperty($baseLink, 'pathExpressionInsideFilter', { value: true })
190
+ } else {
191
+ rejectNonFkNavigation(e, e.on ? $baseLink.definition.name : nextStep)
192
+ }
191
193
  }
192
- // no non-fk traversal in infix filter
193
- if (!expandOrExists && nextStep && !isForeignKeyOf(nextStep, e))
194
- throw new Error(
195
- `Only foreign keys of “${e.name}” can be accessed in infix filter, but found “${nextStep}”`,
196
- )
197
194
  }
198
195
  arg.$refLinks.push({ definition: e, target: definition })
199
196
  // filter paths are flattened
@@ -226,7 +223,7 @@ function infer(originalQuery, model) {
226
223
  // don't miss an exists within an expression
227
224
  token.xpr.forEach(walkTokenStream)
228
225
  } else {
229
- attachRefLinksToArg(token, arg.$refLinks[i], existsPredicate)
226
+ attachRefLinksToArg(token, arg.$refLinks[i], existsPredicate || expandOrExists)
230
227
  existsPredicate = false
231
228
  }
232
229
  }
@@ -235,6 +232,7 @@ function infer(originalQuery, model) {
235
232
  }
236
233
  i += 1
237
234
  }
235
+ if ($baseLink?.pathExpressionInsideFilter) Object.defineProperty(arg, 'join', { value: 'inner' })
238
236
  const { definition, target } = arg.$refLinks[arg.$refLinks.length - 1]
239
237
  if (definition.value) {
240
238
  // nested calculated element
@@ -542,9 +540,19 @@ function infer(originalQuery, model) {
542
540
  const elements = getDefinition(definition.target)?.elements || definition.elements
543
541
  if (elements && id in elements) {
544
542
  const element = elements[id]
545
- rejectNonFkAccess(element)
543
+ if (inInfixFilter) {
544
+ const nextStep = column.ref[1]?.id || column.ref[1]
545
+ if (isNonForeignKeyNavigation(element, nextStep)) {
546
+ if (inExists) {
547
+ Object.defineProperty($baseLink, 'pathExpressionInsideFilter', { value: true })
548
+ } else {
549
+ rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep)
550
+ }
551
+ }
552
+ }
546
553
  const resolvableIn = getDefinition(definition.target) || target
547
- column.$refLinks.push({ definition: elements[id], target: resolvableIn })
554
+ const $refLink = { definition: elements[id], target: resolvableIn }
555
+ column.$refLinks.push($refLink)
548
556
  } else {
549
557
  stepNotFoundInPredecessor(id, definition.name)
550
558
  }
@@ -593,7 +601,16 @@ function infer(originalQuery, model) {
593
601
 
594
602
  const target = getDefinition(definition.target) || column.$refLinks[i - 1].target
595
603
  if (element) {
596
- if ($baseLink) rejectNonFkAccess(element)
604
+ if ($baseLink && inInfixFilter) {
605
+ const nextStep = column.ref[i + 1]?.id || column.ref[i + 1]
606
+ if (isNonForeignKeyNavigation(element, nextStep)) {
607
+ if (inExists) {
608
+ Object.defineProperty($baseLink, 'pathExpressionInsideFilter', { value: true })
609
+ } else {
610
+ rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep)
611
+ }
612
+ }
613
+ }
597
614
  const $refLink = { definition: elements[id], target }
598
615
  column.$refLinks.push($refLink)
599
616
  } else if (firstStepIsSelf) {
@@ -637,7 +654,7 @@ function infer(originalQuery, model) {
637
654
  skipJoinsForFilter = true
638
655
  } else if (token.ref || token.xpr) {
639
656
  inferQueryElement(token, false, column.$refLinks[i], {
640
- inExists: skipJoinsForFilter,
657
+ inExists: skipJoinsForFilter || inExists,
641
658
  inExpr: !!token.xpr,
642
659
  inInfixFilter: true,
643
660
  })
@@ -646,7 +663,7 @@ function infer(originalQuery, model) {
646
663
  applyToFunctionArgs(token.args, inferQueryElement, [
647
664
  false,
648
665
  column.$refLinks[i],
649
- { inExists: skipJoinsForFilter, inExpr: true, inInfixFilter: true },
666
+ { inExists: skipJoinsForFilter || inExists, inExpr: true, inInfixFilter: true },
650
667
  ])
651
668
  }
652
669
  }
@@ -700,31 +717,11 @@ function infer(originalQuery, model) {
700
717
  }
701
718
  }
702
719
  }
703
-
704
- /**
705
- * Check if the next step in the ref is foreign key of `assoc`
706
- * if not, an error is thrown.
707
- *
708
- * @param {CSN.Element} assoc if this is an association, the next step must be a foreign key of the element.
709
- */
710
- function rejectNonFkAccess(assoc) {
711
- if (inInfixFilter && assoc.target) {
712
- // only fk access in infix filter
713
- const nextStep = column.ref[i + 1]?.id || column.ref[i + 1]
714
- // no unmanaged assoc in infix filter path
715
- if (!inExists && assoc.on) {
716
- const err = `Unexpected unmanaged association “${assoc.name}” in filter expression of “${$baseLink.definition.name}”`
717
- throw new Error(err)
718
- }
719
- // no non-fk traversal in infix filter in non-exists path
720
- if (nextStep && !assoc.on && !isForeignKeyOf(nextStep, assoc))
721
- throw new Error(
722
- `Only foreign keys of “${assoc.name}” can be accessed in infix filter, but found “${nextStep}”`,
723
- )
724
- }
725
- }
726
720
  })
727
721
 
722
+ // we need inner joins for the path expressions inside filter expressions after exists predicate
723
+ if ($baseLink?.pathExpressionInsideFilter) Object.defineProperty(column, 'join', { value: 'inner' })
724
+
728
725
  // ignore whole expand if target of assoc along path has ”@cds.persistence.skip”
729
726
  if (column.expand) {
730
727
  const { $refLinks } = column
@@ -1214,6 +1211,26 @@ function infer(originalQuery, model) {
1214
1211
  }
1215
1212
  }
1216
1213
 
1214
+ /**
1215
+ * Determines if a given association is a non-foreign key navigation.
1216
+ *
1217
+ * @param {Object} assoc - The association.
1218
+ * @param {Object} nextStep - The next step in the navigation path.
1219
+ * @returns {boolean} - Returns true if the next step is a non-foreign key navigation, otherwise false.
1220
+ */
1221
+ function isNonForeignKeyNavigation(assoc, nextStep) {
1222
+ if (!nextStep || !assoc.target) return false
1223
+
1224
+ return assoc.on || !isForeignKeyOf(nextStep, assoc)
1225
+ }
1226
+
1227
+ function rejectNonFkNavigation(assoc, additionalInfo) {
1228
+ if (assoc.on) {
1229
+ throw new Error(`Unexpected unmanaged association “${assoc.name}” in filter expression of “${additionalInfo}”`)
1230
+ }
1231
+ throw new Error(`Only foreign keys of “${assoc.name}” can be accessed in infix filter, but found “${additionalInfo}”`)
1232
+ }
1233
+
1217
1234
  /**
1218
1235
  * Returns true if e is a foreign key of assoc.
1219
1236
  * this function is also compatible with unfolded csn (UCSN),
@@ -181,6 +181,7 @@ class JoinTree {
181
181
  // if no root node was found, the column is selected from a subquery
182
182
  if (!node) return
183
183
  while (i < col.ref.length) {
184
+ if(col.join === 'inner') node.join = 'inner'
184
185
  const step = col.ref[i]
185
186
  const { where, args } = step
186
187
  const id = joinId(step, args, where)
package/lib/search.js CHANGED
@@ -114,7 +114,7 @@ const _getSearchableColumns = entity => {
114
114
  deepSearchCandidates.forEach(c => {
115
115
  const element = c.ref.reduce((resolveIn, curr, i) => {
116
116
  const next = resolveIn.elements?.[curr] || resolveIn._target.elements[curr]
117
- if (next.isAssociation && !c.ref[i + 1]) {
117
+ if (next?.isAssociation && !c.ref[i + 1]) {
118
118
  const searchInTarget = _getSearchableColumns(next._target)
119
119
  searchInTarget.forEach(elementRefInTarget => {
120
120
  searchableColumns.push({ ref: c.ref.concat(...elementRefInTarget.ref) })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/db-service",
3
- "version": "1.14.1",
3
+ "version": "1.15.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": {