@sap/cds 5.8.0 → 5.8.1

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,21 @@
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.8.1 - 2022-02-11
8
+
9
+ ### Fixed
10
+
11
+ - Use single transaction for update mutations in GraphQL adapter
12
+ - ODATA to CQN parser returned not selected keys in `@odata.context`
13
+ - Draft: `$expand` with special draft columns in `$orderBy` for active entities
14
+ - Reading distinct values of draft enabled entity
15
+ - Handling of LOB data on HANA
16
+ - Fix streaming draft by navigation
17
+ - Empty to-many arrays are not removed from req.data for inserts
18
+ - `$filter` query option in structured mode (OData flavors `w4` and `x4`)
19
+ + Using JSON-stringified objects no longer occasionally crashes an application
20
+ + Filtering on a structured element with `ne null` condition also selects data having some `null` properties within
21
+
7
22
  ## Version 5.8.0 - 2022-01-27
8
23
 
9
24
  ### Added
@@ -281,7 +281,7 @@ class ContextURLFactory {
281
281
  // If there is one '*' selected, the context URL contains only '*'.
282
282
  if (isAll) {
283
283
  value = ['*']
284
- } else if (type && type.getKind() === EdmTypeKind.ENTITY && isKeyRequired) {
284
+ } else if (type && type.getKind() === EdmTypeKind.ENTITY && isKeyRequired && !cds.env.features.odata_new_parser) {
285
285
  for (const keyName of type.getKeyPropertyRefs().keys()) {
286
286
  if (!value.includes(keyName)) value.push(keyName)
287
287
  }
@@ -34,6 +34,8 @@ class ErrorJsonSerializer extends ErrorSerializer {
34
34
  })
35
35
  }
36
36
 
37
+ if (this._error.innererror) result.innererror = this._error.innererror
38
+
37
39
  addAnnotations(this._error, result)
38
40
 
39
41
  return JSON.stringify({ error: result })
@@ -5,6 +5,9 @@ const { SELECT } = cds.ql
5
5
 
6
6
  const { targetFromPath, isPathToDraft } = require('../../../../common/utils/cqn')
7
7
  const { deepCopyArray } = require('../../../../common/utils/copy')
8
+ const { cqn2cqn4sql } = require('../../../../common/utils/cqn2cqn4sql')
9
+ const { ensureDraftsSuffix } = require('../../../../fiori/utils/handler')
10
+ const { removeIsActiveEntityRecursively, isActiveEntityRequested } = require('../../../../fiori/utils/where')
8
11
 
9
12
  const isStreaming = segments => {
10
13
  const lastSegment = segments[segments.length - 1]
@@ -15,6 +18,34 @@ const isStreaming = segments => {
15
18
  )
16
19
  }
17
20
 
