@sap/cds 8.0.4 → 8.1.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 (52) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/_i18n/i18n_bg.properties +113 -0
  3. package/_i18n/i18n_el.properties +113 -0
  4. package/_i18n/i18n_he.properties +113 -0
  5. package/_i18n/i18n_hr.properties +113 -0
  6. package/_i18n/i18n_kk.properties +113 -0
  7. package/_i18n/i18n_sh.properties +113 -0
  8. package/_i18n/i18n_sk.properties +113 -0
  9. package/_i18n/i18n_sl.properties +113 -0
  10. package/_i18n/i18n_uk.properties +113 -0
  11. package/lib/compile/etc/_localized.js +8 -20
  12. package/lib/dbs/cds-deploy.js +1 -0
  13. package/lib/env/defaults.js +1 -1
  14. package/lib/env/plugins.js +22 -6
  15. package/lib/index.js +3 -2
  16. package/lib/linked/validate.js +4 -3
  17. package/lib/log/cds-log.js +2 -2
  18. package/lib/req/context.js +1 -0
  19. package/lib/req/locale.js +1 -1
  20. package/lib/srv/protocols/hcql.js +5 -5
  21. package/lib/srv/protocols/http.js +23 -11
  22. package/lib/srv/srv-tx.js +1 -0
  23. package/lib/test/expect.js +1 -1
  24. package/lib/utils/cds-test.js +4 -4
  25. package/libx/_runtime/common/error/utils.js +2 -1
  26. package/libx/_runtime/common/generic/input.js +2 -5
  27. package/libx/_runtime/common/generic/stream.js +18 -3
  28. package/libx/_runtime/common/utils/cqn2cqn4sql.js +14 -2
  29. package/libx/_runtime/db/expand/expandCQNToJoin.js +33 -2
  30. package/libx/_runtime/fiori/lean-draft.js +16 -7
  31. package/libx/_runtime/hana/customBuilder/CustomReferenceBuilder.js +1 -1
  32. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
  33. package/libx/_runtime/messaging/event-broker.js +23 -9
  34. package/libx/_runtime/remote/Service.js +3 -1
  35. package/libx/common/assert/utils.js +1 -57
  36. package/libx/odata/middleware/batch.js +5 -6
  37. package/libx/odata/middleware/body-parser.js +2 -3
  38. package/libx/odata/middleware/create.js +5 -0
  39. package/libx/odata/middleware/delete.js +5 -0
  40. package/libx/odata/middleware/error.js +1 -0
  41. package/libx/odata/middleware/operation.js +17 -11
  42. package/libx/odata/middleware/read.js +10 -1
  43. package/libx/odata/middleware/update.js +9 -4
  44. package/libx/odata/parse/grammar.peggy +6 -1
  45. package/libx/odata/parse/multipartToJson.js +1 -1
  46. package/libx/odata/parse/parser.js +1 -1
  47. package/libx/odata/utils/metadata.js +18 -44
  48. package/libx/rest/middleware/error.js +1 -0
  49. package/libx/rest/middleware/parse.js +1 -1
  50. package/package.json +1 -1
  51. package/libx/common/assert/index.js +0 -228
  52. package/libx/common/assert/type-relaxed.js +0 -39
@@ -1,6 +1,6 @@
1
1
  const express = require('express')
2
2
  const cds = require('../../index')
3
- const LOG = cds.log('hcql')
3
+ const DEBUG = cds.debug('hcql')
4
4
  const { inspect } = require('util')
5
5
 
