@sap/cds 7.6.4 → 7.7.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 (97) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/_i18n/i18n.properties +3 -0
  3. package/app/index.js +14 -8
  4. package/bin/serve.js +51 -19
  5. package/common.cds +16 -0
  6. package/lib/auth/ias-auth.js +2 -2
  7. package/lib/auth/index.js +1 -1
  8. package/lib/auth/jwt-auth.js +1 -1
  9. package/lib/compile/cdsc.js +23 -11
  10. package/lib/compile/for/nodejs.js +2 -2
  11. package/lib/compile/for/odata.js +4 -0
  12. package/lib/compile/load.js +7 -2
  13. package/lib/compile/to/sql.js +3 -0
  14. package/lib/dbs/cds-deploy.js +197 -220
  15. package/lib/env/defaults.js +2 -1
  16. package/lib/index.js +8 -2
  17. package/lib/linked/types.js +1 -0
  18. package/lib/log/format/json.js +4 -1
  19. package/lib/plugins.js +2 -2
  20. package/lib/ql/SELECT.js +8 -8
  21. package/lib/req/context.js +22 -13
  22. package/lib/req/request.js +10 -4
  23. package/lib/srv/cds-connect.js +9 -3
  24. package/lib/srv/cds-serve.js +5 -3
  25. package/lib/srv/middlewares/ctx-model.js +1 -1
  26. package/lib/srv/protocols/odata-v4.js +38 -9
  27. package/lib/srv/srv-api.js +98 -140
  28. package/lib/srv/srv-models.js +2 -2
  29. package/lib/srv/srv-tx.js +1 -0
  30. package/lib/utils/cds-utils.js +32 -23
  31. package/lib/utils/data.js +1 -1
  32. package/lib/utils/tar.js +1 -1
  33. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +1 -3
  34. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +0 -2
  35. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +18 -1
  36. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +1 -1
  37. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +7 -3
  38. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +2 -1
  39. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/index.js +5 -0
  40. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +71 -25
  41. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +10 -2
  42. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +6 -1
  43. package/libx/_runtime/cds-services/util/assert.js +50 -240
  44. package/libx/_runtime/cds.js +5 -0
  45. package/libx/_runtime/common/aspects/any.js +53 -45
  46. package/libx/_runtime/common/generic/input.js +14 -10
  47. package/libx/_runtime/common/generic/paging.js +1 -1
  48. package/libx/_runtime/common/utils/cqn.js +1 -1
  49. package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
  50. package/libx/_runtime/common/utils/keys.js +1 -1
  51. package/libx/_runtime/common/utils/quotingStyles.js +1 -1
  52. package/libx/_runtime/common/utils/resolveStructured.js +4 -1
  53. package/libx/_runtime/common/utils/rewriteAsterisks.js +5 -12
  54. package/libx/_runtime/common/utils/stream.js +2 -16
  55. package/libx/_runtime/common/utils/streamProp.js +16 -6
  56. package/libx/_runtime/common/utils/ucsn.js +1 -0
  57. package/libx/_runtime/db/expand/expandCQNToJoin.js +1 -1
  58. package/libx/_runtime/db/sql-builder/InsertBuilder.js +1 -1
  59. package/libx/_runtime/db/utils/columns.js +6 -1
  60. package/libx/_runtime/fiori/generic/activate.js +11 -3
  61. package/libx/_runtime/fiori/generic/edit.js +8 -2
  62. package/libx/_runtime/fiori/lean-draft.js +94 -30
  63. package/libx/_runtime/hana/execute.js +2 -5
  64. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +12 -22
  65. package/libx/_runtime/messaging/service.js +6 -2
  66. package/libx/common/assert/index.js +232 -0
  67. package/libx/common/assert/type.js +109 -0
  68. package/libx/common/assert/utils.js +125 -0
  69. package/libx/common/assert/validation.js +109 -0
  70. package/libx/odata/index.js +5 -5
  71. package/libx/odata/middleware/create.js +83 -0
  72. package/libx/odata/middleware/delete.js +38 -0
  73. package/libx/odata/middleware/error.js +8 -0
  74. package/libx/odata/{metadata.js → middleware/metadata.js} +8 -6
  75. package/libx/odata/middleware/operation.js +78 -0
  76. package/libx/odata/middleware/parse.js +11 -0
  77. package/libx/odata/{read.js → middleware/read.js} +42 -20
  78. package/libx/odata/{service-document.js → middleware/service-document.js} +2 -1
  79. package/libx/odata/middleware/stream.js +237 -0
  80. package/libx/odata/middleware/update.js +165 -0
  81. package/libx/odata/{afterburner.js → parse/afterburner.js} +79 -29
  82. package/libx/odata/{cqn2odata.js → parse/cqn2odata.js} +5 -3
  83. package/libx/odata/{parseToCqn.js → parse/parseToCqn.js} +3 -6
  84. package/libx/odata/{utils.js → utils/index.js} +95 -9
  85. package/libx/outbox/index.js +2 -1
  86. package/libx/rest/RestAdapter.js +0 -1
  87. package/libx/rest/middleware/operation.js +6 -4
  88. package/libx/rest/middleware/parse.js +20 -2
  89. package/package.json +1 -1
  90. package/server.js +43 -71
  91. package/libx/odata/create.js +0 -44
  92. package/libx/odata/delete.js +0 -25
  93. package/libx/odata/error.js +0 -12
  94. package/libx/odata/update.js +0 -110
  95. /package/libx/odata/{grammar.peggy → parse/grammar.peggy} +0 -0
  96. /package/libx/odata/{parser.js → parse/parser.js} +0 -0
  97. /package/libx/odata/{result.js → utils/result.js} +0 -0
