@cap-js/db-service 1.0.0 → 1.1.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,7 +4,6 @@ const cds = require('@sap/cds/lib')
4
4
 
5
5
  const JoinTree = require('./join-tree')
6
6
  const { pseudos } = require('./pseudos')
7
- // REVISIT: we should always return cds.linked elements
8
7
  const cdsTypes = cds.linked({
9
8
  definitions: {
10
9
  Timestamp: { type: 'cds.Timestamp' },
@@ -18,11 +17,10 @@ const cdsTypes = cds.linked({
18
17
  },
19
18
  }).definitions
20
19
  for (const each in cdsTypes) cdsTypes[`cds.${each}`] = cdsTypes[each]
21
-
22
20
  /**
23
- * @param {CQN|CQL} originalQuery
24
- * @param {CSN} [model]
25
- * @returns {InferredCQN} = q with .target and .elements
21
+ * @param {import('@sap/cds/apis/cqn').Query|string} originalQuery
22
+ * @param {import('@sap/cds/apis/csn').CSN} [model]
23
+ * @returns {import('./cqn').Query} = q with .target and .elements
26
24
  */
27
25
  function infer(originalQuery, model = cds.context?.model || cds.model) {
28
26
  if (!model) cds.error('Please specify a model')
@@ -40,6 +38,10 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
40
38
  inferred.CREATE ||
41
39
  inferred.DROP ||
42
40
  inferred.STREAM
41
+
42
+ // cache for already processed calculated elements
43
+ const alreadySeenCalcElements = new Set()
44
+
43
45
  const sources = inferTarget(_.from || _.into || _.entity, {})
44
46
  const joinTree = new JoinTree(sources)
45
47
  const aliases = Object.keys(sources)
@@ -134,11 +136,11 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
134
136
  * next 'ref' step should be looked up.
135
137
  *
136
138
  *
137
- * @param {Object} arg - The argument object that will be augmented with additional properties.
139
+ * @param {object} arg - The argument object that will be augmented with additional properties.
138
140
  * It must contain a 'ref' property, which is an array representing the steps to be processed.
139
141
  * Optionally, it can also contain an 'xpr' property, which is also processed recursively.
140
142
  *
141
- * @param {Object} $baseLink - Optional parameter. It represents the environment in which the first 'ref' step should be
143
+ * @param {object} $baseLink - Optional parameter. It represents the environment in which the first 'ref' step should be
142
144
  * resolved. It's needed for infix filter / expand columns. It must contain a 'definition'
143
145
  * property, which is an object representing the base environment.
144
146
  *
@@ -177,7 +179,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
177
179
  if (!expandOrExists && nextStep && !(nextStep in e.foreignKeys))
178
180
  throw new Error(`Only foreign keys of "${e.name}" can be accessed in infix filter`)
179
181
  }
180
- arg.$refLinks.push({ definition: e, target: e._target || e })
182
+ arg.$refLinks.push({ definition: e, target: definition })
181
183
  // filter paths are flattened
182
184
  // REVISIT: too much augmentation -> better remove flatName..
183
185
  Object.defineProperty(arg, 'flatName', { value: ref.join('_'), writable: true })
@@ -188,7 +190,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
188
190
  }
189
191
  } else {
190
192
  const recent = arg.$refLinks[i - 1]
191
- const { elements } = recent.target
193
+ const { elements } = recent.definition._target || recent.definition
192
194
  const e = elements[id]
193
195
  if (!e) throw new Error(`"${id}" not found in the elements of "${arg.$refLinks[i - 1].definition.name}"`)
194
196
  arg.$refLinks.push({ definition: e, target: e._target || e })
@@ -216,6 +218,11 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
216
218
  } else throw new Error('A filter can only be provided when navigating along associations')
217
219
  }
218
220
  })
221
+ const { definition, target } = arg.$refLinks[arg.$refLinks.length - 1]
222
+ if (definition.value) {
223
+ // nested calculated element
224
+ attachRefLinksToArg(definition.value, { definition: definition.parent, target }, true)
225
+ }
219
226
  }
220
227
 
