@cap-js/db-service 1.6.1 → 1.6.2

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,18 @@
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.2](https://github.com/cap-js/cds-dbs/compare/db-service-v1.6.1...db-service-v1.6.2) (2024-02-16)
8
+
9
+
10
+ ### Fixed
11
+
12
+ * ** `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))
13
+ * 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))
14
+ * dont insert structured elements ([#461](https://github.com/cap-js/cds-dbs/issues/461)) ([f3f688d](https://github.com/cap-js/cds-dbs/commit/f3f688d6ef45f9d42690c13eaf88ab004aa86ff9))
15
+ * 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))
16
+ * 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))
17
+ * 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))
18
+
7
19
  ## [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
20
 
9
21
 
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
  }
package/lib/cqn2sql.js CHANGED
@@ -479,10 +479,11 @@ class CQN2SQLRenderer {
479
479
 
480
480
  buffer += '"'
481
481
  } else {
482
+ if (val === undefined) continue
482
483
  if (elements[key]?.type in BINARY_TYPES) {
483
484
  val = transformBase64(val)
484
485
  }
485
- buffer += `${keyJSON}${val === undefined ? 'null' : JSON.stringify(val)}`
486
+ buffer += `${keyJSON}${JSON.stringify(val)}`
486
487
  }
487
488
  }
488
489
  buffer += '}'
@@ -650,7 +651,7 @@ class CQN2SQLRenderer {
650
651
  let sql = this.INSERT({ __proto__: q, INSERT: UPSERT })
651
652
  let keys = q.target?.keys
652
653
  if (!keys) return this.sql = sql
653
- keys = Object.keys(keys).filter(k => !keys[k].isAssociation)
654
+ keys = Object.keys(keys).filter(k => !keys[k].isAssociation && !keys[k].virtual)
654
655
 
655
656
  let updateColumns = q.UPSERT.entries ? Object.keys(q.UPSERT.entries[0]) : this.columns
656
657
  updateColumns = updateColumns.filter(c => {
@@ -692,6 +693,7 @@ class CQN2SQLRenderer {
692
693
  function _add(data, sql4) {
693
694
  for (let c in data) {
694
695
  if (!elements || (c in elements && !elements[c].virtual)) {
696
+ if (cds.unfold && elements?.[c].is_struct) continue // skip structs from universal csn
695
697
  columns.push({ name: c, sql: sql4(data[c]) })
696
698
  }
697
699
  }
package/lib/cqn4sql.js CHANGED
@@ -976,9 +976,8 @@ 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
980
+ && (model.meta.transformation === 'odata' || model.meta.unfolded?.includes('structs'))
982
981
  )
983
982
  }
984
983
  }
@@ -992,7 +991,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
992
991
  */
993
992
  function getElementForRef(ref, def) {
994
993
  return ref.reduce((prev, res) => {
995
- return prev?.elements?.[res] || prev?._target?.elements[res]
994
+ 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
995
  }, def)
997
996
  }
998
997
 
@@ -1032,7 +1031,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1032
1031
  if (!column) return column
1033
1032
  if (column.val || column.func || column.SELECT) return [column]
1034
1033
 
1035
- const structsAreUnfoldedAlready = model.meta.unfolded?.some(u => u === 'structs')
1034
+ const structsAreUnfoldedAlready = model.meta.unfolded?.includes('structs')
1036
1035
  let { baseName, columnAlias, tableAlias } = names
1037
1036
  const { exclude, replace } = excludeAndReplace || {}
1038
1037
  const { $refLinks, flatName, isJoinRelevant } = column
@@ -1046,7 +1045,6 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
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
@@ -1828,9 +1826,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1828
1826
  // first step is the association itself -> use it's name as it becomes the table alias
1829
1827
  result[i].ref.splice(0, 1, assocRefLink.alias)
1830
1828
  } else if (
1831
- Object.values(
1832
- targetSideRefLink.definition.elements || targetSideRefLink.definition._target.elements,
1833
- ).some(e => e === definition)
1829
+ definition.name in (targetSideRefLink.definition.elements || targetSideRefLink.definition._target.elements)
1834
1830
  ) {
1835
1831
  // first step is association which refers to its foreign key by dot notation
1836
1832
  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
  }
@@ -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.2",
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": {