@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.
- package/CHANGELOG.md +47 -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/defaults.js +1 -1
- package/lib/env/plugins.js +22 -6
- package/lib/index.js +3 -2
- package/lib/linked/validate.js +4 -3
- package/lib/log/cds-log.js +2 -2
- package/lib/req/context.js +1 -0
- package/lib/req/locale.js +1 -1
- package/lib/srv/protocols/hcql.js +5 -5
- package/lib/srv/protocols/http.js +23 -11
- package/lib/srv/srv-tx.js +1 -0
- package/lib/test/expect.js +1 -1
- package/lib/utils/cds-test.js +4 -4
- 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 +14 -2
- package/libx/_runtime/db/expand/expandCQNToJoin.js +33 -2
- package/libx/_runtime/fiori/lean-draft.js +16 -7
- 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 +23 -9
- package/libx/_runtime/remote/Service.js +3 -1
- package/libx/common/assert/utils.js +1 -57
- package/libx/odata/middleware/batch.js +5 -6
- package/libx/odata/middleware/body-parser.js +2 -3
- package/libx/odata/middleware/create.js +5 -0
- package/libx/odata/middleware/delete.js +5 -0
- package/libx/odata/middleware/error.js +1 -0
- package/libx/odata/middleware/operation.js +17 -11
- package/libx/odata/middleware/read.js +10 -1
- package/libx/odata/middleware/update.js +9 -4
- package/libx/odata/parse/grammar.peggy +6 -1
- package/libx/odata/parse/multipartToJson.js +1 -1
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/metadata.js +18 -44
- package/libx/rest/middleware/error.js +1 -0
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const express = require('express')
|
|
2
2
|
const cds = require('../../index')
|
|
3
|
-
const
|
|
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
|
-
|
|
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)
|
|
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/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
|
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
|
}
|
|
@@ -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,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
|
-
.
|
|
762
|
-
.
|
|
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
|
-
|
|
1372
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
|
@@ -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
|
|
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
|
-
)
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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))
|
|
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 (
|
|
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
|
-
|
|
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 {
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
-
|
|
7
|
-
|
|
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)
|