221
228
  /**
@@ -227,7 +234,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
227
234
  * Each entry in the `$combinedElements` dictionary maps from the element name
228
235
  * to an array of objects containing the index and table alias where the element can be found.
229
236
  *
230
- * @returns {Object} The `$combinedElements` dictionary, which maps element names to an array of objects
237
+ * @returns {object} The `$combinedElements` dictionary, which maps element names to an array of objects
231
238
  * containing the index and table alias where the element can be found.
232
239
  */
233
240
  function inferCombinedElements() {
@@ -264,9 +271,9 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
264
271
  * Also walks over other `ref`s in the query, validates them, and attaches `$refLinks`.
265
272
  * This includes handling `where`, infix filters within column `refs`, or other `csn` paths.
266
273
  *
267
- * @param {Object} $combinedElements The `$combinedElements` dictionary of the query, which maps element names
274
+ * @param {object} $combinedElements The `$combinedElements` dictionary of the query, which maps element names
268
275
  * to an array of objects containing the index and table alias where the element can be found.
269
- * @returns {Object} The inferred `elements` dictionary of the query, which maps element names to their corresponding definitions.
276
+ * @returns {object} The inferred `elements` dictionary of the query, which maps element names to their corresponding definitions.
270
277
  */
271
278
  function inferQueryElements($combinedElements) {
272
279
  let queryElements = {}
@@ -275,7 +282,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
275
282
  inferElementsFromWildCard(aliases)
276
283
  } else {
277
284
  let wildcardSelect = false
278
- const refs = []
285
+ const dollarSelfRefs = []
279
286
  columns.forEach(col => {
280
287
  if (col === '*') {
281
288
  wildcardSelect = true
@@ -294,24 +301,24 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
294
301
  }
295
302
  setElementOnColumns(col, queryElements[as])
296
303
  } else if (col.ref) {
297
- refs.push(col)
304
+ const firstStepIsTableAlias =
305
+ (col.ref.length > 1 && col.ref[0] in sources) ||
306
+ // nested projection on table alias
307
+ (col.ref.length === 1 && col.ref[0] in sources && col.inline)
308
+ const firstStepIsSelf =
309
+ !firstStepIsTableAlias && col.ref.length > 1 && ['$self', '$projection'].includes(col.ref[0])
310
+ // we must handle $self references after the query elements have been calculated
311
+ if (firstStepIsSelf) dollarSelfRefs.push(col)
312
+ else handleRef(col)
298
313
  } else if (col.expand) {
299
314
  inferQueryElement(col)
300
315
  } else {
301
316
  throw cds.error`Not supported: ${JSON.stringify(col)}`
302
317
  }
303
318
  })
304
- refs.forEach(col => {
305
- inferQueryElement(col)
306
- const { definition } = col.$refLinks[col.$refLinks.length - 1]
307
- if (col.cast)
308
- // final type overwritten -> element not visible anymore
309
- setElementOnColumns(col, getElementForCast(col))
310
- else if ((col.ref.length === 1) & (col.ref[0] === '$user'))
311
- // shortcut to $user.id
312
- setElementOnColumns(col, queryElements[col.as || '$user'])
313
- else setElementOnColumns(col, definition)
314
- })
319
+
320
+ if (dollarSelfRefs.length) inferDollarSelfRefs(dollarSelfRefs)
321
+
315
322
  if (wildcardSelect) inferElementsFromWildCard(aliases)
316
323
  }
