@cap-js/db-service 2.8.1 → 2.9.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,20 +4,8 @@ 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')
8
- const cdsTypes = cds.linked({
9
- definitions: {
10
- Timestamp: { type: 'cds.Timestamp' },
11
- DateTime: { type: 'cds.DateTime' },
12
- Date: { type: 'cds.Date' },
13
- Time: { type: 'cds.Time' },
14
- String: { type: 'cds.String' },
15
- Decimal: { type: 'cds.Decimal' },
16
- Integer: { type: 'cds.Integer' },
17
- Boolean: { type: 'cds.Boolean' },
18
- },
19
- }).definitions
20
- for (const each in cdsTypes) cdsTypes[`cds.${each}`] = cdsTypes[each]
7
+ const { isCalculatedOnRead, getImplicitAlias, getModelUtils, defineProperty, hasOwnSkip, isRuntimeView } = require('../utils')
8
+ const cdsTypes = cds.builtin.types
21
9
  /**
22
10
  * @param {import('@sap/cds/apis/cqn').Query|string} originalQuery
23
11
  * @param {import('@sap/cds/apis/csn').CSN} [model]
@@ -203,6 +191,7 @@ function infer(originalQuery, model) {
203
191
  const dollarSelfRefs = []
204
192
  columns.forEach(col => {
205
193
  if (col === '*') {
194
+ if (wildcardSelect) throw new Error('Duplicate wildcard "*" in column list')
206
195
  wildcardSelect = true
207
196
  } else if (col.val !== undefined || col.xpr || col.SELECT || col.func || col.param) {
208
197
  const as = col.as || col.func || col.val
@@ -471,10 +460,10 @@ function infer(originalQuery, model) {
471
460
  const element = elements[id]
472
461
  if (inInfixFilter) {
473
462
  const nextStep = arg.ref[1]?.id || arg.ref[1]
474
- if (isNonForeignKeyNavigation(element, nextStep)) {
463
+ if (isNonForeignKeyNavigation(element, nextStep) || arg.ref[0]?.where) {
475
464
  if (inExists) {
476
465
  defineProperty($baseLink, 'pathExpressionInsideFilter', true)
477
- } else {
466
+ } else if (!inFrom) {
478
467
  rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep)
479
468
  }
480
469
  }
@@ -535,10 +524,10 @@ function infer(originalQuery, model) {
535
524
  if (element) {
536
525
  if ($baseLink && inInfixFilter) {
537
526
  const nextStep = arg.ref[i + 1]?.id || arg.ref[i + 1]
538
- if (isNonForeignKeyNavigation(element, nextStep)) {
527
+ if (isNonForeignKeyNavigation(element, nextStep) || arg.ref[i-1]?.where) {
539
528
  if (inExists) {
540
529
  defineProperty($baseLink, 'pathExpressionInsideFilter', true)
541
- } else {
530
+ } else if (!inFrom) {
542
531
  rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep)
543
532
  }
544
533
  }
@@ -577,7 +566,7 @@ function infer(originalQuery, model) {
577
566
  if (step.where) {
578
567
  const danglingFilter = !(arg.ref[i + 1] || arg.expand || arg.inline || inExists)
579
568
  const definition = arg.$refLinks[i].definition
580
- if ((!definition.target && definition.kind !== 'entity') || (!inFrom && danglingFilter))
569
+ if ((!definition.target && definition.kind !== 'entity') || (!inFrom && !inCalcElement && danglingFilter))
581
570
  throw new Error('A filter can only be provided when navigating along associations')
582
571
  if (!inFrom && !arg.expand)defineProperty(arg, 'isJoinRelevant', true)
583
572
  let skipJoinsForFilter = false
@@ -586,9 +575,11 @@ function infer(originalQuery, model) {
586
575
  // books[exists genre[code='A']].title --> column is join relevant but inner exists filter is not
587
576
  skipJoinsForFilter = true
588
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
589
580
  inferArg(token, false, arg.$refLinks[i], {
590
581
  ...context,
591
- inExists: skipJoinsForFilter || inExists,
582
+ inExists: skipJoinsForFilter || inExists || (inFrom && !danglingFilter),
592
583
  inXpr: !!token.xpr,
593
584
  inInfixFilter: true,
594
585
  inFrom,
@@ -598,7 +589,7 @@ function infer(originalQuery, model) {
598
589
  applyToFunctionArgs(token.args, inferArg, [
599
590
  false,
600
591
  arg.$refLinks[i],
601
- { inExists: skipJoinsForFilter || inExists, inXpr: true, inInfixFilter: true, inFrom },
592
+ { inExists: skipJoinsForFilter || inExists || (inFrom && !danglingFilter), inXpr: true, inInfixFilter: true, inFrom },
602
593
  ])
603
594
  }
604
595
  }
@@ -607,7 +598,8 @@ function infer(originalQuery, model) {
607
598
 
608
599
  if(!arg.$refLinks[i].$main)
609
600
  arg.$refLinks[i].alias = !arg.ref[i + 1] && arg.as ? arg.as : id.split('.').pop()
610
- 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
611
603
  if (!arg.ref[i + 1]) {
612
604
  const flatName = nameSegments.join('_')
613
605
  defineProperty(arg, 'flatName', flatName)
@@ -667,7 +659,11 @@ function infer(originalQuery, model) {
667
659
  // ignore whole expand if target of assoc along path has ”@cds.persistence.skip”
668
660
  if (arg.expand) {
669
661
  const { $refLinks } = arg
670
- 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
+ })
671
667
  if (skip) {
672
668
  $refLinks[$refLinks.length - 1].skipExpand = true
673
669
  return
@@ -720,24 +716,65 @@ function infer(originalQuery, model) {
720
716
  )
721
717
  }
722
718
  let elements = {}
719
+ let seenWildcard = false
723
720
  inline.forEach(inlineCol => {
724
721
  inferArg(inlineCol, null, $leafLink, { inXpr: true, baseColumn: col })
725
722
  if (inlineCol === '*') {
723
+ if (seenWildcard) throw new Error(`Duplicate wildcard "*" in inline of "${col.as || col.ref.map(idOnly).join('_')}"`)
724
+ seenWildcard = true
726
725
  const wildCardElements = {}
727
726
  // either the `.elements´ of the struct or the `.elements` of the assoc target
728
- 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 = []
729
732
  Object.entries(leafLinkElements).forEach(([k, v]) => {
730
733
  const name = namePrefix ? `${namePrefix}_${k}` : k
731
734
  // if overwritten/excluded omit from wildcard elements
732
735
  // in elements the names are already flat so consider the prefix
733
736
  // in excluding, the elements are addressed without the prefix
734
- 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
+ }
735
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
+ }
736
773
  elements = { ...elements, ...wildCardElements }
737
774
  } else {
738
775
  const nameParts = namePrefix ? [namePrefix] : []
739
776
  if (inlineCol.as) nameParts.push(inlineCol.as)
740
- else nameParts.push(...inlineCol.ref.map(idOnly))
777
+ else if (inlineCol.ref) nameParts.push(...inlineCol.ref.map(idOnly))
741
778
  const name = nameParts.join('_')
742
779
  if (inlineCol.inline) {
743
780
  const inlineElements = resolveInline(inlineCol, name)
@@ -746,9 +783,11 @@ function infer(originalQuery, model) {
746
783
  const expandElements = resolveExpand(inlineCol)
747
784
  elements = { ...elements, [name]: expandElements }
748
785
  } else if (inlineCol.val) {
749
- elements[name] = { ...getCdsTypeForVal(inlineCol.val) }
786
+ elements[name] = getCdsTypeForVal(inlineCol.val)
750
787
  } else if (inlineCol.func) {
751
788
  elements[name] = {}
789
+ } else if (inlineCol.xpr) {
790
+ elements[name] = {}
752
791
  } else {
753
792
  elements[name] = inlineCol.$refLinks[inlineCol.$refLinks.length - 1].definition
754
793
  }
@@ -776,6 +815,16 @@ function infer(originalQuery, model) {
776
815
  `Unexpected “expand” on “${col.ref.map(idOnly)}”; can only be used after a reference to a structure, association or table alias`,
777
816
  )
778
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
+ }
779
828
  const target = getDefinition($leafLink.definition.target)
780
829
  if (target) {
781
830
  const expandSubquery = {
@@ -867,7 +916,7 @@ function infer(originalQuery, model) {
867
916
  arg,
868
917
  null,
869
918
  { definition: parentElementDefinition, target: parentElementDefinition },
870
- { inCalcElement: true },
919
+ { inCalcElement: true, ...context },
871
920
  )
872
921
  const basePath =
873
922
  column.$refLinks?.length > 1
@@ -1091,7 +1140,10 @@ function infer(originalQuery, model) {
1091
1140
  if ($refLinks?.[$refLinks.length - 1].definition.elements)
1092
1141
  // no cast on structure
1093
1142
  cds.error`Structured elements can't be cast to a different type`
1094
- thing.cast = cdsTypes[cast.type] || cast
1143
+ const cdsType = cdsTypes[cast.type]
1144
+ thing.cast = cdsType ? new cdsType.constructor(cast) : cast
1145
+ if (cdsType)
1146
+ thing.cast.type = cdsType._type
1095
1147
  return thing.cast
1096
1148
  }
1097
1149
 
@@ -1121,11 +1173,11 @@ function infer(originalQuery, model) {
1121
1173
  // if(val === null) return {type:'cds.String'}
1122
1174
  switch (typeof val) {
1123
1175
  case 'string':
1124
- return cdsTypes.String
1176
+ return new cdsTypes.String.constructor()
1125
1177
  case 'boolean':
1126
- return cdsTypes.Boolean
1178
+ return new cdsTypes.Boolean.constructor()
1127
1179
  case 'number':
1128
- return Number.isSafeInteger(val) ? cdsTypes.Integer : cdsTypes.Decimal
1180
+ return Number.isSafeInteger(val) ? new cdsTypes.Integer.constructor() : new cdsTypes.Decimal.constructor()
1129
1181
  default:
1130
1182
  return {}
1131
1183
  }
@@ -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
 
@@ -1,22 +1,23 @@
1
1
  'use strict'
2
2
 
3
- // REVISIT: we should always return cds.linked elements
4
- // > e.g. cds.linked({definitions:{pseudos}})
3
+ const cds = require('@sap/cds')
4
+ const { String, Timestamp } = cds.builtin.types
5
+
5
6
  const pseudos = {
6
7
  elements: {
7
8
  $user: {
8
9
  elements: {
9
- id: { type: 'cds.String' },
10
- locale: { type: 'cds.String' }, // deprecated
11
- tenant: { type: 'cds.String' }, // deprecated
10
+ id: String,
11
+ locale: String, // deprecated
12
+ tenant: String, // deprecated
12
13
  },
13
14
  },
14
- $now: { type: 'cds.Timestamp' },
15
- $at: { type: 'cds.Timestamp' },
16
- $from: { type: 'cds.Timestamp' },
17
- $to: { type: 'cds.Timestamp' },
18
- $locale: { type: 'cds.String' },
19
- $tenant: { type: 'cds.String' },
15
+ $now: Timestamp,
16
+ $at: Timestamp,
17
+ $from: Timestamp,
18
+ $to: Timestamp,
19
+ $locale: String,
20
+ $tenant: String,
20
21
  },
21
22
  }
22
23
 
package/lib/search.js CHANGED
@@ -174,7 +174,7 @@ const computeColumnsToBeSearched = (cqn, entity = { __searchableColumns: [] }) =
174
174
  // only strings can be searched
175
175
  if (element?.type !== DEFAULT_SEARCHABLE_TYPE) {
176
176
  if (column.xpr) return
177
- if (column.func && !(column.func in aggregateFunctions)) return
177
+ if (column.func && !(column.func.toUpperCase() in aggregateFunctions)) return
178
178
  }
179
179
 
180
180
  const searchTerm = {}
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.1",
3
+ "version": "2.9.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
  }