@cap-js/db-service 1.19.1 → 2.0.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,7 @@ const cds = require('@sap/cds')
4
4
 
5
5
  const JoinTree = require('./join-tree')
6
6
  const { pseudos } = require('./pseudos')
7
- const { isCalculatedOnRead, getImplicitAlias } = require('../utils')
7
+ const { isCalculatedOnRead, getImplicitAlias, getModelUtils, defineProperty } = require('../utils')
8
8
  const cdsTypes = cds.linked({
9
9
  definitions: {
10
10
  Timestamp: { type: 'cds.Timestamp' },
@@ -27,6 +27,8 @@ function infer(originalQuery, model) {
27
27
  if (!model) throw new Error('Please specify a model')
28
28
  const inferred = originalQuery
29
29
 
30
+ const { getDefinition } = getModelUtils(model, originalQuery)
31
+
30
32
  // REVISIT: The more edge use cases we support, thes less optimized are we for the 90+% use cases
31
33
  // e.g. there's a lot of overhead for infer( SELECT.from(Books) )
32
34
  if (originalQuery.SET) throw new Error('”UNION” based queries are not supported')
@@ -44,7 +46,7 @@ function infer(originalQuery, model) {
44
46
 
45
47
  let $combinedElements
46
48
 
47
- const sources = inferTarget(_.from || _.into || _.entity, {})
49
+ const sources = inferTarget(_.into || _.from || _.entity, {}) // IMPORTANT: _.into has to go before _.from for INSERT.into().from(SELECT)
48
50
  const joinTree = new JoinTree(sources)
49
51
  const aliases = Object.keys(sources)
50
52
  const target = aliases.length === 1 ? getDefinitionFromSources(sources, aliases[0]) : originalQuery
@@ -73,7 +75,7 @@ function infer(originalQuery, model) {
73
75
  joinTree: { value: joinTree, writable: true, configurable: true }, // REVISIT: eliminate
74
76
  })
75
77
  // also enrich original query -> writable because it may be inferred again
76
- Object.defineProperty(originalQuery, 'elements', { value: elements, writable: true, configurable: true })
78
+ defineProperty(originalQuery, 'elements', elements)
77
79
  }
78
80
  return inferred
79
81
 
@@ -111,13 +113,13 @@ function infer(originalQuery, model) {
111
113
 
112
114
  inferArg(from, null, null, { inFrom: true })
113
115
  const alias =
114
- from.uniqueSubqueryAlias ||
115
- from.as ||
116
- (ref.length === 1
117
- ? getImplicitAlias(first, useTechnicalAlias)
118
- : getImplicitAlias(ref.at(-1).id || ref.at(-1), useTechnicalAlias));
116
+ from.uniqueSubqueryAlias ||
117
+ from.as ||
118
+ (ref.length === 1
119
+ ? getImplicitAlias(first, useTechnicalAlias)
120
+ : getImplicitAlias(ref.at(-1).id || ref.at(-1), useTechnicalAlias))
119
121
  if (alias in querySources) throw new Error(`Duplicate alias "${alias}"`)
120
- querySources[alias] = { definition: target, args }
122
+ querySources[alias] = { definition: getDefinition(target.name), args }
121
123
  const last = from.$refLinks.at(-1)
122
124
  last.alias = alias
123
125
  } else if (from.args) {
@@ -169,10 +171,7 @@ function infer(originalQuery, model) {
169
171
  * @param {csn.Element} element
170
172
  */
171
173
  function setElementOnColumns(col, element) {
172
- Object.defineProperty(col, 'element', {
173
- value: element,
174
- writable: true,
175
- })
174
+ defineProperty(col, 'element', element)
176
175
  }
177
176
 
178
177
  /**
@@ -209,7 +208,7 @@ function infer(originalQuery, model) {
209
208
  if (col.func) {
210
209
  if (col.args) {
211
210
  // {func}.args are optional
212
- applyToFunctionArgs(col.args, inferArg, [false, null, {dollarSelfRefs}])
211
+ applyToFunctionArgs(col.args, inferArg, [false, null, { dollarSelfRefs }])
213
212
  }
214
213
  queryElements[as] = getElementForCast(col)
215
214
  }
@@ -243,7 +242,7 @@ function infer(originalQuery, model) {
243
242
  // link $refLinks -> special name resolution rules for orderBy
244
243
  orderBy.forEach(token => {
245
244
  let $baseLink
246
- let rejectJoinRelevantPath
245
+ let needsElementsOfQueryAsBase
247
246
  // first check if token ref is resolvable in query elements
248
247
  if (columns) {
249
248
  const firstStep = token.ref?.[0].id || token.ref?.[0]
@@ -251,14 +250,11 @@ function infer(originalQuery, model) {
251
250
  const columnName = c.as || c.flatName || c.ref?.at(-1).id || c.ref?.at(-1) || c.func
252
251
  return columnName === firstStep
253
252
  })
254
- const needsElementsOfQueryAsBase =
253
+ needsElementsOfQueryAsBase =
255
254
  tokenPointsToQueryElements &&
256
- queryElements[token.ref?.[0]] &&
257
- /* expand on structure can be addressed */ !queryElements[token.ref?.[0]].$assocExpand
255
+ queryElements[firstStep] &&
256
+ /* expand on structure can be addressed */ !queryElements[firstStep].$assocExpand
258
257
 
259
- // if the ref points into the query itself and follows an exposed association
260
- // to a non-fk column, we must reject the ref, as we can't join with the queries own results
261
- rejectJoinRelevantPath = needsElementsOfQueryAsBase
262
258
  if (needsElementsOfQueryAsBase) $baseLink = { definition: { elements: queryElements }, target: inferred }
263
259
  } else {
264
260
  // fallback to elements of query source
@@ -266,7 +262,9 @@ function infer(originalQuery, model) {
266
262
  }
267
263
 
268
264
  inferArg(token, queryElements, $baseLink, { inQueryModifier: true })
269
- if (token.isJoinRelevant && rejectJoinRelevantPath) {
265
+ // if the ref points into the query itself and follows an exposed association
266
+ // to a non-fk column, we must reject the ref, as we can't join with the queries own results
267
+ if (token.isJoinRelevant && needsElementsOfQueryAsBase) {
270
268
  // reverse the array, find the last association and calculate the index of the association in non-reversed order
271
269
  const assocIndex =
272
270
  token.$refLinks.length - 1 - token.$refLinks.reverse().findIndex(link => link.definition.isAssociation)
@@ -350,7 +348,7 @@ function infer(originalQuery, model) {
350
348
  }
351
349
 
352
350
  function handleRef(col, inXpr) {
353
- inferArg(col, queryElements, null, { inXpr })
351
+ inferArg(col, queryElements, null, { inXpr })
354
352
  const { definition } = col.$refLinks[col.$refLinks.length - 1]
355
353
  if (col.cast)
356
354
  // final type overwritten -> element not visible anymore
@@ -399,22 +397,27 @@ function infer(originalQuery, model) {
399
397
  */
400
398
 
401
399
  function inferArg(arg, queryElements = null, $baseLink = null, context = {}) {
402
- const { inExists, inXpr, inCalcElement, baseColumn, inInfixFilter, inQueryModifier, inFrom, dollarSelfRefs } = context
400
+ const { inExists, inXpr, inCalcElement, baseColumn, inInfixFilter, inQueryModifier, inFrom, dollarSelfRefs } =
401
+ context
403
402
  if (arg.param || arg.SELECT) return // parameter references are only resolved into values on execution e.g. :val, :1 or ?
404
403
  if (arg.args) applyToFunctionArgs(arg.args, inferArg, [null, $baseLink, context])
405
404
  if (arg.list) arg.list.forEach(arg => inferArg(arg, null, $baseLink, context))
406
- if (arg.xpr) arg.xpr.forEach(token => inferArg(token, queryElements, $baseLink, { ...context, inXpr: true })) // e.g. function in expression
405
+ if (arg.xpr)
406
+ arg.xpr.forEach((token, i) =>
407
+ inferArg(token, queryElements, $baseLink, { ...context, inXpr: true, inExists: arg.xpr[i - 1] === 'exists' }),
408
+ ) // e.g. function in expression
407
409
 
408
410
  if (!arg.ref) {
409
411
  if (arg.expand && queryElements) queryElements[arg.as] = resolveExpand(arg)
410
412
  return
411
413
  }
412
414
 
413
- // initialize $refLinks
414
- Object.defineProperty(arg, '$refLinks', {
415
- value: [],
416
- writable: true,
417
- })
415
+ // Before the arg is linked, it's meta information should be cleaned up.
416
+ // This may be important if one manipulates the arg object
417
+ // __after__ a query has been fired and re-uses the manipulated query
418
+ defineProperty(arg, '$refLinks', [])
419
+ defineProperty(arg, 'isJoinRelevant', false)
420
+
418
421
  // if any path step points to an artifact with `@cds.persistence.skip`
419
422
  // we must ignore the element from the queries elements
420
423
  let isPersisted = true
@@ -424,8 +427,8 @@ function infer(originalQuery, model) {
424
427
  firstStepIsSelf = !firstStepIsTableAlias && arg.ref.length > 1 && ['$self', '$projection'].includes(arg.ref[0])
425
428
  expandOnTableAlias = arg.ref.length === 1 && arg.ref[0] in sources && (arg.expand || arg.inline)
426
429
  }
427
- if(dollarSelfRefs && firstStepIsSelf) {
428
- Object.defineProperty(arg, 'inXpr', { value: true, writable: true })
430
+ if (dollarSelfRefs && firstStepIsSelf) {
431
+ defineProperty(arg, 'inXpr', true)
429
432
  dollarSelfRefs.push(arg)
430
433
  return
431
434
  }
@@ -452,7 +455,7 @@ function infer(originalQuery, model) {
452
455
  const nextStep = arg.ref[1]?.id || arg.ref[1]
453
456
  if (isNonForeignKeyNavigation(element, nextStep)) {
454
457
  if (inExists) {
455
- Object.defineProperty($baseLink, 'pathExpressionInsideFilter', { value: true })
458
+ defineProperty($baseLink, 'pathExpressionInsideFilter', true)
456
459
  } else {
457
460
  rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep)
458
461
  }
@@ -516,7 +519,7 @@ function infer(originalQuery, model) {
516
519
  const nextStep = arg.ref[i + 1]?.id || arg.ref[i + 1]
517
520
  if (isNonForeignKeyNavigation(element, nextStep)) {
518
521
  if (inExists) {
519
- Object.defineProperty($baseLink, 'pathExpressionInsideFilter', { value: true })
522
+ defineProperty($baseLink, 'pathExpressionInsideFilter', true)
520
523
  } else {
521
524
  rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep)
522
525
  }
@@ -532,7 +535,7 @@ function infer(originalQuery, model) {
532
535
  } else if (id === '$dummy') {
533
536
  // `some.known.element.$dummy` -> no error; used by cds.ql to simulate joins
534
537
  arg.$refLinks.push({ definition: { name: '$dummy', parent: arg.$refLinks[i - 1].target } })
535
- Object.defineProperty(arg, 'isJoinRelevant', { value: true })
538
+ defineProperty(arg, 'isJoinRelevant', true)
536
539
  } else {
537
540
  const notFoundIn = pseudoPath ? arg.ref[i - 1] : getFullPathForLinkedArg(arg)
538
541
  stepNotFoundInPredecessor(id, notFoundIn)
@@ -558,7 +561,7 @@ function infer(originalQuery, model) {
558
561
  const definition = arg.$refLinks[i].definition
559
562
  if ((!definition.target && definition.kind !== 'entity') || (!inFrom && danglingFilter))
560
563
  throw new Error('A filter can only be provided when navigating along associations')
561
- if (!inFrom && !arg.expand) Object.defineProperty(arg, 'isJoinRelevant', { value: true })
564
+ if (!inFrom && !arg.expand)defineProperty(arg, 'isJoinRelevant', true)
562
565
  let skipJoinsForFilter = false
563
566
  step.where.forEach(token => {
564
567
  if (token === 'exists') {
@@ -587,7 +590,7 @@ function infer(originalQuery, model) {
587
590
  if (getDefinition(arg.$refLinks[i].definition.target)?.['@cds.persistence.skip'] === true) isPersisted = false
588
591
  if (!arg.ref[i + 1]) {
589
592
  const flatName = nameSegments.join('_')
590
- Object.defineProperty(arg, 'flatName', { value: flatName, writable: true })
593
+ defineProperty(arg, 'flatName', flatName)
591
594
  // if column is casted, we overwrite it's origin with the new type
592
595
  if (arg.cast) {
593
596
  const base = getElementForCast(arg)
@@ -632,7 +635,7 @@ function infer(originalQuery, model) {
632
635
  })
633
636
 
634
637
  // we need inner joins for the path expressions inside filter expressions after exists predicate
635
- if ($baseLink?.pathExpressionInsideFilter) Object.defineProperty(arg, 'join', { value: 'inner' })
638
+ if ($baseLink?.pathExpressionInsideFilter) defineProperty(arg, 'join', 'inner')
636
639
 
637
640
  // ignore whole expand if target of assoc along path has ”@cds.persistence.skip”
638
641
  if (arg.expand) {
@@ -652,7 +655,7 @@ function infer(originalQuery, model) {
652
655
  ? { ref: [...baseColumn.ref, ...arg.ref], $refLinks: [...baseColumn.$refLinks, ...arg.$refLinks] }
653
656
  : arg
654
657
  if (isColumnJoinRelevant(colWithBase)) {
655
- Object.defineProperty(arg, 'isJoinRelevant', { value: true })
658
+ defineProperty(arg, 'isJoinRelevant', true)
656
659
  joinTree.mergeColumn(colWithBase, originalQuery.outerQueries)
657
660
  }
658
661
  }
@@ -758,7 +761,7 @@ function infer(originalQuery, model) {
758
761
  const res = $leafLink.definition.is2one
759
762
  ? new cds.struct({ elements: inferredExpandSubquery.elements })
760
763
  : new cds.array({ items: new cds.struct({ elements: inferredExpandSubquery.elements }) })
761
- return Object.defineProperty(res, '$assocExpand', { value: true })
764
+ return defineProperty(res, '$assocExpand', true)
762
765
  } else if ($leafLink.definition.elements) {
763
766
  let elements = {}
764
767
  expand.forEach(e => {
@@ -810,7 +813,8 @@ function infer(originalQuery, model) {
810
813
  else alreadySeenCalcElements.add(calcElement)
811
814
  const { ref, xpr } = calcElement.value
812
815
  if (ref || xpr) {
813
- baseLink = { definition: calcElement.parent, target: calcElement.parent }
816
+ const parentElementDefinition = getDefinition(calcElement.parent.name)
817
+ baseLink = { definition: parentElementDefinition, target: parentElementDefinition }
814
818
  inferArg(calcElement.value, null, baseLink, { inCalcElement: true, ...context })
815
819
  const basePath =
816
820
  column.$refLinks?.length > 1
@@ -825,7 +829,13 @@ function infer(originalQuery, model) {
825
829
 
826
830
  if (calcElement.value.args) {
827
831
  const processArgument = (arg, calcElement, column) => {
828
- inferArg(arg, null, { definition: calcElement.parent, target: calcElement.parent }, { inCalcElement: true })
832
+ const parentElementDefinition = getDefinition(calcElement.parent.name)
833
+ inferArg(
834
+ arg,
835
+ null,
836
+ { definition: parentElementDefinition, target: parentElementDefinition },
837
+ { inCalcElement: true },
838
+ )
829
839
  const basePath =
830
840
  column.$refLinks?.length > 1
831
841
  ? { $refLinks: column.$refLinks.slice(0, -1), ref: column.ref.slice(0, -1) }
@@ -879,8 +889,7 @@ function infer(originalQuery, model) {
879
889
  step[nestedProp].forEach(a => {
880
890
  // reset sub path for each nested argument
881
891
  // e.g. case when <path> then <otherPath> else <anotherPath> end
882
- if(!a.ref)
883
- subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] }
892
+ if (!a.ref) subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] }
884
893
  mergePathsIntoJoinTree(a, subPath)
885
894
  })
886
895
  }
@@ -891,13 +900,13 @@ function infer(originalQuery, model) {
891
900
  const calcElementIsJoinRelevant = isColumnJoinRelevant(p)
892
901
  if (calcElementIsJoinRelevant) {
893
902
  if (!calcElement.value.isJoinRelevant)
894
- Object.defineProperty(step, 'isJoinRelevant', { value: true, writable: true, })
903
+ defineProperty(step, 'isJoinRelevant',true)
895
904
  joinTree.mergeColumn(p, originalQuery.outerQueries)
896
905
  } else {
897
906
  // we need to explicitly set the value to false in this case,
898
907
  // e.g. `SELECT from booksCalc.Books { ID, author.{name }, author {name } }`
899
908
  // --> for the inline column, the name is join relevant, while for the expand, it is not
900
- Object.defineProperty(step, 'isJoinRelevant', { value: false, writable: true })
909
+ defineProperty(step, 'isJoinRelevant', false)
901
910
  }
902
911
  }
903
912
  }
@@ -960,7 +969,7 @@ function infer(originalQuery, model) {
960
969
  if (Object.keys(queryElements).length === 0 && aliases.length === 1) {
961
970
  const { elements } = getDefinitionFromSources(sources, aliases[0])
962
971
  // only one query source and no overwritten columns
963
- for (const k of Object.keys(elements)) {
972
+ for (const k in elements) {
964
973
  if (!exclude(k)) {
965
974
  const element = elements[k]
966
975
  if (element.type !== 'cds.LargeBinary') {
@@ -1055,7 +1064,6 @@ function infer(originalQuery, model) {
1055
1064
  * @returns {object} a copy of @param base with all annotations of @param from
1056
1065
  * @TODO prototype based
1057
1066
  */
1058
- // REVISIT: TODO: inferred.elements should be linked
1059
1067
  function getCopyWithAnnos(from, base) {
1060
1068
  const result = { ...base }
1061
1069
  // REVISIT: we don't need to and hence should not handle annotations at runtime
@@ -1063,7 +1071,7 @@ function infer(originalQuery, model) {
1063
1071
  if (prop.startsWith('@')) result[prop] = from[prop]
1064
1072
  }
1065
1073
 
1066
- if (from.as && base.name !== from.as) Object.defineProperty(result, 'name', { value: from.as }) // TODO double check if this is needed
1074
+ if (from.as && base.name !== from.as) defineProperty(result, 'name', from.as) // TODO double check if this is needed
1067
1075
  // in subqueries we need the linked element if an outer query accesses it
1068
1076
  return Object.setPrototypeOf(result, base)
1069
1077
  }
@@ -1083,12 +1091,6 @@ function infer(originalQuery, model) {
1083
1091
  }
1084
1092
  }
1085
1093
 
1086
- /** returns the CSN definition for the given name from the model */
1087
- function getDefinition(name) {
1088
- if (!name) return null
1089
- return model.definitions[name]
1090
- }
1091
-
1092
1094
  function getDefinitionFromSources(sources, id) {
1093
1095
  return sources[id].definition
1094
1096
  }
@@ -170,7 +170,7 @@ class JoinTree {
170
170
  // find the correct query source
171
171
  if (
172
172
  r.queryArtifact === head.target ||
173
- r.queryArtifact === head.target.target /** might as well be a query for order by */
173
+ r.queryArtifact === head.target._target /** might as well be a query for order by */
174
174
  )
175
175
  node = r
176
176
  })
package/lib/utils.js CHANGED
@@ -40,32 +40,103 @@ function isCalculatedElement(def) {
40
40
 
41
41
  /**
42
42
  * Calculates the implicit table alias for a given string.
43
- *
43
+ *
44
44
  * Based on the last part of the string, the implicit alias is calculated
45
45
  * by taking the first character and prepending it with '$'.
46
46
  * A leading '$' is removed if the last part already starts with '$'.
47
- *
47
+ *
48
48
  * @example
49
49
  * getImplicitAlias('Books') => '$B'
50
50
  * getImplicitAlias('bookshop.Books') => '$B'
51
51
  * getImplicitAlias('bookshop.$B') => '$B'
52
- *
52
+ *
53
53
  * @param {string} str - The input string.
54
- * @returns {string}
54
+ * @returns {string}
55
55
  */
56
56
  function getImplicitAlias(str, useTechnicalAlias = true) {
57
57
  const index = str.lastIndexOf('.')
58
- if(useTechnicalAlias) {
58
+ if (useTechnicalAlias) {
59
59
  const postfix = (index != -1 ? str.substring(index + 1) : str).replace(/^\$/, '')[0] || /* str === '$' */ '$'
60
60
  return '$' + postfix
61
61
  }
62
62
  return index != -1 ? str.substring(index + 1) : str
63
63
  }
64
64
 
65
+ function defineProperty(obj, prop, value) {
66
+ return Object.defineProperty(obj, prop, {
67
+ value,
68
+ writable: true,
69
+ configurable: true,
70
+ })
71
+ }
72
+
73
+ /**
74
+ * Shared utility functions which operate dynamically on the model / query.
75
+ *
76
+ * @param {CSN.model} model
77
+ * @param {CQL} query
78
+ */
79
+ function getModelUtils(model, query) {
80
+ /**
81
+ * Returns the name of the localized entity for the given `definition`.
82
+ *
83
+ * If the query is `localized`, returns the name of the `localized` version of the `definition`.
84
+ * If there is no `localized` version of the `definition`, return the name of the `definition`
85
+ *
86
+ * @param {CSN.definition} definition
87
+ * @returns the name of the localized entity for the given `definition` or `definition.name`
88
+ */
89
+ function getLocalizedName(definition) {
90
+ if (!isLocalized(definition)) return definition.name
91
+ const view = getDefinition(`localized.${definition.name}`)
92
+ return view?.name || definition.name
93
+ }
94
+
95
+ /**
96
+ * Returns true if the definition shall be localized, in the context of the given query.
97
+ *
98
+ * If a given query is required to be translated, the query has
99
+ * the `.localized` property set to `true`. If that is the case,
100
+ * and the definition has not set the `@cds.localized` annotation
101
+ * to `false`, the given definition must be translated.
102
+ *
103
+ * @returns true if the given definition shall be localized
104
+ */
105
+ function isLocalized(definition) {
106
+ return (
107
+ query.SELECT?.localized &&
108
+ definition?.['@cds.localized'] !== false &&
109
+ !query.SELECT.forUpdate &&
110
+ !query.SELECT.forShareLock
111
+ )
112
+ }
113
+
114
+ /**
115
+ * Returns the (potentially localized) CSN definition for the given name from the model.
116
+ *
117
+ * @param {string} name - The name of the definition to retrieve.
118
+ * @returns {Object|null} The CSN definition or null if not found. The definition may be localized.
119
+ */
120
+ function getDefinition(name) {
121
+ if (!name) return null
122
+ const def = model.definitions[name]
123
+ if (!def || !isLocalized(def)) return def
124
+ return model.definitions[`localized.${def.name}`] || def
125
+ }
126
+
127
+ return {
128
+ getLocalizedName,
129
+ isLocalized,
130
+ getDefinition,
131
+ }
132
+ }
133
+
65
134
  // export the function to be used in other modules
66
135
  module.exports = {
67
136
  prettyPrintRef,
68
137
  isCalculatedOnRead,
69
138
  isCalculatedElement,
70
139
  getImplicitAlias,
140
+ defineProperty,
141
+ getModelUtils,
71
142
  }
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@cap-js/db-service",
3
- "version": "1.19.1",
3
+ "version": "2.0.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": {
7
7
  "type": "git",
8
- "url": "https://github.com/cap-js/cds-dbs"
8
+ "url": "git+https://github.com/cap-js/cds-dbs.git"
9
9
  },
10
10
  "bugs": {
11
11
  "url": "https://github.com/cap-js/cds-dbs/issues"
@@ -27,7 +27,7 @@
27
27
  "generic-pool": "^3.9.0"
28
28
  },
29
29
  "peerDependencies": {
30
- "@sap/cds": ">=7.9"
30
+ "@sap/cds": ">=9"
31
31
  },
32
- "license": "SEE LICENSE"
32
+ "license": "Apache-2.0"
33
33
  }