6
6
  class HCQLAdapter extends require('./http') {
@@ -14,8 +14,8 @@ class HCQLAdapter extends require('./http') {
14
14
  */
15
15
  .get('/\\$csn', (_, res) => res.json(this.schema))
16
16
 
17
- .use(express.json()) //> for application/json -> cqn
18
- .use(express.text()) //> for text/plain -> cql -> cqn
17
+ .use(express.json(this.body_parser_options)) //> for application/json -> cqn
18
+ .use(express.text(this.body_parser_options)) //> for text/plain -> cql -> cqn
19
19
 
20
20
  /**
21
21
  * Convenience route for REST-style request formats like that:
@@ -36,7 +36,7 @@ class HCQLAdapter extends require('./http') {
36
36
  */
37
37
  .use((req, res, next) => {
38
38
  let q = this.query4(req)
39
- LOG._info && LOG.info(req.method, decodeURIComponent(req.originalUrl), inspect(q, { colors: true, depth: 11 }))
39
+ DEBUG?.(inspect(q,{depth:11,colors:true}))
40
40
  return srv.run(q).then(r => res.json(r)).catch(next)
41
41
  })
42
42
  }
@@ -46,7 +46,7 @@ class HCQLAdapter extends require('./http') {
46
46
  }
47
47
 
48
48
  query4 (req) {
49
- if (typeof req.body === 'string') return cds.parse.cql(req.body)
49
+ if (typeof req.body === 'string') return req.body = cds.parse.cql(req.body)
50
50
  return req.body //> a plain CQN object
51
51
  }
52
52
  }
