@sap/cds 8.0.4 → 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 +32 -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/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/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/fiori/lean-draft.js +1 -1
- 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/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/operation.js +11 -11
- 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/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
|
}
|
|
@@ -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 } })
|
|
@@ -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()
|
|
@@ -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._ = {}
|
|
@@ -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') {
|
|
@@ -51,9 +51,6 @@ module.exports = adapter => {
|
|
|
51
51
|
let { operation, args } = req._query.SELECT?.from.ref?.slice(-1)[0] || {}
|
|
52
52
|
if (!operation) return next() //> create or read
|
|
53
53
|
|
|
54
|
-
// REVISIT: should not be necessary
|
|
55
|
-
const _originalQuery = JSON.parse(JSON.stringify(req._query))
|
|
56
|
-
|
|
57
54
|
// unbound vs. bound
|
|
58
55
|
let entity, params
|
|
59
56
|
if (service.model.definitions[operation]) {
|
|
@@ -102,7 +99,6 @@ module.exports = adapter => {
|
|
|
102
99
|
|
|
103
100
|
if (operation.returns._type?.match?.(/^cds\./)) {
|
|
104
101
|
const context = `${'../'.repeat(query?.SELECT?.from?.ref?.length)}$metadata#${cds2edm[operation.returns._type]}`
|
|
105
|
-
|
|
106
102
|
result = { '@odata.context': context, value: result }
|
|
107
103
|
return res.send(result)
|
|
108
104
|
}
|
|
@@ -121,15 +117,19 @@ module.exports = adapter => {
|
|
|
121
117
|
if (operation.returns.type !== 'sap.esh.SearchResult') {
|
|
122
118
|
const isCollection = !!operation.returns.items
|
|
123
119
|
const _target = operation.returns.items ?? operation.returns
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
120
|
+
const options = { result, isCollection }
|
|
121
|
+
if (!_target.name) {
|
|
122
|
+
// case: return inline type def
|
|
123
|
+
options.edmName = _opResultName({ service, operation, returnType: _target })
|
|
124
|
+
}
|
|
125
|
+
const SELECT = {
|
|
126
|
+
from: query ? { ref: [...query.SELECT.from.ref, { operation: operation.name }] } : {},
|
|
127
|
+
one: !isCollection
|
|
128
|
+
}
|
|
129
|
+
const metadata = getODataMetadata({ SELECT, _target }, options)
|
|
131
130
|
result = getODataResult(result, metadata, { isCollection })
|
|
132
131
|
}
|
|
132
|
+
|
|
133
133
|
res.send(result)
|
|
134
134
|
})
|
|
135
135
|
.catch(err => {
|
|
@@ -402,6 +402,7 @@
|
|
|
402
402
|
"$skip=" o val:skip { _setLimitOffset(val) } /
|
|
403
403
|
"$search=" o s:search_expand { if (s) SELECT.search = s } /
|
|
404
404
|
"$count=" o count /
|
|
405
|
+
"$apply=" &{ return cds.env.features.skip_apply_parsing } o [^&]* { return null } /
|
|
405
406
|
"$apply=" o trafos:transformations { return trafos } /
|
|
406
407
|
// Workaround to support empty expand even if not OData compliant old adapter supported it and did not crash
|
|
407
408
|
"$expand=" {return null}
|
|
@@ -486,7 +487,11 @@
|
|
|
486
487
|
/ o // Do not add search property for space only
|
|
487
488
|
|
|
488
489
|
search_clause
|
|
489
|
-
= val:$(
|
|
490
|
+
= val:$(($[ ]* (
|
|
491
|
+
[^"&]+
|
|
492
|
+
/ ('"' ("\\\\" / "\\\"" / [^"])+ '"' [ ]*)+
|
|
493
|
+
))+)
|
|
494
|
+
{ return [{ val }] }
|
|
490
495
|
|
|
491
496
|
search_expand
|
|
492
497
|
= val:$( [^;)]+ ) { return [{ val }] }
|