317
324
  if (orderBy) {
@@ -359,6 +366,54 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
359
366
 
360
367
  return queryElements
361
368
 
369
+ /**
370
+ * Processes references starting with `$self`, which are intended to target other query elements.
371
+ * These `$self` paths must be handled after processing the "regular" columns since they are dependent on other query elements.
372
+ *
373
+ * This function checks for `$self` references that may target other `$self` columns, and delays their processing.
374
+ * `$self` references not targeting other `$self` references are handled by the generic `handleRef` function immediately.
375
+ *
376
+ * @param {array} dollarSelfColumns - An array of column objects containing `$self` references.
377
+ */
378
+ function inferDollarSelfRefs(dollarSelfColumns) {
379
+ do {
380
+ const unprocessedColumns = []
381
+
382
+ for (const currentDollarSelfColumn of dollarSelfColumns) {
383
+ const { ref } = currentDollarSelfColumn
384
+ const stepToFind = ref[1]
385
+
386
+ const referencesOtherDollarSelfColumn = dollarSelfColumns.find(
387
+ otherDollarSelfCol =>
388
+ otherDollarSelfCol !== currentDollarSelfColumn &&
389
+ (otherDollarSelfCol.as
390
+ ? stepToFind === otherDollarSelfCol.as
391
+ : stepToFind === otherDollarSelfCol.ref?.[otherDollarSelfCol.ref.length - 1]),
392
+ )
393
+
394
+ if (referencesOtherDollarSelfColumn) {
395
+ unprocessedColumns.push(currentDollarSelfColumn)
396
+ } else {
397
+ handleRef(currentDollarSelfColumn)
398
+ }
399
+ }
400
+
401
+ dollarSelfColumns = unprocessedColumns
402
+ } while (dollarSelfColumns.length > 0)
403
+ }
404
+
405
+ function handleRef(col) {
406
+ inferQueryElement(col)
407
+ const { definition } = col.$refLinks[col.$refLinks.length - 1]
408
+ if (col.cast)
409
+ // final type overwritten -> element not visible anymore
410
+ setElementOnColumns(col, getElementForCast(col))
411
+ else if ((col.ref.length === 1) & (col.ref[0] === '$user'))
412
+ // shortcut to $user.id
413
+ setElementOnColumns(col, queryElements[col.as || '$user'])
414
+ else setElementOnColumns(col, definition)
415
+ }
416
+
362
417
  /**
363
418
  * This function is responsible for inferring a query element based on a provided column.
364
419
  * It initializes and attaches a non-enumerable `$refLinks` property to the column,
@@ -396,7 +451,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
396
451
  */
397
452
 
398
453
  function inferQueryElement(column, insertIntoQueryElements = true, $baseLink = null, context) {
399
- const { inExists, inExpr, inNestedProjection } = context || {}
454
+ const { inExists, inExpr, inNestedProjection, inCalcElement } = context || {}
400
455
  if (column.param) return // parameter references are only resolved into values on execution e.g. :val, :1 or ?
401
456
  if (column.args) column.args.forEach(arg => inferQueryElement(arg, false, $baseLink, context)) // e.g. function in expression
402
457
  if (column.list) column.list.forEach(arg => inferQueryElement(arg, false, $baseLink, context))
@@ -438,7 +493,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
438
493
  const elements = definition.elements || definition._target?.elements
439
494
  if (elements && id in elements) {
440
495
  const element = elements[id]
441
- if (!inExists && !inNestedProjection && element.target) {
496
+ if (!inExists && !inNestedProjection && !inCalcElement && element.target) {
442
497
  // only fk access in infix filter
443
498
  const nextStep = column.ref[1]?.id || column.ref[1]
444
499
  // no unmanaged assoc in infix filter path
@@ -452,7 +507,8 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
452
507
  if (nextStep && !(nextStep in element.foreignKeys))
453
508
  throw new Error(`Only foreign keys of "${element.name}" can be accessed in infix filter`)
454
509
  }
455
- column.$refLinks.push({ definition: elements[id], target })
510
+ const resolvableIn = definition.target ? definition._target : target
511
+ column.$refLinks.push({ definition: elements[id], target: resolvableIn })
456
512
  } else {
457
513
  stepNotFoundInPredecessor(id, definition.name)
458
514
  }
@@ -477,14 +533,25 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
477
533
  } else {
478
534
  const { definition } = column.$refLinks[i - 1]
479
535
  const elements = definition.elements || definition._target?.elements
480
- if (elements && id in elements) {
481
- const $refLink = { definition: elements[id], target: column.$refLinks[i - 1].target }
536
+ const element = elements?.[id]
537
+
538
+ if (firstStepIsSelf && element?.isAssociation) {
539
+ throw cds.error(
540
+ `Paths starting with “$self” must not contain steps of type “cds.Association”: ref: [ ${column.ref.map(
541
+ idOnly,
542
+ )} ]`,
543
+ )
544
+ }
545
+
546
+ const target = definition._target || column.$refLinks[i - 1].target
547
+ if (element) {
548
+ const $refLink = { definition: elements[id], target }
482
549
  column.$refLinks.push($refLink)
483
550
  } else if (firstStepIsSelf) {
484
551
  stepNotFoundInColumnList(id)
485
552
  } else if (column.ref[0] === '$user' && pseudoPath) {
486
553
  // `$user.some.unknown.element` -> no error
487
- column.$refLinks.push({ definition: {}, target: column.$refLinks[i - 1].target })
554
+ column.$refLinks.push({ definition: {}, target })
488
555
  } else if (id === '$dummy') {
489
556
  // `some.known.element.$dummy` -> no error; used by cds.ql to simulate joins
490
557
  column.$refLinks.push({ definition: { name: '$dummy', parent: column.$refLinks[i - 1].target } })
@@ -504,7 +571,9 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
504
571
  : null
505
572
  if (foreignKeyAlias) nameSegments.push(foreignKeyAlias)
506
573
  else if (skipAliasedFkSegmentsOfNameStack[0] === id) skipAliasedFkSegmentsOfNameStack.shift()
507
- else nameSegments.push(id)
574
+ else {
575
+ nameSegments.push(firstStepIsSelf && i === 1 ? element.__proto__.name : id)
576
+ }
508
577
  }
509
578
 
510
579
  if (step.where) {
@@ -567,7 +636,8 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
567
636
  }
568
637
  if (queryElements[elementName] !== undefined)
569
638
  throw new Error(`Duplicate definition of element “${elementName}”`)
570
- queryElements[elementName] = getCopyWithAnnos(column, leafArt)
639
+ const element = getCopyWithAnnos(column, leafArt)
640
+ queryElements[elementName] = element
571
641
  }
572
642
  }
573
643
  }
@@ -584,19 +654,27 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
584
654
  return
585
655
  }