@@ -117,25 +117,11 @@ const enhanceStreamResult = async (req, query, result, model) => {
117
117
  }
118
118
 
119
119
  const pick = element => {
120
- return element['@Core.IsURL']
120
+ return element['@Core.IsURL'] || element['@Core.MediaType']
121
121
  }
122
122
 
123
123
  const processFn = ({ row, key }) => {
124
- row[`${key}@odata.mediaReadLink`] = row[key]
125
124
  delete row[key]
126
125
  }
127
126
 
128
- function transformRedirectProperties(req, service, result) {
129
- if (!result) return
130
- if (!Array.isArray(result)) result = [result]
131
- if (result.length === 0) return
132
-
133
- const template = getTemplate('redirect-properties', service, req.target, { pick })
134
- if (template && template.elements.size) {
135
- for (const row of result) {
136
- templateProcessor({ processFn, row, template })
137
- }
138
- }
139
- }
140
-
141
- module.exports = { enhanceStreamResult, transformRedirectProperties }
127
+ module.exports = { enhanceStreamResult }
@@ -2,7 +2,8 @@ const cds = require('../../cds')
2
2
  const { ensureNoDraftsSuffix, ensureUnlocalized } = require('../../fiori/utils/handler')
3
3
  const { isDuplicate } = require('./rewriteAsterisks')
4
4
 
