@cap-js/db-service 2.8.2 → 2.10.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.
@@ -4,14 +4,14 @@ const cds = require('@sap/cds')
4
4
 
5
5
  const JoinTree = require('./join-tree')
6
6
  const { pseudos } = require('./pseudos')
7
- const { isCalculatedOnRead, getImplicitAlias, getModelUtils, defineProperty, hasOwnSkip } = require('../utils')
7
+ const { isCalculatedOnRead, getImplicitAlias, getModelUtils, defineProperty, hasOwnSkip, isRuntimeView } = require('../utils')
8
8
  const cdsTypes = cds.builtin.types
9
9
  /**
10
10
  * @param {import('@sap/cds/apis/cqn').Query|string} originalQuery
11
11
  * @param {import('@sap/cds/apis/csn').CSN} [model]
12
12
  * @returns {import('./cqn').Query} = q with .target and .elements
13
13
  */
14
- function infer(originalQuery, model) {
14
+ function infer(originalQuery, model, useTechnicalAlias = true) {
15
15
  if (!model) throw new Error('Please specify a model')
16
16
  const inferred = originalQuery
17
17
 
@@ -34,7 +34,7 @@ function infer(originalQuery, model) {
34
34
 
35
35
  let $combinedElements
36
36
 
37
- const sources = inferTarget(_.into || _.from || _.entity, {}) // IMPORTANT: _.into has to go before _.from for INSERT.into().from(SELECT)
37
+ const sources = inferTarget(_.into || _.from || _.entity, {}, useTechnicalAlias) // IMPORTANT: _.into has to go before _.from for INSERT.into().from(SELECT)
38
38
  const aliases = Object.keys(sources)
39
39
  const target = aliases.length === 1 ? getDefinitionFromSources(sources, aliases[0]) : originalQuery
40
40
  Object.defineProperties(inferred, {
@@ -80,7 +80,7 @@ function infer(originalQuery, model) {
80
80
  * Each key is a query source alias, and its value is the corresponding CSN Definition.
81
81
  * @returns {object} The updated `querySources` object with inferred sources from the `from` clause.
82
82
  */
83
- function inferTarget(from, querySources, useTechnicalAlias = true) {
83
+ function inferTarget(from, querySources, useTechnicalAlias) {
84
84
  const { ref } = from
85
85
  // Given a from clause `Root:parent[$main.name = name].parent as Foo`
86
86
  // we need to first resolve until to the last step of the from.ref
@@ -191,6 +191,7 @@ function infer(originalQuery, model) {
191
191
  const dollarSelfRefs = []
192
192
  columns.forEach(col => {
193
193
  if (col === '*') {
194
+ if (wildcardSelect) throw new Error('Duplicate wildcard "*" in column list')
194
195
  wildcardSelect = true
195
196
  } else if (col.val !== undefined || col.xpr || col.SELECT || col.func || col.param) {
196
197
  const as = col.as || col.func || col.val
@@ -459,10 +460,10 @@ function infer(originalQuery, model) {
459
460
  const element = elements[id]
460
461
  if (inInfixFilter) {
461
462
  const nextStep = arg.ref[1]?.id || arg.ref[1]
462
- if (isNonForeignKeyNavigation(element, nextStep)) {
463
+ if (isNonForeignKeyNavigation(element, nextStep) || arg.ref[0]?.where) {
463
464
  if (inExists) {
464
465
  defineProperty($baseLink, 'pathExpressionInsideFilter', true)
465
- } else {
466
+ } else if (!inFrom) {
466
467
  rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep)
467
468
  }
468
469
  }
@@ -523,10 +524,10 @@ function infer(originalQuery, model) {
523
524
  if (element) {
524
525
  if ($baseLink && inInfixFilter) {
525
526
  const nextStep = arg.ref[i + 1]?.id || arg.ref[i + 1]
526
- if (isNonForeignKeyNavigation(element, nextStep)) {
527
+ if (isNonForeignKeyNavigation(element, nextStep) || arg.ref[i-1]?.where) {
527
528
  if (inExists) {
528
529
  defineProperty($baseLink, 'pathExpressionInsideFilter', true)
529
- } else {
530
+ } else if (!inFrom) {
530
531
  rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep)
531
532
  }
532
533
  }
@@ -565,7 +566,7 @@ function infer(originalQuery, model) {
565
566
  if (step.where) {
566
567
  const danglingFilter = !(arg.ref[i + 1] || arg.expand || arg.inline || inExists)
567
568
  const definition = arg.$refLinks[i].definition
568
- if ((!definition.target && definition.kind !== 'entity') || (!inFrom && danglingFilter))
569
+ if ((!definition.target && definition.kind !== 'entity') || (!inFrom && !inCalcElement && danglingFilter))
569
570
  throw new Error('A filter can only be provided when navigating along associations')
570
571
  if (!inFrom && !arg.expand)defineProperty(arg, 'isJoinRelevant', true)
571
572
  let skipJoinsForFilter = false
@@ -574,9 +575,11 @@ function infer(originalQuery, model) {
574
575
  // books[exists genre[code='A']].title --> column is join relevant but inner exists filter is not
575
576
  skipJoinsForFilter = true
576
577
  } else if (token.ref || token.xpr || token.list) {
578
+ // For scoped queries (non-dangling filters in FROM), treat filter contents as EXISTS context
579
+ // because they will become part of an EXISTS subquery
577
580
  inferArg(token, false, arg.$refLinks[i], {
578
581
  ...context,
579
- inExists: skipJoinsForFilter || inExists,
582
+ inExists: skipJoinsForFilter || inExists || (inFrom && !danglingFilter),
580
583
  inXpr: !!token.xpr,
581
584
  inInfixFilter: true,
582
585
  inFrom,
@@ -586,7 +589,7 @@ function infer(originalQuery, model) {
586
589
  applyToFunctionArgs(token.args, inferArg, [
587
590
  false,
588
591
  arg.$refLinks[i],
589
- { inExists: skipJoinsForFilter || inExists, inXpr: true, inInfixFilter: true, inFrom },
592
+ { inExists: skipJoinsForFilter || inExists || (inFrom && !danglingFilter), inXpr: true, inInfixFilter: true, inFrom },
590
593
  ])
591
594
  }
592
595
  }
@@ -595,7 +598,8 @@ function infer(originalQuery, model) {
595
598
 
596
599
  if(!arg.$refLinks[i].$main)
597
600
  arg.$refLinks[i].alias = !arg.ref[i + 1] && arg.as ? arg.as : id.split('.').pop()
598
- if (hasOwnSkip(getDefinition(arg.$refLinks[i].definition.target))) isPersisted = false
601
+ const def = getDefinition(arg.$refLinks[i].definition.target)
602
+ if (hasOwnSkip(def) && !isRuntimeView(def)) isPersisted = false
599
603
  if (!arg.ref[i + 1]) {
600
604
  const flatName = nameSegments.join('_')
601
605
  defineProperty(arg, 'flatName', flatName)
@@ -655,7 +659,11 @@ function infer(originalQuery, model) {
655
659
  // ignore whole expand if target of assoc along path has ”@cds.persistence.skip”
656
660
  if (arg.expand) {
657
661
  const { $refLinks } = arg
658
- const skip = $refLinks.some(link => hasOwnSkip(getDefinition(link.definition.target)))
662
+
663
+ const skip = $refLinks.some(link => {
664
+ const def = getDefinition(link.definition.target)
665
+ return hasOwnSkip(def) && !isRuntimeView(def)
666
+ })
659
667
  if (skip) {
660
668
  $refLinks[$refLinks.length - 1].skipExpand = true
661
669
  return
@@ -708,24 +716,65 @@ function infer(originalQuery, model) {
708
716
  )
709
717
  }
710
718
  let elements = {}
719
+ let seenWildcard = false
711
720
  inline.forEach(inlineCol => {
712
721
  inferArg(inlineCol, null, $leafLink, { inXpr: true, baseColumn: col })
713
722
  if (inlineCol === '*') {
723
+ if (seenWildcard) throw new Error(`Duplicate wildcard "*" in inline of "${col.as || col.ref.map(idOnly).join('_')}"`)
724
+ seenWildcard = true
714
725
  const wildCardElements = {}
715
726
  // either the `.elements´ of the struct or the `.elements` of the assoc target
716
- const leafLinkElements = getDefinition($leafLink.definition.target)?.elements || $leafLink.definition.elements
727
+ const targetDef = getDefinition($leafLink.definition.target)
728
+ const leafLinkElements = targetDef?.elements || $leafLink.definition.elements
729
+ const isAssociation = !!$leafLink.definition.target
730
+
731
+ const deferredCalcElements = []
717
732
  Object.entries(leafLinkElements).forEach(([k, v]) => {
718
733
  const name = namePrefix ? `${namePrefix}_${k}` : k
719
734
  // if overwritten/excluded omit from wildcard elements
720
735
  // in elements the names are already flat so consider the prefix
721
736
  // in excluding, the elements are addressed without the prefix
722
- if (!(name in elements || col.excluding?.includes(k))) wildCardElements[name] = v
737
+ if (!(name in elements || col.excluding?.includes(k))) {
738
+ wildCardElements[name] = v
739
+
740
+ if(v.value) {
741
+ // defer linkCalculatedElement calls until after all association joins are registered
742
+ // so that the join tree order is correct
743
+ deferredCalcElements.push({ k, v })
744
+ }
745
+ else if (isAssociation && !v.virtual && v.type !== 'cds.LargeBinary' && !(v.on && !v.keys)) {
746
+ // Check if this element is a foreign key (FK elements don't need join)
747
+ const isFK = $leafLink.definition.keys?.some(key => key.ref[0] === k)
748
+ if (!isFK) {
749
+ // Create a fake column with ref [<inlined assoc>, <element name>] and proper $refLinks
750
+ const fakeCol = {
751
+ ref: [...col.ref, k],
752
+ }
753
+ // Copy $refLinks and add new link for the target element with proper alias
754
+ const fakeRefLinks = [
755
+ ...$refLinks,
756
+ { definition: v, target: targetDef, alias: k }
757
+ ]
758
+ defineProperty(fakeCol, '$refLinks', fakeRefLinks)
759
+ defineProperty(fakeCol, 'isJoinRelevant', true)
760
+ // Merge into join tree
761
+ inferred.joinTree.mergeColumn(fakeCol, originalQuery.outerQueries)
762
+ }
763
+ }
764
+ }
723
765
  })
766
+ // link calculated elements after association joins are registered in the join tree
767
+ for (const { k, v } of deferredCalcElements) {
768
+ linkCalculatedElement(
769
+ { ref: [k], $refLinks: [{ definition: v, target: targetDef }] },
770
+ $leafLink,
771
+ )
772
+ }
724
773
  elements = { ...elements, ...wildCardElements }
725
774
  } else {
726
775
  const nameParts = namePrefix ? [namePrefix] : []
727
776
  if (inlineCol.as) nameParts.push(inlineCol.as)
728
- else nameParts.push(...inlineCol.ref.map(idOnly))
777
+ else if (inlineCol.ref) nameParts.push(...inlineCol.ref.map(idOnly))
729
778
  const name = nameParts.join('_')
730
779
  if (inlineCol.inline) {
731
780
  const inlineElements = resolveInline(inlineCol, name)
@@ -737,6 +786,8 @@ function infer(originalQuery, model) {
737
786
  elements[name] = getCdsTypeForVal(inlineCol.val)
738
787
  } else if (inlineCol.func) {
739
788
  elements[name] = {}
789
+ } else if (inlineCol.xpr) {
790
+ elements[name] = {}
740
791
  } else {
741
792
  elements[name] = inlineCol.$refLinks[inlineCol.$refLinks.length - 1].definition
742
793
  }
@@ -764,6 +815,16 @@ function infer(originalQuery, model) {
764
815
  `Unexpected “expand” on “${col.ref.map(idOnly)}”; can only be used after a reference to a structure, association or table alias`,
765
816
  )
766
817
  }
818
+ // Check for duplicate wildcards before creating the subquery
819
+ let seenWildcard = false
820
+ for (const e of expand) {
821
+ if (e === '*') {
822
+ if (seenWildcard) {
823
+ throw new Error(`Duplicate wildcard "*" in expand of "${col.as || col.ref.map(idOnly).join('_')}"`)
824
+ }
825
+ seenWildcard = true
826
+ }
827
+ }
767
828
  const target = getDefinition($leafLink.definition.target)
768
829
  if (target) {
769
830
  const expandSubquery = {
@@ -133,20 +133,22 @@ class JoinTree {
133
133
  *
134
134
  * @param {string} alias - The original alias name.
135
135
  * @param {unknown[]} outerQueries - An array of outer queries.
136
+ * @param {string} key - The key to be used for storing the alias in the map. If not provided, the upper-case version of the alias will be used as the key.
136
137
  * @returns {string} - The next unambiguous table alias.
137
138
  */
138
- addNextAvailableTableAlias(alias, outerQueries) {
139
+ addNextAvailableTableAlias(alias, outerQueries, key) {
139
140
  const upperAlias = alias.toUpperCase()
140
- if (this._queryAliases.get(upperAlias) || outerQueries?.some(outer => outerHasAlias(outer))) {
141
+ if (this._queryAliases.get(upperAlias) || outerQueries?.some(outer => outerHasAlias(outer, key))) {
141
142
  let j = 2
142
- while (this._queryAliases.get(upperAlias + j) || outerQueries?.some(outer => outerHasAlias(outer, j))) j += 1
143
+ while (this._queryAliases.get(upperAlias + j) || outerQueries?.some(outer => outerHasAlias(outer, key, j))) j += 1
143
144
  alias += j
144
145
  }
145
- this._queryAliases.set(alias.toUpperCase(), alias)
146
+ this._queryAliases.set(key || alias.toUpperCase(), alias)
146
147
  return alias
147
148
 
148
- function outerHasAlias(outer, number) {
149
- return outer.joinTree._queryAliases.get(number ? upperAlias + number : upperAlias)
149
+ function outerHasAlias(outer, searchInValues = false, number) {
150
+ const currAlias = number ? upperAlias + number : upperAlias
151
+ return searchInValues ? Array.from(outer.joinTree._queryAliases.values()).includes(currAlias) : outer.joinTree._queryAliases.get(currAlias)
150
152
  }
151
153
  }
152
154
 
package/lib/utils.js CHANGED
@@ -1,5 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ const cds = require('@sap/cds')
4
+
3
5
  /**
4
6
  * Formats a ref array into a string representation.
5
7
  * If the first step is an entity, the separator is a colon, otherwise a dot.
@@ -27,6 +29,25 @@ function hasOwnSkip(definition) {
27
29
  )
28
30
  }
29
31
 
32
+ function isRuntimeView(definition) {
33
+ if (!definition || !cds.env.features.runtime_views) return false
34
+ if (definition['_isRuntimeView']) return true
35
+ if (!definition['@cds.persistence.skip']) {
36
+ Object.defineProperty(definition, '_isRuntimeView', {
37
+ value: true,
38
+ writable: false,
39
+ configurable: true,
40
+ enumerable: false
41
+ })
42
+ return true
43
+ }
44
+ // views with "as select from" variant are also runtime views, even if they are annotated with persistence skip
45
+ if (definition.query && !definition.query._target) return true
46
+ if (definition.query) return isRuntimeView(definition.query._target)
47
+
48
+ return false
49
+ }
50
+
30
51
  /**
31
52
  * Determines if a definition is calculated on read.
32
53
  * - Stored calculated elements are not unfolded
@@ -136,6 +157,12 @@ function getModelUtils(model, query) {
136
157
  }
137
158
  }
138
159
 
160
+ function resolveTable(target) {
161
+ if (target.query?._target && !Object.prototype.hasOwnProperty.call(target, '@cds.persistence.table'))
162
+ return resolveTable(target.query._target)
163
+ return target
164
+ }
165
+
139
166
  // export the function to be used in other modules
140
167
  module.exports = {
141
168
  prettyPrintRef,
@@ -145,4 +172,6 @@ module.exports = {
145
172
  defineProperty,
146
173
  getModelUtils,
147
174
  hasOwnSkip,
175
+ isRuntimeView,
176
+ resolveTable
148
177
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/db-service",
3
- "version": "2.8.2",
3
+ "version": "2.10.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": {
@@ -27,7 +27,7 @@
27
27
  "generic-pool": "^3.9.0"
28
28
  },
29
29
  "peerDependencies": {
30
- "@sap/cds": ">=9.4.5"
30
+ "@sap/cds": ">=9.8"
31
31
  },
32
32
  "license": "Apache-2.0"
33
33
  }