@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 +21 -0
- package/lib/SQLService.js +7 -3
- package/lib/cql-functions.js +1 -1
- package/lib/cqn2sql.js +14 -7
- package/lib/cqn4sql.js +20 -18
- package/lib/fill-in-keys.js +45 -53
- package/lib/infer/index.js +9 -15
- package/lib/search.js +1 -2
- package/package.json +1 -1
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
|
-
|
|
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/cql-functions.js
CHANGED
|
@@ -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}${
|
|
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':
|
|
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)
|
|
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
|
|
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?.
|
|
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?.
|
|
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)
|
|
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))
|
|
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]
|
|
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]
|
|
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')
|
|
1819
|
-
|
|
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
|
-
|
|
1832
|
-
|
|
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('_')]
|
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
|
@@ -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
|
|
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?.
|
|
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