5
- const _addColumn = (name, type, columns) => {
5
+ const _addColumn = (name, type, columns, url) => {
6
+ if (cds.env.features.odata_new_adapter) return
6
7
  if (typeof type === 'object') {
7
8
  let mType = type['='].replaceAll(/\./g, '_')
8
9
  const ref = {
@@ -14,13 +15,22 @@ const _addColumn = (name, type, columns) => {
14
15
  const val = { val: type, as: `${name}@odata.mediaContentType` }
15
16
  if (!columns.find(isDuplicate(val))) columns.push(val)
16
17
  }
18
+
19
+ if (url) {
20
+ const ref = {
21
+ ref: [name],
22
+ as: `${name}@odata.mediaReadLink`
23
+ }
24
+ if (!columns.find(isDuplicate(ref))) columns.push(ref)
25
+ }
17
26
  }
18
27
 
19
28
  const _addColumns = (target, columns) => {
29
+ if (cds.env.features.odata_new_adapter) return
20
30
  for (const k in target.elements) {
21
31
  const el = target.elements[k]
22
32
  if (el['@Core.MediaType']) {
23
- _addColumn(el.name, el['@Core.MediaType'], columns)
33
+ _addColumn(el.name, el['@Core.MediaType'], columns, el['@Core.IsURL'])
24
34
  }
25
35
  }
26
36
  }
@@ -38,11 +48,11 @@ const handleStreamProperties = (target, columns, model) => {
38
48
 
39
49
  if (col === '*') {
40
50
  _addColumns(target, columns)
41
- } else if (col.ref && type === 'cds.LargeBinary') {
51
+ } else if (col.ref && (type === 'cds.LargeBinary' || mediaType)) {
42
52
  if (mediaType) {
43
- _addColumn(name, mediaType, columns)
44
- }
45
- if ((mediaType || !cds.env.features.stream_compat) && !element['@Core.IsURL']) {
53
+ _addColumn(name, mediaType, columns, element['@Core.IsURL'])
54
+ columns.splice(index, 1)
55
+ } else if (!cds.env.features.stream_compat) {
46
56
  columns.splice(index, 1)
47
57
  }
48
58
  } else if (col.expand && col.ref) {
@@ -100,6 +100,7 @@ const _cleanup = (row, definition, cleanupNull, cleanupStruct, errors, prefix =
100
100
  }
101
101
  }
102
102
 
103
+ // REVISIT: when needed?
103
104
  function convertStructured(service, definition, data, { cleanupNull = false, cleanupStruct = false, errors } = {}) {
104
105
  if (!definition) return
105
106
  // REVISIT check `structs` mode only for now as uCSN is not yet available
@@ -803,7 +803,7 @@ class JoinCQNFromExpanded {
803
803
  return 'DRAFT.DraftAdministrativeData'
804
804
  }
805
805
 
806
- if (isActiveRequired && !defaultLanguage) {
806
+ if (this._SELECT.localized !== false && isActiveRequired && !defaultLanguage) {
807
807
  const locale = this._locale ? `${this._locale}.` : ''
808
808
  const localized = `localized.${locale}${target}`
809
809
  if (this._csn.definitions[localized]) {
@@ -243,7 +243,7 @@ class InsertBuilder extends BaseBuilder {
243
243
  if (this.uuidKeys) {
244
244
  for (const key of this.uuidKeys) {
245
245
  if (!flattenColumnMap.get(key)) {
246
- columns.push(...this.uuidKeys.map(key => this._quoteElement(key)))
246
+ columns.push(this._quoteElement(key))
247
247
  }
248
248
  }
249
249
  }
@@ -3,6 +3,10 @@ const resolveStructured = require('../../common/utils/resolveStructured')
3
3
 
4
4
  const { DRAFT_COLUMNS_MAP } = require('../../common/constants/draft')
5
5
 
6
+ const _isStreamProperty = element => {
7
+ return element.type === 'cds.LargeBinary' || element['@Core.IsURL']
8
+ }
9
+
6
10
  /**
7
11
  * This method gets all columns for an entity.
8
12
  * It includes the generated foreign keys from managed associations, structured elements and complex and custom types.
@@ -11,7 +15,7 @@ const { DRAFT_COLUMNS_MAP } = require('../../common/constants/draft')
11
15
  * @param entity - the csn entity
12
16
  * @returns {Array} - array of columns
13
17
  */
14
- const getColumns = (entity, { _4db, onlyKeys } = { _4db: true, onlyKeys: false }) => {
18
+ const getColumns = (entity, { _4db, onlyKeys, omitStream } = { _4db: true, onlyKeys: false, omitStream: false }) => {
15
19
  // REVISIT is this correct or just a problem that occurs because of new structure we do not deal with yet?
16
20
  if (!(entity && entity.elements)) return []
17
21
  const columnNames = []
@@ -23,6 +27,7 @@ const getColumns = (entity, { _4db, onlyKeys } = { _4db: true, onlyKeys: false }
23
27
  const element = elements[elementName]
24
28
  if (element['@cds.api.ignore']) continue
25
29
  if (onlyKeys && !element.key) continue
30
+ if (omitStream && _isStreamProperty(element)) continue
26
31
  if (element.isAssociation) continue
27
32
  if (!lean_draft && _4db && entity._isDraftEnabled && elementName in DRAFT_COLUMNS_MAP) continue
28
33
  if (structs && element.elements) {
@@ -171,9 +171,17 @@ const fioriGenericActivate = async function (req, next) {
171
171
  })
172
172
  ])
173
173
 
174
- // REVISIT: we need to use okra API here because it must be set in the batched request
175
- // status code must be set in handler to allow overriding for FE V2
176
- if (event === 'CREATE') req?._?.odataRes?.setStatusCode(201, { overwrite: true })
174
+ if (event === 'CREATE') {
175
+ // REVISIT: we need to use okra API here because it must be set in the batched request
176
+ // status code must be set in handler to allow overriding for FE V2
177
+ // REVISIT: needs reworking for new adapter, especially re $batch
178
+ if (req._?.odataRes) {
179
+ req._?.odataRes?.setStatusCode(201, { overwrite: true })
180
+ } else if (req.http?.res) {
181
+ req.http.res.status(201)
182
+ }
183
+ }
184
+
177
185
  return result
178
186
  }
179
187
 
@@ -141,8 +141,14 @@ const fioriGenericEdit = async function (req, next) {
141
141
  await Promise.all(insertCQNs.map(CQN => dbtx.run(CQN)))
142
142
 
143
143
  // REVISIT: we need to use okra API here because it must be set in the batched request
144
- // status code must be set in handler to allow overriding for FE V2
145
- req?._?.odataRes?.setStatusCode(201, { overwrite: true })
144
+ // status code must be set in handler to allow overriding for FE V2
145
+ // REVISIT: needs reworking for new adapter, especially re $batch
146
+ if (req._?.odataRes) {
147
+ req._?.odataRes?.setStatusCode(201, { overwrite: true })
148
+ } else if (req.http?.res) {
149
+ req.http.res.status(201)
150
+ }
151
+
146
152
  return results[0][0]
147
153
  }
148
154
 
@@ -8,31 +8,71 @@ const original = Symbol('original')
8
8
  const DRAFT_PARAMS = Symbol('draftParams')
9
9
  const AGGREGATION_FUNCTIONS = ['sum', 'min', 'max', 'avg', 'count']
10
10
 
11
+ const calcTimeMs = timeout => {
12
+ const match = timeout.match(/^([0-9]+)(w|d|h|hrs|min)$/)
13
+ if (!match) return
14
+ const [, val, t] = match
15
+ switch (t) {
16
+ case 'w':
17
+ return val * 1000 * 3600 * 24 * 7
18
+ case 'd':
19
+ return val * 1000 * 3600 * 24
20
+ case 'h':
21
+ case 'hrs':
22
+ return val * 1000 * 3600
23
+ case 'min':
24
+ return val * 1000 * 60
25
+ default:
26
+ return val
27
+ }
28
+ }
29
+
30
+ const _config_to_ms = (config, _default) => {
31
+ const timeout = cds.env.fiori?.[config]
32
+ let timeout_ms
33
+ if (timeout === true) {
34
+ timeout_ms = calcTimeMs(_default)
35
+ } else if (typeof timeout === 'string') {
36
+ timeout_ms = calcTimeMs(timeout)
37
+ if (!timeout_ms)
38
+ throw new Error(`
39
+ ${timeout} is an invalid value for \`cds.fiori.${config}\`.
40
+ Please provide a value in format /^([0-9]+)(w|d|h|hrs|min)$/.
41
+ `)
42
+ } else {
43
+ timeout_ms = timeout
44
+ }
45
+
46
+ return timeout_ms
47
+ }
48
+
11
49
  const DEL_TIMEOUT = {
12
50
  get value() {
13
- const delTimeout = cds.env.fiori?.draft_deletion_timeout
14
- const timeout = delTimeout && delTimeout !== false && delTimeout === true ? '30d' : delTimeout
15
- let parts
16
- if (typeof timeout === 'string') {
17
- parts = timeout.match(/^([0-9]+)(d|h)$/)
18
- if (!parts && !Number(timeout)) throw new Error('Invalid value for `cds.fiori.draft_deletion_timeout`')
51
+ const timeout_ms = _config_to_ms('draft_deletion_timeout', '30d')
52
+ Object.defineProperty(DEL_TIMEOUT, 'value', { value: timeout_ms })
53
+ return timeout_ms
54
+ }
55
+ }
56
+
57
+ const LOCK_TIMEOUT = {
58
+ get value() {
59
+ let timeout_ms = _config_to_ms('draft_lock_timeout', '15min')
60
+
61
+ const deprecated = cds.env.drafts?.cancellationTimeout // in min
62
+ if (deprecated) {
63
+ // in order to still support legacy use cases for tests, e. g. 0.000001
64
+ timeout_ms = deprecated * 1000 * 60
19
65
  }
20
- const result =
21
- parts && parts.length
22
- ? parts[2] === 'd'
23
- ? Number(parts[1]) * 1000 * 3600 * 24
24
- : Number(parts[1]) * 1000 * 3600
25
- : Number(timeout) || 0
26
-
27
- Object.defineProperty(DEL_TIMEOUT, 'value', { value: result })
28
- return result
66
+
67
+ Object.defineProperty(LOCK_TIMEOUT, 'value', { value: timeout_ms })
68
+ return timeout_ms
29
69
  }
30
70
  }
31
71
 
32
72
  const reject_bypassed_draft = req => {
33
73
  const msg =
34
74
  !cds.profiles?.includes('production') &&
35
- '`cds.env.fiori.bypass_draft` must be enabled to support the directly modification of active instances.'
75
+ '`cds.env.fiori.bypass_draft` must be enabled or the entity must be annotated with `@odata.draft.bypass` to support the directly modification of active instances.'
36
76
  return req.reject(501, msg)
37
77
  }
38
78
 
@@ -116,13 +156,10 @@ const _inProcessByUserXpr = lockShiftedNow => ({
116
156
 
117
157
  const _lock = {
118
158
  get shiftedNow() {
119
- return new Date(Math.max(0, Date.now() - DRAFT_CANCEL_TIMEOUT_IN_MIN() * 60 * 1000)).toISOString()
159
+ return new Date(Math.max(0, Date.now() - LOCK_TIMEOUT.value)).toISOString()
120
160
  }
121
161
  }
122
162
 
123
- const DRAFT_CANCEL_TIMEOUT_IN_MIN = () =>
124
- (cds.env.drafts?.cancellationTimeout && Number(cds.env.drafts?.cancellationTimeout)) || 15
125
-
126
163
  const _redirectRefToDrafts = (ref, model) => {
127
164
  const [root, ...tail] = ref
128
165
  const draft = model.definitions[root.id || root].drafts
@@ -281,7 +318,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
281
318
  if (req.event === 'NEW' || req.event === 'CANCEL' || req.event === 'draftPrepare') {
282
319
  if (req.event === 'draftPrepare' && draftParams.IsActiveEntity) req.reject(400)
283
320
  if (req.event === 'NEW' && req.data?.IsActiveEntity === true) {
284
- if (!cds.env.fiori.bypass_draft) return reject_bypassed_draft(req)
321
+ if (!cds.env.fiori.bypass_draft && !req.target['@odata.draft.bypass']) return reject_bypassed_draft(req)
285
322
  const containsDraftRoot =
286
323
  this.model.definitions[query.INSERT.into?.ref?.[0]?.id || query.INSERT.into?.ref?.[0] || query.INSERT.into][
287
324
  '@Common.DraftRoot.ActivationAction'
@@ -390,6 +427,19 @@ cds.ApplicationService.prototype.handle = async function (req) {
390
427
  const HasActiveEntity = res.HasActiveEntity
391
428
  delete res.HasActiveEntity
392
429
 
430
+ if (cds.env.features.cds_assert) {
431
+ const assertOptions = { path: [req.target.actions[req.event]['@cds.odata.bindingparameter.name'] || 'in'] }
432
+ const errs = cds.assert(res, req.target, assertOptions)
433
+ if (errs) {
434
+ if (errs.length === 1) throw Object.assign(errs[0], { '@Common.numericSeverity': 4 })
435
+ throw Object.assign(new Error('MULTIPLE_ERRORS'), {
436
+ statusCode: 400,
437
+ details: errs,
438
+ '@Common.numericSeverity': 4 //> TODO: should not be needed here
439
+ })
440
+ }
441
+ }
442
+
393
443
  // First run the handlers as they might need access to DraftAdministrativeData or the draft entities
394
444
  const result = await run(
395
445
  HasActiveEntity
@@ -402,12 +452,18 @@ cds.ApplicationService.prototype.handle = async function (req) {
402
452
  DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: DraftAdministrativeData_DraftUUID })
403
453
  ])
404
454
 
405
- if (!HasActiveEntity) req?._?.odataRes?.setStatusCode(201, { overwrite: true })
455
+ if (!HasActiveEntity) {
456
+ // REVISIT: we need to use okra API here because it must be set in the batched request
457
+ // status code must be set in handler to allow overriding for FE V2
458
+ // REVISIT: needs reworking for new adapter, especially re $batch
459
+ if (req._?.odataRes) {
460
+ req._?.odataRes?.setStatusCode(201, { overwrite: true })
461
+ } else if (req.http?.res) {
462
+ req.http.res.status(201)
463
+ }
464
+ }
406
465
 
407
466
  return Object.assign(result, { IsActiveEntity: true })
408
-
409
- // REVISIT: we need to use okra API here because it must be set in the batched request
410
- // status code must be set in handler to allow overriding for FE V2
411
467
  }
412
468
 
413
469
  if (req.target.actions?.[req.event] && draftParams.IsActiveEntity === false) {
@@ -457,7 +513,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
457
513
 
458
514
  LOG.debug('patch active')
459
515
 
460
- if (!cds.env.fiori.bypass_draft) return reject_bypassed_draft(req)
516
+ if (!cds.env.fiori.bypass_draft && !req.target['@odata.draft.bypass']) return reject_bypassed_draft(req)
461
517
 
462
518
  const entityRef = query.UPDATE.entity.ref
463
519
 
@@ -1056,7 +1112,10 @@ function _cleansed(query, model) {
1056
1112
  }
1057
1113
  if (ignoredElements.has(e) && xpr[i + 2]) {
1058
1114
  let { val } = xpr[i + 2]
1059
- draftParams[x.ref.join('_')] = xpr[i + 1] === '!=' ? (typeof val === 'boolean' ? !val : 'not ' + val) : val
1115
+ const param = x.ref.join('_')
1116
+ // outer-most parameters win
1117
+ if (draftParams[param] === undefined)
1118
+ draftParams[param] = xpr[i + 1] === '!=' ? (typeof val === 'boolean' ? !val : 'not ' + val) : val
1060
1119
  i += 2
1061
1120
  const last = cleansed[cleansed.length - 1]
1062
1121
  if (last === 'and' || last === 'or') cleansed.pop()
@@ -1298,7 +1357,7 @@ async function onEdit(req) {
1298
1357
  }
1299
1358
 
1300
1359
  if (!res) req.reject(404)
1301
- const preserveChanges = req.context?.data?.PreserveChanges
1360
+ const preserveChanges = req.data?.PreserveChanges
1302
1361
  const inProcessByUser = draft?.DraftAdministrativeData?.InProcessByUser
1303
1362
 
1304
1363
  if (draft) {
@@ -1332,8 +1391,13 @@ async function onEdit(req) {
1332
1391
  await INSERT.into(targetDraft).entries(res)
1333
1392
 
1334
1393
  // REVISIT: we need to use okra API here because it must be set in the batched request
1335
- // status code must be set in handler to allow overriding for FE V2
1336
- req?._?.odataRes?.setStatusCode(201, { overwrite: true })
1394
+ // status code must be set in handler to allow overriding for FE V2
1395
+ // REVISIT: needs reworking for new adapter, especially re $batch
1396
+ if (req._?.odataRes) {
1397
+ req._?.odataRes?.setStatusCode(201, { overwrite: true })
1398
+ } else if (req.http?.res) {
1399
+ req.http.res.status(201)
1400
+ }
1337
1401
 
1338
1402
  return { ...res, IsActiveEntity: false } // REVISIT: Flatten?
1339
1403
  }
@@ -15,6 +15,7 @@ const {
15
15
  readStreamWithHdb
16
16
  } = require('./streaming')
17
17
  const { convertStream } = require('../db/utils/stream')
18
+ const { isBase64String } = require('../../common/assert/utils')
18
19
 
19
20
  function _cqnToSQL(model, query, user, locale, txTimestamp) {
20
21
  return sqlFactory(
@@ -48,8 +49,6 @@ function _getBinaries(stmt) {
48
49
  }, [])
49
50
  }
50
51
 
51
- const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}={2})$/
52
-
53
52
  function _getProcedureNameAndSchema(sql) {
54
53
  // name delimited with "" allows any character
55
54
  const match = sql
@@ -142,9 +141,7 @@ function _executeAsPreparedStatement(dbc, sql, values, reject, resolve) {
142
141
  const vals = Array.isArray(values[0]) ? values : [values]
143
142
  for (const i of binaries) {
144
143
  for (const row of vals) {
145
- if (row[i] && typeof row[i] === 'string' && row[i].match(BASE64)) {
146
- row[i] = Buffer.from(row[i], 'base64')
147
- }
144
+ if (row[i] && isBase64String(row[i])) row[i] = Buffer.from(row[i], 'base64')
148
145
  }
149
146
  }
150
147
  }
@@ -17,15 +17,15 @@ class EndpointRegistry {
17
17
  if (isSecured()) {
18
18
  if (cds.requires.auth.impl) {
19
19
  if (cds.env.requires.middlewares !== false) {
20
- paths.forEach(path => cds.app.use(path, cds.middlewares.before)) // contains auth, trace, context
20
+ cds.app.use(basePath, cds.middlewares.before) // contains auth, trace, context
21
21
  } else {
22
22
  const impl = _require(cds.resolve(cds.requires.auth.impl))
23
- paths.forEach(path => cds.app.use(path, impl))
23
+ cds.app.use(basePath, impl)
24
24
  }
25
25
  } else {
26
26
  if (cds.env.requires.middlewares !== false) {
27
27
  const jwt_auth = require('../../../../lib/auth/jwt-auth.js')
28
- paths.forEach(path => cds.app.use(path, jwt_auth(cds.requires.auth)))
28
+ cds.app.use(basePath, jwt_auth(cds.requires.auth))
29
29
  } else {
30
30
  const JWTStrategy = require('../../auth/strategies/JWT.js')
31
31
  // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
@@ -33,28 +33,22 @@ class EndpointRegistry {
33
33
  // REVISIT: It's unclear if the credentials from cds.requires.auth need to be used here.
34
34
  // In principle, user-facing endpoints might differ from messaging ones.
35
35
  passport.use(new JWTStrategy(cds.requires.auth.credentials))
36
- paths.forEach(path => {
37
- cds.app.use(path, passport.initialize())
38
- cds.app.use(path, passport.authenticate('JWT', { session: false }))
39
- })
36
+ cds.app.use(basePath, passport.initialize())
37
+ cds.app.use(basePath, passport.authenticate('JWT', { session: false }))
40
38
  }
41
39
  }
42
40
  // unsuccessful auth doesn't automatically reject!
43
- paths.forEach(path => {
44
- cds.app.use(path, (req, res, next) => {
45
- // REVISIT: we should probably pass an error into next so that a (custom) error middleware can handle it
46
- if (!req.user) res.status(401).json({ error: ODATA_UNAUTHORIZED })
47
- next()
48
- })
41
+ cds.app.use(basePath, (req, res, next) => {
42
+ // REVISIT: we should probably pass an error into next so that a (custom) error middleware can handle it
43
+ if (!req.user) res.status(401).json({ error: ODATA_UNAUTHORIZED })
44
+ next()
49
45
  })
50
46
  } else if (process.env.NODE_ENV === 'production') {
51
47
  LOG.warn('Messaging endpoints not secured')
52
48
  }
53
- paths.forEach(path => {
54
- cds.app.use(path, express.json({ type: 'application/*+json' }))
55
- cds.app.use(path, express.json())
56
- cds.app.use(path, express.urlencoded({ extended: true }))
57
- })
49
+ cds.app.use(basePath, express.json({ type: 'application/*+json' }))
50
+ cds.app.use(basePath, express.json())
51
+ cds.app.use(basePath, express.urlencoded({ extended: true }))
58
52
  LOG._debug && LOG.debug('Register inbound endpoint', { basePath, method: 'OPTIONS' })
59
53
 
60
54
  // Clear cds.context as it would interfere with subsequent transactions
@@ -62,10 +56,6 @@ class EndpointRegistry {
62
56
  cds.context = undefined
63
57
  next()
64
58
  })
65
- cds.app.use(deployPath, (_req, _res, next) => {
66
- cds.context = undefined
67
- next()
68
- })
69
59
 
70
60
  cds.app.options(basePath, (req, res) => {
71
61
  try {
@@ -126,8 +126,12 @@ class MessagingService extends cds.Service {
126
126
  const subscribedEvent =
127
127
  this.subscribedTopics.get(_msg.event) ||
128
128
  (this.wildcarded && this.subscribedTopics.get(this.wildcarded(_msg.event)))
129
- if (!subscribedEvent && !this._listenToAll.value)
130
- throw new Error(`No handler for incoming message with topic '${_msg.event}' found.`)
129
+ if (!subscribedEvent && !this._listenToAll.value) {
130
+ const err = new Error(`No handler for incoming message with topic '${_msg.event}' found.`)
131
+ err.code = 'NO_HANDLER_FOUND' // consumers might want to react to that
132
+ throw err
133
+ }
134
+
131
135
  _msg.event = subscribedEvent || _msg.event
132
136
  }
133
137
  return _msg