@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 +12 -0
- package/lib/SQLService.js +7 -3
- package/lib/cqn2sql.js +4 -2
- package/lib/cqn4sql.js +5 -9
- package/lib/fill-in-keys.js +45 -53
- package/lib/infer/index.js +8 -14
- package/lib/search.js +1 -2
- package/package.json +1 -1
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
|
-
|
|
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.
|
|
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) &&
|
|
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}${
|
|
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
|
-
|
|
980
|
-
|
|
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?.
|
|
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
|
-
|
|
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('_')]
|
package/lib/fill-in-keys.js
CHANGED
|
@@ -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
|
|
8
|
-
if (
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
})
|
|
28
|
+
const fkeys4 = (e) => {
|
|
29
|
+
let fkeys = e._foreignKeys
|
|
30
|
+
return typeof fkeys === 'function' ? fkeys.call(e) : fkeys
|
|
31
|
+
}
|
|
35
32
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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(
|
|
72
|
-
|
|
73
|
-
|
|
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
|
}
|
package/lib/infer/index.js
CHANGED
|
@@ -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?.
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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