@sap/cds 6.2.3 → 6.3.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.
Files changed (74) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/apis/connect.d.ts +1 -1
  3. package/apis/cqn.d.ts +1 -1
  4. package/apis/internal/inference.d.ts +14 -0
  5. package/apis/ql.d.ts +40 -36
  6. package/apis/services.d.ts +23 -6
  7. package/bin/build/buildTaskEngine.js +15 -12
  8. package/bin/build/buildTaskHandler.js +3 -3
  9. package/bin/build/constants.js +2 -0
  10. package/bin/build/provider/buildTaskHandlerEdmx.js +1 -1
  11. package/bin/build/provider/buildTaskHandlerFeatureToggles.js +4 -3
  12. package/bin/build/provider/buildTaskHandlerInternal.js +2 -2
  13. package/bin/build/provider/java/index.js +2 -1
  14. package/bin/build/provider/mtx/index.js +2 -1
  15. package/bin/build/provider/mtx/resourcesTarBuilder.js +3 -2
  16. package/bin/build/provider/mtx-extension/index.js +2 -1
  17. package/bin/build/provider/mtx-sidecar/index.js +3 -1
  18. package/bin/build/util.js +2 -2
  19. package/bin/deploy/to-hana/cfUtil.js +46 -62
  20. package/lib/auth/index.js +2 -1
  21. package/lib/auth/jwt-auth.js +64 -3
  22. package/lib/auth/xsuaa-auth.js +2 -3
  23. package/lib/compile/cdsc.js +1 -0
  24. package/lib/compile/etc/_localized.js +1 -0
  25. package/lib/dbs/cds-deploy.js +2 -1
  26. package/lib/env/cds-env.js +14 -49
  27. package/lib/env/cds-requires.js +13 -7
  28. package/lib/env/defaults.js +4 -0
  29. package/lib/i18n/localize.js +11 -8
  30. package/lib/index.js +1 -1
  31. package/lib/log/cds-log.js +2 -2
  32. package/lib/log/format/cf.js +16 -0
  33. package/lib/log/format/kibana.js +15 -2
  34. package/lib/ql/INSERT.js +12 -11
  35. package/lib/ql/Query.js +14 -7
  36. package/lib/ql/UPSERT.js +1 -0
  37. package/lib/ql/Whereable.js +6 -2
  38. package/lib/ql/cds-ql.js +2 -4
  39. package/lib/req/request.js +2 -0
  40. package/lib/srv/bindings.js +1 -0
  41. package/lib/srv/middlewares/cds-context.js +1 -1
  42. package/lib/srv/srv-dispatch.js +1 -0
  43. package/lib/srv/srv-tx.js +3 -3
  44. package/lib/utils/cds-utils.js +75 -30
  45. package/lib/utils/inflect.js +24 -0
  46. package/libx/_runtime/auth/strategies/ias-auth.js +1 -1
  47. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +9 -1
  48. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +23 -6
  49. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -0
  50. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/validator/ValueValidator.js +27 -15
  51. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +1 -1
  52. package/libx/_runtime/cds-services/services/utils/compareJson.js +11 -10
  53. package/libx/_runtime/cds-services/services/utils/differ.js +6 -4
  54. package/libx/_runtime/common/composition/data.js +29 -40
  55. package/libx/_runtime/common/composition/update.js +6 -19
  56. package/libx/_runtime/common/generic/paging.js +1 -1
  57. package/libx/_runtime/common/utils/resolveView.js +7 -13
  58. package/libx/_runtime/db/utils/generateAliases.js +1 -0
  59. package/libx/_runtime/fiori/generic/before.js +5 -2
  60. package/libx/_runtime/fiori/generic/read.js +11 -4
  61. package/libx/_runtime/hana/execute.js +2 -2
  62. package/libx/_runtime/hana/search2Contains.js +3 -1
  63. package/libx/_runtime/hana/search2cqn4sql.js +1 -0
  64. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +1 -1
  65. package/libx/_runtime/messaging/enterprise-messaging-utils/EMManagement.js +5 -2
  66. package/libx/_runtime/messaging/enterprise-messaging.js +7 -1
  67. package/libx/_runtime/messaging/file-based.js +1 -1
  68. package/libx/_runtime/messaging/message-queuing.js +5 -2
  69. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  70. package/libx/_runtime/messaging/service.js +5 -3
  71. package/libx/odata/cqn2odata.js +4 -1
  72. package/libx/odata/utils.js +8 -7
  73. package/libx/rest/RestAdapter.js +1 -4
  74. package/package.json +1 -1
@@ -77,7 +77,7 @@ const _hasOpDeep = (entry, element) => {
77
77
  return false
78
78
  }
79
79
 