@@ -17,38 +17,50 @@ class HttpAdapter {
17
17
 
18
18
  /** The actual Router factory. Subclasses override this to add specific handlers. */
19
19
  get router() {
20
- let router = super.router = (new express.Router) .use (this.http_log.bind(this))
21
- let assert_roles = this.requires_check()
22
- if (assert_roles) router.use (assert_roles)
20
+ let router = super.router = (new express.Router)
21
+ this.use (this.http_log)
22
+ this.use (this.requires_check)
23
23
  return router
24
24
  }
25
25
 
26
- /** Handler to log all incoming requests */
27
- http_log (r,_,next) {
28
- this.logger = cds.log(this.kind)
29
- this.log(r)
30
- next()
26
+ use (middleware) {
27
+ if (middleware) this.router.use (middleware)
28
+ return this
31
29
  }
32
30
 
33
31
  /** Subclasses may override this method to log incoming requests. */
34
- log (req, LOG = this.logger) { LOG._info && LOG.info (
32
+ log (req, LOG = this.logger) { LOG.info (
35
33
  req.method,
36
34
  decodeURI (req.baseUrl + req.path),
37
35
  Object.keys (req.query).length ? { ...req.query } : ''
38
36
  )}
39
37
 
38
+ /** Returns a handler to log all incoming requests */
39
+ get http_log() {
40
+ const LOG = this.logger = cds.log(this.kind); if (!LOG._info) return undefined
41
+ const log = this.log.bind(this)
42
+ return function http_log (req,_,next) { log(req,LOG); next() }
43
+ }
44
+
40
45
  /** Returns a handler to check required roles, or null if no check required. */
41
- requires_check() {
46
+ get requires_check() {
42
47
  const d = this.service.definition
43
48
  const roles = d['@requires'] || d['@restrict']?.map(r => r.to).flat().filter(r => r)
44
49
  const required = !roles?.length ? restricted_by_default : Array.isArray(roles) ? roles : [roles]
45
- if (required) return function requires_check (req, res, next) {
50
+ return required && function requires_check (req, res, next) {
46
51
  const user = cds.context.user
47
52
  if (required.some(role => user.has(role))) return next()
48
53
  else if (user._is_anonymous) return next(401) // request login
49
54
  else throw Object.assign(new Error, { code: 403, reason: `User '${user.id}' is lacking required roles: [${required}]`, user, required })
50
55
  }
51
56
  }
57
+
58
+ get body_parser_options() {
59
+ let options = cds.env.server.body_parser
60
+ let limit = this.service.definition['@cds.server.body_parser.limit']
61
+ if (limit) options = { ...options, limit }
62
+ return super.body_parser_options = options
63
+ }
52
64
  }
53
65
 
54
66
 
package/lib/srv/srv-tx.js CHANGED
@@ -96,6 +96,7 @@ class Transaction {
96
96
  * synchronous modification of passed error only
97
97
  * err is undefined if nested tx (cf. "root.before ('failed', ()=> this.rollback())")
98
98
  */
99
+ // FIXME: with noa, this.context === cds.context and not the individual cds.Request
99
100
  if (err) for (const each of this._handlers._error) each.handler.call(this, err, this.context)
100
101
 
101
102
  if (this.ready) { //> nothing to do if no transaction started at all
@@ -112,7 +112,7 @@ class Core {
112
112
  if (is.string(a)) return a.includes(x)
113
113
  if (is.array(a)) return a.includes(x) || this._deep && a.some(o => compare(o, x, true))
114
114
  if (is.set(a)) return a.has(x)
115
- if (this._deep && is.object(a)) return compare(a, x, true)
115
+ if (is.object(a)) return compare(a, x, this._deep)
116
116
  }, _fail)
117
117
  }
118
118
 
@@ -160,7 +160,7 @@ class Test extends require('./axios') {
160
160
  `)}}
161
161
  }
162
162
  set expect(x) { super.expect = x }
163
- get expect() { return this.chai.expect }
163
+ get expect() { return _expect || this.chai.expect }
164
164
  get assert() { return this.chai.assert }
165
165
  get should() { return this.chai.should() }
166
166
  }
@@ -174,6 +174,7 @@ Object.setPrototypeOf (exports, Test.prototype)
174
174
 
175
175
 
176
176
  // Provide same global functions for jest and mocha
177
+ let _expect = undefined
177
178
  ;(function _support_jest_and_mocha() {
178
179
  const _global = p => Object.getOwnPropertyDescriptor(global,p)?.value
179
180
  const is_jest = _global('beforeAll')
@@ -185,7 +186,7 @@ Object.setPrototypeOf (exports, Test.prototype)
185
186
  global.afterAll = global.after = (msg,fn) => repl.on?.('exit',fn||msg)
186
187
  global.beforeEach = global.afterEach = ()=>{}
187
188
  global.describe = ()=>{}
188
- exports.expect = global.expect = require('../test/expect')
189
+ global.expect = _expect = require('../test/expect')
189
190
 
190
191
  } else if (is_mocha) { // it's mocha
191
192
 
@@ -219,8 +220,7 @@ Object.setPrototypeOf (exports, Test.prototype)
219
220
  global.afterAll = global.after = (msg,fn) => after(fn||msg)
220
221
  global.beforeEach = beforeEach
221
222
  global.afterEach = afterEach
222
- global.expect = require('../test/expect')
223
- exports.expect = global.expect
223
+ global.expect = _expect = require('../test/expect')
224
224
  suite ('<next>', ()=>{}) //> to signal the start of a test file
225
225
 
226
226
  }
@@ -12,7 +12,8 @@ const i18n = (...args) => {
12
12
  * @returns localized error message
13
13
  */
14
14
  function getErrorMessage(error, locale) {
15
- const txt = i18n(error.message || error.code || error.status || error.statusCode, locale, error.args)
15
+ const key = error.message || error.code || error.status || error.statusCode || '500'
16
+ const txt = i18n(key, locale, error.args)
16
17
  return txt || error.message || String(error.code || error.status || error.statusCode)
17
18
  }
18
19
 
@@ -349,15 +349,12 @@ const _getOperation = (req, service) => {
349
349
 
350
350
  function _actionFunctionHandler(req) {
351
351
  const operation = _getOperation(req, this)
352
- if (!operation || !operation.params) return
352
+ if (!operation) return
353
353
 
354
354
  const data = req.data || {}
355
355
 
356
- // REVISIT: skip for mtxs as their models contain invalidities (e.g., properties modeled as strings but provided as objects)
357
- const is_mtxs = operation.name.match(/^cds\.xt\./)
358
-
359
356
  // validate data
360
- if (cds.env.features.cds_validate && !is_mtxs) {
357
+ if (cds.env.features.cds_validate) {
361
358
  const assertOptions = { mandatories: true }
362
359
  let errs = cds.validate(data, operation, assertOptions)
363
360
  if (errs) {
@@ -1,4 +1,14 @@
1
1
  const cds = require('../../cds')
2
+ // REVISIT: Remove after removing okra
3
+ const { isStreaming } = require('../../cds-services/adapter/odata-v4/utils/stream')
4
+
5
+ const _isStream = query => {
6
+ const { _propertyAccess, target } = query
7
+ if (!_propertyAccess) return
8
+
9
+ const element = target.elements[_propertyAccess]
10
+ return element._type === 'cds.LargeBinary' && element['@Core.MediaType']
11
+ }
2
12
 
3
13
  const _getStreamingProperties = elements => {
4
14
  const result = []
@@ -14,9 +24,7 @@ const _getStreamingProperties = elements => {
14
24
 
15
25
  const _getMediaTypeValue = () => {
16
26
  const ctx = cds.context
17
- return (
18
- !ctx?.http?.req?.headers?.['content-type']?.match(/json|multipart/i) && ctx?.http?.req?.headers?.['content-type']
19
- )
27
+ return !ctx?.http?.req?.headers?.['content-type']?.match(/multipart/i) && ctx?.http?.req?.headers?.['content-type']
20
28
  }
21
29
 
22
30
  function _addContentType(req, mtValue) {
@@ -27,12 +35,19 @@ function _addContentType(req, mtValue) {
27
35
 
28
36
  async function addContentType(req) {
29
37
  if (!req.query || !req.target) return
38
+ if (req._.odataReq) {
39
+ if (!isStreaming(req._.odataReq.getUriInfo().getPathSegments())) return
40
+ } else if (req.req?._query) {
41
+ if (!_isStream(req.req._query)) return
42
+ }
43
+
30
44
  const mtValue = _getMediaTypeValue()
31
45
  if (!mtValue) return
32
46
 
33
47
  _addContentType(req, mtValue)
34
48
  }
35
49
 
50
+ // register after input.js in order to write content-type also for @Core.Computed fields
36
51
  module.exports = cds.service.impl(function () {
37
52
  this.before(['PATCH', 'UPDATE'], '*', addContentType)
38
53
  })
@@ -758,13 +758,25 @@ const _convertSelect = (query, model, _options) => {
758
758
  // old db expects it as cqn xpr
759
759
  if (query.SELECT.search.length === 1) {
760
760
  query.SELECT.search = query.SELECT.search[0].val
761
- .replace(/"/g, '')
762
- .split(' ')
761
+ .match(/("")|("(?:[^"]|\\")*(?:[^\\]|\\\\)")|(\S*)/g)
762
+ .filter(el => el.length)
763
+ .map(el => {
764
+ if (el[0] === '"' && el.at(-1) === '"') {
765
+ try {
766
+ return JSON.parse(el)
767
+ } catch {
768
+ return el
769
+ }
770
+ }
771
+ return el
772
+ })
763
773
  .reduce((arr, val, i) => {
764
774
  if (i > 0) arr.push('and')
765
775
  arr.push({ val })
766
776
  return arr
767
777
  }, [])
778
+
779
+ if (!query.SELECT.search.length) query.SELECT.search = [{ val: '' }]
768
780
  }
769
781
 
770
782
  search2cqn4sql(query, model, { ...query._searchOptions, ...{ entityName, alias } })
@@ -779,6 +779,20 @@ class JoinCQNFromExpanded {
779
779
  })
780
780
 
781
781
  const targetEntity = this._getEntityForTable(target)
782
+
783
+ // ignore structured keys for now
784
+ let targetKeys = entity_keys(targetEntity).filter(key => !targetEntity.keys[key]._isStructured)
785
+ // ignore groupBy for now
786
+ if (targetKeys.length > 0 && !readToOneCQN.groupBy) {
787
+ const notOnlyExpandInColumns = !givenColumns.some(col => col.expand)
788
+ if (notOnlyExpandInColumns) {
789
+ const missingKeys = targetKeys.filter(keyName => !givenColumns.some(col => keyName === col.ref?.[0]))
790
+ if (missingKeys.length) {
791
+ givenColumns.push(...missingKeys.map(keyName => ({ ref: [keyName] })))
792
+ }
793
+ }
794
+ }
795
+
782
796
  if (
783
797
  'IsActiveEntity' in targetEntity.elements &&
784
798
  this._isNotIncludedIn(givenColumns)('IsActiveEntity') &&
@@ -1365,11 +1379,28 @@ class JoinCQNFromExpanded {
1365
1379
  const columns = []
1366
1380
  const outerColumns = []
1367
1381
 
1382
+ const _sameRef = (col1, col2) => {
1383
+ if (!col1.ref || !col2.ref) return false // only handle refs
1384
+ if (col1.ref.length !== col2.ref.length) return false
1385
+ if (col1.as !== col2.as) return false
1386
+ for (let i = 0; i < col1.ref.length; i++) {
1387
+ if (col1.ref[i] !== col2.ref[i]) return false
1388
+ }
1389
+ return true
1390
+ }
1391
+
1368
1392
  for (const entry of on) {
1369
1393
  if (entry.xpr) {
1370
1394
  const { columns: cols, outerColumns: outerCols } = this._getFilterColumns(readToOneCQN, entry.xpr, parentAlias)
1371
- columns.push(...cols)
1372
- outerColumns.push(...outerCols)
1395
+
1396
+ // de-duplicate
1397
+ for (const col of cols) {
1398
+ if (!columns.some(c => _sameRef(c, col))) columns.push(col)
1399
+ }
1400
+ for (const col of outerCols) {
1401
+ if (!outerColumns.some(c => _sameRef(c, col))) outerColumns.push(col)
1402
+ }
1403
+
1373
1404
  continue
1374
1405
  }
1375
1406
 
@@ -207,7 +207,7 @@ const _redirectRefToActives = (ref, model) => {
207
207
  }
208
208
 
209
209
  const lastCheckMap = new Map()
210
- const _cleanUpOldDrafts = async (service, tenant) => {
210
+ const _cleanUpOldDrafts = (service, tenant) => {
211
211
  if (!DEL_TIMEOUT.value) return
212
212
 
213
213
  const expiryDate = new Date(Date.now() - DEL_TIMEOUT.value).toISOString()
@@ -1081,14 +1081,23 @@ function _cleansed(query, model) {
1081
1081
  }
1082
1082
  cds.infer(draftsQuery, model.definitions)
1083
1083
  // draftsQuery._target = draftsQuery._target?.drafts || draftsQuery._target
1084
- if (query.SELECT.columns && query._target.drafts)
1085
- draftsQuery.SELECT.columns = _cleanseCols(query.SELECT.columns, REDUCED_DRAFT_ELEMENTS, draft)
1084
+ if (query.SELECT.columns && query._target.drafts) {
1085
+ if (draftsQuery._target.isDraft)
1086
+ draftsQuery.SELECT.columns = _cleanseCols(query.SELECT.columns, REDUCED_DRAFT_ELEMENTS, draft)
1087
+ else draftsQuery.SELECT.columns = _cleanseCols(query.SELECT.columns, DRAFT_ELEMENTS, draft)
1088
+ }
1086
1089
 
1087
- if (query.SELECT.where && query._target.drafts)
1088
- draftsQuery.SELECT.where = _cleanseWhere(query.SELECT.where, {}, DRAFT_ELEMENTS_WITHOUT_HASACTIVE)
1090
+ if (query.SELECT.where && query._target.drafts) {
1091
+ if (draftsQuery._target.isDraft)
1092
+ draftsQuery.SELECT.where = _cleanseWhere(query.SELECT.where, {}, DRAFT_ELEMENTS_WITHOUT_HASACTIVE)
1093
+ else draftsQuery.SELECT.where = _cleanseWhere(query.SELECT.where, {}, DRAFT_ELEMENTS)
1094
+ }
1089
1095
 
1090
- if (query.SELECT.orderBy && query._target.drafts)
1091
- draftsQuery.SELECT.orderBy = _cleanseWhere(query.SELECT.orderBy, {}, REDUCED_DRAFT_ELEMENTS)
1096
+ if (query.SELECT.orderBy && query._target.drafts) {
1097
+ if (draftsQuery._target.isDraft)
1098
+ draftsQuery.SELECT.orderBy = _cleanseWhere(query.SELECT.orderBy, {}, REDUCED_DRAFT_ELEMENTS)
1099
+ else draftsQuery.SELECT.orderBy = _cleanseWhere(query.SELECT.orderBy, {}, DRAFT_ELEMENTS)
1100
+ }
1092
1101
 
1093
1102
  if (draftsQuery._target.name.endsWith('.DraftAdministrativeData')) {
1094
1103
  draftsQuery.SELECT.columns = _tweakAdminCols(draftsQuery.SELECT.columns)
@@ -15,7 +15,7 @@ class CustomReferenceBuilder extends ReferenceBuilder {
15
15
  const args = Object.keys(ref[0].args)
16
16
  .map(argKey => {
17
17
  this._outputObj.values.push(ref[0].args[argKey].val)
18
- return `${argKey} => ${this._options.placeholder}`
18
+ return `${this._quoteElement(argKey)} => ${this._options.placeholder}`
19
19
  })
20
20
  .join(', ')
21
21
 
@@ -22,7 +22,7 @@ class EndpointRegistry {
22
22
  // unsuccessful auth doesn't automatically reject!
23
23
  cds.app.use(basePath, (req, res, next) => {
24
24
  // REVISIT: we should probably pass an error into next so that a (custom) error middleware can handle it
25
- if (!cds.context.user._is_anonymous) res.status(401).json({ error: ODATA_UNAUTHORIZED })
25
+ if (cds.context.user._is_anonymous) return res.status(401).json({ error: ODATA_UNAUTHORIZED })
26
26
  next()
27
27
  })
28
28
  } else if (process.env.NODE_ENV === 'production') {
@@ -32,7 +32,7 @@ class EndpointRegistry {
32
32
  cds.app.use(basePath, cds.middlewares.context())
33
33
  }
34
34
  cds.app.use(basePath, express.json({ type: 'application/*+json' }))
35
- cds.app.use(basePath, express.json()) // REVISIT: Do we need both?
35
+ cds.app.use(basePath, express.json())
36
36
  cds.app.use(basePath, express.urlencoded({ extended: true }))
37
37
  LOG._debug && LOG.debug('Register inbound endpoint', { basePath, method: 'OPTIONS' })
38
38
 
@@ -48,31 +48,34 @@ function _validateCertificate(req, res, next) {
48
48
  return res.status(401).json({ message: 'Authentication Failed' })
49
49
  }
50
50
 
51
- const bindingCert = new crypto.X509Certificate(this.options.credentials.certificate).toLegacyObject()
52
- const clientCert = new crypto.X509Certificate(
51
+ const clientCertObj = new crypto.X509Certificate(
53
52
  `-----BEGIN CERTIFICATE-----\n${req.headers['x-forwarded-client-cert']}\n-----END CERTIFICATE-----`
54
- ).toLegacyObject()
53
+ )
54
+ const clientCert = clientCertObj.toLegacyObject()
55
+
56
+ if (!this.isMultitenancy && !clientCertObj.checkPrivateKey(this.privateKey))
57
+ return res.status(401).josn({ message: 'Authentication Failed' })
55
58
 
56
59
  const cfSubject = Buffer.from(req.headers['x-ssl-client-subject-cn'], 'base64').toString()
57
- if (bindingCert.subject.CN !== clientCert.subject.CN || bindingCert.subject.CN !== cfSubject) {
60
+ if (this.validationCert.subject.CN !== clientCert.subject.CN || this.validationCert.subject.CN !== cfSubject) {
58
61
  this.LOG.info('certificate subject does not match')
59
62
  return res.status(401).json({ message: 'Authentication Failed' })
60
63
  }
61
64
  this.LOG.debug('incoming Subject CN is valid.')
62
65
 
63
- if (bindingCert.issuer.CN !== clientCert.issuer.CN) {
66
+ if (this.validationCert.issuer.CN !== clientCert.issuer.CN) {
64
67
  this.LOG.info('Certificate issuer subject does not match')
65
68
  return res.status(401).json({ message: 'Authentication Failed' })
66
69
  }
67
70
  this.LOG.debug('incoming issuer subject CN is valid.')
68
71
 
69
- if (bindingCert.issuer.O !== clientCert.issuer.O) {
72
+ if (this.validationCert.issuer.O !== clientCert.issuer.O) {
70
73
  this.LOG.info('Certificate issuer org does not match')
71
74
  return res.status(401).json({ message: 'Authentication Failed' })
72
75
  }
73
76
  this.LOG.debug('incoming Issuer Org is valid.')
74
77
 
75
- if (bindingCert.issuer.OU !== clientCert.issuer.OU) {
78
+ if (this.validationCert.issuer.OU !== clientCert.issuer.OU) {
76
79
  this.LOG.info('certificate issuer OU does not match')
77
80
  return res.status(401).json({ message: 'Authentication Failed' })
78
81
  }
@@ -103,6 +106,11 @@ class EventBroker extends cds.MessagingService {
103
106
  this.startListening()
104
107
  })
105
108
  this.agent = this.getAgent()
109
+ this.isMultitenancy = cds.requires.multitenancy || cds.env.profiles.includes('mtx-sidecar')
110
+ this.validationCert = new crypto.X509Certificate(
111
+ this.isMultitenancy ? this.options.credentials.certificate : this.agent.options.cert
112
+ ).toLegacyObject()
113
+ this.privateKey = !this.isMultitenancy && crypto.createPrivateKey(this.agent.options.key)
106
114
  }
107
115
 
108
116
  getAgent() {
@@ -189,7 +197,13 @@ class EventBroker extends cds.MessagingService {
189
197
  }
190
198
 
191
199
  prepareHeaders(headers, event) {
192
- if (!('source' in headers)) headers.source = `${this.options.credentials.ceSource[0]}/${cds.context.tenant}`
200
+ if (!('source' in headers)) {
201
+ if (!this.options.credentials.ceSource)
202
+ throw new Error(
203
+ 'Cannot publish event because of missing source information, currently not part of binding information.'
204
+ )
205
+ headers.source = `${this.options.credentials.ceSource[0]}/${cds.context.tenant}`
206
+ }
193
207
  super.prepareHeaders(headers, event)
194
208
  }
195
209
 
@@ -214,7 +228,7 @@ class EventBroker extends cds.MessagingService {
214
228
  const msg = normalizeIncomingMessage(req.body)
215
229
  msg.event = event
216
230
  Object.assign(msg.headers, headers)
217
- if (tenant) msg.tenant = tenant
231
+ if (this.isMultitenancy) msg.tenant = tenant
218
232
 
219
233
  // for cds.context.http
220
234
  msg._ = {}
@@ -261,7 +261,9 @@ class RemoteService extends cds.Service {
261
261
  const returnType = req._returnType
262
262
  const additionalOptions = { destination, kind, resolvedTarget, returnType, destinationOptions }
263
263
 
264
- const jwt = req?.context?.headers?.authorization?.split(/^bearer /i)[1]
264
+ // REVISIT: i don't believe req.context.headers is an official API
265
+ let jwt = req?.context?.headers?.authorization?.split(/^bearer /i)[1]
266
+ if (!jwt) jwt = req?.context?.http?.req?.headers?.authorization?.split(/^bearer /i)[1]
265
267
  if (jwt) additionalOptions.jwt = jwt
266
268
 
267
269
  // hidden compat flag in order to suppress logging response body of failed request
@@ -1,30 +1,3 @@
1
- function getNested(k, obj) {
2
- let cur = obj
3
- let p = ''
4
- const parts = k.split('_')
5
- while (parts.length) {
6
- const q = parts.shift()
7
- if (q in cur) {
8
- cur = cur[q]
9
- p = ''
10
- } else {
11
- p = p ? p + '_' + q : q
12
- if (p in cur) {
13
- cur = cur[p]
14
- p = ''
15
- } else {
16
- if (Object.keys(cur).some(k => k.startsWith(p + '_'))) {
17
- // continue for now as there's still a chance
18
- } else {
19
- // abort
20
- return undefined
21
- }
22
- }
23
- }
24
- }
25
- return cur[p] || cur !== obj ? cur : undefined
26
- }
27
-
28
1
  const getNormalizedDecimal = val => {
29
2
  let v = `${val}`
30
3
  const cgs = v.match(/^(\d*\.*\d*)e([+|-]*)(\d*)$/)
@@ -88,39 +61,10 @@ const resolveCDSType = ele => {
88
61
  return ele
89
62
  }
90
63
 
91
- function resolveSegment(prev, obj, def) {
92
- if (prev.keys) {
93
- let keys = []
94
- for (const k of prev.keys) {
95
- let val
96
- if (k in obj) val = obj[k]
97
- else val = getNested(k, obj)
98
- if (val == null) {
99
- // in some cases, k is not given, e.g., POST into collection via navigation
100
- // TODO: what to put in target? "null", "transient", ...?
101
- if (k === 'IsActiveEntity')
102
- keys.push(`${k}=false`) //> always false if not in obj as it must be a draft activate
103
- else keys.push(`${k}=null`)
104
- } else {
105
- const cdsType = resolveCDSType(def.elements[k])
106
- const odataType = def.elements[k]['@odata.Type']
107
- if (!odataType && cdsType === 'cds.String' || odataType === 'Edm.String') val = `'${val}'`
108
- // TODO: more proper val encoding based on type
109
- keys.push(`${k}=${val}`)
110
- }
111
- }
112
- return `${prev.assoc}(${keys.join(',')})`
113
- }
114
- if (prev.index) {
115
- return `${prev.prop}[${prev.index}]`
116
- }
117
- }
118
64
 
119
65
  module.exports = {
120
- getNested,
121
66
  getNormalizedDecimal,
122
67
  getTarget,
123
68
  isBase64String,
124
- resolveCDSType,
125
- resolveSegment
69
+ resolveCDSType
126
70
  }
@@ -553,12 +553,11 @@ const _formatResponseJson = (request, atomicityGroup) => {
553
553
  */
554
554
 
555
555
  module.exports = adapter => {
556
- const { options: config, router, service } = adapter
557
- const { max_content_length } = config //> max_content_length is unofficial config
558
-
559
- const options = { type: '*/*' }
560
- if (max_content_length) options.limit = max_content_length
561
- const textBodyParser = express.text(options)
556
+ const { router, service } = adapter
557
+ const textBodyParser = express.text({
558
+ ...adapter.body_parser_options,
559
+ type: '*/*' // REVISIT: why do we need to override type here?
560
+ })
562
561
 
563
562
  return function odata_batch(req, res, next) {
564
563
  if (req.headers['content-type'].includes('application/json')) {
@@ -3,9 +3,8 @@ const express = require('express')
3
3
  // basically express.json() with string representation of body stored in req._raw for recovery
4
4
  // REVISIT: why do we need our own body parser? Only because of req._raw?
5
5
  module.exports = function bodyParser4(adapter, options = {}) {
6
- if (!options.type) options.type = 'json'
7
- const { max_content_length } = adapter.options //> max_content_length is unofficial config
8
- if (!options.limit && max_content_length) options.limit = max_content_length
6
+ Object.assign(options, adapter.body_parser_options)
7
+ options.type ??= 'json' // REVISIT: why do we need to override type here?
9
8
  const textParser = express.text(options)
10
9
  return function http_body_parser(req, res, next) {
11
10
  if (typeof req.body === 'object') {
@@ -85,6 +85,11 @@ module.exports = (adapter, isUpsert) => {
85
85
  .catch(err => {
86
86
  handleSapMessages(cdsReq, req, res)
87
87
 
88
+ // REVISIT: invoke service.on('error') for failed batch subrequests
89
+ if (cdsReq.http.req.path.startsWith('/$batch') && service._handlers._error.length) {
90
+ for (const each of service._handlers._error) each.handler.call(service, err, cdsReq)
91
+ }
92
+
88
93
  next(err)
89
94
  })
90
95
  }
@@ -62,6 +62,11 @@ module.exports = adapter => {
62
62
  .catch(err => {
63
63
  handleSapMessages(cdsReq, req, res)
64
64
 
65
+ // REVISIT: invoke service.on('error') for failed batch subrequests
66
+ if (cdsReq.http.req.path.startsWith('/$batch') && service._handlers._error.length) {
67
+ for (const each of service._handlers._error) each.handler.call(service, err, cdsReq)
68
+ }
69
+
65
70
  next(err)
66
71
  })
67
72
  }
@@ -7,6 +7,7 @@ const { normalizeError, unwrapMultipleErrors } = require('../../_runtime/common/
7
7
  module.exports = () => {
8
8
  return function odata_error(err, req, res, next) {
9
9
  if (err == 401 || err.code == 401) return next(err) // speed up logins, at least temporary until we reviewed and eliminated overhead that may be involved below
10
+
10
11
  // REVISIT: keep?
11
12
  // log the error (4xx -> warn)
12
13
  _log(err)