586
656
  }
587
- const virtual = (column.$refLinks[column.$refLinks.length - 1].definition.virtual || !isPersisted) && !inExpr
657
+ const leafArt = column.$refLinks[column.$refLinks.length - 1].definition
658
+ const virtual = (leafArt.virtual || !isPersisted) && !inExpr
588
659
  // check if we need to merge the column `ref` into the join tree of the query
589
- if (!inExists && !virtual && isColumnJoinRelevant(column)) {
660
+ if (!inExists && !virtual && !inCalcElement && isColumnJoinRelevant(column, firstStepIsSelf)) {
661
+ if (originalQuery.UPDATE)
662
+ throw cds.error(
663
+ 'Path expressions for UPDATE statements are not supported. Use “where exists” with infix filters instead.',
664
+ )
590
665
  Object.defineProperty(column, 'isJoinRelevant', { value: true })
591
- joinTree.mergeColumn(column)
666
+ joinTree.mergeColumn(column, $baseLink)
667
+ }
668
+ if (leafArt.value && !leafArt.value.stored) {
669
+ resolveCalculatedElement(leafArt, column)
592
670
  }
593
671
 
594
672
  /**
595
673
  * Resolves and processes the inline attribute of a column in a database query.
596
674
  *
597
- * @param {Object} col - The column object with properties: `inline` and `$refLinks`.
675
+ * @param {object} col - The column object with properties: `inline` and `$refLinks`.
598
676
  * @param {string} [namePrefix=col.as || col.flatName] - Prefix for naming new columns. Defaults to `col.as` or `col.flatName`.
599
- * @returns {Object} - An object with resolved and processed inline column definitions.
677
+ * @returns {object} - An object with resolved and processed inline column definitions.
600
678
  *
601
679
  * Procedure:
602
680
  * 1. Iterate through `inline` array. For each `inlineCol`:
@@ -650,8 +728,8 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
650
728
  /**
651
729
  * Resolves a query column which has an `expand` property.
652
730
  *
653
- * @param {Object} col - The column object with properties: `expand` and `$refLinks`.
654
- * @returns {Object} - A `cds.struct` object with expanded column definitions.
731
+ * @param {object} col - The column object with properties: `expand` and `$refLinks`.
732
+ * @returns {object} - A `cds.struct` object with expanded column definitions.
655
733
  *
656
734
  * Procedure:
657
735
  * - if `$leafLink` is an association, constructs an `expandSubquery` and infers a new query structure.
@@ -717,6 +795,58 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
717
795
  throw new Error(err)
718
796
  }
719
797
  }
798
+ function resolveCalculatedElement(calcElement) {
799
+ if (alreadySeenCalcElements.has(calcElement)) return
800
+ else alreadySeenCalcElements.add(calcElement)
801
+ const { ref, xpr, func } = calcElement.value
802
+ if (ref || xpr) {
803
+ attachRefLinksToArg(calcElement.value, { definition: calcElement.parent, target: calcElement.parent }, true)
804
+ // column is now fully linked, now we need to find out if we need to merge it into the join tree
805
+ // for that, we calculate all paths from a calc element and merge them into the join tree
806
+ mergePathsIntoJoinTree(calcElement.value)
807
+ }
808
+ if (func) calcElement.value.args?.forEach(arg => inferQueryElement(arg, false)) // {func}.args are optional
809
+ function mergePathsIntoJoinTree(e, basePath = null) {
810
+ basePath = basePath || { $refLinks: [], ref: [] }
811
+ if (e.ref) {
812
+ e.$refLinks.forEach((link, i) => {
813
+ const { definition } = link
814
+ if (!definition.value) {
815
+ basePath.$refLinks.push(link)
816
+ basePath.ref.push(e.ref[i])
817
+ }
818
+ })
819
+ const leafOfCalculatedElementRef = e.$refLinks[e.$refLinks.length - 1].definition
820
+ if (leafOfCalculatedElementRef.value) mergePathsIntoJoinTree(leafOfCalculatedElementRef.value, basePath)
821
+
822
+ mergePathIfNecessary(basePath, e)
823
+ } else if (e.xpr) {
824
+ e.xpr.forEach(step => {
825
+ if (step.ref) {
826
+ const subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] }
827
+ step.$refLinks.forEach((link, i) => {
828
+ const { definition } = link
829
+ if (definition.value) {
830
+ mergePathsIntoJoinTree(definition.value)
831
+ } else {
832
+ subPath.$refLinks.push(link)
833
+ subPath.ref.push(step.ref[i])
834
+ }
835
+ })
836
+ mergePathIfNecessary(subPath, step)
837
+ }
838
+ })
839
+ }
840
+
841
+ function mergePathIfNecessary(p, step) {
842
+ const calcElementIsJoinRelevant = isColumnJoinRelevant(p)
843
+ if (calcElementIsJoinRelevant) {
844
+ if (!calcElement.value.isColumnJoinRelevant) Object.defineProperty(step, 'isJoinRelevant', { value: true })
845
+ joinTree.mergeColumn(p)
846
+ }
847
+ }
848
+ }
849
+ }
720
850
 
721
851
  /**
722
852
  * Checks whether or not the `ref` of the given column is join relevant.
@@ -760,7 +890,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
760
890
 
761
891
  if (!assoc) return false
762
892
  if (fkAccess) return false
763
- else return true
893
+ return true
764
894
  }
765
895
 
766
896
  /**
@@ -768,15 +898,20 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
768
898
  * if there is not already an element with the same name present.
769
899
  */
770
900
  function inferElementsFromWildCard() {
901
+ const exclude = _.excluding ? x => _.excluding.includes(x) : () => false
902
+
771
903
  if (Object.keys(queryElements).length === 0 && aliases.length === 1) {
772
904
  // only one query source and no overwritten columns
773
905
  Object.entries(sources[aliases[0]].elements).forEach(([name, element]) => {
774
- if (element.type !== 'cds.LargeBinary') queryElements[name] = element
906
+ if (!exclude(name) && element.type !== 'cds.LargeBinary') queryElements[name] = element
907
+ if (element.value) {
908
+ // we might have join relevant calculated elements
909
+ resolveCalculatedElement(element)
910
+ }
775
911
  })
776
912
  return
777
913
  }
778
914
 
779
- const exclude = _.excluding ? x => _.excluding.includes(x) : () => false
780
915
  const ambiguousElements = {}
781
916
  Object.entries($combinedElements).forEach(([name, tableAliases]) => {
782
917
  if (Object.keys(tableAliases).length > 1) {
@@ -1,50 +1,92 @@
1
1
  'use strict'
2
2
 
3
+ // REVISIT: define following unknown types
4
+
5
+ /**
6
+ * @typedef {unknown} $refLink
7
+ */
8
+
9
+ /**
10
+ * @typedef {unknown} parent
11
+ */
12
+
13
+ /**
14
+ * @typedef {unknown} where
15
+ */
16
+
17
+ /**
18
+ * @typedef {unknown} children
19
+ */
20
+
21
+ /**
22
+ * @typedef {unknown} queryArtifact
23
+ */
24
+
25
+ /**
26
+ * @typedef {string} alias
27
+ */
28
+
29
+ /**
30
+ * @typedef {Map<alias,Root>} _roots
31
+ */
32
+
33
+ /**
34
+ * @typedef {Object.<string, unknown>} sources
35
+ */
36
+
3
37
  /**
4
38
  * A class representing a Node in the join tree.
5
- *
6
- * @property {$refLink} - A reference link to this node.
7
- * @property {parent} - The parent Node of this node.
8
- * @property {where} - An optional condition to be applied to this node.
9
- * @property {children} - A Map of children nodes belonging to this node.
10
39
  */
11
40
  class Node {
41
+ /**
42
+ * @param {$refLink} $refLink
43
+ * @param {parent} parent
44
+ * @param {where} where
45
+ */
12
46
  constructor($refLink, parent, where = null) {
47
+ /** @type {$refLink} - A reference link to this node. */
13
48
  this.$refLink = $refLink
49
+ /** @type {parent} - The parent Node of this node. */
14
50
  this.parent = parent
51
+ /** @type {where} - An optional condition to be applied to this node. */
15
52
  this.where = where
53
+ /** @type {children} - A Map of children nodes belonging to this node. */
16
54
  this.children = new Map()
17
55
  }
18
56
  }
19
57
 
20
58
  /**
21
59
  * A class representing the root of the join tree.
22
- *
23
- * @property {queryArtifact} - The artifact used to make the query.
24
- * @property {alias} - The alias of the artifact.
25
- * @property {parent} - The parent Node of this root, null for the root Node.
26
- * @property {children} - A Map of children nodes belonging to this root.
27
60
  */
28
61
  class Root {
62
+ /**
63
+ * @param {[alias, queryArtifact]} querySource
64
+ */
29
65
  constructor(querySource) {
30
66
  const [alias, queryArtifact] = querySource
67
+ /** @type {queryArtifact} - The artifact used to make the query. */
31
68
  this.queryArtifact = queryArtifact
69
+ /** @type {alias} - The alias of the artifact. */
32
70
  this.alias = alias
71
+ /** @type {parent} - The parent Node of this root, null for the root Node. */
33
72
  this.parent = null
73
+ /** @type {children} - A Map of children nodes belonging to this root. */
34
74
  this.children = new Map()
35
75
  }
36
76
  }
37
77
 
38
78
  /**
39
79
  * A class representing a Join Tree.
40
- *
41
- * @property {_roots} - A Map of root nodes.
42
- * @property {isInitial} - A boolean indicating if the join tree is in its initial state.
43
- * @property {_queryAliases} - A Map of query aliases, which is used during the association to join translation.
44
80
  */
45
81
  class JoinTree {
82
+ /**
83
+ *
84
+ * @param {sources} sources
85
+ */
46
86
  constructor(sources) {
87
+ /** @type {_roots} - A Map of root nodes. */
47
88
  this._roots = new Map()
89
+ /** @type {boolean} - A boolean indicating if the join tree is in its initial state. */
48
90
  this.isInitial = true
49
91
  /**
50
92
  * A map that holds query aliases which are used during the
@@ -53,6 +95,7 @@ class JoinTree {
53
95
  *
54
96
  * The table aliases are treated case insensitive. The index of each
55
97
  * table alias entry, is the capitalized version of the alias.
98
+ * @type {Map<string, string>}
56
99
  */
57
100
  this._queryAliases = new Map()
58
101
  Object.entries(sources).forEach(entry => {
@@ -82,6 +125,7 @@ class JoinTree {
82
125
  * Calculates and adds the next available table alias to the alias map.
83
126
  *
84
127
  * @param {string} alias - The original alias name.
128
+ * @param {unknown[]} outerQueries - An array of outer queries.
85
129
  * @returns {string} - The next unambiguous table alias.
86
130
  */
87
131
  addNextAvailableTableAlias(alias, outerQueries) {
@@ -107,7 +151,7 @@ class JoinTree {
107
151
  * For each step, it checks whether it has been seen before. If so, it resets the $refLink to point to the already merged $refLink.
108
152
  * If not, it creates a new Node and ensures proper aliasing and foreign key access.
109
153
  *
110
- * @param {Object} col - The column object to be merged into the existing join tree. This object should have the properties $refLinks and ref.
154
+ * @param {object} col - The column object to be merged into the existing join tree. This object should have the properties $refLinks and ref.
111
155
  * @returns {boolean} - Always returns true, indicating the column has been successfully merged into the join tree.
112
156
  */
113
157
  mergeColumn(col) {
@@ -145,8 +189,9 @@ class JoinTree {
145
189
  }
146
190
  const child = new Node($refLink, node, where)
147
191
  if (child.$refLink.definition.isAssociation) {
148
- if (child.where) {
149
- // always join relevant
192
+ if (child.where || col.inline) {
193
+ // filter is always join relevant
194
+ // if the column ends up in an `inline` -> each assoc step is join relevant
150
195
  child.$refLink.onlyForeignKeyAccess = false
151
196
  } else {
152
197
  child.$refLink.onlyForeignKeyAccess = true
package/package.json CHANGED
@@ -1,14 +1,22 @@
1
1
  {
2
2
  "name": "@cap-js/db-service",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "CDS base database service",
5
- "homepage": "https://cap.cloud.sap/",
5
+ "homepage": "https://github.com/cap-js/cds-dbs/tree/main/db-service#cds-base-database-service",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/cap-js/cds-dbs"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/cap-js/cds-dbs/issues"
12
+ },
6
13
  "keywords": [
7
14
  "CAP",
8
15
  "CDS"
9
16
  ],
10
17
  "author": "SAP SE (https://www.sap.com)",
11
18
  "main": "index.js",
19
+ "types": "./dist/index.d.ts",
12
20
  "files": [
13
21
  "lib",
14
22
  "CHANGELOG.md"
@@ -19,12 +27,16 @@
19
27
  },
20
28
  "scripts": {
21
29
  "prettier": "npx prettier --write .",
22
- "test": "npx jest --silent",
30
+ "test": "npm run build && npx jest --silent",
31
+ "build": "tsc && find lib/ -type f -name '*.d.ts' -exec cp '{}' 'dist/{}' ';' && cp ts.eslintrc.cjs dist/.eslintrc.cjs && npx eslint ./dist --ext .d.ts",
23
32
  "lint": "npx eslint . && npx prettier --check . "
24
33
  },
25
- "dependencies": {},
26
34
  "peerDependencies": {
27
- "@sap/cds": ">=7"
35
+ "@sap/cds": ">=7.1.1"
28
36
  },
29
- "license": "SEE LICENSE"
37
+ "license": "SEE LICENSE",
38
+ "devDependencies": {
39
+ "@typescript-eslint/eslint-plugin": "^6.2.0",
40
+ "typescript": "^5.1.6"
41
+ }
30
42
  }