80
- const _addCompositionsToResult = (result, entity, prop, newValue, oldValue) => {
80
+ const _addCompositionsToResult = (result, entity, prop, newValue, oldValue, opts) => {
81
81
  /*
82
82
  * REVISIT: the current impl results in {} instead of keeping null for compo to one.
83
83
  * unfortunately, many follow-up errors occur (e.g., prop in null checks) if changed.
@@ -89,9 +89,9 @@ const _addCompositionsToResult = (result, entity, prop, newValue, oldValue) => {
89
89
  !Array.isArray(newValue[prop]) &&
90
90
  Object.keys(newValue[prop]).length === 0
91
91
  ) {
92
- composition = compareJsonDeep(entity.elements[prop]._target, undefined, oldValue && oldValue[prop])
92
+ composition = compareJsonDeep(entity.elements[prop]._target, undefined, oldValue && oldValue[prop], opts)
93
93
  } else {
94
- composition = compareJsonDeep(entity.elements[prop]._target, newValue[prop], oldValue && oldValue[prop])
94
+ composition = compareJsonDeep(entity.elements[prop]._target, newValue[prop], oldValue && oldValue[prop], opts)
95
95
  }
96
96
  if (composition.some(c => _hasOpDeep(c, entity.elements[prop]))) {
97
97
  result[prop] = entity.elements[prop].is2one ? composition[0] : composition
@@ -154,7 +154,7 @@ const _skipToMany = (entity, prop) => {
154
154
  return entity.elements[prop] && entity.elements[prop].is2many && _skip(entity, prop)
155
155
  }
156
156
 
157
- const _iteratePropsInNewEntry = (newEntry, keys, result, oldEntry, entity) => {
157
+ const _iteratePropsInNewEntry = (newEntry, keys, result, oldEntry, entity, opts) => {
158
158
  // On app-service layer, generated foreign keys are not enumerable,
159
159
  // include them here too.
160
160
  for (const prop of Object.getOwnPropertyNames(newEntry)) {
@@ -164,7 +164,7 @@ const _iteratePropsInNewEntry = (newEntry, keys, result, oldEntry, entity) => {
164
164
  }
165
165
 
166
166
  // if value did not change --> ignored
167
- if (newEntry[prop] === (oldEntry && oldEntry[prop]) || prop in DRAFT_COLUMNS_MAP) {
167
+ if (newEntry[prop] === (oldEntry && oldEntry[prop]) || (opts.ignoreDraftColumns && prop in DRAFT_COLUMNS_MAP)) {
168
168
  continue
169
169
  }
170
170
 
@@ -177,7 +177,7 @@ const _iteratePropsInNewEntry = (newEntry, keys, result, oldEntry, entity) => {
177
177
  }
178
178
 
179
179
  if (entity.elements[prop] && entity.elements[prop].isComposition) {
180
- _addCompositionsToResult(result, entity, prop, newEntry, oldEntry)
180
+ _addCompositionsToResult(result, entity, prop, newEntry, oldEntry, opts)
181
181
  continue
182
182
  }
183
183
 
@@ -185,7 +185,7 @@ const _iteratePropsInNewEntry = (newEntry, keys, result, oldEntry, entity) => {
185
185
  }
186
186
  }
187
187
 
188
- const compareJsonDeep = (entity, newValue = [], oldValue = []) => {
188
+ const compareJsonDeep = (entity, newValue = [], oldValue = [], opts) => {
189
189
  const resultsArray = []
190
190
  const keys = _getKeysOfEntity(entity)
191
191
 
@@ -200,7 +200,7 @@ const compareJsonDeep = (entity, newValue = [], oldValue = []) => {
200
200
 
201
201
  _addKeysToEntryIfNotExists(keys, newEntry)
202
202
 
203
- _iteratePropsInNewEntry(newEntry, keys, result, oldEntry, entity)
203
+ _iteratePropsInNewEntry(newEntry, keys, result, oldEntry, entity, opts)
204
204
 
205
205
  resultsArray.push(result)
206
206
  }
@@ -259,8 +259,9 @@ const compareJsonDeep = (entity, newValue = [], oldValue = []) => {
259
259
  *
260
260
  * @returns {Array}
261
261
  */
262
- const compareJson = (newValue, oldValue, entity) => {
263
- const result = compareJsonDeep(entity, newValue, oldValue)
262
+ const compareJson = (newValue, oldValue, entity, opts = {}) => {
263
+ const options = Object.assign({ ignoreDraftColumns: false }, opts)
264
+ const result = compareJsonDeep(entity, newValue, oldValue, options)
264
265
 
265
266
  // in case of batch insert, result is an array
266
267
  // in all other cases it is an array with just one entry
@@ -44,7 +44,7 @@ module.exports = class Differ {
44
44
  return cds
45
45
  .tx(req)
46
46
  .run(query)
47
- .then(dbState => compareJson(undefined, dbState, req.target))
47
+ .then(dbState => compareJson(undefined, dbState, req.target, { ignoreDraftColumns: true }))
48
48
  }
49
49
 
50
50
  async _addPartialPersistentState(req) {
@@ -59,7 +59,7 @@ module.exports = class Differ {
59
59
  const combinedData = providedData || Object.assign({}, req.query.UPDATE.data || {}, req.query.UPDATE.with || {})
60
60
  const lastTransition = newQuery.UPDATE._transitions[newQuery.UPDATE._transitions.length - 1]
61
61
  const revertedPersistent = revertData(req._.partialPersistentState, lastTransition, this._srv)
62
- return compareJson(combinedData, revertedPersistent, req.target)
62
+ return compareJson(combinedData, revertedPersistent, req.target, { ignoreDraftColumns: true })
63
63
  }
64
64
 
65
65
  async _diffPatch(req, providedData) {
@@ -73,7 +73,9 @@ module.exports = class Differ {
73
73
  .tx(req)
74
74
  .run(SELECT.from(draftRef).where(removeIsActiveEntityRecursively(where)).limit(1))
75
75
 
76
- return compareJson(providedData || req.data, req._.partialPersistentState, req.target)
76
+ return compareJson(providedData || req.data, req._.partialPersistentState, req.target, {
77
+ ignoreDraftColumns: true
78
+ })
77
79
  }
78
80
  }
79
81
 
@@ -83,7 +85,7 @@ module.exports = class Differ {
83
85
  ? req.query.INSERT.entries[0]
84
86
  : req.query.INSERT.entries
85
87
 
86
- return compareJson(originalData, undefined, req.target)
88
+ return compareJson(originalData, undefined, req.target, { ignoreDraftColumns: true })
87
89
  }
88
90
 
89
91
  async calculate(req, providedData) {
@@ -8,6 +8,8 @@ const { cqn2cqn4sql } = require('../utils/cqn2cqn4sql')
8
8
  const cds = require('../../cds')
9
9
  const { SELECT } = cds.ql
10
10
 
11
+ const CHUNK_SIZE = cds.env.features.chunk_deep || Number.MAX_VALUE
12
+
11
13
  /*
12
14
  * own utils
13
15
  */
@@ -62,13 +64,17 @@ const _getLinksOfCompTree = compositionTree => {
62
64
  return links
63
65
  }
64
66
 
65
- const _whereKeys = keys => {
66
- const where = []
67
- keys.forEach(key => {
68
- if (where.length) where.push('or')
69
- where.push({ xpr: [...ctUtils.whereKey(key)] })
70
- })
71
- return where
67
+ const _whereKeys = keySet => {
68
+ if (!keySet.length || !Object.keys(keySet[0]).length) return []
69
+
70
+ const keys0 = Object.keys(keySet[0])
71
+ const keys = { list: keys0.map(pk => ({ ref: [pk] })) }
72
+ const values = {
73
+ list: keySet.map(row => ({
74
+ list: keys0.map(k => ({ val: row[k] }))
75
+ }))
76
+ }
77
+ return [keys, 'in', values]
72
78
  }
73
79
 
74
80
  const _parentKey = (element, key) => {
@@ -136,28 +142,19 @@ const _subWhere = (result, element) => {
136
142
  const links = [...element.backLinks, ...element.customBackLinks]
137
143
  if (result.length && links && links.length > 0) {
138
144
  where = {}
139
- const chunkSize = cds.env.features.chunk_deep
140
145
  const keys0 = Object.keys(_getWhereObj(result[0], links))
141
- if (chunkSize && result.length > chunkSize && keys0.length) {
146
+ if (keys0.length) {
142
147
  const keys = { list: keys0.map(pk => ({ ref: [pk] })) }
143
- for (let i = 0; i < result.length; i += chunkSize) {
148
+ for (let i = 0; i < result.length; i += CHUNK_SIZE) {
144
149
  const values = {
145
- list: result
146
- .slice(i, i + chunkSize)
147
- .map(row => ({ list: keys0.map(k => ({ val: _getWhereObj(row, links)[k] || null })) }))
150
+ list: result.slice(i, i + CHUNK_SIZE).map(row => ({
151
+ list: keys0.map(k => {
152
+ return { val: _getWhereObj(row, links)[k] }
153
+ })
154
+ }))
148
155
  }
149
156
  where[i] = [keys, 'in', values]
150
157
  }
151
- } else {
152
- where = []
153
- for (const row of result) {
154
- const whereObj = _getWhereObj(row, links)
155
- const whereCQN = ctUtils.whereKey(whereObj)
156
- if (whereCQN.length) {
157
- if (where.length > 0) where.push('or')
158
- where.push({ xpr: [...whereCQN] })
159
- }
160
- }
161
158
  }
162
159
  }
163
160
  return where
@@ -232,29 +229,22 @@ const _select = ({
232
229
  }
233
230
 
234
231
  const _selectDeepUpdateData = async args => {
235
- const { model, compositionTree, entityName, data, root, selectData, tx, selectAllColumns } = args
232
+ const { model, compositionTree, entityName, data, root, selectData, tx, selectAllColumns, where, parentKeys } = args
236
233
  let result = []
237
- const chunkSize = cds.env.features.chunk_deep
238
- if (
239
- chunkSize &&
240
- !args.where &&
241
- args.parentKeys &&
242
- args.parentKeys.length > chunkSize &&
243
- Object.keys(args.parentKeys[0]).length
244
- ) {
245
- const keys0 = Object.keys(args.parentKeys[0])
234
+ if (!where && parentKeys && parentKeys.length && Object.keys(parentKeys[0]).length) {
235
+ const keys0 = Object.keys(parentKeys[0])
246
236
  const keys = { list: keys0.map(pk => ({ ref: [pk] })) }
247
- for (let i = 0; i < args.parentKeys.length; i += chunkSize) {
237
+ for (let i = 0; i < parentKeys.length; i += CHUNK_SIZE) {
248
238
  const values = {
249
- list: args.parentKeys.slice(i, i + chunkSize).map(row => ({ list: keys0.map(k => ({ val: row[k] || null })) }))
239
+ list: parentKeys.slice(i, i + CHUNK_SIZE).map(row => ({ list: keys0.map(k => ({ val: row[k] })) }))
250
240
  }
251
- const _args = { ...args, where: [keys, 'in', values] }
241
+ const _args = { ...args, where: [keys, 'in', values], parentKeys: undefined }
252
242
  const selectCQN = _select(_args)
253
243
  result.push(...(await tx.run(selectCQN)))
254
244
  }
255
- } else if (chunkSize && args.where && !Array.isArray(args.where)) {
256
- for (let where of Object.values(args.where)) {
257
- const _args = { ...args, where }
245
+ } else if (where && !Array.isArray(where)) {
246
+ for (let w of Object.values(where)) {
247
+ const _args = { ...args, where: w }
258
248
  const selectCQN = _select(_args)
259
249
  result.push(...(await tx.run(selectCQN)))
260
250
  }
@@ -262,7 +252,6 @@ const _selectDeepUpdateData = async args => {
262
252
  const selectCQN = _select(args)
263
253
  result = await tx.run(selectCQN)
264
254
  }
265
-
266
255
  if (!result.length) return Promise.resolve(result)
267
256
 
268
257
  const keys = _keys(model.definitions[entityName], result)
@@ -10,6 +10,8 @@ const { deepCopyObject } = require('../utils/copy')
10
10
 
11
11
  const getError = require('../../common/error')
12
12
 
13
+ const CHUNK_SIZE = cds.env.features.chunk_deep || Number.MAX_VALUE
14
+
13
15
  /*
14
16
  * own utils
15
17
  */
@@ -35,13 +37,11 @@ const _dataByKey = (entity, data) => {
35
37
  }
36
38
 
37
39
  function _addSubDeepUpdateCQNForDelete({ entity, data, selectData, entityName, deleteCQNs }) {
38
- const chunkSize = cds.env.features.chunk_deep
39
40
  const dataByKey = _dataByKey(entity, data)
40
- if (chunkSize && selectData.length > chunkSize && Object.keys(selectData[0]).length) {
41
- // REVISIT: Usage of "where in" syntax would be better
42
- for (let j = 0; j < selectData.length; j += chunkSize) {
41
+ if (selectData.length && selectData[0] && Object.keys(selectData[0]).length) {
42
+ for (let j = 0; j < selectData.length; j += CHUNK_SIZE) {
43
43
  const deleteCQN = { DELETE: { from: entityName, where: [] } }
44
- for (let i = j; i < j + chunkSize; i++) {
44
+ for (let i = j; i < j + CHUNK_SIZE && i < selectData.length; i++) {
45
45
  const selectEntry = selectData[i]
46
46
  if (!selectEntry) continue
47
47
  const dataEntry = dataByKey.get(_serializedKey(entity, selectEntry))
@@ -52,21 +52,8 @@ function _addSubDeepUpdateCQNForDelete({ entity, data, selectData, entityName, d
52
52
  deleteCQN.DELETE.where.push({ xpr: [...ctUtils.whereKey(ctUtils.key(entity, selectEntry))] })
53
53
  }
54
54
  }
55
- deleteCQNs.push(deleteCQN)
56
- }
57
- } else {
58
- const deleteCQN = { DELETE: { from: entityName, where: [] } }
59
- for (const selectEntry of selectData) {
60
- if (!selectEntry) continue
61
- const dataEntry = dataByKey.get(_serializedKey(entity, selectEntry))
62
- if (!dataEntry) {
63
- if (deleteCQN.DELETE.where.length > 0) {
64
- deleteCQN.DELETE.where.push('or')
65
- }
66
- deleteCQN.DELETE.where.push({ xpr: [...ctUtils.whereKey(ctUtils.key(entity, selectEntry))] })
67
- }
55
+ if (deleteCQN.DELETE.where.length) deleteCQNs.push(deleteCQN)
68
56
  }
69
- deleteCQNs.push(deleteCQN)
70
57
  }
71
58
  }
72
59
 
@@ -3,7 +3,7 @@ const { getDefaultPageSize, getMaxPageSize } = require('../utils/page')
3
3
 
4
4
  const commonGenericPaging = function (req) {
5
5
  // only if http request
6
- if (!req._.req) return
6
+ if (!(req.http?.req || req._.req)) return
7
7
 
8
8
  // target === null if view with parameters
9
9
  if (!req.target || !req.query.SELECT || req.query.SELECT.one) return
@@ -353,7 +353,7 @@ const _newSelect = (query, transitions, service) => {
353
353
  if (!newSelect.columns && targetTransition.mapping.size) newSelect.columns = _initialColumns(targetTransition)
354
354
  if (newSelect.columns) {
355
355
  rewriteAsterisks({ SELECT: query.SELECT }, service.model, {
356
- _4db: service instanceof cds.DatabaseService,
356
+ _4db: service instanceof cds.DatabaseService || service.kind === 'better-sqlite',
357
357
  target: targetTransition.queryTarget
358
358
  })
359
359
  newSelect.columns = _newColumns(newSelect.columns, targetTransition, service, service.kind !== 'app-service')
@@ -517,26 +517,20 @@ const _getTransitionData = (target, columns, service, skipForbiddenViewCheck) =>
517
517
  if (!skipForbiddenViewCheck) _checkForForbiddenViews(target)
518
518
  const targetStartsWithSrvName = service.namespace && target.name.startsWith(`${service.namespace}.`)
519
519
  const persistenceTable = _isPersistenceTable(target)
520
- columns = _queryColumns(
521
- target,
522
- columns,
523
- persistenceTable,
524
- !(service instanceof cds.DatabaseService) && !targetStartsWithSrvName
525
- )
526
- if (persistenceTable && service instanceof cds.DatabaseService) {
520
+ const isDatabaseService = service instanceof cds.DatabaseService || service.kind === 'better-sqlite'
521
+ columns = _queryColumns(target, columns, persistenceTable, !isDatabaseService && !targetStartsWithSrvName)
522
+ // REVISIT: Change once we expose database service
523
+ if (persistenceTable && isDatabaseService) {
527
524
  return { target, transitionColumns: columns }
528
525
  }
529
526
  // stop projection resolving if it starts with the service name prefix
530
- if (!(service instanceof cds.DatabaseService) && targetStartsWithSrvName) {
527
+ if (!isDatabaseService && targetStartsWithSrvName) {
531
528
  return { target, transitionColumns: columns }
532
529
  }
533
530
  // continue projection resolving if the target is a projection
534
531
  if (target.query && target.query._target) {
535
532
  const newTarget = target.query._target
536
- if (
537
- service instanceof cds.DatabaseService ||
538
- !(service.namespace && newTarget.name.startsWith(`${service.namespace}.`))
539
- ) {
533
+ if (isDatabaseService || !(service.namespace && newTarget.name.startsWith(`${service.namespace}.`))) {
540
534
  return _getTransitionData(newTarget, columns, service, skipForbiddenViewCheck)
541
535
  }
542
536
  return { target: newTarget, transitionColumns: columns }
@@ -57,6 +57,7 @@ const _generateAliases = (partialCqn, aliasMap = new Map()) => {
57
57
  _redirectXpr(partialCqn.SELECT.having, selectMap)
58
58
  _redirectXpr(partialCqn.SELECT.columns, selectMap)
59
59
  _redirectXpr(partialCqn.SELECT.groupBy, selectMap)
60
+ _redirectXpr(partialCqn.SELECT.orderBy, selectMap)
60
61
  return
61
62
  }
62
63
 
@@ -77,7 +77,7 @@ const _getRoot = req => {
77
77
  return root
78
78
  }
79
79
 
80
- const _getDraftDataFromExistingDraft = async (req, root) => {
80
+ const _getDraftDataFromExistingDraft = async (req, root, isBoundAction) => {
81
81
  if (!root) return []
82
82
  if (root?.IsActiveEntity === false) {
83
83
  const query = _getSelectDraftDataCqn(root.entityName, root.where)
@@ -85,6 +85,9 @@ const _getDraftDataFromExistingDraft = async (req, root) => {
85
85
  return result
86
86
  }
87
87
 
88
+ // do not expect validate draft ownership for action call on active instances
89
+ if (isBoundAction) return []
90
+
88
91
  const rootWhere = getKeysCondition(req)
89
92
  const query = _getSelectDraftDataCqn(ensureNoDraftsSuffix(req.target.name), rootWhere)
90
93
  const result = await cds.tx(req).run(query)
@@ -147,8 +150,8 @@ const _deleteCancel = async function (req) {
147
150
  }
148
151
 
149
152
  const _validateDraftBoundAction = async function (req) {
150
- const result = await _getDraftDataFromExistingDraft(req, _getRoot(req))
151
153
  const isBoundAction = true
154
+ const result = await _getDraftDataFromExistingDraft(req, _getRoot(req), isBoundAction)
152
155
  if (result && result.length > 0) _validateDraft(req, result, isBoundAction)
153
156
  }
154
157
 
@@ -187,16 +187,23 @@ const _getDraftPropertiesDetermineDraft = (req, where, tableName, calcDraftUUID
187
187
  }
188
188
 
189
189
  function _copyCQNPartial(partial) {
190
- if (partial.SELECT && partial.SELECT.where) {
190
+ if (partial.SELECT) {
191
191
  const newPartial = Object.assign({}, partial)
192
192
  const newSELECT = Object.assign({}, partial.SELECT)
193
193
  newSELECT.from = _copyCQNPartial(partial.SELECT.from)
194
194
  newPartial.SELECT = newSELECT
195
+ if (partial.SELECT._4odata) newSELECT._4odata = true
195
196
  if (partial.SELECT.columns) newPartial.SELECT.columns = _copyArray(partial.SELECT.columns)
196
197
  if (partial.SELECT.where) newPartial.SELECT.where = _copyArray(partial.SELECT.where)
197
198
  return newPartial
198
199
  }
199
200
 
201
+ if (partial.id) {
202
+ const res = Object.assign({}, partial)
203
+ if (res.where) res.where = _copyArray(res.where)
204
+ return res
205
+ }
206
+
200
207
  if (partial.ref) {
201
208
  return Object.assign({}, partial, { ref: _copyArray(partial.ref) })
202
209
  }
@@ -1272,12 +1279,12 @@ const fioriGenericRead = async function (req) {
1272
1279
  req.target = _getLocalizedEntity(this.model, req.target, req.user) || req.target
1273
1280
 
1274
1281
  // REVISIT DRAFT HANDLING: cqn2cqn4sql must not be called here
1275
- const query4sql = cqn2cqn4sql(req.query, this.model, { _4fiori: true })
1282
+ const query4sql = cqn2cqn4sql(_copyCQNPartial(req.query), this.model, { _4fiori: true })
1276
1283
 
1277
1284
  // Clone the request. Do not clone with Object.assign as that would skip all non-enumerable properties.
1278
1285
  // REVISIT: query4sql.clone() doesn't really clone the original query, hence _generateCQN will heavily modify
1279
1286
  // it, e.g. IsActiveEntity is stripped. This is a problem for subsequent handlers which rely on this information.
1280
- const reqClone = { __proto__: req, query: query4sql.clone() }
1287
+ const reqClone = { __proto__: req, query: query4sql }
1281
1288
  // Clone draft restrictions to the cloned query.
1282
1289
  reqClone.query._draftRestrictions = query._draftRestrictions
1283
1290
 
@@ -1320,7 +1327,7 @@ const fioriGenericRead = async function (req) {
1320
1327
  _adaptColumns4readAfterWrite(req, cqnScenario, query4sql)
1321
1328
 
1322
1329
  const result = await cds.tx(req).send({ query: cqnScenario.cqn, target: req.target })
1323
- return _postProcess(result, req, cqnScenario, enhancedWithLastChangeDateTime)
1330
+ return _postProcess(result, reqClone, cqnScenario, enhancedWithLastChangeDateTime)
1324
1331
  }
1325
1332
 
1326
1333
  module.exports = cds.service.impl(function (srv, entity) {
@@ -51,8 +51,8 @@ const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Z
51
51
 
52
52
  function _getProcedureName(sql) {
53
53
  // name delimited with "" allows any character
54
- const match = sql.trim().match(/^call \s*(("(?<delimited>.+)")|(?<undelimited>\w+))\s*\(/i)
55
- return match && (match.groups.undelimited || match.groups.delimited)
54
+ const match = sql.trim().match(/^call \s*(("\w+"\.)?("(?<delimited>.+)")|(\w+\.)?(?<undelimited>\w+))\s*\(/i)
55
+ return match && (match.groups.undelimited ?? match.groups.delimited)
56
56
  }
57
57
 
58
58
  function _hdbGetResultForProcedure(rows, args, outParameters) {
@@ -95,7 +95,9 @@ const isContainsPredicateSupported = (query, entity, columns2Search) => {
95
95
  const _isColumnFunc = (columns2Search, columnsDefs) =>
96
96
  columns2Search.some(column2Search => {
97
97
  if (column2Search.func) return true
98
- return columnsDefs?.some(columnDef => columnDef.func && columnDef.as === column2Search.ref[0])
98
+ return columnsDefs?.some(
99
+ columnDef => (columnDef.func && columnDef.as === column2Search.ref[0]) || columnDef?.xpr?.some(xpr => xpr.func)
100
+ )
99
101
  })
100
102
 
101
103
  module.exports = {
@@ -82,6 +82,7 @@ const _addAliasToQuery = (query, entity, columnsToBeSearched) => {
82
82
  SELECT.columns = addAliasToExpression(SELECT.columns, getEntityName)
83
83
  columnsToBeSearched = addAliasToExpression(columnsToBeSearched, getEntityName)
84
84
  SELECT.groupBy = addAliasToExpression(SELECT.groupBy, getEntityName)
85
+ SELECT.orderBy = addAliasToExpression(SELECT.orderBy, getEntityName)
85
86
  SELECT.where = addAliasToExpression(SELECT.where, getEntityName)
86
87
  return columnsToBeSearched
87
88
  }
@@ -38,7 +38,7 @@ class AMQPWebhookMessaging extends MessagingService {
38
38
  }
39
39
 
40
40
  startListening(opt = {}) {
41
- if (!this.subscribedTopics.size) return
41
+ if (!this._listenToAll && !this.subscribedTopics.size) return
42
42
  if (!opt.doNotDeploy) {
43
43
  const management = this.getManagement()
44
44
  this.queued(management.createQueueAndSubscriptions.bind(management))()
@@ -87,12 +87,15 @@ class EMManagement {
87
87
  this.subdomain ? { queue: queueName, subdomain: this.subdomain } : { queue: queueName }
88
88
  )
89
89
  try {
90
+ const queueConfig = this.queueConfig && { ...this.queueConfig }
91
+ if (queueConfig?.deadMsgQueue)
92
+ queueConfig.deadMsgQueue = queueConfig.deadMsgQueue.replace(/\$namespace/g, this.namespace)
90
93
  const res = await authorizedRequest({
91
94
  method: 'PUT',
92
95
  uri: this.options.uri,
93
96
  path: `/hub/rest/api/v1/management/messaging/queues/${encodeURIComponent(queueName)}`,
94
97
  oa2: this.options.oa2,
95
- dataObj: this.queueConfig,
98
+ dataObj: queueConfig,
96
99
  tokenStore: this
97
100
  })
98
101
  if (res.statusCode === 201) return true
@@ -389,7 +392,7 @@ class EMManagement {
389
392
  this.LOG._info && this.LOG.info('Unchanged subscriptions', unchangedSubs, ' ', this.subdomainInfo)
390
393
  await Promise.all([
391
394
  ...obsoleteSubs.map(s => this.deleteSubscription(s)),
392
- ...additionalSubs.map(async s => this.createSubscription(s))
395
+ ...additionalSubs.map(async t => this.createSubscription(t))
393
396
  ])
394
397
  return
395
398
  }
@@ -139,7 +139,7 @@ class EnterpriseMessaging extends AMQPWebhookMessaging {
139
139
  const doNotDeploy = _multitenancyEnabled() && !this.options.deployForProvider
140
140
  if (doNotDeploy) this.LOG._info && this.LOG.info('Skipping deployment of messaging artifacts for provider account')
141
141
  super.startListening({ doNotDeploy })
142
- if (!doNotDeploy && this.subscribedTopics.size) {
142
+ if (!doNotDeploy && (this._listenToAll || this.subscribedTopics.size)) {
143
143
  const management = this.getManagement()
144
144
  // Webhooks will perform an OPTIONS call on creation to check the availability of the app.
145
145
  // On systems like Cloud Foundry the app URL will only be advertised once
@@ -218,6 +218,11 @@ class EnterpriseMessaging extends AMQPWebhookMessaging {
218
218
  const topic = msg.event
219
219
  const message = { ...(msg.headers || {}), data: msg.data }
220
220
 
221
+ const contentType =
222
+ msg.headers && ['id', 'source', 'specversion', 'type'].every(el => el in msg.headers)
223
+ ? 'application/cloudevents+json'
224
+ : 'application/json'
225
+
221
226
  await this.queued(() => {})()
222
227
 
223
228
  try {
@@ -229,6 +234,7 @@ class EnterpriseMessaging extends AMQPWebhookMessaging {
229
234
  tenant,
230
235
  dataObj: message,
231
236
  headers: {
237
+ 'Content-Type': contentType,
232
238
  'x-qos': 1
233
239
  },
234
240
  tokenStore: {}
@@ -35,7 +35,7 @@ class FileBasedMessaging extends MessagingService {
35
35
  }
36
36
 
37
37
  startWatching() {
38
- if (!this.subscribedTopics.size) return
38
+ if (!this._listenToAll && !this.subscribedTopics.size) return
39
39
  const watcher = async () => {
40
40
  if (!(await touched(this.file, this.recent))) return // > not touched since last check
41
41
  // REVISIT: Bad if lock file wasn't cleaned up (due to crashes...)
@@ -61,12 +61,15 @@ class MQManagement {
61
61
  async createQueue(queueName = this.queueName) {
62
62
  this.LOG._info && this.LOG.info('Create queue', { queue: queueName })
63
63
  try {
64
+ const queueConfig = this.queueConfig && { ...this.queueConfig }
65
+ if (queueConfig?.deadMessageQueue)
66
+ queueConfig.deadMessageQueue = queueConfig.deadMessageQueue.replace(/\$namespace/g, '')
64
67
  const res = await authorizedRequest({
65
68
  method: 'PUT',
66
69
  uri: this.options.url,
67
70
  path: `/v1/management/queues/${encodeURIComponent(queueName)}`,
68
71
  oa2: this.options.auth.oauth2,
69
- dataObj: this.queueConfig,
72
+ dataObj: queueConfig,
70
73
  tokenStore: this
71
74
  })
72
75
  if (res.statusCode === 201) return true
@@ -182,7 +185,7 @@ class MQManagement {
182
185
  .filter(s => !existingSubscriptions.some(e => s === e))
183
186
  await Promise.all([
184
187
  ...obsoleteSubs.map(s => this.deleteSubscription(s)),
185
- ...additionalSubs.map(s => this.createSubscription(s))
188
+ ...additionalSubs.map(t => this.createSubscription(t))
186
189
  ])
187
190
  return
188
191
  }
@@ -50,7 +50,7 @@ const _processSingleMessage = async (service, message, succeededMessages) => {
50
50
  // Promise resolve is necessary because we want to set `cds.context` only
51
51
  // inside this call
52
52
  return Promise.resolve().then(async () => {
53
- if (userId) cds.context.user = new cds.User.Privileged(userId)
53
+ if (userId) cds.context = { user: new cds.User.Privileged(userId) }
54
54
  try {
55
55
  service._emitImmediate && (await service._emitImmediate(msg))
56
56
  succeededMessages.push(message.ID)
@@ -97,7 +97,8 @@ class MessagingService extends OutboxService {
97
97
  on(event, cb) {
98
98
  const _event = _warnAndStripTopicPrefix(event, this.LOG)
99
99
  // save all subscribed topics (not needed for local-messaging)
100
- this.subscribedTopics.set(this.prepareTopic(_event, true), _event)
100
+ if (event !== '*') this.subscribedTopics.set(this.prepareTopic(_event, true), _event)
101
+ else this._listenToAll = true
101
102
  return super.on(_event, cb)
102
103
  }
103
104
 
@@ -134,8 +135,9 @@ class MessagingService extends OutboxService {
134
135
  const subscribedEvent =
135
136
  this.subscribedTopics.get(_msg.event) ||
136
137
  (this.wildcarded && this.subscribedTopics.get(this.wildcarded(_msg.event)))
137
- if (!subscribedEvent) throw new Error(`No handler for incoming message with topic '${_msg.event}' found.`)
138
- _msg.event = subscribedEvent
138
+ if (!subscribedEvent && !this._listenToAll)
139
+ throw new Error(`No handler for incoming message with topic '${_msg.event}' found.`)
140
+ _msg.event = subscribedEvent || _msg.event
139
141
  }
140
142
  return _msg
141
143
  }
@@ -516,12 +516,15 @@ const _select = (cqn, kind, model) => {
516
516
  const _insert = (cqn, kind, model) => {
517
517
  const INSERT = getProp(cqn, 'INSERT')
518
518
  const { url } = _from(getProp(INSERT, 'into'), kind, model)
519
- const body = Array.isArray(INSERT.entries) && INSERT.entries.length === 1 ? INSERT.entries[0] : INSERT.entries
519
+ const body = _copyData(
520
+ Array.isArray(INSERT.entries) && INSERT.entries.length === 1 ? INSERT.entries[0] : INSERT.entries
521
+ )
520
522
  return { method: 'POST', path: url, body }
521
523
  }
522
524
 
523
525
  const _copyData = data => {
524
526
  // only works on flat structures
527
+ if (Array.isArray(data)) return data.map(_copyData)
525
528
  const copied = {}
526
529
  for (const property in data) {
527
530
  copied[property] =
@@ -52,10 +52,9 @@ const formatVal = (val, elementName, csnTarget, kind) => {
52
52
  if (typeof val === 'boolean') return val
53
53
  if (typeof val === 'number') return getSafeNumber(val)
54
54
  if (!csnTarget && typeof val === 'string' && UUID.test(val)) return kind === 'odata-v2' ? `guid'${val}'` : val
55
- const { type } = _getElement(csnTarget, elementName)
56
-
55
+ const element = _getElement(csnTarget, elementName)
57
56
  if (kind === 'odata-v2') {
58
- switch (type) {
57
+ switch (element.type) {
59
58
  case 'cds.Decimal':
60
59
  case 'cds.Integer64':
61
60
  case 'cds.Int64':
@@ -64,20 +63,22 @@ const formatVal = (val, elementName, csnTarget, kind) => {
64
63
  case 'cds.LargeBinary':
65
64
  return `binary'${toBase64url(val)}'`
66
65
  case 'cds.Date':
67
- return `datetime'${val}T00:00:00'`
66
+ return element['@odata.Type'] === 'Edm.DateTimeOffset'
67
+ ? `datetimeoffset'${val}T00:00:00'`
68
+ : `datetime'${val}T00:00:00'`
68
69
  case 'cds.DateTime':
69
- return `datetime'${val}'`
70
+ return element['@odata.Type'] === 'Edm.DateTimeOffset' ? `datetimeoffset'${val}'` : `datetime'${val}'`
70
71
  case 'cds.Time':
71
72
  return `time'${_PT(val.split(':'))}'`
72
73
  case 'cds.Timestamp':
73
- return `datetimeoffset'${val}'`
74
+ return element['@odata.Type'] === 'Edm.DateTime' ? `datetime'${val}'` : `datetimeoffset'${val}'`
74
75
  case 'cds.UUID':
75
76
  return `guid'${val}'`
76
77
  default:
77
78
  return `'${val}'`
78
79
  }
79
80
  } else {
80
- switch (type) {
81
+ switch (element.type) {
81
82
  case 'cds.Binary':
82
83
  case 'cds.LargeBinary':
83
84
  return `binary'${toBase64url(val)}'`