21
+ const _adaptSubSelectsDraft = select => {
22
+ if (select.SELECT.from.ref) {
23
+ const index = select.SELECT.from.ref.length - 1
24
+ select.SELECT.from.ref[index] = ensureDraftsSuffix(select.SELECT.from.ref[index])
25
+ }
26
+
27
+ if (select.SELECT.where) {
28
+ for (let i = 0; i < select.SELECT.where.length; i++) {
29
+ const element = select.SELECT.where[i]
30
+ if (element.SELECT) {
31
+ _adaptSubSelectsDraft(element)
32
+ } else if (element.xpr) {
33
+ for (const ele of element.xpr.filter(e => e.SELECT)) {
34
+ _adaptSubSelectsDraft(ele)
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }
40
+
41
+ const adaptStreamCQN = (cqn, isDraft = false) => {
42
+ if (isDraft || !isActiveEntityRequested(cqn.SELECT.where)) {
43
+ _adaptSubSelectsDraft(cqn)
44
+ } else {
45
+ cqn.SELECT.where = removeIsActiveEntityRecursively(cqn.SELECT.where)
46
+ }
47
+ }
48
+
18
49
  const getStreamProperties = (req, model) => {
19
50
  const mediaTypeProperty = Object.values(req.target.elements).find(val => val['@Core.MediaType'])
20
51
 
@@ -56,14 +87,17 @@ const getStreamProperties = (req, model) => {
56
87
 
57
88
  if (columns.length && cds.db && !req.target._hasPersistenceSkip) {
58
89
  // used cloned path
59
- const select = SELECT.one.from({ ref: deepCopyArray(req.query.SELECT.from.ref) }).columns(columns)
60
-
61
- if (req.target._isDraftEnabled && isPathToDraft(select.SELECT.from.ref, model))
62
- select.SELECT.from.ref[0].id = select.SELECT.from.ref[0].id + '_drafts'
90
+ let select = SELECT.one.from({ ref: deepCopyArray(req.query.SELECT.from.ref) }).columns(columns)
63
91
 
64
92
  // new parser has media property as last ref element -> remove
65
93
  if (targetFromPath(select.SELECT.from.ref, model).kind === 'element') select.SELECT.from.ref.pop()
66
94
 
95
+ const pathToDraft = isPathToDraft(select.SELECT.from.ref, model)
96
+ if (req.target._isDraftEnabled && pathToDraft) {
97
+ select = cqn2cqn4sql(select, model)
98
+ adaptStreamCQN(select, pathToDraft)
99
+ }
100
+
67
101
  return cds
68
102
  .tx(req)
69
103
  .run(select)
@@ -79,5 +113,6 @@ const getStreamProperties = (req, model) => {
79
113
 
80
114
  module.exports = {
81
115
  isStreaming,
82
- getStreamProperties
116
+ getStreamProperties,
117
+ adaptStreamCQN
83
118
  }
@@ -1,5 +1,5 @@
1
1
  const { getCompositionTree, getCompositionRoot } = require('./tree')
2
- const { hasDeepInsert, getDeepInsertCQNs, cleanEmptyCompositionsOfMany } = require('./insert')
2
+ const { hasDeepInsert, getDeepInsertCQNs } = require('./insert')
3
3
  const { hasDeepUpdate, getDeepUpdateCQNs } = require('./update')
4
4
  const { hasDeepDelete, getDeepDeleteCQNs, getSetNullParentForeignKeyCQNs } = require('./delete')
5
5
  const { selectDeepUpdateData } = require('./data')
@@ -11,7 +11,6 @@ module.exports = {
11
11
  // insert
12
12
  hasDeepInsert,
13
13
  getDeepInsertCQNs,
14
- cleanEmptyCompositionsOfMany,
15
14
  // update
16
15
  hasDeepUpdate,
17
16
  getDeepUpdateCQNs,
@@ -10,9 +10,9 @@ const { deepCopyArray } = require('../utils/copy')
10
10
  * own utils
11
11
  */
12
12
 
13
- function _hasCompOrAssocIgnoreEmptyToMany(entity, k, data) {
13
+ function _hasCompOrAssoc(entity, k) {
14
14
  // TODO once REST also uses same logic as odata structured check if we can omit 'entity.elements[k] &&'
15
- return entity.elements[k] && (entity.elements[k].is2one || (entity.elements[k].is2many && data[k] && data[k].length))
15
+ return entity.elements[k] && (entity.elements[k].is2one || entity.elements[k].is2many)
16
16
  }
17
17
 
18
18
  const _addSubDeepInsertCQN = (model, compositionTree, data, cqns, draft) => {
@@ -60,25 +60,13 @@ const _entityFromINSERT = (model, INSERT) => {
60
60
  }
61
61
  }
62
62
 
63
- const cleanEmptyCompositionsOfMany = (model, cqn) => {
64
- const entity = _entityFromINSERT(model, cqn.INSERT)
65
- if (!entity) return
66
- for (const entry of cqn.INSERT.entries || []) {
67
- for (const elName in entry || {}) {
68
- const el = entity.elements[elName]
69
- if (!el) continue
70
- if (el.is2many && !entry[elName].length) delete entry[elName]
71
- }
72
- }
73
- }
74
-
75
63
  const hasDeepInsert = (model, cqn) => {
76
64
  if (cqn.INSERT.entries) {
77
65
  const entity = _entityFromINSERT(model, cqn.INSERT)
78
66
  if (entity) {
79
67
  return !!cqn.INSERT.entries.find(entry => {
80
68
  return !!Object.keys(entry || {}).find(k => {
81
- return _hasCompOrAssocIgnoreEmptyToMany(entity, k, entry)
69
+ return _hasCompOrAssoc(entity, k)
82
70
  })
83
71
  })
84
72
  }
@@ -111,7 +99,6 @@ const getDeepInsertCQNs = (model, cqn) => {
111
99
  }
112
100
 
113
101
  module.exports = {
114
- cleanEmptyCompositionsOfMany,
115
102
  hasDeepInsert,
116
103
  getDeepInsertCQNs
117
104
  }
@@ -30,9 +30,8 @@ const _getFiltered = err => {
30
30
  Object.keys(err)
31
31
  .concat(['message'])
32
32
  .forEach(k => {
33
- if (k === 'innererror' && process.env.NODE_ENV === 'production') {
34
- return
35
- }
33
+ // REVISIT: do not remove innererror with cds^6
34
+ if (k === 'innererror' && process.env.NODE_ENV === 'production' && !err[SKIP_SANITIZATION]) return
36
35
  if (ALLOWED_PROPERTIES.includes(k) || k.startsWith('@')) {
37
36
  error[k] = err[k]
38
37
  } else if (k === 'numericSeverity') {
@@ -50,7 +50,7 @@ const _getDataSubjectUp = (role, model, entity, prev, next, result) => {
50
50
  }
51
51
 
52
52
  const _getDataSubjectDown = (role, entity, prev, next) => {
53
- const associations = Object.values(entity.associations).filter(e => !e._isBacklink)
53
+ const associations = Object.values(entity.associations || {}).filter(e => !e._isBacklink)
54
54
  for (const element of associations) {
55
55
  const me = { entity, relative: entity, element }
56
56
  if (_ifDataSubject(element._target, role)) {
@@ -99,6 +99,7 @@ const _getVal = (data, name) => {
99
99
 
100
100
  const _filterForStructProperty = (structElement, structData, op, prefix = '', nav = []) => {
101
101
  const filterArray = []
102
+ const andOr = op === '!=' ? 'or' : 'and'
102
103
 
103
104
  for (const elementName in structElement.elements) {
104
105
  const element = structElement.elements[elementName]
@@ -123,8 +124,8 @@ const _filterForStructProperty = (structElement, structData, op, prefix = '', na
123
124
  for (const key in assoc._target.keys) {
124
125
  if (element.name === `${assocName}_${key}`) {
125
126
  const ref = [`${prefix}_${assocName}_${key}`]
126
- const val = _getVal(structData[assocName], key)
127
- filterArray.push({ ref }, op, { val }, 'and')
127
+ const val = _getVal(structData && structData[assocName], key)
128
+ filterArray.push({ ref }, op, { val }, andOr)
128
129
  }
129
130
  }
130
131
  }
@@ -134,7 +135,7 @@ const _filterForStructProperty = (structElement, structData, op, prefix = '', na
134
135
  { ref: [...nav, `${prefix}_${element.name}`] },
135
136
  op,
136
137
  { val: _getVal(structData, element.name) },
137
- 'and'
138
+ andOr
138
139
  )
139
140
  }
140
141
  }
@@ -182,7 +183,12 @@ const _transformStructToFlatWhereHaving = ([first, op, second], resArray, struct
182
183
  } else {
183
184
  // transform complex structured to multiple single structured
184
185
  const { nestedElement, prefix } = _nestedStructElement(structProperties, structElement)
185
- resArray.push(..._filterForStructProperty(nestedElement, structData, op, prefix, nav))
186
+ const filterForStructProperty = _filterForStructProperty(nestedElement, structData, op, prefix, nav)
187
+ if (filterForStructProperty.length) {
188
+ filterForStructProperty.pop() // last and/or
189
+ if (op === '!=') resArray.push('(', ...filterForStructProperty, ')')
190
+ else resArray.push(...filterForStructProperty)
191
+ }
186
192
  }
187
193
 
188
194
  if (resArray[resArray.length - 1] === 'and') {
@@ -520,6 +520,17 @@ class JoinCQNFromExpanded {
520
520
 
521
521
  // REVISIT required for other cqn properties as well?
522
522
  this.adjustOrderBy(readToOneCQN.orderBy, mappings, column, tableAlias)
523
+
524
+ // In case active parent entity has orderBy with draft specific columns we need to add them to parent CQN
525
+ if (
526
+ readToOneCQN[IS_ACTIVE] &&
527
+ readToOneCQN.orderBy &&
528
+ column.as &&
529
+ (column.as === 'IsActiveEntity' || column.as === 'HasActiveEntity' || column.as === 'HasDraftEntity')
530
+ ) {
531
+ readToOneCQNCopy.orderBy = readToOneCQN.orderBy
532
+ this._addColumnsInCaseOrderByHasDraft(readToOneCQNCopy, readToOneCQN.columns[readToOneCQN.columns.length - 1])
533
+ }
523
534
  }
524
535
  }
525
536
 
@@ -551,6 +562,14 @@ class JoinCQNFromExpanded {
551
562
  }
552
563
  }
553
564
 
565
+ _addColumnsInCaseOrderByHasDraft(readToOneCQNCopy, column) {
566
+ readToOneCQNCopy.orderBy.forEach(order => {
567
+ if (order.as === column.as) {
568
+ readToOneCQNCopy.columns.push(column)
569
+ }
570
+ })
571
+ }
572
+
554
573
  /**
555
574
  * Follow the tree to get to the relevant config object.
556
575
  *
@@ -1326,6 +1345,17 @@ class JoinCQNFromExpanded {
1326
1345
  }
1327
1346
  }
1328
1347
 
1348
+ if (readToOneCQN[IS_ACTIVE] && readToOneCQN.columns.length > 0) {
1349
+ readToOneCQN.columns.forEach(column => {
1350
+ if (
1351
+ column.as === `${parentAlias}_IsActiveEntity` ||
1352
+ column.as === `${parentAlias}_HasActiveEntity` ||
1353
+ column.as === `${parentAlias}_HasDraftEntity`
1354
+ )
1355
+ columns.push(column)
1356
+ })
1357
+ }
1358
+
1329
1359
  const subSelect = Object.assign({}, readToOneCQN, { columns })
1330
1360
 
1331
1361
  const SELECT = { from: { SELECT: subSelect }, columns: outerColumns, distinct: true }
@@ -1,4 +1,4 @@
1
- const { hasDeepInsert, getDeepInsertCQNs, cleanEmptyCompositionsOfMany } = require('../../common/composition')
1
+ const { hasDeepInsert, getDeepInsertCQNs } = require('../../common/composition')
2
2
  const { getFlatArray, processCQNs } = require('../utils/deep')
3
3
  const { timestampToISO } = require('../data-conversion/timestamp')
4
4
 
@@ -15,7 +15,6 @@ const insert = executeInsertCQN => async (model, dbc, query, req) => {
15
15
  return getFlatArray(results)
16
16
  }
17
17
 
18
- cleanEmptyCompositionsOfMany(model, query)
19
18
  return executeInsertCQN(model, dbc, query, user, locale, isoTs)
20
19
  }
21
20
 
@@ -96,7 +96,7 @@ class SelectBuilder extends BaseBuilder {
96
96
  }
97
97
 
98
98
  if (this._obj.SELECT.orderBy && this._obj.SELECT.orderBy.length) {
99
- this._orderBy()
99
+ this._orderBy(noQuoting)
100
100
  }
101
101
 
102
102
  if (this._obj.SELECT.limit || this._obj.SELECT.one) {
@@ -373,7 +373,11 @@ class SelectBuilder extends BaseBuilder {
373
373
  this._outputObj.values.push(...values)
374
374
  }
375
375
 
376
- _orderBy() {
376
+ _getOrderByElement(noQuoting, name, element) {
377
+ return (noQuoting ? name : this._quote(name)) + ' ' + (element.sort || 'asc').toUpperCase()
378
+ }
379
+
380
+ _orderBy(noQuoting) {
377
381
  const sqls = []
378
382
  this._outputObj.sql.push('ORDER BY')
379
383
  for (const element of this._obj.SELECT.orderBy) {
@@ -385,7 +389,7 @@ class SelectBuilder extends BaseBuilder {
385
389
  if (!columns.find(c => JSON.stringify(c.ref) === serialized)) {
386
390
  const toMatch = element.as || (element.ref && element.ref.length === 1 && element.ref[0])
387
391
  if (toMatch && columns.find(c => c.as === toMatch)) {
388
- sqls.push(this._quote(toMatch) + ' ' + (element.sort || 'asc').toUpperCase())
392
+ sqls.push(this._getOrderByElement(noQuoting, toMatch, element))
389
393
  continue
390
394
  }
391
395
  }
@@ -3,10 +3,8 @@ const { SELECT } = cds.ql
3
3
 
4
4
  const { cqn2cqn4sql, convertWhereExists } = require('../../common/utils/cqn2cqn4sql')
5
5
  const { getElementDeep } = require('../../common/utils/csn')
6
-
7
6
  const { DRAFT_COLUMNS, DRAFT_COLUMNS_MAP, SCENARIO } = require('../../common/constants/draft')
8
7
  const {
9
- adaptStreamCQN,
10
8
  addColumnAlias,
11
9
  draftIsLocked,
12
10
  ensureDraftsSuffix,
@@ -18,8 +16,8 @@ const {
18
16
  filterKeys
19
17
  } = require('../utils/handler')
20
18
  const { deleteCondition, readAndDeleteKeywords, removeIsActiveEntityRecursively } = require('../utils/where')
21
-
22
19
  const { getColumns } = require('../../cds-services/services/utils/columns')
20
+ const { adaptStreamCQN } = require('../../cds-services/adapter/odata-v4/utils/stream')
23
21
 
24
22
  const _findRootSubSelectFor = query => {
25
23
  if (query.SELECT.where) {
@@ -1,6 +1,5 @@
1
1
  const cds = require('../../cds')
2
2
  const { UPDATE, SELECT } = cds.ql
3
- const { removeIsActiveEntityRecursively, isActiveEntityRequested } = require('./where')
4
3
  const { getColumns } = require('../../cds-services/services/utils/columns')
5
4
  const { ensureNoDraftsSuffix, ensureDraftsSuffix, ensureUnlocalized } = require('../../common/utils/draft')
6
5
  const getTemplate = require('../../common/utils/template')
@@ -126,7 +125,7 @@ const getEnrichedCQN = (cqn, select, draftWhere, scenarioAlias, addLimitOrder =
126
125
  }
127
126
 
128
127
  if (select.distinct) {
129
- cqn.distinct()
128
+ cqn.SELECT.distinct = true
130
129
  }
131
130
 
132
131
  const alias = (select.from && select.from.as) || scenarioAlias
@@ -249,14 +248,6 @@ const replaceRefWithDraft = ref => {
249
248
  ref[0] = ensureDraftsSuffix(ref[0])
250
249
  }
251
250
 
252
- const adaptStreamCQN = cqn => {
253
- if (isActiveEntityRequested(cqn.SELECT.where)) {
254
- cqn.SELECT.where = removeIsActiveEntityRecursively(cqn.SELECT.where)
255
- } else {
256
- replaceRefWithDraft(cqn.SELECT.from.ref)
257
- }
258
- }
259
-
260
251
  const draftIsLocked = lastChangedAt => {
261
252
  // default timeout timer is 15 minutes
262
253
  const DRAFT_CANCEL_TIMEOUT_IN_MS = ((cds.env.drafts && cds.env.drafts.cancellationTimeout) || 15) * 60 * 1000
@@ -289,7 +280,6 @@ module.exports = {
289
280
  hasDraft,
290
281
  proxifyToNoDraftsName,
291
282
  addColumnAlias,
292
- adaptStreamCQN,
293
283
  replaceRefWithDraft,
294
284
  getKeyProperty,
295
285
  filterKeys,
@@ -46,7 +46,8 @@ const HANA_TYPE_CONVERSION_MAP = new Map([
46
46
  ['cds.Integer64', convertInt64ToString],
47
47
  ['cds.DateTime', convertToISONoMillis],
48
48
  ['cds.Timestamp', convertToISO],
49
- ['cds.LargeString', convertToString]
49
+ ['cds.LargeString', convertToString],
50
+ ['cds.hana.CLOB', convertToString]
50
51
  ])
51
52
 
52
53
  if (cds.env.features.bigjs) {
@@ -51,8 +51,6 @@ function _getOutputParameters(stmt) {
51
51
  const BINARY_TYPES = {
52
52
  12: 'BINARY',
53
53
  13: 'VARBINARY',
54
- 25: 'CLOB',
55
- 26: 'NCLOB',
56
54
  27: 'BLOB'
57
55
  }
58
56
 
@@ -72,10 +72,7 @@ const search2cqn4sql = (query, entity, options) => {
72
72
  return query
73
73
  }
74
74
 
75
- const _getLocalizedAssociation = entity => {
76
- const associations = entity.associations
77
- return associations && associations.localized
78
- }
75
+ const _getLocalizedAssociation = entity => entity.associations && entity.associations.localized
79
76
 
80
77
  // The inner join modifies the original SELECT ... FROM query and adds ambiguity,
81
78
  // therefore add the table/entity name (as a preceding element) to the columns ref
@@ -246,6 +246,7 @@ const run = async (
246
246
 
247
247
  LOG._warn && LOG.warn(sanitizedError)
248
248
 
249
+ // REVISIT: switch from innererror to reason in cds^6
249
250
  throw Object.assign(new Error(e.message), { statusCode: 502, innererror: sanitizedError })
250
251
  }
251
252
 
@@ -267,6 +268,7 @@ const run = async (
267
268
 
268
269
  LOG._warn && LOG.warn(sanitizedError)
269
270
 
271
+ // REVISIT: switch from innererror to reason in cds^6
270
272
  throw Object.assign(new Error(`Error during request to remote service: ${e.message}`), {
271
273
  statusCode: 502,
272
274
  innererror: sanitizedError
@@ -294,6 +296,8 @@ const run = async (
294
296
  : 'Request to remote service failed.'
295
297
  const sanitizedError = _getSanitizedError(contentJSON, requestConfig)
296
298
  LOG._warn && LOG.warn(sanitizedError)
299
+
300
+ // REVISIT: switch from innererror to reason in cds^6
297
301
  throw Object.assign(new Error(contentJSON.message), { statusCode: 502, innererror: sanitizedError })
298
302
  }
299
303
  }
@@ -5,16 +5,14 @@ const { entriesStructureToEntityStructure } = require('./utils')
5
5
  module.exports = async (service, entityFQN, selection) => {
6
6
  const filter = getArgumentByName(selection.arguments, ARGUMENT.FILTER)
7
7
 
8
- let queryBeforeUpdate = service.read(entityFQN)
8
+ const queryBeforeUpdate = service.read(entityFQN)
9
9
  queryBeforeUpdate.columns(astToColumns(selection.selectionSet.selections))
10
10
 
11
11
  if (filter) {
12
12
  queryBeforeUpdate.where(astToWhere(filter))
13
13
  }
14
14
 
15
- const resultBeforeUpdate = await service.tx(tx => tx.run(queryBeforeUpdate))
16
-
17
- let query = service.update(entityFQN)
15
+ const query = service.update(entityFQN)
18
16
 
19
17
  if (filter) {
20
18
  query.where(astToWhere(filter))
@@ -24,7 +22,12 @@ module.exports = async (service, entityFQN, selection) => {
24
22
  const entries = entriesStructureToEntityStructure(service, entityFQN, astToEntries(input))
25
23
  query.with(entries)
26
24
 
27
- const result = await service.tx(tx => tx.run(query))
25
+ let resultBeforeUpdate
26
+ const result = await service.tx(async tx => {
27
+ // read needs to be done before the update, otherwise the where clause might become invalid (case that properties in where clause are updated by the mutation)
28
+ resultBeforeUpdate = await service.tx(tx => tx.run(queryBeforeUpdate))
29
+ return tx.run(query)
30
+ })
28
31
 
29
32
  // Merge selected fields with updated data
30
33
  return resultBeforeUpdate.map(original => ({ ...original, ...result }))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "5.8.0",
3
+ "version": "5.8.1",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [