@sap/cds 8.0.3 → 8.1.0
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 +40 -0
- package/_i18n/i18n_bg.properties +113 -0
- package/_i18n/i18n_el.properties +113 -0
- package/_i18n/i18n_he.properties +113 -0
- package/_i18n/i18n_hr.properties +113 -0
- package/_i18n/i18n_kk.properties +113 -0
- package/_i18n/i18n_sh.properties +113 -0
- package/_i18n/i18n_sk.properties +113 -0
- package/_i18n/i18n_sl.properties +113 -0
- package/_i18n/i18n_uk.properties +113 -0
- package/lib/compile/etc/_localized.js +8 -20
- package/lib/dbs/cds-deploy.js +1 -0
- package/lib/env/cds-requires.js +1 -0
- package/lib/env/defaults.js +1 -1
- package/lib/env/plugins.js +22 -6
- package/lib/linked/validate.js +1 -1
- package/lib/log/cds-log.js +2 -2
- package/lib/srv/protocols/hcql.js +5 -5
- package/lib/srv/protocols/http.js +23 -11
- package/lib/test/expect.js +1 -1
- package/lib/utils/cds-test.js +4 -4
- package/libx/_runtime/common/composition/insert.js +1 -1
- package/libx/_runtime/common/error/utils.js +2 -1
- package/libx/_runtime/common/generic/input.js +2 -5
- package/libx/_runtime/common/generic/stream.js +18 -3
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +5 -2
- package/libx/_runtime/db/query/read.js +18 -9
- package/libx/_runtime/fiori/lean-draft.js +2 -2
- package/libx/_runtime/hana/customBuilder/CustomReferenceBuilder.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
- package/libx/_runtime/messaging/event-broker.js +76 -24
- package/libx/common/assert/utils.js +1 -57
- package/libx/odata/middleware/batch.js +86 -40
- package/libx/odata/middleware/body-parser.js +2 -3
- package/libx/odata/middleware/operation.js +11 -11
- package/libx/odata/middleware/read.js +1 -1
- package/libx/odata/parse/grammar.peggy +6 -1
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/metadata.js +18 -44
- package/libx/rest/middleware/error.js +9 -2
- package/libx/rest/middleware/parse.js +1 -1
- package/package.json +1 -1
- package/libx/common/assert/index.js +0 -228
- package/libx/common/assert/type-relaxed.js +0 -39
|
@@ -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)
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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.
|
|
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
|
-
|
|
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/test/expect.js
CHANGED
|
@@ -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 (
|
|
115
|
+
if (is.object(a)) return compare(a, x, this._deep)
|
|
116
116
|
}, _fail)
|
|
117
117
|
}
|
|
118
118
|
|
package/lib/utils/cds-test.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
|
@@ -82,7 +82,7 @@ const hasDeepInsert = (model, cqn) => {
|
|
|
82
82
|
const getDeepInsertCQNs = (model, cqn) => {
|
|
83
83
|
const into = (cqn.INSERT.into.ref && cqn.INSERT.into.ref[0]) || cqn.INSERT.into.name || cqn.INSERT.into
|
|
84
84
|
const entityName = ensureNoDraftsSuffix(into)
|
|
85
|
-
const draft = entityName !== into
|
|
85
|
+
const draft = entityName !== into || into.endsWith('.drafts') //lean draft
|
|
86
86
|
const dataEntries = cqn.INSERT.entries ? deepCopy(cqn.INSERT.entries) : []
|
|
87
87
|
const entity = model.definitions[entityName]
|
|
88
88
|
const compositionTree = getCompositionTree({
|
|
@@ -12,7 +12,8 @@ const i18n = (...args) => {
|
|
|
12
12
|
* @returns localized error message
|
|
13
13
|
*/
|
|
14
14
|
function getErrorMessage(error, locale) {
|
|
15
|
-
const
|
|
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
|
|
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
|
|
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,16 @@ 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
|
-
.
|
|
762
|
-
.
|
|
761
|
+
.match(/("")|("(?:[^"]|\\")*(?:[^\\]|\\\\)")|(\S*)/g)
|
|
762
|
+
.filter(el => el.length)
|
|
763
|
+
.map(el => (el.match(/^["](.*)["]$/) ? JSON.parse(el) : el))
|
|
763
764
|
.reduce((arr, val, i) => {
|
|
764
765
|
if (i > 0) arr.push('and')
|
|
765
766
|
arr.push({ val })
|
|
766
767
|
return arr
|
|
767
768
|
}, [])
|
|
769
|
+
|
|
770
|
+
if (!query.SELECT.search.length) query.SELECT.search = [{ val: '' }]
|
|
768
771
|
}
|
|
769
772
|
|
|
770
773
|
search2cqn4sql(query, model, { ...query._searchOptions, ...{ entityName, alias } })
|
|
@@ -60,17 +60,26 @@ const read = (executeSelectCQN, executeStreamCQN, convertStreams) => (model, dbc
|
|
|
60
60
|
|
|
61
61
|
if (query.SELECT.count) {
|
|
62
62
|
if (query.SELECT.limit) {
|
|
63
|
+
// IMPORTANT: do not change order!
|
|
64
|
+
// 1. create the count query synchronously, because it works on a copy
|
|
65
|
+
// 2. run the result query, duplicate names ( SELECT ID, ID ...) throw an error synchronously
|
|
66
|
+
// 3. run the count query
|
|
67
|
+
// reason is that executeSelectCQN modifies the query
|
|
63
68
|
const countQuery = _createCountQuery(query)
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
const resultPromise =
|
|
70
|
+
query.SELECT.limit?.rows?.val === 0
|
|
71
|
+
? Promise.resolve([])
|
|
72
|
+
: executeSelectCQN(model, dbc, query, user, locale, isoTs)
|
|
73
|
+
const countPromise = executeSelectCQN(model, dbc, countQuery, user, locale, isoTs)
|
|
69
74
|
|
|
70
|
-
|
|
71
|
-
return Promise.
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
// use allSettled here, so all executions are awaited before we rollback
|
|
76
|
+
return Promise.allSettled([countPromise, resultPromise]).then(results => {
|
|
77
|
+
const rejection = results.find(p => p.status === 'rejected')
|
|
78
|
+
if (rejection) throw rejection.reason
|
|
79
|
+
|
|
80
|
+
const [{ value: countResult }, { value: result }] = results
|
|
81
|
+
return _arrayWithCount(result, countValue(countResult))
|
|
82
|
+
})
|
|
74
83
|
}
|
|
75
84
|
|
|
76
85
|
return executeSelectCQN(model, dbc, query, user, locale, isoTs).then(result =>
|
|
@@ -207,7 +207,7 @@ const _redirectRefToActives = (ref, model) => {
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
const lastCheckMap = new Map()
|
|
210
|
-
const _cleanUpOldDrafts =
|
|
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()
|
|
@@ -824,7 +824,7 @@ const Read = {
|
|
|
824
824
|
else ownNewDrafts.push(draft)
|
|
825
825
|
}
|
|
826
826
|
|
|
827
|
-
const $count = ownDrafts.length + (isCount ? actives[0]?.$count : actives.$count ?? 0)
|
|
827
|
+
const $count = ownDrafts.length + (isCount ? actives[0]?.$count : (actives.$count ?? 0))
|
|
828
828
|
if (isCount) return { $count }
|
|
829
829
|
|
|
830
830
|
Read.merge(query._target, ownDrafts, [], row => {
|
|
@@ -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 (
|
|
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())
|
|
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
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const cds = require('../cds')
|
|
2
2
|
|
|
3
|
+
const normalizeIncomingMessage = require('./common-utils/normalizeIncomingMessage')
|
|
3
4
|
const express = require('express')
|
|
4
5
|
const https = require('https')
|
|
5
6
|
const crypto = require('crypto')
|
|
@@ -47,31 +48,34 @@ function _validateCertificate(req, res, next) {
|
|
|
47
48
|
return res.status(401).json({ message: 'Authentication Failed' })
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
const
|
|
51
|
-
const clientCert = new crypto.X509Certificate(
|
|
51
|
+
const clientCertObj = new crypto.X509Certificate(
|
|
52
52
|
`-----BEGIN CERTIFICATE-----\n${req.headers['x-forwarded-client-cert']}\n-----END CERTIFICATE-----`
|
|
53
|
-
)
|
|
53
|
+
)
|
|
54
|
+
const clientCert = clientCertObj.toLegacyObject()
|
|
55
|
+
|
|
56
|
+
if (!this.isMultitenancy && !clientCertObj.checkPrivateKey(this.privateKey))
|
|
57
|
+
return res.status(401).josn({ message: 'Authentication Failed' })
|
|
54
58
|
|
|
55
59
|
const cfSubject = Buffer.from(req.headers['x-ssl-client-subject-cn'], 'base64').toString()
|
|
56
|
-
if (
|
|
60
|
+
if (this.validationCert.subject.CN !== clientCert.subject.CN || this.validationCert.subject.CN !== cfSubject) {
|
|
57
61
|
this.LOG.info('certificate subject does not match')
|
|
58
62
|
return res.status(401).json({ message: 'Authentication Failed' })
|
|
59
63
|
}
|
|
60
64
|
this.LOG.debug('incoming Subject CN is valid.')
|
|
61
65
|
|
|
62
|
-
if (
|
|
66
|
+
if (this.validationCert.issuer.CN !== clientCert.issuer.CN) {
|
|
63
67
|
this.LOG.info('Certificate issuer subject does not match')
|
|
64
68
|
return res.status(401).json({ message: 'Authentication Failed' })
|
|
65
69
|
}
|
|
66
70
|
this.LOG.debug('incoming issuer subject CN is valid.')
|
|
67
71
|
|
|
68
|
-
if (
|
|
72
|
+
if (this.validationCert.issuer.O !== clientCert.issuer.O) {
|
|
69
73
|
this.LOG.info('Certificate issuer org does not match')
|
|
70
74
|
return res.status(401).json({ message: 'Authentication Failed' })
|
|
71
75
|
}
|
|
72
76
|
this.LOG.debug('incoming Issuer Org is valid.')
|
|
73
77
|
|
|
74
|
-
if (
|
|
78
|
+
if (this.validationCert.issuer.OU !== clientCert.issuer.OU) {
|
|
75
79
|
this.LOG.info('certificate issuer OU does not match')
|
|
76
80
|
return res.status(401).json({ message: 'Authentication Failed' })
|
|
77
81
|
}
|
|
@@ -102,6 +106,11 @@ class EventBroker extends cds.MessagingService {
|
|
|
102
106
|
this.startListening()
|
|
103
107
|
})
|
|
104
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)
|
|
105
114
|
}
|
|
106
115
|
|
|
107
116
|
getAgent() {
|
|
@@ -141,30 +150,63 @@ class EventBroker extends cds.MessagingService {
|
|
|
141
150
|
|
|
142
151
|
// TODO: What if we're in single tenant variant?
|
|
143
152
|
try {
|
|
144
|
-
const ceSource = `${this.options.credentials.ceSource[0]}/${cds.context.tenant}`
|
|
145
153
|
const hostname = this.options.credentials.eventing.http.x509.url.replace(/^https?:\/\//, '')
|
|
146
|
-
|
|
154
|
+
|
|
155
|
+
// take over and cleanse cloudevents headers
|
|
156
|
+
const headers = { ...(msg.headers ?? {}) }
|
|
157
|
+
|
|
158
|
+
const ceId = headers.id
|
|
159
|
+
delete headers.id
|
|
160
|
+
|
|
161
|
+
const ceSource = headers.source
|
|
162
|
+
delete headers.source
|
|
163
|
+
|
|
164
|
+
const ceType = headers.type
|
|
165
|
+
delete headers.type
|
|
166
|
+
|
|
167
|
+
const ceSpecversion = headers.specversion
|
|
168
|
+
delete headers.specversion
|
|
169
|
+
|
|
170
|
+
// const ceDatacontenttype = headers.datacontenttype // not part of the HTTP API
|
|
171
|
+
delete headers.datacontenttype
|
|
172
|
+
|
|
173
|
+
// const ceTime = headers.time // not part of the HTTP API
|
|
174
|
+
delete headers.time
|
|
175
|
+
|
|
147
176
|
const options = {
|
|
148
177
|
hostname: hostname,
|
|
149
178
|
method: 'POST',
|
|
150
179
|
headers: {
|
|
151
|
-
'ce-id':
|
|
180
|
+
'ce-id': ceId,
|
|
152
181
|
'ce-source': ceSource,
|
|
153
|
-
'ce-type':
|
|
154
|
-
'ce-specversion':
|
|
155
|
-
'Content-Type': 'application/json'
|
|
182
|
+
'ce-type': ceType,
|
|
183
|
+
'ce-specversion': ceSpecversion,
|
|
184
|
+
'Content-Type': 'application/json' // because of { data, ...headers } format
|
|
156
185
|
},
|
|
157
186
|
agent: this.agent
|
|
158
187
|
}
|
|
159
188
|
this.LOG.debug('HTTP headers:', JSON.stringify(options.headers))
|
|
160
189
|
this.LOG.debug('HTTP body:', JSON.stringify(msg.data))
|
|
161
|
-
|
|
190
|
+
// what about headers?
|
|
191
|
+
// TODO: Clarify if we should send `{ data, ...headers }` vs. `data` + HTTP headers (`ce-*`)
|
|
192
|
+
await request(options, { data: msg.data, ...headers }) // TODO: fetch does not work with mTLS as of today, requires another module. see https://github.com/nodejs/node/issues/48977
|
|
162
193
|
if (this.LOG._info) this.LOG.info('Emit', { topic: msg.event })
|
|
163
194
|
} catch (e) {
|
|
164
195
|
this.LOG.error('Emit failed:', e.message)
|
|
165
196
|
}
|
|
166
197
|
}
|
|
167
198
|
|
|
199
|
+
prepareHeaders(headers, event) {
|
|
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
|
+
}
|
|
207
|
+
super.prepareHeaders(headers, event)
|
|
208
|
+
}
|
|
209
|
+
|
|
168
210
|
async registerWebhookEndpoints() {
|
|
169
211
|
const webhookBasePath = this.options.webhookPath || '/-/cds/event-broker/webhook'
|
|
170
212
|
cds.app.post(webhookBasePath, _validateCertificate.bind(this))
|
|
@@ -176,17 +218,27 @@ class EventBroker extends cds.MessagingService {
|
|
|
176
218
|
try {
|
|
177
219
|
const event = req.headers['ce-type'] // TG27: type contains namespace, so there's no collision
|
|
178
220
|
const tenant = req.headers['ce-sapconsumertenant']
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
headers: req.headers
|
|
221
|
+
|
|
222
|
+
// take over cloudevents headers (`ce-*`) without the prefix
|
|
223
|
+
const headers = {}
|
|
224
|
+
for (const header in req.headers) {
|
|
225
|
+
if (header.startsWith('ce-')) headers[header.slice(3)] = req.headers[header]
|
|
185
226
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
227
|
+
|
|
228
|
+
const msg = normalizeIncomingMessage(req.body)
|
|
229
|
+
msg.event = event
|
|
230
|
+
Object.assign(msg.headers, headers)
|
|
231
|
+
if (this.isMultitenancy) msg.tenant = tenant
|
|
232
|
+
|
|
233
|
+
// for cds.context.http
|
|
234
|
+
msg._ = {}
|
|
235
|
+
msg._.req = req
|
|
236
|
+
msg._.res = res
|
|
237
|
+
|
|
238
|
+
const context = { user: cds.User.privileged, _: msg._ }
|
|
239
|
+
if (msg.tenant) context.tenant = msg.tenant
|
|
240
|
+
|
|
241
|
+
await this.tx(context, tx => tx.emit(msg))
|
|
190
242
|
this.LOG.debug('Event processed successfully.')
|
|
191
243
|
return res.status(200).json({ message: 'OK' })
|
|
192
244
|
} catch (e) {
|
|
@@ -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
|
}
|