@cap-js/db-service 1.6.1 → 1.6.3

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,27 @@
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.6.3](https://github.com/cap-js/cds-dbs/compare/db-service-v1.6.2...db-service-v1.6.3) (2024-02-20)
8
+
9
+
10
+ ### Fixed
11
+
12
+ * **`cqn4sql`:** be robust against `$self.<element>;` references ([#471](https://github.com/cap-js/cds-dbs/issues/471)) ([2921b0e](https://github.com/cap-js/cds-dbs/commit/2921b0e8ada33b172a001d89904893268e751efd))
13
+ * **`infer`:** Always use srv.model ([#451](https://github.com/cap-js/cds-dbs/issues/451)) ([41cf4a2](https://github.com/cap-js/cds-dbs/commit/41cf4a24cf2f5e2411be0dc647af6eb628a6d312))
14
+ * Throw 'new Error' instead of string on $search with multiple words ([#472](https://github.com/cap-js/cds-dbs/issues/472)) ([51be94d](https://github.com/cap-js/cds-dbs/commit/51be94d2333b4a4007f354c805d1b974b19d6d2d))
15
+
16
+ ## [1.6.2](https://github.com/cap-js/cds-dbs/compare/db-service-v1.6.1...db-service-v1.6.2) (2024-02-16)
17
+
18
+
19
+ ### Fixed
20
+
21
+ * ** `infer`:** unique alias for scoped subqueries ([#465](https://github.com/cap-js/cds-dbs/issues/465)) ([5dbaa8e](https://github.com/cap-js/cds-dbs/commit/5dbaa8e414102ee1dd0d8f76058c9eeff899666e))
22
+ * Allow only for array of arrays as data for plain SQL ([#449](https://github.com/cap-js/cds-dbs/issues/449)) ([22e1c43](https://github.com/cap-js/cds-dbs/commit/22e1c43c38709c6597be9e642619476338ef824a))
23
+ * dont insert structured elements ([#461](https://github.com/cap-js/cds-dbs/issues/461)) ([f3f688d](https://github.com/cap-js/cds-dbs/commit/f3f688d6ef45f9d42690c13eaf88ab004aa86ff9))
24
+ * ignore virtual keys in UPSERT([#463](https://github.com/cap-js/cds-dbs/issues/463)) ([49adbf3](https://github.com/cap-js/cds-dbs/commit/49adbf35f243d6365f84a8cf0193f028798aa366))
25
+ * INSERT entries containing undefined values ([#453](https://github.com/cap-js/cds-dbs/issues/453)) ([d3aad75](https://github.com/cap-js/cds-dbs/commit/d3aad7580f45ccde8528ddfa261f81d155354574))
26
+ * select without columns from unknown entity ([#466](https://github.com/cap-js/cds-dbs/issues/466)) ([eb857de](https://github.com/cap-js/cds-dbs/commit/eb857def41a89e9afe5e72686c3e55273c983b98))
27
+
7
28
  ## [1.6.1](https://github.com/cap-js/cds-dbs/compare/db-service-v1.6.0...db-service-v1.6.1) (2024-02-05)
8
29
 
9
30
 
package/lib/SQLService.js CHANGED
@@ -36,7 +36,7 @@ class SQLService extends DatabaseService {
36
36
  return Object.assign(acc, obj)
37
37
  }, {}),
38
38
  )
39
- const invalidColumns = columns.filter(c => !(c in elements))
39
+ const invalidColumns = columns.filter(c => !(c in elements))
40
40
 
41
41
  if (invalidColumns.length > 0) {
42
42
  cds.error(`STRICT MODE: Trying to ${kind} non existent columns (${invalidColumns})`)
@@ -114,7 +114,11 @@ class SQLService extends DatabaseService {
114
114
  * @type {Handler}
115
115
  */
116
116
  async onSELECT({ query, data }) {
117
- query.SELECT.expand = 'root'
117
+ if (query.target && !query.target._unresolved) {
118
+ // Will return multiple rows with objects inside
119
+ query.SELECT.expand = 'root'
120
+ }
121
+
118
122
  const { sql, values, cqn } = this.cqn2sql(query, data)
119
123
  let ps = await this.prepare(sql)
120
124
  let rows = await ps.all(values)
@@ -245,7 +249,7 @@ class SQLService extends DatabaseService {
245
249
  DEBUG?.(query, data)
246
250
  const ps = await this.prepare(query)
247
251
  const exec = this.hasResults(query) ? d => ps.all(d) : d => ps.run(d)
248
- if (Array.isArray(data) && typeof data[0] === 'object') return await Promise.all(data.map(exec))
252
+ if (Array.isArray(data) && Array.isArray(data[0])) return await Promise.all(data.map(exec))
249
253
  else return exec(data)
250
254
  } else return next()
251
255
  }
@@ -21,7 +21,7 @@ const StandardFunctions = {
21
21
  * @returns {string}
22
22
  */
23
23
  search: function (ref, arg) {
24
- if (!('val' in arg)) throw `Only single value arguments are allowed for $search`
24
+ if (!('val' in arg)) throw new Error(`Only single value arguments are allowed for $search`)
25
25
  const refs = ref.list || [ref],
26
26
  { toString } = ref
27
27
  return '(' + refs.map(ref2 => this.contains(this.tolower(toString(ref2)), this.tolower(arg))).join(' or ') + ')'
package/lib/cqn2sql.js CHANGED
@@ -31,6 +31,7 @@ class CQN2SQLRenderer {
31
31
  this.context = srv?.context || cds.context // Using srv.context is required due to stakeholders doing unmanaged txs without cds.context being set
32
32
  this.class = new.target // for IntelliSense
33
33
  this.class._init() // is a noop for subsequent calls
34
+ this.model = srv?.model
34
35
  }
35
36
 
36
37
  static _add_mixins(aspect, mixins) {
@@ -94,6 +95,10 @@ class CQN2SQLRenderer {
94
95
  return q.target ? q : cds_infer(q)
95
96
  }
96
97
 
98
+ cqn4sql(q) {
99
+ return cqn4sql(q, this.model)
100
+ }
101
+
97
102
  // CREATE Statements ------------------------------------------------
98
103
 
99
104
  /**
@@ -109,7 +114,7 @@ class CQN2SQLRenderer {
109
114
  this.sql =
110
115
  !query || target['@cds.persistence.table']
111
116
  ? `CREATE TABLE ${name} ( ${this.CREATE_elements(target.elements)} )`
112
- : `CREATE VIEW ${name} AS ${this.SELECT(cqn4sql(query))}`
117
+ : `CREATE VIEW ${name} AS ${this.SELECT(this.cqn4sql(query))}`
113
118
  this.values = []
114
119
  return
115
120
  }
@@ -479,10 +484,11 @@ class CQN2SQLRenderer {
479
484
 
480
485
  buffer += '"'
481
486
  } else {
487
+ if (val === undefined) continue
482
488
  if (elements[key]?.type in BINARY_TYPES) {
483
489
  val = transformBase64(val)
484
490
  }
485
- buffer += `${keyJSON}${val === undefined ? 'null' : JSON.stringify(val)}`
491
+ buffer += `${keyJSON}${JSON.stringify(val)}`
486
492
  }
487
493
  }
488
494
  buffer += '}'
@@ -612,7 +618,7 @@ class CQN2SQLRenderer {
612
618
  c => c in elements && !elements[c].virtual && !elements[c].isAssociation,
613
619
  ))
614
620
  this.sql = `INSERT INTO ${entity}${alias ? ' as ' + this.quote(alias) : ''} (${columns}) ${this.SELECT(
615
- cqn4sql(INSERT.as),
621
+ this.cqn4sql(INSERT.as),
616
622
  )}`
617
623
  this.entries = [this.values]
618
624
  return this.sql
@@ -650,7 +656,7 @@ class CQN2SQLRenderer {
650
656
  let sql = this.INSERT({ __proto__: q, INSERT: UPSERT })
651
657
  let keys = q.target?.keys
652
658
  if (!keys) return this.sql = sql
653
- keys = Object.keys(keys).filter(k => !keys[k].isAssociation)
659
+ keys = Object.keys(keys).filter(k => !keys[k].isAssociation && !keys[k].virtual)
654
660
 
655
661
  let updateColumns = q.UPSERT.entries ? Object.keys(q.UPSERT.entries[0]) : this.columns
656
662
  updateColumns = updateColumns.filter(c => {
@@ -692,6 +698,7 @@ class CQN2SQLRenderer {
692
698
  function _add(data, sql4) {
693
699
  for (let c in data) {
694
700
  if (!elements || (c in elements && !elements[c].virtual)) {
701
+ if (cds.unfold && elements?.[c].is_struct) continue // skip structs from universal csn
695
702
  columns.push({ name: c, sql: sql4(data[c]) })
696
703
  }
697
704
  }
@@ -817,8 +824,8 @@ class CQN2SQLRenderer {
817
824
  */
818
825
  ref({ ref }) {
819
826
  switch (ref[0]) {
820
- case '$now': return this.func({ func: 'session_context', args: [{ val: '$now', param: false }] }) // REVISIT: why do we need param: false here?
821
- case '$user': return this.func({ func: 'session_context', args: [{ val: '$user.'+ref[1]||'id', param: false }] }) // REVISIT: same here?
827
+ case '$now': return this.func({ func: 'session_context', args: [{ val: '$now', param: false }] }) // REVISIT: why do we need param: false here?
828
+ case '$user': return this.func({ func: 'session_context', args: [{ val: '$user.' + ref[1] || 'id', param: false }] }) // REVISIT: same here?
822
829
  default: return ref.map(r => this.quote(r)).join('.')
823
830
  }
824
831
  }
@@ -986,6 +993,6 @@ const _empty = a => !a || a.length === 0
986
993
  * @param {import('@sap/cds/apis/cqn').Query} q
987
994
  * @param {import('@sap/cds/apis/csn').CSN} m
988
995
  */
989
- module.exports = (q, m) => new CQN2SQLRenderer().render(cqn4sql(q, m), m)
996
+ module.exports = (q, m) => new CQN2SQLRenderer({ model: m }).render(cqn4sql(q, m))
990
997
  module.exports.class = CQN2SQLRenderer
991
998
  module.exports.classDefinition = CQN2SQLRenderer // class is a reserved typescript word
package/lib/cqn4sql.js CHANGED
@@ -43,7 +43,7 @@ const { pseudos } = require('./infer/pseudos')
43
43
  * @param {object} model
44
44
  * @returns {object} transformedQuery the transformed query
45
45
  */
46
- function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
46
+ function cqn4sql(originalQuery, model) {
47
47
  const inferred = infer(originalQuery, model)
48
48
  if (originalQuery.SELECT?.from.args && !originalQuery.joinTree) return inferred
49
49
 
@@ -122,7 +122,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
122
122
  primaryKey.list.push({ ref: [transformedFrom.as, k.name] })
123
123
  })
124
124
 
125
- const transformedSubquery = cqn4sql(subquery)
125
+ const transformedSubquery = cqn4sql(subquery, model)
126
126
 
127
127
  // replace where condition of original query with the transformed subquery
128
128
  // correlate UPDATE / DELETE query with subquery by primary key matches
@@ -918,7 +918,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
918
918
  if (q.SELECT.from.uniqueSubqueryAlias) return
919
919
  const last = q.SELECT.from.ref.at(-1)
920
920
  const uniqueSubqueryAlias = inferred.joinTree.addNextAvailableTableAlias(
921
- getLastStringSegment(last.id||last),
921
+ getLastStringSegment(last.id || last),
922
922
  originalQuery.outerQueries,
923
923
  )
924
924
  Object.defineProperty(q.SELECT.from, 'uniqueSubqueryAlias', { value: uniqueSubqueryAlias })
@@ -976,9 +976,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
976
976
  */
977
977
  function isManagedAssocInFlatMode(e) {
978
978
  return (
979
- (model.meta.transformation === 'odata' || model.meta.unfolded?.some(u => u === 'assocs')) &&
980
- e.isAssociation &&
981
- e.keys
979
+ e.isAssociation && e.keys && (model.meta.transformation === 'odata' || model.meta.unfolded?.includes('structs'))
982
980
  )
983
981
  }
984
982
  }
@@ -992,7 +990,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
992
990
  */
993
991
  function getElementForRef(ref, def) {
994
992
  return ref.reduce((prev, res) => {
995
- return prev?.elements?.[res] || prev?._target?.elements[res]
993
+ return (prev?.elements || prev?.foreignKeys)?.[res] || prev?._target?.elements[res] // PLEASE REVIEW: should we add the .foreignKey check here for the non-ucsn case?
996
994
  }, def)
997
995
  }
998
996
 
@@ -1032,21 +1030,21 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1032
1030
  if (!column) return column
1033
1031
  if (column.val || column.func || column.SELECT) return [column]
1034
1032
 
1035
- const structsAreUnfoldedAlready = model.meta.unfolded?.some(u => u === 'structs')
1033
+ const structsAreUnfoldedAlready = model.meta.unfolded?.includes('structs')
1036
1034
  let { baseName, columnAlias, tableAlias } = names
1037
1035
  const { exclude, replace } = excludeAndReplace || {}
1038
1036
  const { $refLinks, flatName, isJoinRelevant } = column
1039
1037
  let leafAssoc
1040
1038
  let element = $refLinks ? $refLinks[$refLinks.length - 1].definition : column
1041
1039
  if (isWildcard && element.type === 'cds.LargeBinary') return []
1042
- if (element.on && !element.keys) return [] // unmanaged doesn't make it into columns
1040
+ if (element.on && !element.keys)
1041
+ return [] // unmanaged doesn't make it into columns
1043
1042
  else if (element.virtual === true) return []
1044
1043
  else if (!isJoinRelevant && flatName) baseName = flatName
1045
1044
  else if (isJoinRelevant) {
1046
1045
  const leaf = column.$refLinks[column.$refLinks.length - 1]
1047
1046
  leafAssoc = [...column.$refLinks].reverse().find(link => link.definition.isAssociation)
1048
1047
  let elements
1049
- //> REVISIT: remove once UCSN is standard (no more .foreignKeys)
1050
1048
  elements = leafAssoc.definition.elements || leafAssoc.definition.foreignKeys
1051
1049
  if (elements && leaf.alias in elements) {
1052
1050
  element = leafAssoc.definition
@@ -1278,7 +1276,8 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1278
1276
  }
1279
1277
  } else {
1280
1278
  const { list } = token
1281
- if (list.every(e => e.val)) // no need for transformation
1279
+ if (list.every(e => e.val))
1280
+ // no need for transformation
1282
1281
  transformedTokenStream.push({ list })
1283
1282
  else transformedTokenStream.push({ list: getTransformedTokenStream(list, $baseLink) })
1284
1283
  }
@@ -1776,13 +1775,13 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1776
1775
  // naive assumption: if first step is the association itself, all following ref steps must be resolvable
1777
1776
  // within target `assoc.assoc.fk` -> `assoc.assoc_fk`
1778
1777
  else if (
1779
- lhs.$refLinks[0].definition ===
1778
+ lhs.$refLinks[0]?.definition ===
1780
1779
  getParentEntity(assocRefLink.definition).elements[assocRefLink.definition.name]
1781
1780
  )
1782
1781
  result[i].ref = [assocRefLink.alias, lhs.ref.slice(1).join('_')]
1783
1782
  // naive assumption: if the path starts with an association which is not the association from
1784
1783
  // which the on-condition originates, it must be a foreign key and hence resolvable in the source
1785
- else if (lhs.$refLinks[0].definition.target) result[i].ref = [result[i].ref.join('_')]
1784
+ else if (lhs.$refLinks[0]?.definition.target) result[i].ref = [result[i].ref.join('_')]
1786
1785
  }
1787
1786
  }
1788
1787
  if (backlink) {
@@ -1815,8 +1814,12 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1815
1814
  result.splice(i, 3, ...(wrapInXpr ? [asXpr(backlinkOnCondition)] : backlinkOnCondition))
1816
1815
  i += wrapInXpr ? 1 : backlinkOnCondition.length // skip inserted tokens
1817
1816
  } else if (lhs.ref) {
1818
- if (lhs.ref[0] === '$self') result[i].ref.splice(0, 1, targetSideRefLink.alias)
1819
- else if (lhs.ref.length > 1) {
1817
+ if (lhs.ref[0] === '$self') { // $self in ref of length > 1
1818
+ // if $self is followed by association, the alias of the association must be used
1819
+ if (lhs.$refLinks[1].definition.isAssociation) result[i].ref.splice(0, 1)
1820
+ // otherwise $self is replaced by the alias of the entity
1821
+ else result[i].ref.splice(0, 1, targetSideRefLink.alias)
1822
+ } else if (lhs.ref.length > 1) {
1820
1823
  if (
1821
1824
  !(lhs.ref[0] in pseudos.elements) &&
1822
1825
  lhs.ref[0] !== assocRefLink.alias &&
@@ -1828,9 +1831,8 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1828
1831
  // first step is the association itself -> use it's name as it becomes the table alias
1829
1832
  result[i].ref.splice(0, 1, assocRefLink.alias)
1830
1833
  } else if (
1831
- Object.values(
1832
- targetSideRefLink.definition.elements || targetSideRefLink.definition._target.elements,
1833
- ).some(e => e === definition)
1834
+ definition.name in
1835
+ (targetSideRefLink.definition.elements || targetSideRefLink.definition._target.elements)
1834
1836
  ) {
1835
1837
  // first step is association which refers to its foreign key by dot notation
1836
1838
  result[i].ref = [targetSideRefLink.alias, lhs.ref.join('_')]
@@ -4,78 +4,70 @@ const cds = require('@sap/cds')
4
4
  const propagateForeignKeys = require('@sap/cds/libx/_runtime/common/utils/propagateForeignKeys')
5
5
  const { enrichDataWithKeysFromWhere } = require('@sap/cds/libx/_runtime/common/utils/keys')
6
6
 
7
- const generateUUIDandPropagateKeys = (target, data, event) => {
8
- if (!data) return
9
- const keys = target.keys
10
- for (const key in keys) {
11
- const keyElement = keys[key]
12
- if (
13
- keyElement.type === 'cds.UUID' &&
14
- !data[key] && event === 'CREATE' &&
15
- !keyElement.parent.elements[keyElement._foreignKey4]?._isAssociationStrict
16
- ) {
17
- data[key] = cds.utils.uuid()
7
+ const assoc4 = (e) => e.own('$fk4', ()=> {
8
+ const old = e['@odata.foreignKey4']; if (old) return old
9
+ if (!e.parent || !e.name || !e.name.includes('_')) return
10
+ if (e.name === 'ID_texts') return
11
+ if (e.name === 'DraftAdministrativeData_DraftUUID') return 'DraftAdministrativeData'
12
+ if (e.name.startsWith('up__')) return 'up_' // assumes up_ is a reserved name
13
+ const {elements} = e.parent, path = e.name.split('_')
14
+ for (let [p]=path, a, i=1; i < path.length; p += '_'+path[i++]) {
15
+ if ((a = elements[p])) {
16
+ if (a.keys) {
17
+ const tail = path.slice(i)
18
+ if (a.keys.some (k => k.ref.every((r,i) => r === tail[i]))) {
19
+ // process.stdout.write('> resolved assoc: ' + a.name + ' for: ' + this.name + '\n')
20
+ return a.name
21
+ }
22
+ }
23
+ return // not an assoc, or not the one we're looking for
18
24
  }
19
25
  }
20
- const elements = target.elements
21
- for (const element in elements) {
22
- // if assoc keys are structured, do not ignore them, as they need to be flattened in propagateForeignKeys
23
- if (
24
- elements[element].key &&
25
- !(elements[element]._isAssociationStrict && elements[element].is2one && element in data)
26
- ) {
27
- continue
28
- }
26
+ })
29
27
 
30
- if (elements[element].is2one || elements[element].is2many) {
31
- // propagate own foreign keys to propagate further to sub data
32
- propagateForeignKeys(element, data, elements[element]._foreignKeys, elements[element].isComposition, {
33
- deleteAssocs: true,
34
- })
28
+ const fkeys4 = (e) => {
29
+ let fkeys = e._foreignKeys
30
+ return typeof fkeys === 'function' ? fkeys.call(e) : fkeys
31
+ }
35
32
 
36
- let subData = data[element]
37
- if (subData) {
38
- if (!Array.isArray(subData)) {
39
- subData = [subData]
40
- }
41
- for (const sub of subData) {
42
- // For subData the event is set to 'CREATE' as require UUID generation
43
- generateUUIDandPropagateKeys(elements[element]._target, sub, 'CREATE')
44
- }
45
- }
33
+ const generateUUIDandPropagateKeys = (entity, data, event) => {
34
+ if (event === 'CREATE') {
35
+ const keys = entity.keys
36
+ for (const k in keys)
37
+ if (keys[k].isUUID && !data[k] && !assoc4(keys[k])) //> skip key assocs, and foreign keys thereof
38
+ data[k] = cds.utils.uuid()
39
+ }
40
+ for (const each in entity.elements) {
41
+ const e = entity.elements[each]
42
+ // if assoc keys are structured, do not ignore them, as they need to be flattened in propagateForeignKeys
43
+ if (!e.isAssociation || e.key && (e.isComposition || e.is2many || !(each in data))) continue
44
+ // propagate own foreign keys to propagate further to sub data
45
+ propagateForeignKeys (each, data, fkeys4(e), e.isComposition, { deleteAssocs: true, })
46
+
47
+ let subData = data[each]; if (!subData) continue
48
+ if (!Array.isArray(subData)) subData = [subData]
49
+ for (const sub of subData) {
50
+ // For subData the event is set to 'CREATE' as require UUID generation
51
+ generateUUIDandPropagateKeys (e._target, sub, 'CREATE')
46
52
  }
47
53
  }
48
54
  }
49
55
 
50
- /**
51
- * @callback nextCallback
52
- * @param {Error|undefined} error
53
- * @returns {Promise<unknown>}
54
- */
55
56
 
56
- /**
57
- * @param {import('@sap/cds/apis/services').Request} req
58
- * @param {nextCallback} next
59
- */
60
57
  module.exports = async function fill_in_keys(req, next) {
61
58
  // REVISIT dummy handler until we have input processing
62
59
  if (!req.target || !this.model || req.target._unresolved) return next()
63
-
64
60
  if (req.event === 'UPDATE') {
65
61
  // REVISIT for deep update we need to inject the keys first
66
- enrichDataWithKeysFromWhere(req.data, req, this)
62
+ enrichDataWithKeysFromWhere (req.data, req, this)
67
63
  }
68
64
 
69
65
  // REVISIT no input processing for INPUT with rows/values
70
66
  if (!(req.query.INSERT?.rows || req.query.INSERT?.values)) {
71
- if (Array.isArray(req.data)) {
72
- for (const d of req.data) {
73
- generateUUIDandPropagateKeys(req.target, d, req.event)
74
- }
75
- } else {
76
- generateUUIDandPropagateKeys(req.target, req.data, req.event)
67
+ let {data} = req; if (!Array.isArray(data)) data = [data]
68
+ for (const d of data) {
69
+ generateUUIDandPropagateKeys (req.target, d, req.event)
77
70
  }
78
71
  }
79
-
80
72
  return next()
81
73
  }
@@ -22,7 +22,7 @@ for (const each in cdsTypes) cdsTypes[`cds.${each}`] = cdsTypes[each]
22
22
  * @param {import('@sap/cds/apis/csn').CSN} [model]
23
23
  * @returns {import('./cqn').Query} = q with .target and .elements
24
24
  */
25
- function infer(originalQuery, model = cds.context?.model || cds.model) {
25
+ function infer(originalQuery, model) {
26
26
  if (!model) throw new Error('Please specify a model')
27
27
  const inferred = typeof originalQuery === 'string' ? cds.parse.cql(originalQuery) : cds.ql.clone(originalQuery)
28
28
 
@@ -114,6 +114,8 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
114
114
  (ref.length === 1 ? first.match(/[^.]+$/)[0] : ref[ref.length - 1].id || ref[ref.length - 1])
115
115
  if (alias in querySources) throw new Error(`Duplicate alias "${alias}"`)
116
116
  querySources[alias] = target
117
+ const last = from.$refLinks.at(-1)
118
+ last.alias = alias
117
119
  } else if (from.args) {
118
120
  from.args.forEach(a => inferTarget(a, querySources))
119
121
  } else if (from.SELECT) {
@@ -543,8 +545,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
543
545
 
544
546
  const target = definition._target || column.$refLinks[i - 1].target
545
547
  if (element) {
546
- if($baseLink)
547
- rejectNonFkAccess(element)
548
+ if ($baseLink) rejectNonFkAccess(element)
548
549
  const $refLink = { definition: elements[id], target }
549
550
  column.$refLinks.push($refLink)
550
551
  } else if (firstStepIsSelf) {
@@ -660,9 +661,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
660
661
  // no unmanaged assoc in infix filter path
661
662
  if (!inExists && assoc.on)
662
663
  throw new Error(
663
- `"${assoc.name}" in path "${column.ref
664
- .map(idOnly)
665
- .join('.')}" must not be an unmanaged association`
664
+ `"${assoc.name}" in path "${column.ref.map(idOnly).join('.')}" must not be an unmanaged association`,
666
665
  )
667
666
  // no non-fk traversal in infix filter in non-exists path
668
667
  if (nextStep && !assoc.on && !isForeignKeyOf(nextStep, assoc))
@@ -729,7 +728,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
729
728
  // if overwritten/excluded omit from wildcard elements
730
729
  // in elements the names are already flat so consider the prefix
731
730
  // in excluding, the elements are addressed without the prefix
732
- if (!(name in elements || col.excluding?.some(e => e === k))) wildCardElements[name] = v
731
+ if (!(name in elements || col.excluding?.includes(k))) wildCardElements[name] = v
733
732
  })
734
733
  elements = { ...elements, ...wildCardElements }
735
734
  } else {
@@ -896,7 +895,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
896
895
  })
897
896
  mergePathIfNecessary(subPath, step)
898
897
  } else if (step.args || step.xpr) {
899
- const nestedProp = step.xpr ? 'xpr' : 'args'
898
+ const nestedProp = step.xpr ? 'xpr' : 'args'
900
899
  step[nestedProp].forEach(a => {
901
900
  mergePathsIntoJoinTree(a, subPath)
902
901
  })
@@ -1134,15 +1133,10 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
1134
1133
  * Returns true if e is a foreign key of assoc.
1135
1134
  * this function is also compatible with unfolded csn (UCSN),
1136
1135
  * where association do not have foreign keys anymore.
1137
- *
1138
- * @param {*} e
1139
- * @param {*} assoc
1140
- * @returns
1141
1136
  */
1142
1137
  function isForeignKeyOf(e, assoc) {
1143
- if(!assoc.isAssociation) return false
1144
- if(assoc.foreignKeys) return e in assoc.foreignKeys
1145
- return assoc.elements && e in assoc.elements
1138
+ if (!assoc.isAssociation) return false
1139
+ return e in (assoc.elements || assoc.foreignKeys)
1146
1140
  }
1147
1141
  const idOnly = ref => ref.id || ref
1148
1142
 
package/lib/search.js CHANGED
@@ -28,7 +28,6 @@ const getColumns = (
28
28
  entity,
29
29
  { onlyNames = false, removeIgnore = false, filterDraft = true, filterVirtual = false, keysOnly = false },
30
30
  ) => {
31
- const skipDraft = filterDraft && entity._isDraftEnabled
32
31
  const columns = []
33
32
  const elements = entity.elements
34
33
 
@@ -37,7 +36,7 @@ const getColumns = (
37
36
  if (element.isAssociation) continue
38
37
  if (filterVirtual && element.virtual) continue
39
38
  if (removeIgnore && element['@cds.api.ignore']) continue
40
- if (skipDraft && each in DRAFT_COLUMNS_UNION) continue
39
+ if (filterDraft && each in DRAFT_COLUMNS_UNION) continue
41
40
  if (keysOnly && !element.key) continue
42
41
  columns.push(onlyNames ? each : element)
43
42
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/db-service",
3
- "version": "1.6.1",
3
+ "version": "1.6.3",
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": {