@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 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
@@ -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
- const rows = Array.isArray(result) ? result : [result]
254
-
255
- // process each row
256
- const processFn = _processorFn(req, previousResult, options)
257
-
258
- for (const row of rows) {
259
- const args = {
260
- processFn,
261
- row,
262
- template,
263
- pathOptions: {
264
- includeKeyValues: false
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
- templateProcessor(args)
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 unionArgs = SELECT.from.args
95
- const isJoinOfTwoUnions = unionArgs && unionArgs.every(a => a.SELECT)
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, isJoinOfTwoUnions ? 'filterExpand' : tableAlias)
113
+ const readToOneCQN = this._getReadToOneCQN(SELECT, isJoinOfTwoSelects ? 'filterExpand' : tableAlias)
102
114
 
103
- if (isJoinOfTwoUnions) {
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[unionArgs[0].SELECT.from.SET.args[1].SELECT.from.ref[0]]
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({ entity, readToOneCQN, tableAlias, toManyColumns, toManyTree, mappings, defaultLanguage })
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({ entity, readToOneCQN, tableAlias, toManyColumns, toManyTree, mappings, defaultLanguage }) {
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 _toArray = (result, elements) => {
6
- // REVISIT: This is a very expensive loop in loop...
7
- // and 100% overhead the results don't contain arrayed elements,
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
- // .items marks arrayed element
15
- if (elements[column].items) {
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
- if (entity) _toArray(result, entity.elements)
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
- const sdkUtils = require('@sap-cloud-sdk/util')
9
- sdkUtils.setGlobalLogLevel('error')
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "5.6.1",
3
+ "version": "5.6.2",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [
package/server.js CHANGED
@@ -138,13 +138,17 @@ function cors (req, res, next) {
138
138
  next()
139
139
  }
140
140
 
141
- function correlate (req,_,next) {
142
- if (!cds.context) cds.context = {}
143
- const id = cds.context.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.id = req.headers['x-correlation-id'] = id
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()