@sap/cds 5.6.1 → 5.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 +13 -0
- package/lib/core/reflect.js +1 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/SetResponseHeadersCommand.js +1 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +18 -15
- package/libx/_runtime/common/composition/update.js +6 -1
- package/libx/_runtime/db/expand/expandCQNToJoin.js +44 -9
- package/libx/_runtime/db/generic/arrayed.js +13 -28
- package/libx/_runtime/fiori/generic/edit.js +1 -1
- package/libx/_runtime/remote/Service.js +6 -2
- package/package.json +1 -1
- package/server.js +9 -5
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@
|
|
|
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 5.6.2 - 2021-11-08
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Handle arrayed elements using templating mechanism
|
|
12
|
+
- OData requests to `$count` endpoint of ETag enabled entity
|
|
13
|
+
- `cds.test` does no longer crash if executed in `cds repl` on a remote service call
|
|
14
|
+
- Crash on draft activate after draft edit for not existing composition of one
|
|
15
|
+
- Ensure request correlation (with default server)
|
|
16
|
+
- `<entity>.texts` points to real text entity
|
|
17
|
+
- Draft union with expand to to-one and to-many
|
|
18
|
+
- No columns in draft lock statement (i.e., use `SELECT 1`)
|
|
19
|
+
|
|
7
20
|
## Version 5.6.1 - 2021-11-02
|
|
8
21
|
|
|
9
22
|
### Fixed
|
package/lib/core/reflect.js
CHANGED
|
@@ -26,6 +26,7 @@ class LinkedCSN extends any {
|
|
|
26
26
|
/* else: */ any.prototype
|
|
27
27
|
)
|
|
28
28
|
if (p.key && !d.key && d.kind === 'element') Object.defineProperty (d,'key',{value:undefined}) //> don't propagate .key
|
|
29
|
+
if (d.elements && d.elements.localized) Object.defineProperty (d,'texts',{value: defs [d.elements.localized.target] })
|
|
29
30
|
try { return Object.setPrototypeOf(d,p) } //> link d to resolved proto
|
|
30
31
|
catch(e) { //> cyclic proto error
|
|
31
32
|
let msg = d.name; for (; p && p.name; p = p.__proto__) msg += ' > '+p.name
|
|
@@ -75,6 +75,7 @@ class SetResponseHeadersCommand extends Command {
|
|
|
75
75
|
? lastSegment.getTarget() && lastSegment.getTarget().isConcurrent()
|
|
76
76
|
: this._request.getConcurrentResource()) &&
|
|
77
77
|
representationKind !== RepresentationKinds.ENTITY_COLLECTION &&
|
|
78
|
+
representationKind !== RepresentationKinds.COUNT &&
|
|
78
79
|
representationKind !== RepresentationKinds.REFERENCE &&
|
|
79
80
|
representationKind !== RepresentationKinds.REFERENCE_COLLECTION
|
|
80
81
|
) {
|
|
@@ -249,23 +249,26 @@ const postProcess = (req, res, service, result, previousResult) => {
|
|
|
249
249
|
if (template.elements.size === 0) return
|
|
250
250
|
|
|
251
251
|
// normalize result to rows
|
|
252
|
-
result = result.value && Object.keys(result).filter(k => !k.match(/^\W/)).length === 1 ? result.value : result
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
252
|
+
result = result.value != null && Object.keys(result).filter(k => !k.match(/^\W/)).length === 1 ? result.value : result
|
|
253
|
+
|
|
254
|
+
if (typeof result === 'object' && result != null) {
|
|
255
|
+
const rows = Array.isArray(result) ? result : [result]
|
|
256
|
+
|
|
257
|
+
// process each row
|
|
258
|
+
const processFn = _processorFn(req, previousResult, options)
|
|
259
|
+
|
|
260
|
+
for (const row of rows) {
|
|
261
|
+
const args = {
|
|
262
|
+
processFn,
|
|
263
|
+
row,
|
|
264
|
+
template,
|
|
265
|
+
pathOptions: {
|
|
266
|
+
includeKeyValues: false
|
|
267
|
+
}
|
|
265
268
|
}
|
|
266
|
-
}
|
|
267
269
|
|
|
268
|
-
|
|
270
|
+
templateProcessor(args)
|
|
271
|
+
}
|
|
269
272
|
}
|
|
270
273
|
|
|
271
274
|
applyOmitValuesPreference(res, options.omitValuesPreference)
|
|
@@ -205,11 +205,16 @@ function _addSubDeepUpdateCQNRecursion({ definitions, compositionTree, entity, d
|
|
|
205
205
|
const selectSubData = []
|
|
206
206
|
for (const entry of data) {
|
|
207
207
|
if (element.name in entry) {
|
|
208
|
-
_addToData(subData, entity, element, entry)
|
|
209
208
|
const selectEntry = selectDataByKey.get(_serializedKey(entity, entry))
|
|
209
|
+
|
|
210
210
|
if (selectEntry && element.name in selectEntry) {
|
|
211
|
+
if (selectEntry[element.name] === null && Object.keys(entry[element.name]).length === 0) {
|
|
212
|
+
continue
|
|
213
|
+
}
|
|
211
214
|
_addToData(selectSubData, entity, element, selectEntry)
|
|
212
215
|
}
|
|
216
|
+
|
|
217
|
+
_addToData(subData, entity, element, entry)
|
|
213
218
|
}
|
|
214
219
|
}
|
|
215
220
|
_addSubDeepUpdateCQN({
|
|
@@ -11,15 +11,27 @@ const { isAsteriskColumn } = require('../../common/utils/rewriteAsterisks')
|
|
|
11
11
|
const GET_KEY_VALUE = Symbol.for('sap.cds.getKeyValue')
|
|
12
12
|
const TO_MANY = Symbol.for('sap.cds.toMany')
|
|
13
13
|
const TO_ACTIVE = Symbol.for('sap.cds.toActive')
|
|
14
|
-
|
|
15
14
|
const SKIP_MAPPING = Symbol.for('sap.cds.skipMapping')
|
|
16
15
|
const IDENTIFIER = Symbol.for('sap.cds.identifier')
|
|
17
16
|
const IS_ACTIVE = Symbol.for('sap.cds.isActive')
|
|
18
17
|
const IS_UNION_DRAFT = Symbol.for('sap.cds.isUnionDraft')
|
|
18
|
+
|
|
19
19
|
const { DRAFT_COLUMNS } = require('../../common/constants/draft')
|
|
20
20
|
|
|
21
21
|
const { getCQNUnionFrom } = require('../../common/utils/union')
|
|
22
22
|
|
|
23
|
+
function getCqnCopy(readToOneCQN) {
|
|
24
|
+
const readToOneCQNCopy = JSON.parse(JSON.stringify(readToOneCQN))
|
|
25
|
+
if (readToOneCQN[GET_KEY_VALUE] !== undefined) readToOneCQNCopy[GET_KEY_VALUE] = readToOneCQN[GET_KEY_VALUE]
|
|
26
|
+
if (readToOneCQN[TO_MANY] !== undefined) readToOneCQNCopy[TO_MANY] = readToOneCQN[TO_MANY]
|
|
27
|
+
if (readToOneCQN[TO_ACTIVE] !== undefined) readToOneCQNCopy[TO_ACTIVE] = readToOneCQN[TO_ACTIVE]
|
|
28
|
+
if (readToOneCQN[SKIP_MAPPING] !== undefined) readToOneCQNCopy[SKIP_MAPPING] = readToOneCQN[SKIP_MAPPING]
|
|
29
|
+
if (readToOneCQN[IDENTIFIER] !== undefined) readToOneCQNCopy[IDENTIFIER] = readToOneCQN[IDENTIFIER]
|
|
30
|
+
if (readToOneCQN[IS_ACTIVE] !== undefined) readToOneCQNCopy[IS_ACTIVE] = readToOneCQN[IS_ACTIVE]
|
|
31
|
+
if (readToOneCQN[IS_UNION_DRAFT] !== undefined) readToOneCQNCopy[IS_UNION_DRAFT] = readToOneCQN[IS_UNION_DRAFT]
|
|
32
|
+
return readToOneCQNCopy
|
|
33
|
+
}
|
|
34
|
+
|
|
23
35
|
class JoinCQNFromExpanded {
|
|
24
36
|
constructor(cqn, csn, locale) {
|
|
25
37
|
this._SELECT = Object.assign({}, cqn.SELECT)
|
|
@@ -91,16 +103,16 @@ class JoinCQNFromExpanded {
|
|
|
91
103
|
* @private
|
|
92
104
|
*/
|
|
93
105
|
_createJoinCQNFromExpanded(SELECT, toManyTree, defaultLanguage) {
|
|
94
|
-
const
|
|
95
|
-
const
|
|
106
|
+
const joinArgs = SELECT.from.args
|
|
107
|
+
const isJoinOfTwoSelects = joinArgs && joinArgs.every(a => a.SELECT)
|
|
96
108
|
|
|
97
109
|
const unionTableRef = this._getUnionTable(SELECT)
|
|
98
110
|
const unionTable = unionTableRef && unionTableRef.table
|
|
99
111
|
const tableAlias = this._getTableAlias(SELECT, toManyTree, unionTable)
|
|
100
112
|
|
|
101
|
-
const readToOneCQN = this._getReadToOneCQN(SELECT,
|
|
113
|
+
const readToOneCQN = this._getReadToOneCQN(SELECT, isJoinOfTwoSelects ? 'filterExpand' : tableAlias)
|
|
102
114
|
|
|
103
|
-
if (
|
|
115
|
+
if (isJoinOfTwoSelects) {
|
|
104
116
|
// mappings
|
|
105
117
|
const mappings = this._getMappingObject(toManyTree)
|
|
106
118
|
const prefix = `${tableAlias}_`
|
|
@@ -110,7 +122,7 @@ class JoinCQNFromExpanded {
|
|
|
110
122
|
mappings[c.as.replace(prefix, '')] = c.as
|
|
111
123
|
})
|
|
112
124
|
// expand to one
|
|
113
|
-
const entity = this._csn.definitions[
|
|
125
|
+
const entity = this._csn.definitions[joinArgs[0].SELECT.from.SET.args[1].SELECT.from.ref[0]]
|
|
114
126
|
const givenColumns = readToOneCQN.columns
|
|
115
127
|
readToOneCQN.columns = []
|
|
116
128
|
this._expandedToFlat({ entity, givenColumns, readToOneCQN, tableAlias, toManyTree, defaultLanguage })
|
|
@@ -128,6 +140,9 @@ class JoinCQNFromExpanded {
|
|
|
128
140
|
this._expandedToFlat({ entity, givenColumns, readToOneCQN, tableAlias, toManyTree, defaultLanguage })
|
|
129
141
|
}
|
|
130
142
|
|
|
143
|
+
// brute force hack
|
|
144
|
+
readToOneCQN.columns = readToOneCQN.columns.filter(c => c.as !== 'filterExpand_IsActiveEntity')
|
|
145
|
+
|
|
131
146
|
// Add at start, so that the deepest level is post processed first
|
|
132
147
|
this.queries.push({
|
|
133
148
|
SELECT: readToOneCQN,
|
|
@@ -480,6 +495,8 @@ class JoinCQNFromExpanded {
|
|
|
480
495
|
const toManyColumns = []
|
|
481
496
|
const mappings = this._getMappingObject(toManyTree)
|
|
482
497
|
|
|
498
|
+
const readToOneCQNCopy = getCqnCopy(readToOneCQN)
|
|
499
|
+
|
|
483
500
|
for (const column of givenColumns) {
|
|
484
501
|
let navigation
|
|
485
502
|
if (column.expand) {
|
|
@@ -523,7 +540,16 @@ class JoinCQNFromExpanded {
|
|
|
523
540
|
}
|
|
524
541
|
|
|
525
542
|
// only as second step handle expand to many, or else keys might still be unknown
|
|
526
|
-
this._toMany({
|
|
543
|
+
this._toMany({
|
|
544
|
+
entity,
|
|
545
|
+
readToOneCQN,
|
|
546
|
+
tableAlias,
|
|
547
|
+
toManyColumns,
|
|
548
|
+
toManyTree,
|
|
549
|
+
mappings,
|
|
550
|
+
defaultLanguage,
|
|
551
|
+
readToOneCQNCopy
|
|
552
|
+
})
|
|
527
553
|
}
|
|
528
554
|
|
|
529
555
|
adjustOrderBy(orderBy, mappings, column, tableAlias) {
|
|
@@ -957,7 +983,16 @@ class JoinCQNFromExpanded {
|
|
|
957
983
|
return DRAFT_COLUMNS.includes(ref[0])
|
|
958
984
|
}
|
|
959
985
|
|
|
960
|
-
_toMany({
|
|
986
|
+
_toMany({
|
|
987
|
+
entity,
|
|
988
|
+
readToOneCQN,
|
|
989
|
+
tableAlias,
|
|
990
|
+
toManyColumns,
|
|
991
|
+
toManyTree,
|
|
992
|
+
mappings,
|
|
993
|
+
defaultLanguage,
|
|
994
|
+
readToOneCQNCopy
|
|
995
|
+
}) {
|
|
961
996
|
if (toManyColumns.length === 0) {
|
|
962
997
|
return
|
|
963
998
|
}
|
|
@@ -968,7 +1003,7 @@ class JoinCQNFromExpanded {
|
|
|
968
1003
|
const select = this._buildExpandedCQN({
|
|
969
1004
|
column,
|
|
970
1005
|
entity,
|
|
971
|
-
readToOneCQN,
|
|
1006
|
+
readToOneCQN: readToOneCQNCopy,
|
|
972
1007
|
toManyTree,
|
|
973
1008
|
mappings,
|
|
974
1009
|
parentAlias,
|
|
@@ -1,28 +1,13 @@
|
|
|
1
|
-
// REVISIT: use templating mechanism (resp. results.metadata, once available) to make more efficient
|
|
2
|
-
|
|
3
1
|
const { getEntityFromCQN } = require('../../common/utils/entityFromCqn')
|
|
2
|
+
const getTemplate = require('../../common/utils/template')
|
|
3
|
+
const templateProcessor = require('../../common/utils/templateProcessor')
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
// and 100% of all currently existing stakeholder projects don't.
|
|
9
|
-
// but that's not that easy to fix -> see comment about results.metadata below
|
|
10
|
-
for (const row of result) {
|
|
11
|
-
for (const column in row) {
|
|
12
|
-
if (elements[column] === undefined || row[column] === undefined) continue
|
|
5
|
+
const _pick = element => {
|
|
6
|
+
if (element.kind === 'element' && element.items) return 'arrayed'
|
|
7
|
+
}
|
|
13
8
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
row[column] = JSON.parse(row[column])
|
|
17
|
-
} else if (elements[column].is2many) {
|
|
18
|
-
_toArray(row[column], elements[column]._target.elements)
|
|
19
|
-
} else if (elements[column].is2one) {
|
|
20
|
-
_toArray([row[column]], elements[column]._target.elements)
|
|
21
|
-
} else if (elements[column].elements) {
|
|
22
|
-
_toArray([row[column]], elements[column].elements)
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
9
|
+
const _processFn = ({ row, key, plain }) => {
|
|
10
|
+
if (plain === 'arrayed' && row && row[key]) row[key] = JSON.parse(row[key])
|
|
26
11
|
}
|
|
27
12
|
|
|
28
13
|
/**
|
|
@@ -35,12 +20,12 @@ const _toArray = (result, elements) => {
|
|
|
35
20
|
module.exports = function (result, req) {
|
|
36
21
|
if (!this.model) return
|
|
37
22
|
|
|
38
|
-
if (!Array.isArray(result)) result = [result]
|
|
39
|
-
|
|
40
|
-
// REVISIT: We need results.metadata to make that more efficient
|
|
41
|
-
// results.metadata ~= cds.infer(req.query).metadata
|
|
42
|
-
// REVISIT: No entity for sets/unions outside of common draft scenarios
|
|
43
23
|
const entity = getEntityFromCQN(req, this)
|
|
44
24
|
if (!entity) return
|
|
45
|
-
|
|
25
|
+
|
|
26
|
+
const template = getTemplate('db-arrayed', this, entity, { pick: _pick })
|
|
27
|
+
if (template.elements.size === 0) return
|
|
28
|
+
|
|
29
|
+
for (const row of Array.isArray(result) ? result : [result])
|
|
30
|
+
templateProcessor({ processFn: _processFn, row, template })
|
|
46
31
|
}
|
|
@@ -106,7 +106,7 @@ const _handler = async function (req) {
|
|
|
106
106
|
// Only allows one active entity to be processed at a time, locking out other
|
|
107
107
|
// users who need to edit the same record simultaneously.
|
|
108
108
|
// .forUpdate(): lock the record, a wait of 0 is equivalent to no wait
|
|
109
|
-
const lockRecordCQN = SELECT.from(lockTargetEntity).where(lockWhere).forUpdate({ wait: 0 })
|
|
109
|
+
const lockRecordCQN = SELECT.from(lockTargetEntity, [1]).where(lockWhere).forUpdate({ wait: 0 })
|
|
110
110
|
|
|
111
111
|
const columnNames = getColumns(req.target, { onlyNames: true, filterVirtual: true })
|
|
112
112
|
const rootCQN = SELECT.from(req.target, columnNames).where(rootWhere)
|
|
@@ -5,8 +5,12 @@ const LOG = cds.log('remote')
|
|
|
5
5
|
|
|
6
6
|
// disable sdk logger if not in debug mode
|
|
7
7
|
if (!LOG._debug) {
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
try {
|
|
9
|
+
const sdkUtils = require('@sap-cloud-sdk/util')
|
|
10
|
+
sdkUtils.setGlobalLogLevel('error')
|
|
11
|
+
} catch (err) {
|
|
12
|
+
/* might fail in cds repl due to winston's exception handler, see cap/issues#10134 */
|
|
13
|
+
}
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
const { resolveView, getTransition, restoreLink, findQueryTarget } = require('../common/utils/resolveView')
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -138,13 +138,17 @@ function cors (req, res, next) {
|
|
|
138
138
|
next()
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
function correlate (req,
|
|
142
|
-
|
|
143
|
-
const id =
|
|
144
|
-
|| req.headers['x-correlation-id'] || req.headers['x-correlationid']
|
|
141
|
+
function correlate (req, res, next) {
|
|
142
|
+
// derive correlation id from req
|
|
143
|
+
const id = req.headers['x-correlation-id'] || req.headers['x-correlationid']
|
|
145
144
|
|| req.headers['x-request-id'] || req.headers['x-vcap-request-id']
|
|
146
145
|
|| cds.utils.uuid()
|
|
147
|
-
cds.context
|
|
146
|
+
// new intermediate cds.context, if necessary
|
|
147
|
+
if (!cds.context) cds.context = { id }
|
|
148
|
+
// guarantee x-correlation-id going forward and set on res
|
|
149
|
+
req.headers['x-correlation-id'] = id
|
|
150
|
+
res.set('x-correlation-id', id)
|
|
151
|
+
// guaranteed access to cds.context._.req -> REVISIT
|
|
148
152
|
if (!cds.context._) cds.context._ = {}
|
|
149
153
|
if (!cds.context._.req) cds.context._.req = req
|
|
150
154
|
next()
|