@sap/cds 6.8.1 → 6.8.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 +20 -0
- package/README.md +1 -0
- package/bin/build/provider/mtx-extension/index.js +13 -1
- package/bin/build/util.js +1 -1
- package/bin/plugins.js +2 -1
- package/bin/version.js +3 -0
- package/lib/compile/for/lean_drafts.js +0 -1
- package/lib/req/request.js +2 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +2 -0
- package/libx/_runtime/cds-services/util/assert.js +28 -5
- package/libx/_runtime/common/aspects/any.js +3 -0
- package/libx/_runtime/common/generic/crud.js +1 -1
- package/libx/_runtime/common/utils/generateOnCond.js +18 -22
- package/libx/_runtime/db/expand/expandCQNToJoin.js +30 -24
- package/libx/_runtime/db/generic/rewrite.js +3 -0
- package/libx/_runtime/fiori/lean-draft.js +79 -47
- package/libx/_runtime/hana/customBuilder/CustomReferenceBuilder.js +2 -1
- package/libx/_runtime/hana/execute.js +18 -11
- package/libx/_runtime/remote/Service.js +19 -10
- package/libx/_runtime/remote/utils/client.js +4 -10
- package/libx/_runtime/sqlite/execute.js +2 -0
- package/libx/rest/middleware/update.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,26 @@
|
|
|
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
|
+
## Version 6.8.3 - 2023-06-13
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- `cds build` no longer reports CAP Java Classic runtime usage by mistake.
|
|
12
|
+
- `cds version` prints the local `@sap/cds` version, even if called from a different `@sap/cds` installation.
|
|
13
|
+
- User challenges handling in case of `cds.env.auth.restrict_all_services: false`
|
|
14
|
+
|
|
15
|
+
## Version 6.8.2 - 2023-05-26
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- EDMX texts for extended tenants based on `@sap/cds-mtx` now appear correctly again.
|
|
20
|
+
- `@assert.range` for DateTime/Date/Time/Timestamp
|
|
21
|
+
- Nested `$expand` OData query to the `texts` compiler-generated composition for entities with localized elements.
|
|
22
|
+
For example, similar OData requests `Entity?$expand=items($expand=item($expand=texts))` now should work as expected.
|
|
23
|
+
- `req.subject` would occasionally be incorrect when a query had been executed prior to it.
|
|
24
|
+
- cds plugins are also fetched from `devDependencies`
|
|
25
|
+
- `cds build` now correctly resolves complex models of mtx extension projects
|
|
26
|
+
|
|
7
27
|
## Version 6.8.1 - 2023-05-04
|
|
8
28
|
|
|
9
29
|
### Fixed
|
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ class MtxExtensionModuleBuilder extends BuildTaskHandlerInternal {
|
|
|
32
32
|
if (model) {
|
|
33
33
|
// extension CSN using parsed format
|
|
34
34
|
const options = { ...this.options(), flavor: 'parsed' }
|
|
35
|
-
const extModel = await cds.load(this.
|
|
35
|
+
const extModel = await cds.load(this._resolveExtensionFiles(model), options)
|
|
36
36
|
if (extModel.requires) {
|
|
37
37
|
extModel.requires.length = 0
|
|
38
38
|
}
|
|
@@ -79,6 +79,18 @@ class MtxExtensionModuleBuilder extends BuildTaskHandlerInternal {
|
|
|
79
79
|
await new ResourcesTarBuilder(this).writeTarFile(path.join(this.task.dest, 'extension.tgz'), destExt)
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
_resolveExtensionFiles(model) {
|
|
83
|
+
const node_modules = path.join(this.task.src, 'node_modules')
|
|
84
|
+
const paths = model['$sources'].reduce((acc, file) => {
|
|
85
|
+
if (file.startsWith(this.task.src) && !file.startsWith(node_modules)) {
|
|
86
|
+
acc.push(file)
|
|
87
|
+
}
|
|
88
|
+
return acc
|
|
89
|
+
}, [])
|
|
90
|
+
|
|
91
|
+
return paths
|
|
92
|
+
}
|
|
93
|
+
|
|
82
94
|
_lintExtModel(extModel, model) {
|
|
83
95
|
const linter = this._linter()
|
|
84
96
|
if (!linter) {
|
package/bin/build/util.js
CHANGED
|
@@ -88,7 +88,7 @@ async function isOldJavaStack(dirs) {
|
|
|
88
88
|
if (files.length > 0) {
|
|
89
89
|
return (await Promise.all(files.map(async file => {
|
|
90
90
|
const content = await fs.promises.readFile(file, 'utf-8')
|
|
91
|
-
return content && /<groupId>\s*com\.sap\.cloud\.servicesdk
|
|
91
|
+
return content && /<groupId>\s*com\.sap\.cloud\.servicesdk\.prov\s*<\/groupId>/.test(content)
|
|
92
92
|
}))).some(result => result)
|
|
93
93
|
}
|
|
94
94
|
return false
|
package/bin/plugins.js
CHANGED
|
@@ -14,7 +14,8 @@ module.exports = async function load_plugins (log = console.log) {
|
|
|
14
14
|
}
|
|
15
15
|
} else
|
|
16
16
|
try {
|
|
17
|
-
const
|
|
17
|
+
const pkg = require(cds.root + '/package.json')
|
|
18
|
+
const dependencies = { ...pkg.dependencies, ...(process.env.NODE_ENV !== 'production' && pkg.devDependencies) }
|
|
18
19
|
for (let each in dependencies) {
|
|
19
20
|
plugins.push(_load_plugin(each + '/cds-plugin'))
|
|
20
21
|
}
|
package/bin/version.js
CHANGED
|
@@ -57,6 +57,7 @@ function list_versions(args, options) { //NOSONAR
|
|
|
57
57
|
function info(o) {
|
|
58
58
|
const { npmGlobalModules } = require('./utils/modules');
|
|
59
59
|
const main = _findPackage (require.main.filename)
|
|
60
|
+
const sap_cds = require.resolve('@sap/cds/package.json', {paths:[process.cwd(), __dirname]})
|
|
60
61
|
return {
|
|
61
62
|
// REVISIT: Why do we need all these different hard-coded ways, including proliferation of arguments?
|
|
62
63
|
..._versions4(main, {}, true), // usually sap/cds-dk or sap/cds
|
|
@@ -66,6 +67,7 @@ function info(o) {
|
|
|
66
67
|
..._versions4(process.cwd(), {}, null, o),
|
|
67
68
|
..._versions4('..', {}, null, o),
|
|
68
69
|
..._findMTX(),
|
|
70
|
+
'@sap/cds': require(sap_cds).version, // ensure effective sap/cds version is listed
|
|
69
71
|
'Node.js': process.version,
|
|
70
72
|
'home': __dirname.slice(0,-4)
|
|
71
73
|
}
|
|
@@ -84,6 +86,7 @@ function _versions4 (pkg_name, info, parent, o={}) {
|
|
|
84
86
|
}
|
|
85
87
|
const pkg = require(path)
|
|
86
88
|
info[o.label || pkg.name] = pkg.version
|
|
89
|
+
// console.log(o.label || pkg.name, pkg.version, path)
|
|
87
90
|
if (!parent || o.all) for (let d in pkg.dependencies) { // recurse sap packages in dependencies...
|
|
88
91
|
if (!(d in info) && (d.startsWith('@sap/') || d.startsWith('@cap-js/'))) _versions4(d, info, pkg.name, o)
|
|
89
92
|
}
|
package/lib/req/request.js
CHANGED
|
@@ -106,6 +106,7 @@ class Request extends require('./event') {
|
|
|
106
106
|
// create key value pair without IsActiveEntity & adjust root target
|
|
107
107
|
const keys = {}
|
|
108
108
|
let id = item.id
|
|
109
|
+
if (!id) return
|
|
109
110
|
for (let j = 0; j < item?.where?.length; j = j + 4) {
|
|
110
111
|
const key = item.where[j].ref[0]
|
|
111
112
|
const value = item.where[j + 2].val
|
|
@@ -126,7 +127,7 @@ class Request extends require('./event') {
|
|
|
126
127
|
warn (...args) { return this._messages.add (3, ...args) }
|
|
127
128
|
error (...args) { return this._errors.add (4, ...args) }
|
|
128
129
|
reject (...args) {
|
|
129
|
-
if (args[0] === 401 && this._.req?.login) return this._.req.login()
|
|
130
|
+
if (args[0] === 401 && this._.req?.login && !this._.req?.logIn) return this._.req.login()
|
|
130
131
|
let e = this.error(...args)
|
|
131
132
|
if (!e.stack) Error.captureStackTrace (e = Object.assign(new Error,e), this.reject)
|
|
132
133
|
throw e
|
|
@@ -22,6 +22,8 @@ const metadata = service => {
|
|
|
22
22
|
const { 'cds.xt.ModelProviderService': mps } = cds.services
|
|
23
23
|
let edmx = mps
|
|
24
24
|
? await mps.getEdmx({ tenant, model: service.model, service: service.definition.name, locale })
|
|
25
|
+
: cds.mtx && cds.mtx.isExtended(tenant)
|
|
26
|
+
? await cds.mtx.getEdmx(tenant, service.definition.name, locale)
|
|
25
27
|
: cds.localize(
|
|
26
28
|
service.model,
|
|
27
29
|
locale,
|
|
@@ -112,10 +112,33 @@ const _checkISODateTime = value => (_checkString(value) && ISO_DATE_TIME_REGEX.t
|
|
|
112
112
|
|
|
113
113
|
const _checkISOTimestamp = value => (_checkString(value) && ISO_TIMESTAMP_REGEX.test(value)) || value instanceof Date
|
|
114
114
|
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
const _checkDateValue = (val, r1, r2) => {
|
|
116
|
+
const dateVal = new Date(val)
|
|
117
|
+
return (dateVal - new Date(r1)) * (dateVal - new Date(r2)) <= 0
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const _toDate = val => `2000-01-01T${val}Z`
|
|
121
|
+
|
|
122
|
+
const _checkInRange = (val, range, type) => {
|
|
123
|
+
switch (type) {
|
|
124
|
+
case 'cds.Date':
|
|
125
|
+
return _checkISODate(val) && _checkDateValue(val, range[0], range[1])
|
|
126
|
+
case 'cds.DateTime':
|
|
127
|
+
return _checkISODateTime(val) && _checkDateValue(val, range[0], range[1])
|
|
128
|
+
case 'cds.Timestamp':
|
|
129
|
+
return _checkISOTimestamp(val) && _checkDateValue(val, range[0], range[1])
|
|
130
|
+
case 'cds.Time':
|
|
131
|
+
return _checkISOTime(val) && _checkDateValue(_toDate(val), _toDate(range[0]), _toDate(range[1]))
|
|
132
|
+
default:
|
|
133
|
+
return (val - range[0]) * (val - range[1]) <= 0
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const _resolveCDSType = element => {
|
|
138
|
+
if (element.type.startsWith('cds.')) return element.type
|
|
139
|
+
if (!element.type) return
|
|
140
|
+
|
|
141
|
+
return _resolveCDSType(element.__proto__)
|
|
119
142
|
}
|
|
120
143
|
|
|
121
144
|
// process.env.CDS_ASSERT_FORMAT_FLAGS not official!
|
|
@@ -227,7 +250,7 @@ const _checkEnumElement = (element, value, errors, key, pathSegmentsInfo) => {
|
|
|
227
250
|
|
|
228
251
|
const _checkRangeElement = (element, value, errors, key, pathSegmentsInfo) => {
|
|
229
252
|
const rangeElements = element['@assert.range'] && !_getEnumElement(element) ? element['@assert.range'] : undefined
|
|
230
|
-
if (rangeElements && !_checkInRange(value, rangeElements)) {
|
|
253
|
+
if (rangeElements && !_checkInRange(value, rangeElements, _resolveCDSType(element))) {
|
|
231
254
|
const args = [value, ...element['@assert.range']]
|
|
232
255
|
errors.push(assertError({ code: ASSERT_RANGE, args }, element, value, key, pathSegmentsInfo))
|
|
233
256
|
}
|
|
@@ -40,11 +40,14 @@ const _relationHandler = relation => ({
|
|
|
40
40
|
if (newRelation) {
|
|
41
41
|
target[prop] = new Proxy(_exposeRelation(newRelation), _relationHandler(newRelation))
|
|
42
42
|
}
|
|
43
|
+
|
|
43
44
|
return target[prop]
|
|
44
45
|
}
|
|
46
|
+
|
|
45
47
|
target[prop] = path.reduce((relation, value) => relation[value] || relation.csn._relations[value], relation)
|
|
46
48
|
target[prop].path = path
|
|
47
49
|
}
|
|
50
|
+
|
|
48
51
|
return target[prop]
|
|
49
52
|
}
|
|
50
53
|
})
|
|
@@ -43,6 +43,7 @@ exports.impl = cds.service.impl(function () {
|
|
|
43
43
|
// - INSERT has no where clause to do this in one roundtrip
|
|
44
44
|
// - SELECT returns [] -> really empty collection or invalid path?
|
|
45
45
|
let pathExistsQuery
|
|
46
|
+
|
|
46
47
|
const { ref } = (req.query.INSERT && req.query.INSERT.into) || (req.query.SELECT && req.query.SELECT.from) || {}
|
|
47
48
|
// REVISIT: why is copy necessary?
|
|
48
49
|
if (ref && ref.length > 1) pathExistsQuery = SELECT(1).from({ ref: deepCopyArray(ref.slice(0, -1)) })
|
|
@@ -59,7 +60,6 @@ exports.impl = cds.service.impl(function () {
|
|
|
59
60
|
|
|
60
61
|
// if no keys available, select all columns so we can delete the singleton with same content
|
|
61
62
|
if (keyColumns.length) selectSingleton.columns(keyColumns)
|
|
62
|
-
|
|
63
63
|
const singleton = await cds.tx(req).run(selectSingleton)
|
|
64
64
|
if (!singleton) req.reject(404)
|
|
65
65
|
|
|
@@ -8,6 +8,7 @@ const _toRef = (alias, column) => {
|
|
|
8
8
|
const _adaptRefs = (onCond, path, { select, join }) => {
|
|
9
9
|
const _adaptEl = el => {
|
|
10
10
|
const ref = el.ref
|
|
11
|
+
|
|
11
12
|
if (ref) {
|
|
12
13
|
if (ref[0] === path.join('_') && ref[1]) {
|
|
13
14
|
return _toRef(select, ref.slice(1))
|
|
@@ -19,41 +20,34 @@ const _adaptRefs = (onCond, path, { select, join }) => {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
return _toRef(join, ref.slice(0))
|
|
22
|
-
} else if (el.xpr) {
|
|
23
|
-
return { xpr: el.xpr.map(_adaptEl) }
|
|
24
23
|
}
|
|
25
24
|
|
|
25
|
+
if (el.xpr) return { xpr: el.xpr.map(_adaptEl) }
|
|
26
26
|
return el
|
|
27
27
|
}
|
|
28
|
+
|
|
28
29
|
return onCond.map(_adaptEl)
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
const _args = (csnElement, path, aliases) => {
|
|
32
33
|
const onCond = csnElement.on
|
|
33
|
-
|
|
34
|
-
if (
|
|
35
|
-
return []
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (onCond.length < 3) {
|
|
39
|
-
return onCond
|
|
40
|
-
}
|
|
41
|
-
|
|
34
|
+
if (!onCond || onCond.length === 0) return []
|
|
35
|
+
if (onCond.length < 3 && !onCond[0]?.xpr) return onCond
|
|
42
36
|
if (!csnElement._isSelfManaged) return _adaptRefs(onCond, path, aliases)
|
|
43
37
|
|
|
44
38
|
// revert join and select aliases because of backlink
|
|
45
|
-
const
|
|
39
|
+
const mutOnCond = _newOnConditions(csnElement._backlink, [csnElement._backlink.name], {
|
|
46
40
|
select: aliases.join,
|
|
47
41
|
join: aliases.select
|
|
48
42
|
})
|
|
49
43
|
|
|
50
44
|
if (onCond.some(e => e === 'and')) {
|
|
51
|
-
// managed with ON-conditions must contain `$self`, which we replace with `
|
|
52
|
-
const
|
|
53
|
-
|
|
45
|
+
// managed with ON-conditions must contain `$self`, which we replace with `mutOnCond`
|
|
46
|
+
const onCondWithoutSelf = _adaptRefs(_onCondWithout$self(onCond), path, aliases)
|
|
47
|
+
mutOnCond.push('and', ...onCondWithoutSelf)
|
|
54
48
|
}
|
|
55
49
|
|
|
56
|
-
return
|
|
50
|
+
return mutOnCond
|
|
57
51
|
}
|
|
58
52
|
|
|
59
53
|
const _isSelfRef = e => e && e.ref && e.ref[0] === '$self'
|
|
@@ -64,23 +58,27 @@ const _onCondWithout$self = onCond => {
|
|
|
64
58
|
if (e === 'and') return _isSelfRef(on[i + 1]) || _isSelfRef(on[i + 3])
|
|
65
59
|
return on[i + 1] === '=' && (_isSelfRef(e) || _isSelfRef(on[i + 2]))
|
|
66
60
|
})
|
|
61
|
+
|
|
67
62
|
onCondWithoutSelf.splice(selfIndex, 4)
|
|
68
63
|
return onCondWithoutSelf
|
|
69
64
|
}
|
|
70
65
|
|
|
66
|
+
// this is only for 2one managed w/o on-conditions, i.e. no static values are possible
|
|
71
67
|
const _foreignToOn = (csnElement, path, { select, join }) => {
|
|
72
|
-
// this is only for 2one managed w/o ON-conditions i.e. no static values are possible
|
|
73
68
|
const on = []
|
|
69
|
+
|
|
74
70
|
for (const key of csnElement._foreignKeys) {
|
|
75
71
|
if (on.length !== 0) {
|
|
76
72
|
on.push('and')
|
|
77
73
|
}
|
|
74
|
+
|
|
78
75
|
const prefixChild = prefixForStruct(key.childElement)
|
|
79
76
|
const ref1 = _toRef(select, prefixChild + key.childElement.name)
|
|
80
77
|
const structPrefix = path.length > 1 ? path.slice(0, -1) : []
|
|
81
78
|
const ref2 = _toRef(join, [...structPrefix, key.parentElement.name])
|
|
82
79
|
on.push(ref1, '=', ref2)
|
|
83
80
|
}
|
|
81
|
+
|
|
84
82
|
return on
|
|
85
83
|
}
|
|
86
84
|
|
|
@@ -93,10 +91,8 @@ const _newOnConditions = (csnElement, path, aliases) => {
|
|
|
93
91
|
}
|
|
94
92
|
|
|
95
93
|
const getOnCond = (csnElement, path = [], aliases = { select: '', join: '' }) => {
|
|
96
|
-
const
|
|
97
|
-
return [{ xpr:
|
|
94
|
+
const onCond = _newOnConditions(csnElement, path, aliases)
|
|
95
|
+
return [{ xpr: onCond }]
|
|
98
96
|
}
|
|
99
97
|
|
|
100
|
-
module.exports = {
|
|
101
|
-
getOnCond
|
|
102
|
-
}
|
|
98
|
+
module.exports = { getOnCond }
|
|
@@ -117,11 +117,9 @@ class JoinCQNFromExpanded {
|
|
|
117
117
|
_createJoinCQNFromExpanded(SELECT, toManyTree, defaultLanguage) {
|
|
118
118
|
const joinArgs = SELECT.from.args
|
|
119
119
|
const isJoinOfTwoSelects = joinArgs?.every(a => a.SELECT)
|
|
120
|
-
|
|
121
120
|
const unionTableRef = this._getUnionTable(SELECT)
|
|
122
121
|
const unionTable = unionTableRef?.table
|
|
123
122
|
const tableAlias = this._getTableAlias(SELECT, toManyTree)
|
|
124
|
-
|
|
125
123
|
const readToOneCQN = this._getReadToOneCQN(SELECT, isJoinOfTwoSelects ? 'filterExpand' : tableAlias)
|
|
126
124
|
|
|
127
125
|
if (isJoinOfTwoSelects) {
|
|
@@ -133,6 +131,7 @@ class JoinCQNFromExpanded {
|
|
|
133
131
|
.forEach(c => {
|
|
134
132
|
mappings[c.as.replace(prefix, '')] = c.as
|
|
135
133
|
})
|
|
134
|
+
|
|
136
135
|
// expand to one
|
|
137
136
|
const entity = this._csn.definitions[joinArgs[0].SELECT.from.SET.args[1].SELECT.from.ref[0]]
|
|
138
137
|
this._addImplicitOrderBy(readToOneCQN, entity, tableAlias)
|
|
@@ -146,9 +145,7 @@ class JoinCQNFromExpanded {
|
|
|
146
145
|
const entity = this._getEntityForTable(table)
|
|
147
146
|
this._addImplicitOrderBy(readToOneCQN, entity, tableAlias)
|
|
148
147
|
if (unionTable) readToOneCQN[IS_UNION_DRAFT] = true
|
|
149
|
-
|
|
150
148
|
readToOneCQN[IS_ACTIVE] = isDraftTree ? this._isDraftTargetActive(table) : true
|
|
151
|
-
|
|
152
149
|
const givenColumns = readToOneCQN.columns
|
|
153
150
|
readToOneCQN.columns = []
|
|
154
151
|
if (entity['@cds.localized'] === false) defaultLanguage = true
|
|
@@ -500,11 +497,11 @@ class JoinCQNFromExpanded {
|
|
|
500
497
|
_expandedToFlat({ entity, givenColumns, readToOneCQN, tableAlias, toManyTree, defaultLanguage }) {
|
|
501
498
|
const toManyColumns = []
|
|
502
499
|
const mappings = this._getMappingObject(toManyTree)
|
|
503
|
-
|
|
504
500
|
const readToOneCQNCopy = getCqnCopy(readToOneCQN)
|
|
505
501
|
|
|
506
502
|
for (const column of givenColumns) {
|
|
507
503
|
let navigation
|
|
504
|
+
|
|
508
505
|
if (column.expand) {
|
|
509
506
|
navigation = getNavigationIfStruct(entity, tableAlias === column.ref[0] ? column.ref.slice(1) : column.ref)
|
|
510
507
|
if (this._skip(navigation && navigation._target)) continue
|
|
@@ -520,6 +517,7 @@ class JoinCQNFromExpanded {
|
|
|
520
517
|
// Expands with to one target can be processed directly
|
|
521
518
|
const navProp = column.ref[column.ref.length - 1]
|
|
522
519
|
const navTarget = entity.elements[navProp]
|
|
520
|
+
|
|
523
521
|
if (
|
|
524
522
|
entity._isDraftEnabled &&
|
|
525
523
|
navTarget._isAssociationStrict &&
|
|
@@ -529,6 +527,7 @@ class JoinCQNFromExpanded {
|
|
|
529
527
|
) {
|
|
530
528
|
mappings[navProp] = { [TO_ACTIVE]: true }
|
|
531
529
|
}
|
|
530
|
+
|
|
532
531
|
this._addJoinAndElements({
|
|
533
532
|
column,
|
|
534
533
|
entity,
|
|
@@ -539,7 +538,8 @@ class JoinCQNFromExpanded {
|
|
|
539
538
|
})
|
|
540
539
|
} else {
|
|
541
540
|
// No expand, directly add the column and its mapping.
|
|
542
|
-
|
|
541
|
+
const columnAliased = this._addAliasToColumn(column, entity, tableAlias, mappings)
|
|
542
|
+
readToOneCQN.columns.push(columnAliased)
|
|
543
543
|
|
|
544
544
|
// REVISIT required for other cqn properties as well?
|
|
545
545
|
this.adjustOrderBy(readToOneCQN.orderBy, mappings, column, tableAlias)
|
|
@@ -556,6 +556,7 @@ class JoinCQNFromExpanded {
|
|
|
556
556
|
}
|
|
557
557
|
}
|
|
558
558
|
}
|
|
559
|
+
|
|
559
560
|
// only as second step handle expand to many, or else keys might still be unknown
|
|
560
561
|
this._toMany({
|
|
561
562
|
entity,
|
|
@@ -658,7 +659,6 @@ class JoinCQNFromExpanded {
|
|
|
658
659
|
const extendedToManyTree = toManyTree.concat(column.ref[0] === parentAlias ? column.ref.slice(1) : column.ref)
|
|
659
660
|
const tableAlias = this._createAlias(extendedToManyTree.join(':'))
|
|
660
661
|
const target = this._getTarget(entity, column, parentAlias)
|
|
661
|
-
|
|
662
662
|
const name = column.ref[column.ref.length - 1]
|
|
663
663
|
const element = name && entity.elements[name]
|
|
664
664
|
|
|
@@ -765,8 +765,7 @@ class JoinCQNFromExpanded {
|
|
|
765
765
|
const givenColumns = column.expand.map(col => {
|
|
766
766
|
if (
|
|
767
767
|
activeTableRequired &&
|
|
768
|
-
col.ref &&
|
|
769
|
-
col.ref.length &&
|
|
768
|
+
col.ref?.length &&
|
|
770
769
|
(col.ref[0] === 'IsActiveEntity' || col.ref[0] === 'HasActiveEntity')
|
|
771
770
|
) {
|
|
772
771
|
return {
|
|
@@ -974,9 +973,7 @@ class JoinCQNFromExpanded {
|
|
|
974
973
|
*/
|
|
975
974
|
_addAliasToColumn(column, entity, tableAlias, mappings) {
|
|
976
975
|
// No identifier for this row entry or technical column
|
|
977
|
-
if (this._isAliasNotNeeded(column))
|
|
978
|
-
return column
|
|
979
|
-
}
|
|
976
|
+
if (this._isAliasNotNeeded(column)) return column
|
|
980
977
|
|
|
981
978
|
if (Array.isArray(column.xpr)) {
|
|
982
979
|
return this._buildNewAliasColumn(
|
|
@@ -995,6 +992,7 @@ class JoinCQNFromExpanded {
|
|
|
995
992
|
mappings
|
|
996
993
|
)
|
|
997
994
|
}
|
|
995
|
+
|
|
998
996
|
return this._buildNewAliasColumn(column, entity, tableAlias, mappings)
|
|
999
997
|
}
|
|
1000
998
|
|
|
@@ -1075,10 +1073,7 @@ class JoinCQNFromExpanded {
|
|
|
1075
1073
|
defaultLanguage,
|
|
1076
1074
|
readToOneCQNCopy
|
|
1077
1075
|
}) {
|
|
1078
|
-
if (toManyColumns.length === 0)
|
|
1079
|
-
return
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1076
|
+
if (toManyColumns.length === 0) return
|
|
1082
1077
|
this._addKeysIfNeeded({ entity, readToOneCQN, tableAlias })
|
|
1083
1078
|
|
|
1084
1079
|
for (const { column, parentAlias } of toManyColumns) {
|
|
@@ -1091,6 +1086,7 @@ class JoinCQNFromExpanded {
|
|
|
1091
1086
|
parentAlias,
|
|
1092
1087
|
defaultLanguage
|
|
1093
1088
|
})
|
|
1089
|
+
|
|
1094
1090
|
this._createJoinCQNFromExpanded(select, toManyTree.concat([column.ref[column.ref.length - 1]]), defaultLanguage)
|
|
1095
1091
|
}
|
|
1096
1092
|
}
|
|
@@ -1162,7 +1158,6 @@ class JoinCQNFromExpanded {
|
|
|
1162
1158
|
// eslint-disable-next-line complexity
|
|
1163
1159
|
_buildExpandedCQN({ column, entity, readToOneCQN, toManyTree, mappings, parentAlias, defaultLanguage }) {
|
|
1164
1160
|
const isUnion = !!readToOneCQN.from.SET
|
|
1165
|
-
|
|
1166
1161
|
const colRef = parentAlias === column.ref[0] ? column.ref.slice(1) : column.ref.slice(0)
|
|
1167
1162
|
const element = entity.elements[colRef[0]]
|
|
1168
1163
|
const colTarget = ensureUnlocalized(element.target)
|
|
@@ -1170,24 +1165,21 @@ class JoinCQNFromExpanded {
|
|
|
1170
1165
|
defaultLanguage ||
|
|
1171
1166
|
entity['@cds.localized'] === false ||
|
|
1172
1167
|
this._csn.definitions[colTarget]['@cds.localized'] === false
|
|
1173
|
-
|
|
1174
1168
|
const expandActive =
|
|
1175
1169
|
readToOneCQN[IS_ACTIVE] ||
|
|
1176
1170
|
(element._isAssociationStrict && !element['@odata.draft.enclosed']) ||
|
|
1177
1171
|
!this._csn.definitions[colTarget]._isDraftEnabled
|
|
1178
|
-
|
|
1179
1172
|
const ref = this._refFromRefByExpand(column.ref[0], colTarget, defaultLanguageThis, expandActive)
|
|
1180
1173
|
const tableAlias = this._createAlias(toManyTree.concat(colRef).join(':'))
|
|
1181
1174
|
const on = entity._relations[colRef[0]].join(tableAlias, 'filterExpand')
|
|
1182
1175
|
const filterExpand = this._getFilterExpandCQN(readToOneCQN, on, parentAlias, entity.keys)
|
|
1183
1176
|
const expandedEntity = this._csn.definitions[colTarget]
|
|
1184
1177
|
const joinColumns = this._getJoinColumnsFromOnAddToMapping(mappings[colRef[0]], parentAlias, on, entity)
|
|
1185
|
-
|
|
1186
1178
|
let cqn = {
|
|
1187
1179
|
from: {
|
|
1188
1180
|
join: 'inner',
|
|
1189
1181
|
args: [{ ref: [ref], as: tableAlias }, filterExpand],
|
|
1190
|
-
on
|
|
1182
|
+
on
|
|
1191
1183
|
}
|
|
1192
1184
|
}
|
|
1193
1185
|
|
|
@@ -1213,16 +1205,17 @@ class JoinCQNFromExpanded {
|
|
|
1213
1205
|
}
|
|
1214
1206
|
|
|
1215
1207
|
if (column.limit) throw getError(501, 'Pagination is not supported in expand')
|
|
1216
|
-
|
|
1217
1208
|
cqn = this._adaptWhereOrderBy(cqn, tableAlias)
|
|
1218
1209
|
|
|
1219
1210
|
if (isUnion) {
|
|
1220
1211
|
const cols = column.expand.filter(c => !c.expand && !(c.ref[0] in DRAFT_COLUMNS_MAP)).map(c => c.ref[0])
|
|
1212
|
+
|
|
1221
1213
|
// ensure the join columns are selected
|
|
1222
1214
|
for (const each of joinColumns) {
|
|
1223
1215
|
const col = each.ref[each.ref.length - 1]
|
|
1224
1216
|
if (!cols.includes(col)) cols.push(col)
|
|
1225
1217
|
}
|
|
1218
|
+
|
|
1226
1219
|
// ensure the foreign keys are selected in case of expand to one
|
|
1227
1220
|
for (const each of cqn.columns) {
|
|
1228
1221
|
if (each.expand) {
|
|
@@ -1240,14 +1233,17 @@ class JoinCQNFromExpanded {
|
|
|
1240
1233
|
)
|
|
1241
1234
|
const user = (cds.context && cds.context.user && cds.context.user.id) || 'anonymous'
|
|
1242
1235
|
const unionFrom = getCQNUnionFrom(cols, ref.replace(/_drafts$/, ''), ref, ks, user)
|
|
1236
|
+
|
|
1243
1237
|
for (const each of cqn.columns) {
|
|
1244
1238
|
if (!each.as) continue
|
|
1239
|
+
|
|
1245
1240
|
// replace val with ref
|
|
1246
1241
|
if (each.as === 'IsActiveEntity' || each.as === 'HasActiveEntity') {
|
|
1247
1242
|
delete each.val
|
|
1248
1243
|
each.ref = [tableAlias, each.as]
|
|
1249
1244
|
each.as = tableAlias + '_' + each.as
|
|
1250
1245
|
}
|
|
1246
|
+
|
|
1251
1247
|
// ensure the cast
|
|
1252
1248
|
if (
|
|
1253
1249
|
each.as.match(/IsActiveEntity$/) ||
|
|
@@ -1257,6 +1253,7 @@ class JoinCQNFromExpanded {
|
|
|
1257
1253
|
each.cast = { type: 'cds.Boolean' }
|
|
1258
1254
|
}
|
|
1259
1255
|
}
|
|
1256
|
+
|
|
1260
1257
|
const cs = cqn.columns
|
|
1261
1258
|
.filter(c => !c.expand && c.ref && c.ref[0] === tableAlias)
|
|
1262
1259
|
.map(c => ({ ref: [c.ref[1]] }))
|
|
@@ -1280,6 +1277,7 @@ class JoinCQNFromExpanded {
|
|
|
1280
1277
|
const sort = element.sort
|
|
1281
1278
|
if (element.args)
|
|
1282
1279
|
return { func: element.func, args: this._copyOrderBy(element.args, alias, expandedEntity), sort }
|
|
1280
|
+
|
|
1283
1281
|
const ref =
|
|
1284
1282
|
element.ref[0] === alias
|
|
1285
1283
|
? [...element.ref]
|
|
@@ -1288,6 +1286,7 @@ class JoinCQNFromExpanded {
|
|
|
1288
1286
|
: this._isPathExpressionToOne(element.ref, expandedEntity)
|
|
1289
1287
|
? [alias, ...element.ref]
|
|
1290
1288
|
: [alias, element.ref[1]]
|
|
1289
|
+
|
|
1291
1290
|
return (sort && { ref, sort }) || { ref }
|
|
1292
1291
|
})
|
|
1293
1292
|
}
|
|
@@ -1306,6 +1305,7 @@ class JoinCQNFromExpanded {
|
|
|
1306
1305
|
where: where
|
|
1307
1306
|
}
|
|
1308
1307
|
}
|
|
1308
|
+
|
|
1309
1309
|
return {
|
|
1310
1310
|
xpr: ['case', 'when', hasDraftQuery, 'IS NOT NULL', 'then', 'true', 'else', 'false', 'end'],
|
|
1311
1311
|
as: 'HasDraftEntity',
|
|
@@ -1370,6 +1370,7 @@ class JoinCQNFromExpanded {
|
|
|
1370
1370
|
outerColumns.push(...outerCols)
|
|
1371
1371
|
continue
|
|
1372
1372
|
}
|
|
1373
|
+
|
|
1373
1374
|
if (typeof entry === 'object' && entry.ref && entry.ref[0] === 'filterExpand') {
|
|
1374
1375
|
columns.push(this._getColumnObjectForFilterExpand(readToOneCQN, parentAlias, entry.ref[1]))
|
|
1375
1376
|
outerColumns.push({ ref: [entry.ref[1]] })
|
|
@@ -1466,6 +1467,7 @@ class JoinCQNFromExpanded {
|
|
|
1466
1467
|
}
|
|
1467
1468
|
struct = current.elements[key.replace(parentAlias + '_', '')]
|
|
1468
1469
|
}
|
|
1470
|
+
|
|
1469
1471
|
// build value for spreading (cf. mapping[GET_KEY_VALUE])
|
|
1470
1472
|
value = []
|
|
1471
1473
|
for (const k in struct.elements) {
|
|
@@ -1482,7 +1484,8 @@ class JoinCQNFromExpanded {
|
|
|
1482
1484
|
|
|
1483
1485
|
_addColumNames(entity, parentAlias, columnNames) {
|
|
1484
1486
|
for (const keyName in entity.keys) {
|
|
1485
|
-
|
|
1487
|
+
const key = entity.keys[keyName]
|
|
1488
|
+
if (key.is2one || key.is2many) continue
|
|
1486
1489
|
const columnNameAlt = keyName === 'IsActiveEntity' ? 'IsActiveEntity' : `${parentAlias}_${keyName}`
|
|
1487
1490
|
if (!columnNames.includes(columnNameAlt)) {
|
|
1488
1491
|
columnNames.push(columnNameAlt)
|
|
@@ -1511,6 +1514,7 @@ class JoinCQNFromExpanded {
|
|
|
1511
1514
|
columns.push(...this._getJoinColumnsFromOnAddToMapping(mapping, parentAlias, entry.xpr, entity))
|
|
1512
1515
|
continue
|
|
1513
1516
|
}
|
|
1517
|
+
|
|
1514
1518
|
if (typeof entry === 'object' && entry.ref && entry.ref[0] !== 'filterExpand') {
|
|
1515
1519
|
const as = entry.ref.join('_')
|
|
1516
1520
|
columns.push({
|
|
@@ -1530,6 +1534,7 @@ class JoinCQNFromExpanded {
|
|
|
1530
1534
|
|
|
1531
1535
|
for (const key of keyList) {
|
|
1532
1536
|
const parts = key.split('_')
|
|
1537
|
+
|
|
1533
1538
|
// For draft-enabled entities, associations may not take over 'IsActiveEntity', e.g.
|
|
1534
1539
|
// when a draft points to an active entity
|
|
1535
1540
|
if (parts[parts.length - 1] !== 'IsActiveEntity') {
|
|
@@ -1577,7 +1582,6 @@ class JoinCQNFromExpanded {
|
|
|
1577
1582
|
this._addMissingJoinElements(columns, joinColumns)
|
|
1578
1583
|
this._addMissingKeyColumns(columns, tableAlias, keys, isActive, entity)
|
|
1579
1584
|
this._addMissingParentKeyColumns(columns, 'filterExpand', parentKeys, isActive)
|
|
1580
|
-
|
|
1581
1585
|
return columns
|
|
1582
1586
|
}
|
|
1583
1587
|
|
|
@@ -1606,11 +1610,13 @@ class JoinCQNFromExpanded {
|
|
|
1606
1610
|
columns.push(this._createCalculatedBooleanColumn('IsActiveEntity', isActive))
|
|
1607
1611
|
return
|
|
1608
1612
|
}
|
|
1613
|
+
|
|
1609
1614
|
if (isActive) {
|
|
1610
1615
|
if (columnName === 'HasActiveEntity') {
|
|
1611
1616
|
columns.push(this._createCalculatedBooleanColumn('HasActiveEntity', false))
|
|
1612
1617
|
return
|
|
1613
1618
|
}
|
|
1619
|
+
|
|
1614
1620
|
if (columnName === 'HasDraftEntity') {
|
|
1615
1621
|
columns.push(this._getHasDraftEntityXpr(entity, tableAlias))
|
|
1616
1622
|
return
|
|
@@ -14,6 +14,9 @@ const _isLinked = req => {
|
|
|
14
14
|
function handler(req) {
|
|
15
15
|
if (typeof req.query === 'string') return
|
|
16
16
|
|
|
17
|
+
// invoke req.subject before it gets modified
|
|
18
|
+
req.subject
|
|
19
|
+
|
|
17
20
|
if (!this.model) {
|
|
18
21
|
// best-effort rewrite of path in from
|
|
19
22
|
req.query = cqn2cqn4sql(req.query, { definitions: {} }, { service: this })
|
|
@@ -25,6 +25,30 @@ const DRAFT_ADMIN_ELEMENTS = [
|
|
|
25
25
|
'DraftIsProcessedByMe'
|
|
26
26
|
]
|
|
27
27
|
|
|
28
|
+
const _fillIsActiveEntity = (row, IsActiveEntity, target) => {
|
|
29
|
+
if (target.drafts) row.IsActiveEntity = IsActiveEntity
|
|
30
|
+
for (const key in target.associations) {
|
|
31
|
+
const prop = row[key]
|
|
32
|
+
if (!prop) continue
|
|
33
|
+
const el = target.elements[key]
|
|
34
|
+
const childIsActiveEntity = el._target.isDraft ? IsActiveEntity : true
|
|
35
|
+
if (Array.isArray(prop)) prop.map(r => _fillIsActiveEntity(r, childIsActiveEntity, el._target))
|
|
36
|
+
else if (typeof prop === 'object') _fillIsActiveEntity(prop, childIsActiveEntity, el._target)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// REVISIT: should not be necessary
|
|
41
|
+
const _runWithContext = (srv, req, obj) => {
|
|
42
|
+
const r = new cds.Request(obj)
|
|
43
|
+
r.event // invoke getter
|
|
44
|
+
r._ = Object.assign(r._, req._)
|
|
45
|
+
if (req.getUriInfo) r.getUriInfo = () => req.getUriInfo()
|
|
46
|
+
if (req.getUrlObject) r.getUrlObject = () => req.getUrlObject()
|
|
47
|
+
r._.params = req.params
|
|
48
|
+
r._.query = req.query
|
|
49
|
+
return srv.dispatch(r)
|
|
50
|
+
}
|
|
51
|
+
|
|
28
52
|
/// It's important to wait for the completion of all promises, otherwise a rollback might happen too soon
|
|
29
53
|
const _promiseAll = async array => {
|
|
30
54
|
const results = await Promise.allSettled(array)
|
|
@@ -109,16 +133,15 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
109
133
|
(query.DELETE && 'DELETE') ||
|
|
110
134
|
req.event
|
|
111
135
|
_req.target = query._target
|
|
112
|
-
_req._ = Object.assign({}, req._ || {}) // don't share the same `_` object
|
|
113
|
-
_req._.params = req.params
|
|
114
136
|
_req.params = req.params
|
|
137
|
+
_req._ = Object.assign({}, req._ || {})
|
|
138
|
+
_req._.params = req.params
|
|
115
139
|
_req._.query = query
|
|
140
|
+
const props = ['_isRest', '_isOData', 'isConcurrentResource', 'isConditional', 'validateEtag']
|
|
141
|
+
props.forEach(p => {
|
|
142
|
+
if (req[p]) _req.p = req[p]
|
|
143
|
+
})
|
|
116
144
|
_req._ = req._
|
|
117
|
-
_req._isRest = req._isRest
|
|
118
|
-
_req._isOData = req._isOData
|
|
119
|
-
_req.isConcurrentResource = req.isConcurrentResource
|
|
120
|
-
_req.isConditional = req.isConditional
|
|
121
|
-
_req.validateEtag = req.validateEtag
|
|
122
145
|
const cqnData = _req.query.UPDATE?.data || _req.query.INSERT?.entries?.[0]
|
|
123
146
|
if (cqnData) _req.data = cqnData // must point to the same object
|
|
124
147
|
Object.defineProperty(_req, '_messages', {
|
|
@@ -369,17 +392,17 @@ const Read = {
|
|
|
369
392
|
drafts = []
|
|
370
393
|
}
|
|
371
394
|
}
|
|
372
|
-
Read.merge(query._target, actives, drafts, (row, other) =>
|
|
373
|
-
other
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
)
|
|
395
|
+
Read.merge(query._target, actives, drafts, (row, other) => {
|
|
396
|
+
if (other) Object.assign(row, other, { HasActiveEntity: false, HasDraftEntity: true })
|
|
397
|
+
else
|
|
398
|
+
Object.assign(row, {
|
|
399
|
+
HasActiveEntity: false,
|
|
400
|
+
HasDraftEntity: false,
|
|
401
|
+
DraftAdministrativeData: null,
|
|
402
|
+
DraftAdministrativeData_DraftUUID: null
|
|
403
|
+
})
|
|
404
|
+
_fillIsActiveEntity(row, true, query._target)
|
|
405
|
+
})
|
|
383
406
|
return _requested(actives, query)
|
|
384
407
|
},
|
|
385
408
|
unchanged: async function (run, query) {
|
|
@@ -421,12 +444,12 @@ const Read = {
|
|
|
421
444
|
cds.context.user.id
|
|
422
445
|
)
|
|
423
446
|
const drafts = await run(draftsQuery)
|
|
424
|
-
Read.merge(query._target, drafts, [], row =>
|
|
447
|
+
Read.merge(query._target, drafts, [], row => {
|
|
425
448
|
Object.assign(row, {
|
|
426
|
-
HasDraftEntity: false
|
|
427
|
-
IsActiveEntity: false
|
|
449
|
+
HasDraftEntity: false
|
|
428
450
|
})
|
|
429
|
-
|
|
451
|
+
_fillIsActiveEntity(row, false, query._drafts._target)
|
|
452
|
+
})
|
|
430
453
|
return _requested(drafts, query)
|
|
431
454
|
},
|
|
432
455
|
all: async function (run, query) {
|
|
@@ -468,18 +491,17 @@ const Read = {
|
|
|
468
491
|
const count = isFirstPage ? ownNewDrafts.length + (isCount ? actives[0]?.$count : actives.$count) : actives.$count
|
|
469
492
|
if (isCount) return { $count: count }
|
|
470
493
|
|
|
471
|
-
Read.merge(query._target, ownDrafts, [], row =>
|
|
494
|
+
Read.merge(query._target, ownDrafts, [], row => {
|
|
472
495
|
Object.assign(row, {
|
|
473
|
-
IsActiveEntity: false,
|
|
474
496
|
HasDraftEntity: false
|
|
475
497
|
})
|
|
476
|
-
|
|
498
|
+
_fillIsActiveEntity(row, false, query._drafts._target)
|
|
499
|
+
})
|
|
477
500
|
Read.delete(query._target, actives, ownEditDrafts)
|
|
478
501
|
const otherEditDrafts = await Read.complementaryDrafts(run, query, actives)
|
|
479
502
|
Read.merge(query._target, actives, otherEditDrafts, (row, other) => {
|
|
480
503
|
if (other) {
|
|
481
504
|
Object.assign(row, {
|
|
482
|
-
IsActiveEntity: true,
|
|
483
505
|
HasDraftEntity: true,
|
|
484
506
|
HasActiveEntity: false,
|
|
485
507
|
DraftAdministrativeData_DraftUUID: other.DraftAdministrativeData_DraftUUID,
|
|
@@ -487,13 +509,13 @@ const Read = {
|
|
|
487
509
|
})
|
|
488
510
|
} else {
|
|
489
511
|
Object.assign(row, {
|
|
490
|
-
IsActiveEntity: true,
|
|
491
512
|
HasDraftEntity: false,
|
|
492
513
|
HasActiveEntity: false,
|
|
493
514
|
DraftAdministrativeData_DraftUUID: null,
|
|
494
515
|
DraftAdministrativeData: null
|
|
495
516
|
})
|
|
496
517
|
}
|
|
518
|
+
_fillIsActiveEntity(row, true, query._target)
|
|
497
519
|
})
|
|
498
520
|
const res = isFirstPage ? [...ownNewDrafts, ...ownEditDrafts, ...actives] : actives
|
|
499
521
|
if (query.SELECT.count) res.$count = count
|
|
@@ -519,11 +541,11 @@ const Read = {
|
|
|
519
541
|
const actives = drafts.length
|
|
520
542
|
? await run(query.where(Read.whereIn(query._target, drafts)))
|
|
521
543
|
: Object.assign([], { $count: 0 })
|
|
522
|
-
Read.merge(query._target, actives, drafts, (row, other) =>
|
|
523
|
-
other
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
)
|
|
544
|
+
Read.merge(query._target, actives, drafts, (row, other) => {
|
|
545
|
+
if (other) Object.assign(row, other, { HasDraftEntity: true, HasActiveEntity: false })
|
|
546
|
+
else Object.assign({ HasDraftEntity: false, HasActiveEntity: false })
|
|
547
|
+
_fillIsActiveEntity(row, true, query._target)
|
|
548
|
+
})
|
|
527
549
|
return _requested(actives, query)
|
|
528
550
|
},
|
|
529
551
|
unsavedChangesByAnotherUser: async function (run, query) {
|
|
@@ -834,14 +856,14 @@ async function onNew(req) {
|
|
|
834
856
|
let DraftUUID
|
|
835
857
|
if (isRoot) DraftUUID = cds.utils.uuid()
|
|
836
858
|
else {
|
|
837
|
-
const rootData = await this
|
|
838
|
-
SELECT.one(req.query.INSERT.into.ref[0].id)
|
|
859
|
+
const rootData = await _runWithContext(this, req, {
|
|
860
|
+
query: SELECT.one(req.query.INSERT.into.ref[0].id)
|
|
839
861
|
.columns([
|
|
840
862
|
{ ref: ['DraftAdministrativeData_DraftUUID'] },
|
|
841
863
|
{ ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] }
|
|
842
864
|
])
|
|
843
865
|
.where(req.query.INSERT.into.ref[0].where)
|
|
844
|
-
)
|
|
866
|
+
})
|
|
845
867
|
if (!rootData) req.reject(404)
|
|
846
868
|
if (rootData.DraftAdministrativeData?.InProcessByUser !== req.user.id)
|
|
847
869
|
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [rootData.DraftAdministrativeData.InProcessByUser])
|
|
@@ -889,7 +911,7 @@ async function onNew(req) {
|
|
|
889
911
|
delete draftData.IsActiveEntity
|
|
890
912
|
const draftCQN = INSERT.into(req.target).entries(draftData)
|
|
891
913
|
|
|
892
|
-
await _promiseAll([cds.run(adminDataCQN), this
|
|
914
|
+
await _promiseAll([cds.run(adminDataCQN), _runWithContext(this, req, { query: draftCQN })])
|
|
893
915
|
req._.readAfterWrite = true
|
|
894
916
|
return { ...draftData, IsActiveEntity: false }
|
|
895
917
|
}
|
|
@@ -935,12 +957,12 @@ async function onEdit(req) {
|
|
|
935
957
|
// It's not possible to use `FOR UPDATE` in HANA if the view contains joins/unions. Unfortunately, we can't resolve the table entity
|
|
936
958
|
// because we must trigger the app-service request on the target entity (which could be delegated to a remote service).
|
|
937
959
|
// The best we can do is to catch a potential error
|
|
938
|
-
await this
|
|
960
|
+
await _runWithContext(this, req, { query: activeCheck }).catch(_ => {})
|
|
939
961
|
|
|
940
962
|
const [res, draft] = await _promiseAll([
|
|
941
|
-
this
|
|
963
|
+
_runWithContext(this, req, { query: activeCQN }),
|
|
942
964
|
// no user check must be done here...
|
|
943
|
-
this
|
|
965
|
+
_runWithContext(this, req, { query: existingDraft })
|
|
944
966
|
])
|
|
945
967
|
|
|
946
968
|
if (!res) req.reject(404)
|
|
@@ -952,7 +974,7 @@ async function onEdit(req) {
|
|
|
952
974
|
for (const key in req.target.drafts.keys) keys[key] = res[key]
|
|
953
975
|
await _promiseAll([
|
|
954
976
|
DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID }),
|
|
955
|
-
this
|
|
977
|
+
_runWithContext(this, req, { query: DELETE.from(req.target.drafts).where(keys) })
|
|
956
978
|
])
|
|
957
979
|
}
|
|
958
980
|
|
|
@@ -973,7 +995,7 @@ async function onEdit(req) {
|
|
|
973
995
|
res.DraftAdministrativeData_DraftUUID = DraftUUID
|
|
974
996
|
res.HasActiveEntity = true
|
|
975
997
|
delete res.DraftAdministrativeData
|
|
976
|
-
await this
|
|
998
|
+
await _runWithContext(this, req, { query: INSERT.into(targetDraft).entries(res) })
|
|
977
999
|
|
|
978
1000
|
// REVISIT: we need to use okra API here because it must be set in the batched request
|
|
979
1001
|
// status code must be set in handler to allow overriding for FE V2
|
|
@@ -995,18 +1017,28 @@ async function onCancel(req) {
|
|
|
995
1017
|
])
|
|
996
1018
|
// do not add InProcessByUser restriction
|
|
997
1019
|
Object.defineProperty(draftDelete, '_draftParams', { value: draftParams, enumerable: false })
|
|
998
|
-
const draft = await this
|
|
1020
|
+
const draft = await _runWithContext(this, req, { query: draftDelete })
|
|
999
1021
|
if (draftParams.IsActiveEntity === false && !draft) req.reject(404)
|
|
1000
1022
|
if (draft && draft.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
|
|
1001
1023
|
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [draft.DraftAdministrativeData?.InProcessByUser])
|
|
1002
|
-
const
|
|
1024
|
+
const queries = !draft ? [] : [_runWithContext(this, req, { query: DELETE.from({ ref: req.query.DELETE.from.ref }) })]
|
|
1003
1025
|
if (draft && req.target['@Common.DraftRoot.ActivationAction'])
|
|
1004
1026
|
// only for draft root
|
|
1005
|
-
|
|
1027
|
+
queries.push(
|
|
1006
1028
|
DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: draft.DraftAdministrativeData_DraftUUID })
|
|
1007
1029
|
)
|
|
1008
|
-
|
|
1009
|
-
|
|
1030
|
+
else
|
|
1031
|
+
queries.push(
|
|
1032
|
+
UPDATE('Draft.DraftAdministrativeData')
|
|
1033
|
+
.data({
|
|
1034
|
+
InProcessByUser: cds.context.user.id,
|
|
1035
|
+
LastChangedByUser: cds.context.user.id,
|
|
1036
|
+
LastChangeDateTime: cds.context.timestamp.toISOString()
|
|
1037
|
+
})
|
|
1038
|
+
.where({ DraftUUID: draft.DraftAdministrativeData_DraftUUID })
|
|
1039
|
+
)
|
|
1040
|
+
if (draftParams.IsActiveEntity) queries.push(_runWithContext(this, req, { query: DELETE.from({ ref: activeRef }) }))
|
|
1041
|
+
await _promiseAll(queries)
|
|
1010
1042
|
return req.data
|
|
1011
1043
|
}
|
|
1012
1044
|
|
|
@@ -1026,7 +1058,7 @@ async function onPrepare(req) {
|
|
|
1026
1058
|
.columns(keys)
|
|
1027
1059
|
.where(where)
|
|
1028
1060
|
Object.defineProperty(draftQuery, '_draftParams', { value: draftParams, enumerable: false })
|
|
1029
|
-
const data = await this
|
|
1061
|
+
const data = await _runWithContext(this, req, { query: draftQuery })
|
|
1030
1062
|
if (!data) req.reject(404)
|
|
1031
1063
|
if (data.DraftAdministrativeData?.InProcessByUser !== req.user.id)
|
|
1032
1064
|
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [data.DraftAdministrativeData?.InProcessByUser])
|
|
@@ -25,7 +25,8 @@ class CustomReferenceBuilder extends ReferenceBuilder {
|
|
|
25
25
|
return
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
const sql = ref.map(el => this._quoteElement(el)).join('.')
|
|
29
|
+
this._outputObj.sql.push(sql)
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -58,6 +58,7 @@ function _getProcedureName(sql) {
|
|
|
58
58
|
function _hdbGetResultForProcedure(rows, args, outParameters) {
|
|
59
59
|
// on hdb, rows already contains results for scalar params
|
|
60
60
|
const result = rows || {}
|
|
61
|
+
|
|
61
62
|
// merge table output params into scalar params
|
|
62
63
|
if (args && args.length && outParameters) {
|
|
63
64
|
const params = outParameters.filter(md => !(md.PARAMETER_NAME in rows))
|
|
@@ -65,6 +66,7 @@ function _hdbGetResultForProcedure(rows, args, outParameters) {
|
|
|
65
66
|
result[params[i].PARAMETER_NAME] = args[i]
|
|
66
67
|
}
|
|
67
68
|
}
|
|
69
|
+
|
|
68
70
|
return result
|
|
69
71
|
}
|
|
70
72
|
|
|
@@ -79,6 +81,7 @@ function _hcGetResultForProcedure(stmt, resultSet, outParameters) {
|
|
|
79
81
|
}
|
|
80
82
|
}
|
|
81
83
|
}
|
|
84
|
+
|
|
82
85
|
// merge table output params into scalar params
|
|
83
86
|
const params = Array.isArray(outParameters) && outParameters.filter(md => !(md.PARAMETER_NAME in result))
|
|
84
87
|
if (params && params.length) {
|
|
@@ -91,6 +94,7 @@ function _hcGetResultForProcedure(stmt, resultSet, outParameters) {
|
|
|
91
94
|
resultSet.nextResult()
|
|
92
95
|
}
|
|
93
96
|
}
|
|
97
|
+
|
|
94
98
|
return result
|
|
95
99
|
}
|
|
96
100
|
|
|
@@ -160,7 +164,6 @@ function _executeAsPreparedStatement(dbc, sql, values, reject, resolve) {
|
|
|
160
164
|
}
|
|
161
165
|
|
|
162
166
|
stmt.drop(() => {})
|
|
163
|
-
|
|
164
167
|
resolve(result)
|
|
165
168
|
})
|
|
166
169
|
})
|
|
@@ -180,6 +183,7 @@ function _executeSimpleSQL(dbc, sql, values) {
|
|
|
180
183
|
if (dbc.name !== 'hdb' && typeof values === 'object') {
|
|
181
184
|
values = Object.values(values)
|
|
182
185
|
}
|
|
186
|
+
|
|
183
187
|
// ensure that stored procedure with parameters is always executed as prepared
|
|
184
188
|
if (_hasValues(values) || !!_getProcedureName(sql)) {
|
|
185
189
|
_executeAsPreparedStatement(dbc, sql, values, reject, resolve)
|
|
@@ -230,12 +234,12 @@ function executeSelectCQN(model, dbc, query, user, locale, txTimestamp) {
|
|
|
230
234
|
) {
|
|
231
235
|
return expandV2(model, dbc, query, user, locale, txTimestamp, executeSelectCQN)
|
|
232
236
|
}
|
|
237
|
+
|
|
233
238
|
return _processExpand(model, dbc, query, user, locale, txTimestamp)
|
|
234
239
|
}
|
|
235
240
|
|
|
236
241
|
const { sql, values = [] } = _cqnToSQL(model, query, user, locale, txTimestamp)
|
|
237
242
|
const postProcessMapper = getPostProcessMapper(HANA_TYPE_CONVERSION_MAP, model, query)
|
|
238
|
-
|
|
239
243
|
return _executeSelectSQL(dbc, sql, values, query.SELECT.one, postProcessMapper)
|
|
240
244
|
}
|
|
241
245
|
|
|
@@ -245,17 +249,17 @@ function _getValuesProxy(values) {
|
|
|
245
249
|
if (prop.length > 1 && prop.startsWith(':')) {
|
|
246
250
|
return Object.getOwnPropertyDescriptor(obj, prop.slice(1))
|
|
247
251
|
}
|
|
252
|
+
|
|
248
253
|
return Object.getOwnPropertyDescriptor(obj, prop)
|
|
249
254
|
},
|
|
250
255
|
get: (obj, prop) => {
|
|
251
256
|
if (prop.length > 1 && prop.startsWith(':')) {
|
|
252
257
|
return obj[prop.slice(1)]
|
|
253
258
|
}
|
|
259
|
+
|
|
254
260
|
return obj[prop]
|
|
255
261
|
},
|
|
256
|
-
ownKeys: target => {
|
|
257
|
-
return Reflect.ownKeys(target).map(key => `:${key}`)
|
|
258
|
-
}
|
|
262
|
+
ownKeys: target => Reflect.ownKeys(target).map(key => `:${key}`)
|
|
259
263
|
})
|
|
260
264
|
}
|
|
261
265
|
|
|
@@ -276,6 +280,7 @@ function executeInsertCQN(model, dbc, query, user, locale, txTimestamp) {
|
|
|
276
280
|
if (dbc.name === 'hdb') {
|
|
277
281
|
return writeStreamWithHdb(dbc, sql, values)
|
|
278
282
|
}
|
|
283
|
+
|
|
279
284
|
return writeStreamWithHanaClient(dbc, sql, values)
|
|
280
285
|
}
|
|
281
286
|
|
|
@@ -284,6 +289,7 @@ function executeInsertCQN(model, dbc, query, user, locale, txTimestamp) {
|
|
|
284
289
|
const affectedRowsCount = Array.isArray(affectedRows)
|
|
285
290
|
? affectedRows.reduce((sum, rows) => sum + rows, 0)
|
|
286
291
|
: affectedRows
|
|
292
|
+
|
|
287
293
|
if (entriesOrRows && entriesOrRows.length !== affectedRowsCount) {
|
|
288
294
|
LOG._warn &&
|
|
289
295
|
LOG.warn(
|
|
@@ -295,13 +301,17 @@ function executeInsertCQN(model, dbc, query, user, locale, txTimestamp) {
|
|
|
295
301
|
query
|
|
296
302
|
}
|
|
297
303
|
)
|
|
304
|
+
|
|
298
305
|
throw new Error('Possible data loss by INSERT into HANA db. Please, update a corresponding HANA driver.')
|
|
299
306
|
}
|
|
307
|
+
|
|
300
308
|
// InsertResult needs an object per row with its values
|
|
301
309
|
// query.INSERT.values -> one row
|
|
302
310
|
if (query.INSERT.values) return [{ affectedRows: 1, values: [values] }]
|
|
311
|
+
|
|
303
312
|
// query.INSERT.entries or .rows -> multiple rows
|
|
304
313
|
if (entriesOrRows) return values.map(v => ({ affectedRows: 1, values: v }))
|
|
314
|
+
|
|
305
315
|
// INSERT into SELECT
|
|
306
316
|
return [{ affectedRows }]
|
|
307
317
|
})
|
|
@@ -331,20 +341,17 @@ function executeGenericCQN(model, dbc, query, user, locale, txTimestamp) {
|
|
|
331
341
|
async function executeSelectStreamCQN(model, dbc, query, user, locale, txTimestamp) {
|
|
332
342
|
const { sql, values = [] } = _cqnToSQL(model, query, user, locale, txTimestamp)
|
|
333
343
|
let result
|
|
344
|
+
|
|
334
345
|
if (dbc.name === 'hdb') {
|
|
335
346
|
result = await readStreamWithHdb(dbc, sql, values)
|
|
336
347
|
} else {
|
|
337
348
|
result = await readStreamWithHanaClient(dbc, sql, values)
|
|
338
349
|
}
|
|
339
350
|
|
|
340
|
-
if (result.length === 0)
|
|
341
|
-
return
|
|
342
|
-
}
|
|
351
|
+
if (result.length === 0) return
|
|
343
352
|
|
|
344
353
|
const val = Object.values(result[0])[0]
|
|
345
|
-
if (val === null)
|
|
346
|
-
return null
|
|
347
|
-
}
|
|
354
|
+
if (val === null) return null
|
|
348
355
|
|
|
349
356
|
return { value: val }
|
|
350
357
|
}
|
|
@@ -38,6 +38,7 @@ const _setCorrectValue = (el, data, params, kind) => {
|
|
|
38
38
|
const _buildPartialUrlFunctions = (url, data, params, kind = 'odata-v4') => {
|
|
39
39
|
const funcParams = []
|
|
40
40
|
const queryOptions = []
|
|
41
|
+
|
|
41
42
|
// REVISIT: take params from params after importer fix (the keys should not be part of params)
|
|
42
43
|
for (const param in _extractParamsFromData(data, params)) {
|
|
43
44
|
if (kind === 'odata-v2') {
|
|
@@ -47,6 +48,7 @@ const _buildPartialUrlFunctions = (url, data, params, kind = 'odata-v4') => {
|
|
|
47
48
|
queryOptions.push(`@${param}=${_setCorrectValue(param, data, params, kind)}`)
|
|
48
49
|
}
|
|
49
50
|
}
|
|
51
|
+
|
|
50
52
|
return kind === 'odata-v2'
|
|
51
53
|
? `${url}?${funcParams.join('&')}`
|
|
52
54
|
: `${url}(${funcParams.join(',')})?${queryOptions.join('&')}`
|
|
@@ -61,9 +63,11 @@ const _extractParamsFromData = (data, params = {}) => {
|
|
|
61
63
|
|
|
62
64
|
const _buildKeys = (req, kind) => {
|
|
63
65
|
const keys = []
|
|
66
|
+
|
|
64
67
|
if (req.params && req.params.length > 0) {
|
|
65
68
|
const p1 = req.params[0]
|
|
66
69
|
if (typeof p1 !== 'object') return [p1]
|
|
70
|
+
|
|
67
71
|
for (const key in req.target.keys) {
|
|
68
72
|
keys.push(`${key}=${formatVal(p1[key], key, req.target, kind)}`)
|
|
69
73
|
}
|
|
@@ -73,6 +77,7 @@ const _buildKeys = (req, kind) => {
|
|
|
73
77
|
keys.push(`${key}=${formatVal(req.data[key], key, req.target, kind)}`)
|
|
74
78
|
}
|
|
75
79
|
}
|
|
80
|
+
|
|
76
81
|
return keys
|
|
77
82
|
}
|
|
78
83
|
|
|
@@ -98,6 +103,7 @@ const _handleUnboundActionFunction = (srv, def, req, event) => {
|
|
|
98
103
|
def &&
|
|
99
104
|
def.returns &&
|
|
100
105
|
(def.returns.type === 'cds.LargeBinary' || def.returns.type === 'cds.Binary')
|
|
106
|
+
|
|
101
107
|
return srv.send({ method: 'POST', path: `/${event}`, data: req.data, _binary: isBinary })
|
|
102
108
|
}
|
|
103
109
|
|
|
@@ -121,22 +127,26 @@ const _handleV2ActionFunction = (srv, def, req, event, kind) => {
|
|
|
121
127
|
const _handleV2BoundActionFunction = (srv, def, req, event, kind) => {
|
|
122
128
|
const params = []
|
|
123
129
|
const data = req.data
|
|
130
|
+
|
|
124
131
|
// REVISIT: take params from def.params, after importer fix (the keys should not be part of params)
|
|
125
132
|
for (const param in _extractParamsFromData(req.data, def.params)) {
|
|
126
133
|
params.push(`${param}=${formatVal(data[param], param, { elements: def.params }, kind)}`)
|
|
127
134
|
}
|
|
135
|
+
|
|
128
136
|
const keys = _buildKeys(req, this.kind)
|
|
129
137
|
if (keys.length === 1 && typeof req.params[0] !== 'object') {
|
|
130
138
|
params.push(`${Object.keys(req.target.keys)[0]}=${keys[0]}`)
|
|
131
139
|
} else {
|
|
132
140
|
params.push(...keys)
|
|
133
141
|
}
|
|
142
|
+
|
|
134
143
|
const url = `${`/${event}`}?${params.join('&')}`
|
|
135
144
|
return _sendV2RequestActionFunction(srv, def, url)
|
|
136
145
|
}
|
|
137
146
|
|
|
138
147
|
const _addHandlerActionFunction = (srv, def, target) => {
|
|
139
148
|
const event = def.name.match(/\w*$/)[0]
|
|
149
|
+
|
|
140
150
|
if (target) {
|
|
141
151
|
srv.on(event, target, async function (req) {
|
|
142
152
|
const shortEntityName = req.target.name.replace(`${this.namespace}.`, '')
|
|
@@ -144,18 +154,18 @@ const _addHandlerActionFunction = (srv, def, target) => {
|
|
|
144
154
|
const url = `/${shortEntityName}(${_buildKeys(req, this.kind).join(',')})/${this.namespace}.${event}`
|
|
145
155
|
return _handleBoundActionFunction(srv, def, req, url)
|
|
146
156
|
})
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (this.kind === 'odata-v2') return _handleV2ActionFunction(srv, def, req, event, this.kind)
|
|
150
|
-
return _handleUnboundActionFunction(srv, def, req, event)
|
|
151
|
-
})
|
|
157
|
+
|
|
158
|
+
return
|
|
152
159
|
}
|
|
153
|
-
}
|
|
154
160
|
|
|
155
|
-
|
|
156
|
-
|
|
161
|
+
srv.on(event, async function (req) {
|
|
162
|
+
if (this.kind === 'odata-v2') return _handleV2ActionFunction(srv, def, req, event, this.kind)
|
|
163
|
+
return _handleUnboundActionFunction(srv, def, req, event)
|
|
164
|
+
})
|
|
157
165
|
}
|
|
158
166
|
|
|
167
|
+
const _selectOnlyWithAlias = q => q?.SELECT && !q.SELECT._transitions && q.SELECT?.columns?.some(hasAliasedColumns)
|
|
168
|
+
|
|
159
169
|
const resolvedTargetOfQuery = q => {
|
|
160
170
|
const transitions = (typeof q === 'object' && (q.SELECT || q.INSERT || q.UPDATE || q.DELETE)._transitions) || []
|
|
161
171
|
return transitions.length && [transitions.length - 1].target
|
|
@@ -213,6 +223,7 @@ class RemoteService extends cds.Service {
|
|
|
213
223
|
// eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
|
|
214
224
|
const sdkUtils = require('@sap-cloud-sdk/util')
|
|
215
225
|
sdkUtils.setGlobalLogLevel('error')
|
|
226
|
+
|
|
216
227
|
// disable sdk logger once
|
|
217
228
|
sdkLoggerDisabled = true
|
|
218
229
|
} catch (err) {
|
|
@@ -264,7 +275,6 @@ class RemoteService extends cds.Service {
|
|
|
264
275
|
|
|
265
276
|
let result = await run(reqOptions, additionalOptions)
|
|
266
277
|
result = typeof query === 'object' && query.SELECT?.one && Array.isArray(result) ? result[0] : result
|
|
267
|
-
|
|
268
278
|
return result
|
|
269
279
|
})
|
|
270
280
|
}
|
|
@@ -312,7 +322,6 @@ class RemoteService extends cds.Service {
|
|
|
312
322
|
// REVISIT: We need to provide target explicitly because it's cached already within ensure_target
|
|
313
323
|
const newReq = new cds.Request({ query: q, target: t, headers: req.headers, _resolved: true, method: req.method })
|
|
314
324
|
const result = await super.dispatch(newReq)
|
|
315
|
-
|
|
316
325
|
return postProcess(q, result, this, true)
|
|
317
326
|
}
|
|
318
327
|
|
|
@@ -306,12 +306,9 @@ const run = async (
|
|
|
306
306
|
// > axios received status >= 400 -> gateway error
|
|
307
307
|
const msg = e?.response?.data?.error?.message?.value ?? e?.response?.data?.error?.message ?? e.message
|
|
308
308
|
e.message = msg ? 'Error during request to remote service: \n' + msg : 'Request to remote service failed.'
|
|
309
|
-
|
|
310
|
-
const sanitizedError = _getSanitizedError(e, requestConfig, {
|
|
311
|
-
suppressRemoteResponseBody: suppressRemoteResponseBody
|
|
312
|
-
})
|
|
313
|
-
|
|
309
|
+
const sanitizedError = _getSanitizedError(e, requestConfig, { suppressRemoteResponseBody })
|
|
314
310
|
const err = Object.assign(new Error(e.message), { statusCode: 502, reason: sanitizedError })
|
|
311
|
+
|
|
315
312
|
LOG._warn && LOG.warn(err)
|
|
316
313
|
throw err
|
|
317
314
|
}
|
|
@@ -329,11 +326,7 @@ const run = async (
|
|
|
329
326
|
) {
|
|
330
327
|
const e = new Error("Received content-type 'text/html' which is not part of accepted content types")
|
|
331
328
|
e.response = response
|
|
332
|
-
|
|
333
|
-
const sanitizedError = _getSanitizedError(e, requestConfig, {
|
|
334
|
-
suppressRemoteResponseBody: suppressRemoteResponseBody
|
|
335
|
-
})
|
|
336
|
-
|
|
329
|
+
const sanitizedError = _getSanitizedError(e, requestConfig, { suppressRemoteResponseBody })
|
|
337
330
|
const err = Object.assign(new Error(`Error during request to remote service: ${e.message}`), {
|
|
338
331
|
statusCode: 502,
|
|
339
332
|
reason: sanitizedError
|
|
@@ -383,6 +376,7 @@ const run = async (
|
|
|
383
376
|
if (response.data.d) {
|
|
384
377
|
return _purgeODataV2(response.data, resolvedTarget, returnType, requestConfig.headers)
|
|
385
378
|
}
|
|
379
|
+
|
|
386
380
|
return _purgeODataV4(response.data)
|
|
387
381
|
}
|
|
388
382
|
|
|
@@ -114,8 +114,10 @@ function executeSelectCQN(model, dbc, query, user, locale, txTimestamp) {
|
|
|
114
114
|
) {
|
|
115
115
|
return expandV2(model, dbc, query, user, locale, txTimestamp, executeSelectCQN)
|
|
116
116
|
}
|
|
117
|
+
|
|
117
118
|
return _processExpand(model, dbc, query, user, locale, txTimestamp)
|
|
118
119
|
}
|
|
120
|
+
|
|
119
121
|
const { sql, values = [] } = sqlFactory(
|
|
120
122
|
query,
|
|
121
123
|
{
|
|
@@ -9,7 +9,6 @@ const { deepCopyObject } = require('../../_runtime/common/utils/copy')
|
|
|
9
9
|
|
|
10
10
|
module.exports = async (_req, _res, next) => {
|
|
11
11
|
let { _srv: srv, _query: query, _target, _data, _params } = _req
|
|
12
|
-
|
|
13
12
|
let result,
|
|
14
13
|
status = 200
|
|
15
14
|
|
|
@@ -19,6 +18,7 @@ module.exports = async (_req, _res, next) => {
|
|
|
19
18
|
try {
|
|
20
19
|
// add the data (as copy, if upsert allowed)
|
|
21
20
|
query.data(UPSERT_ALLOWED ? deepCopyObject(_data) : _data)
|
|
21
|
+
|
|
22
22
|
// REVISIT: if PUT, req.method should be PUT -> Crud2Http maps UPSERT to PUT
|
|
23
23
|
result = await srv.dispatch(new RestRequest({ query, _target, method: _req.method, params: _params }))
|
|
24
24
|
if (_params && result) Object.assign(result, _params[_params.length - 1])
|