@sap/cds 9.0.4 → 9.2.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 +68 -0
- package/bin/deploy.js +29 -0
- package/bin/serve.js +1 -5
- package/lib/compile/etc/csv.js +11 -6
- package/lib/compile/for/lean_drafts.js +29 -7
- package/lib/compile/load.js +8 -5
- package/lib/compile/to/hdbtabledata.js +1 -1
- package/lib/dbs/cds-deploy.js +5 -34
- package/lib/env/cds-env.js +2 -1
- package/lib/env/cds-requires.js +4 -1
- package/lib/env/defaults.js +0 -11
- package/lib/env/schemas/cds-rc.js +218 -6
- package/lib/index.js +38 -38
- package/lib/log/cds-error.js +12 -11
- package/lib/log/format/json.js +1 -1
- package/lib/ql/SELECT.js +31 -0
- package/lib/ql/resolve.js +1 -1
- package/lib/req/context.js +1 -1
- package/lib/req/request.js +1 -1
- package/lib/req/validate.js +17 -19
- package/lib/srv/cds.Service.js +18 -28
- package/lib/srv/middlewares/auth/ias-auth.js +29 -2
- package/lib/srv/middlewares/auth/jwt-auth.js +11 -1
- package/lib/srv/middlewares/auth/xssec.js +1 -1
- package/lib/srv/srv-models.js +1 -1
- package/lib/srv/srv-tx.js +2 -2
- package/lib/utils/cds-utils.js +35 -2
- package/lib/utils/csv-reader.js +1 -1
- package/lib/utils/inflect.js +2 -2
- package/lib/utils/tar.js +60 -23
- package/lib/utils/version.js +18 -0
- package/libx/_runtime/cds.js +1 -1
- package/libx/_runtime/common/aspects/any.js +1 -23
- package/libx/_runtime/common/generic/crud.js +1 -3
- package/libx/_runtime/common/generic/input.js +113 -52
- package/libx/_runtime/common/generic/sorting.js +1 -1
- package/libx/_runtime/common/generic/temporal.js +0 -6
- package/libx/_runtime/common/utils/draft.js +1 -1
- package/libx/_runtime/common/utils/entityFromCqn.js +1 -1
- package/libx/_runtime/common/utils/propagateForeignKeys.js +1 -1
- package/libx/_runtime/common/utils/resolveView.js +2 -2
- package/libx/_runtime/common/utils/structured.js +2 -2
- package/libx/_runtime/common/utils/templateProcessor.js +0 -5
- package/libx/_runtime/common/utils/vcap.js +1 -1
- package/libx/_runtime/fiori/lean-draft.js +529 -143
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +3 -2
- package/libx/_runtime/messaging/service.js +1 -1
- package/libx/_runtime/remote/utils/client.js +2 -1
- package/libx/common/assert/utils.js +2 -12
- package/libx/common/utils/streaming.js +4 -9
- package/libx/http/location.js +1 -0
- package/libx/odata/ODataAdapter.js +47 -43
- package/libx/odata/index.js +1 -1
- package/libx/odata/middleware/batch.js +6 -2
- package/libx/odata/middleware/create.js +1 -1
- package/libx/odata/middleware/error.js +27 -17
- package/libx/odata/middleware/operation.js +15 -21
- package/libx/odata/middleware/stream.js +1 -1
- package/libx/odata/parse/afterburner.js +22 -8
- package/libx/odata/parse/cqn2odata.js +16 -10
- package/libx/odata/parse/grammar.peggy +185 -134
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/index.js +1 -36
- package/libx/odata/utils/metadata.js +34 -1
- package/libx/odata/utils/odataBind.js +2 -1
- package/libx/odata/utils/result.js +22 -20
- package/libx/queue/index.js +7 -4
- package/libx/rest/RestAdapter.js +1 -2
- package/libx/rest/middleware/create.js +5 -2
- package/package.json +2 -2
- package/server.js +1 -1
- package/bin/deploy/to-hana.js +0 -1
- package/lib/utils/check-version.js +0 -9
- package/lib/utils/unit.js +0 -19
- package/libx/_runtime/cds-services/util/assert.js +0 -181
- package/libx/_runtime/types/api.js +0 -129
- package/libx/common/assert/validation.js +0 -109
|
@@ -29,9 +29,10 @@ class EndpointRegistry {
|
|
|
29
29
|
if (cds.context.user._is_anonymous) return res.status(401).end()
|
|
30
30
|
next()
|
|
31
31
|
})
|
|
32
|
-
} else if (process.env.NODE_ENV === 'production') {
|
|
33
|
-
LOG.warn('Messaging endpoints not secured')
|
|
34
32
|
} else {
|
|
33
|
+
if (process.env.NODE_ENV === 'production') {
|
|
34
|
+
LOG.warn('Messaging endpoints not secured')
|
|
35
|
+
}
|
|
35
36
|
// auth middlewares set cds.context.user
|
|
36
37
|
cds.app.use(basePath, cds.middlewares.context())
|
|
37
38
|
}
|
|
@@ -48,7 +48,7 @@ module.exports = class MessagingService extends cds.Service {
|
|
|
48
48
|
srv.on(event, async msg => {
|
|
49
49
|
const { data, headers } = msg
|
|
50
50
|
const messaging = await cds.connect.to('messaging') // needed for potential outbox
|
|
51
|
-
return messaging.
|
|
51
|
+
return messaging.emit({ event: topic, data, headers })
|
|
52
52
|
})
|
|
53
53
|
}
|
|
54
54
|
})
|
|
@@ -231,7 +231,7 @@ const run = async (requestConfig, options) => {
|
|
|
231
231
|
e.message = msg ? 'Error during request to remote service: ' + msg : 'Request to remote service failed.'
|
|
232
232
|
const sanitizedError = _getSanitizedError(e, requestConfig, { suppressRemoteResponseBody })
|
|
233
233
|
const err = Object.assign(new Error(e.message), { statusCode: 502, reason: sanitizedError })
|
|
234
|
-
|
|
234
|
+
cds.repl || LOG.warn(err)
|
|
235
235
|
throw err
|
|
236
236
|
}
|
|
237
237
|
|
|
@@ -306,6 +306,7 @@ const _cqnToReqOptions = (query, service, req) => {
|
|
|
306
306
|
const reqOptions = {
|
|
307
307
|
method: queryObject.method,
|
|
308
308
|
url: queryObject.path
|
|
309
|
+
// REVISIT: remove when we can assume that number of remote services running Okra is negligible
|
|
309
310
|
// ugly workaround for Okra not allowing spaces in ( x eq 1 )
|
|
310
311
|
.replace(/\( /g, '(')
|
|
311
312
|
.replace(/ \)/g, ')')
|
|
@@ -50,21 +50,11 @@ function isBase64String(string, strict = false) {
|
|
|
50
50
|
return true
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
// REVISIT: when is ele._type not set and sufficient?
|
|
55
|
-
if (ele._type?.match(/^cds\./)) return ele._type
|
|
56
|
-
if (ele.type) {
|
|
57
|
-
if (ele.type.match(/^cds\./)) return ele.type
|
|
58
|
-
return resolveCDSType(ele.__proto__)
|
|
59
|
-
}
|
|
60
|
-
if (ele.items) return resolveCDSType(ele.items)
|
|
61
|
-
return ele
|
|
62
|
-
}
|
|
53
|
+
|
|
63
54
|
|
|
64
55
|
|
|
65
56
|
module.exports = {
|
|
66
57
|
getNormalizedDecimal,
|
|
67
58
|
getTarget,
|
|
68
|
-
isBase64String
|
|
69
|
-
resolveCDSType
|
|
59
|
+
isBase64String
|
|
70
60
|
}
|
|
@@ -64,14 +64,9 @@ exports.collectStreamMetadata = (result, operation, query) => {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
exports.getReadable = function readable4 (result) {
|
|
67
|
-
if (result == null) return
|
|
68
|
-
if (typeof result !== 'object') {
|
|
69
|
-
const stream = new Readable()
|
|
70
|
-
stream.push(result)
|
|
71
|
-
stream.push(null)
|
|
72
|
-
return stream
|
|
73
|
-
}
|
|
67
|
+
if (result == null) return null
|
|
74
68
|
if (result instanceof Readable) return result
|
|
75
|
-
if (result
|
|
76
|
-
if (
|
|
69
|
+
if (Array.isArray(result)) return readable4 (result[0]) // compat
|
|
70
|
+
if (typeof result === 'object' && 'value' in result) return readable4 (result.value)
|
|
71
|
+
cds.error(500, `Unexpected result type for streaming: Expected stream.Readable or null but got ${typeof result}`)
|
|
77
72
|
}
|
package/libx/http/location.js
CHANGED
|
@@ -30,62 +30,66 @@ module.exports = class ODataAdapter extends HttpAdapter {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
get router() {
|
|
33
|
+
function set_odata_version(_, res, next) {
|
|
34
|
+
res.set('OData-Version', '4.0')
|
|
35
|
+
next()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function validate_representation_headers(req, res, next) {
|
|
39
|
+
if (req.method === 'PUT' && isStream(req._query)) {
|
|
40
|
+
req.body = { value: req }
|
|
41
|
+
return next()
|
|
42
|
+
}
|
|
43
|
+
if (req.method === 'POST' && req.headers['content-type']?.match(/multipart\/mixed/)) {
|
|
44
|
+
return next()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const contentLength = req.headers['content-length']
|
|
48
|
+
const [type = '', suffix] = (req.headers['content-type'] || '').split(';')
|
|
49
|
+
// header ending with semicolon is not allowed
|
|
50
|
+
const isJson = type.match(/^application\/json$/) && suffix !== ''
|
|
51
|
+
|
|
52
|
+
// POST with empty body is allowed if no content-type header is set
|
|
53
|
+
if (req.method === 'POST' && (!contentLength || isJson)) return jsonBodyParser(req, res, next)
|
|
54
|
+
|
|
55
|
+
if (req.method in { POST: 1, PUT: 1, PATCH: 1 }) {
|
|
56
|
+
if (!isJson) {
|
|
57
|
+
res.status(415).json({ error: { message: 'Unsupported Media Type', statusCode: 415, code: '415' } })
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
if (contentLength === '0') {
|
|
61
|
+
res.status(400).json({ error: { message: 'Expected non-empty body', statusCode: 400, code: '400' } })
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return jsonBodyParser(req, res, next)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function validate_operation_http_method(req, _, next) {
|
|
70
|
+
// operations must have been handled above (POST or GET)
|
|
71
|
+
const { operation } = req._query.SELECT?.from.ref?.slice(-1)[0] || {}
|
|
72
|
+
next(operation ? { code: 405 } : undefined)
|
|
73
|
+
}
|
|
74
|
+
|
|
33
75
|
const jsonBodyParser = bodyParser4(this)
|
|
34
76
|
return (
|
|
35
77
|
super.router
|
|
36
|
-
.use(
|
|
37
|
-
res.set('OData-Version', '4.0')
|
|
38
|
-
next()
|
|
39
|
-
})
|
|
40
|
-
// REVISIT: add middleware for negative cases?
|
|
78
|
+
.use(set_odata_version)
|
|
41
79
|
// service root
|
|
42
|
-
.
|
|
43
|
-
.
|
|
80
|
+
.all('/', require('./middleware/service-document')(this))
|
|
81
|
+
.all('/\\$metadata', require('./middleware/metadata')(this))
|
|
44
82
|
// parse
|
|
45
83
|
.use(require('./middleware/parse')(this))
|
|
46
|
-
.use(
|
|
47
|
-
if (req.method === 'PUT' && isStream(req._query)) {
|
|
48
|
-
req.body = { value: req }
|
|
49
|
-
return next()
|
|
50
|
-
}
|
|
51
|
-
if (req.method === 'POST' && req.headers['content-type']?.match(/multipart\/mixed/)) {
|
|
52
|
-
return next()
|
|
53
|
-
}
|
|
54
|
-
// POST with empty body is allowed by actions
|
|
55
|
-
if (req.method in { PUT: 1, PATCH: 1 }) {
|
|
56
|
-
if (req.headers['content-length'] === '0') {
|
|
57
|
-
res.status(400).json({ error: { message: 'Expected non-empty body', statusCode: 400, code: '400' } })
|
|
58
|
-
return
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
if (req.method in { POST: 1, PUT: 1, PATCH: 1 }) {
|
|
62
|
-
const contentType = req.headers['content-type'] ?? ''
|
|
63
|
-
let contentLength = req.headers['content-length']
|
|
64
|
-
contentLength = contentLength ? parseInt(contentLength) : 0
|
|
65
|
-
|
|
66
|
-
const parts = contentType.split(';')
|
|
67
|
-
// header ending with semicolon is not allowed
|
|
68
|
-
if ((contentLength && !parts[0].match(/^application\/json$/)) || parts[1] === '') {
|
|
69
|
-
res.status(415).json({ error: { message: 'Unsupported Media Type', statusCode: 415, code: '415' } })
|
|
70
|
-
return
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return jsonBodyParser(req, res, next)
|
|
75
|
-
})
|
|
84
|
+
.use(validate_representation_headers)
|
|
76
85
|
// batch
|
|
77
86
|
// .all is used deliberately instead of .use so that the matched path is not stripped from req properties
|
|
78
87
|
.all('/\\$batch', require('./middleware/batch')(this))
|
|
79
88
|
// handle
|
|
80
|
-
// REVISIT: with old adapter, we return 405 for HEAD requests -> check OData spec
|
|
81
89
|
.head('*', (_, res) => res.sendStatus(405))
|
|
82
90
|
.post('*', operation4(this), create4(this))
|
|
83
91
|
.get('*', operation4(this), stream4(this), read4(this))
|
|
84
|
-
.use(
|
|
85
|
-
// operations must have been handled above (POST or GET)
|
|
86
|
-
const { operation } = req._query.SELECT?.from.ref?.slice(-1)[0] || {}
|
|
87
|
-
next(operation ? { code: 405 } : undefined)
|
|
88
|
-
})
|
|
92
|
+
.use(validate_operation_http_method)
|
|
89
93
|
.put('*', update4(this), create4(this, 'upsert'))
|
|
90
94
|
.patch('*', update4(this), create4(this, 'upsert'))
|
|
91
95
|
.delete('*', delete4(this))
|
package/libx/odata/index.js
CHANGED
|
@@ -62,7 +62,6 @@ const _validateBatch = body => {
|
|
|
62
62
|
throw _deserializationError(`Method '${method}' is not allowed. Only DELETE, GET, PATCH, POST or PUT are.`)
|
|
63
63
|
|
|
64
64
|
_validateProperty('url', url, 'string')
|
|
65
|
-
// TODO: need similar validation in multipart/mixed batch
|
|
66
65
|
if (url.startsWith('/$batch')) throw _deserializationError('Nested batch requests are not allowed.')
|
|
67
66
|
|
|
68
67
|
// TODO: support for non JSON bodies?
|
|
@@ -260,7 +259,12 @@ const _tx_done = async (tx, responses, isJson) => {
|
|
|
260
259
|
// here, the commit was rejected even though all requests were successful (e.g., by custom handler or db consistency check)
|
|
261
260
|
rejected = 'rejected'
|
|
262
261
|
// construct commit error (without modifying original error)
|
|
263
|
-
const error = normalizeError(Object.create(e), {
|
|
262
|
+
const error = normalizeError(Object.create(e), {
|
|
263
|
+
get locale() {
|
|
264
|
+
return cds.context.locale
|
|
265
|
+
},
|
|
266
|
+
get() {}
|
|
267
|
+
})
|
|
264
268
|
// replace all responses with commit error
|
|
265
269
|
for (const res of responses) {
|
|
266
270
|
res.status = 'fail'
|
|
@@ -72,7 +72,7 @@ module.exports = (adapter, isUpsert) => {
|
|
|
72
72
|
|
|
73
73
|
handleSapMessages(cdsReq, req, res)
|
|
74
74
|
|
|
75
|
-
if (!target._isSingleton) {
|
|
75
|
+
if (!target._isSingleton && !res.hasHeader('location')) {
|
|
76
76
|
res.set('location', location4(cdsReq.target, service, result || cdsReq.data))
|
|
77
77
|
}
|
|
78
78
|
|
|
@@ -4,17 +4,8 @@ const { shutdown_on_uncaught_errors } = cds.env.server
|
|
|
4
4
|
exports = module.exports = () =>
|
|
5
5
|
function odata_error(err, req, res, next) {
|
|
6
6
|
if (exports.pass_through(err)) return next(err)
|
|
7
|
+
else req._is_odata = true
|
|
7
8
|
if (err.details) err = _fioritized(err)
|
|
8
|
-
const content_id = req.headers['content-id']
|
|
9
|
-
if (content_id) err['@Core.ContentID'] = content_id
|
|
10
|
-
err['@Common.numericSeverity'] ??= err.numericSeverity || 4
|
|
11
|
-
|
|
12
|
-
// propagate content-id and set @Common.numericSeverity
|
|
13
|
-
err.details?.forEach(e => {
|
|
14
|
-
if (content_id) e['@Core.ContentID'] = content_id
|
|
15
|
-
e['@Common.numericSeverity'] ??= e.numericSeverity || 4 // Fiori expects this in error.details
|
|
16
|
-
})
|
|
17
|
-
|
|
18
9
|
exports.normalizeError(err, req)
|
|
19
10
|
return next(err)
|
|
20
11
|
}
|
|
@@ -25,14 +16,18 @@ exports.pass_through = err => {
|
|
|
25
16
|
}
|
|
26
17
|
|
|
27
18
|
exports.normalizeError = (err, req, cleanse = ODATA_PROPERTIES) => {
|
|
28
|
-
|
|
29
|
-
err.status = _normalize(err, locale, cleanse) || 500
|
|
19
|
+
err.status = _normalize(err, req, cleanse) || 500
|
|
30
20
|
return err
|
|
31
21
|
}
|
|
32
22
|
|
|
33
|
-
exports.
|
|
23
|
+
exports.getLocalizedMessages = (messages, req) => {
|
|
34
24
|
const locale = cds.i18n.locale.from(req)
|
|
35
|
-
for (let m of messages) _normalize(m,
|
|
25
|
+
for (let m of messages) _normalize(m, req, SAP_MSG_PROPERTIES, locale)
|
|
26
|
+
return messages
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
exports.getSapMessages = (messages, req) => {
|
|
30
|
+
messages = exports.getLocalizedMessages(messages, req)
|
|
36
31
|
return JSON.stringify(messages).replace(
|
|
37
32
|
/[\u007F-\uFFFF]/g,
|
|
38
33
|
c => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0')
|
|
@@ -45,10 +40,13 @@ const SAP_MSG_PROPERTIES = { ...ODATA_PROPERTIES, longtextUrl: 2, transition: 2,
|
|
|
45
40
|
const BAD_REQUESTS = { ENTITY_ALREADY_EXISTS: 1, FK_CONSTRAINT_VIOLATION: 2, UNIQUE_CONSTRAINT_VIOLATION: 3 }
|
|
46
41
|
|
|
47
42
|
// prettier-ignore
|
|
48
|
-
const _normalize = (err,
|
|
43
|
+
const _normalize = (err, req, keep,
|
|
44
|
+
locale = cds.i18n.locale.from(req) || cds.env.i18n.default_language,
|
|
45
|
+
content_id = req.get('content-id')
|
|
46
|
+
) => {
|
|
49
47
|
|
|
50
48
|
// Determine status code if not already set
|
|
51
|
-
const details = err.details?.map?.(each => _normalize (each, locale,
|
|
49
|
+
const details = err.details?.map?.(each => _normalize (each, req, keep, locale, content_id))
|
|
52
50
|
const status = err.status || err.statusCode || _status4(err) || _reduce(details)
|
|
53
51
|
|
|
54
52
|
// Determine error code and message
|
|
@@ -64,7 +62,11 @@ const _normalize = (err, locale = cds.env.i18n.default_language, keep) => {
|
|
|
64
62
|
Object.defineProperty(err, 'toJSON', { value: function() {
|
|
65
63
|
const that = keep ? {} : {...this}
|
|
66
64
|
if (keep) for (let k in this) if (k in keep || k[0] === '@') that[k] = this[k]
|
|
67
|
-
if (
|
|
65
|
+
if (req._is_odata && keep !== SAP_MSG_PROPERTIES) {
|
|
66
|
+
that['@Common.numericSeverity'] ??= err.numericSeverity || 4
|
|
67
|
+
if (content_id) that['@Core.ContentID'] = content_id
|
|
68
|
+
}
|
|
69
|
+
if (locale) that.message = _message4 (err, key, locale)
|
|
68
70
|
if (!that.message) that.message = this.message
|
|
69
71
|
return that
|
|
70
72
|
}})
|
|
@@ -76,6 +78,14 @@ const _status4 = err => {
|
|
|
76
78
|
if (err.message in BAD_REQUESTS) return 400 // REVISIT: should we use 409 or 500 instead?
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
const _message4 = (err, key, locale) => {
|
|
82
|
+
if (err.i18n) {
|
|
83
|
+
key = /{i18n>(.*)}/.exec(err.i18n)?.[1]
|
|
84
|
+
if (!key) return err.i18n
|
|
85
|
+
}
|
|
86
|
+
return i18n.messages.at(key, locale, err.args)
|
|
87
|
+
}
|
|
88
|
+
|
|
79
89
|
const _reduce = details => {
|
|
80
90
|
const unique = [...new Set(details)]
|
|
81
91
|
if (unique.length === 1) return unique[0] // if only one unique status exists, we use that
|
|
@@ -2,7 +2,7 @@ const cds = require('../../../')
|
|
|
2
2
|
|
|
3
3
|
const { pipeline } = require('node:stream/promises')
|
|
4
4
|
|
|
5
|
-
const {
|
|
5
|
+
const { handleSapMessages } = require('../utils')
|
|
6
6
|
const getODataMetadata = require('../utils/metadata')
|
|
7
7
|
const postProcess = require('../utils/postProcess')
|
|
8
8
|
const getODataResult = require('../utils/result')
|
|
@@ -143,12 +143,6 @@ module.exports = adapter => {
|
|
|
143
143
|
return res.sendStatus(204)
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
if (operation.returns._type?.match?.(/^cds\./)) {
|
|
147
|
-
const context = `${'../'.repeat(query?.SELECT?.from?.ref?.length)}$metadata#${cds2edm[operation.returns._type]}`
|
|
148
|
-
result = { '@odata.context': context, value: result }
|
|
149
|
-
return res.send(result)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
146
|
if (res.statusCode === 201 && !res.hasHeader('location')) {
|
|
153
147
|
const location = location4(operation.returns, service, result)
|
|
154
148
|
if (location) res.set('location', location)
|
|
@@ -160,21 +154,21 @@ module.exports = adapter => {
|
|
|
160
154
|
}
|
|
161
155
|
|
|
162
156
|
// REVISIT: enterprise search result? -> simply return what was provided
|
|
163
|
-
if (operation.returns.type
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const metadata = getODataMetadata({ SELECT, _target }, options)
|
|
176
|
-
result = getODataResult(result, metadata, { isCollection })
|
|
157
|
+
if (operation.returns.type === 'sap.esh.SearchResult') return res.send(result)
|
|
158
|
+
|
|
159
|
+
const isCollection = !!operation.returns.items
|
|
160
|
+
const _target = operation.returns.items ?? operation.returns
|
|
161
|
+
const options = { result, isCollection }
|
|
162
|
+
if (!_target.name) {
|
|
163
|
+
// case: return inline type def
|
|
164
|
+
options.edmName = _opResultName({ service, operation, returnType: _target })
|
|
165
|
+
}
|
|
166
|
+
const SELECT = {
|
|
167
|
+
from: query ? { ref: [...query.SELECT.from.ref, { operation: operation.name }] } : {},
|
|
168
|
+
one: !isCollection
|
|
177
169
|
}
|
|
170
|
+
const metadata = getODataMetadata({ SELECT, _target }, options)
|
|
171
|
+
result = getODataResult(result, metadata, { isCollection })
|
|
178
172
|
|
|
179
173
|
res.send(result)
|
|
180
174
|
})
|
|
@@ -149,7 +149,7 @@ module.exports = adapter => {
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
const stream = getReadable(result)
|
|
152
|
-
if (
|
|
152
|
+
if (stream == null) return res.sendStatus(204)
|
|
153
153
|
|
|
154
154
|
validateMimetypeIsAcceptedOrThrow(req.headers, mimetype)
|
|
155
155
|
if (!res.get('content-type')) res.set('content-type', mimetype)
|
|
@@ -249,13 +249,17 @@ function _handleCollectionBoundActions(current, ref, i, namespace, one) {
|
|
|
249
249
|
)
|
|
250
250
|
|
|
251
251
|
if (onCollection && one) {
|
|
252
|
-
const msg = `${action.kind.at(0).toUpperCase() + action.kind.slice(1)} "${
|
|
252
|
+
const msg = `${action.kind.at(0).toUpperCase() + action.kind.slice(1)} "${
|
|
253
|
+
action.name
|
|
254
|
+
}" must be called on a collection of ${current.name}`
|
|
253
255
|
throw Object.assign(new Error(msg), { statusCode: 400 })
|
|
254
256
|
}
|
|
255
257
|
|
|
256
258
|
if (incompleteKeys) {
|
|
257
259
|
if (!onCollection) {
|
|
258
|
-
const msg = `${action.kind.at(0).toUpperCase() + action.kind.slice(1)} "${
|
|
260
|
+
const msg = `${action.kind.at(0).toUpperCase() + action.kind.slice(1)} "${
|
|
261
|
+
action.name
|
|
262
|
+
}" must be called on a single instance of ${current.name}`
|
|
259
263
|
throw Object.assign(new Error(msg), { statusCode: 400 })
|
|
260
264
|
}
|
|
261
265
|
|
|
@@ -510,7 +514,9 @@ function _processSegments(from, model, namespace, cqn, protocol) {
|
|
|
510
514
|
|
|
511
515
|
if (incompleteKeys) {
|
|
512
516
|
// > last segment not fully qualified
|
|
513
|
-
const msg = `Entity "${current.name}" has ${keysOf(current).length} keys. Only ${keyCount} ${
|
|
517
|
+
const msg = `Entity "${current.name}" has ${keysOf(current).length} keys. Only ${keyCount} ${
|
|
518
|
+
keyCount === 1 ? 'was' : 'were'
|
|
519
|
+
} provided.`
|
|
514
520
|
throw Object.assign(new Error(msg), { statusCode: 400 })
|
|
515
521
|
}
|
|
516
522
|
|
|
@@ -820,7 +826,13 @@ module.exports = (cqn, model, namespace, protocol) => {
|
|
|
820
826
|
|
|
821
827
|
// hierarchy requests, quick check to avoid unnecessary traversing
|
|
822
828
|
// REVISIT: Should be done via annotation on backlink, would make lookup easier
|
|
823
|
-
|
|
829
|
+
const _getRecurse = SELECT => {
|
|
830
|
+
if (SELECT.recurse) return SELECT.recurse
|
|
831
|
+
if (SELECT.from && SELECT.from.SELECT) return _getRecurse(SELECT.from.SELECT)
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const _recurse = _getRecurse(cqn.SELECT)
|
|
835
|
+
if (_recurse) {
|
|
824
836
|
let uplinkName
|
|
825
837
|
for (const key in target) {
|
|
826
838
|
if (key.match(/@Aggregation\.RecursiveHierarchy\s*#.*\.ParentNavigationProperty/)) {
|
|
@@ -829,10 +841,12 @@ module.exports = (cqn, model, namespace, protocol) => {
|
|
|
829
841
|
break
|
|
830
842
|
}
|
|
831
843
|
}
|
|
832
|
-
if (uplinkName)
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
844
|
+
if (!uplinkName || !target.elements[uplinkName])
|
|
845
|
+
throw new cds.error(
|
|
846
|
+
500,
|
|
847
|
+
'Cannot resolve `ParentNavigationProperty` in `@Aggregation.RecursiveHierarchy` annotation'
|
|
848
|
+
)
|
|
849
|
+
_recurse.ref[0] = uplinkName
|
|
836
850
|
}
|
|
837
851
|
|
|
838
852
|
// REVISIT: better
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const cds = require('
|
|
1
|
+
const cds = require('../../..')
|
|
2
2
|
|
|
3
3
|
const { formatVal } = require('../utils')
|
|
4
4
|
|
|
@@ -65,10 +65,7 @@ function hasValidProps(obj, ...names) {
|
|
|
65
65
|
for (const propName of names) {
|
|
66
66
|
const validate = validators[propName]
|
|
67
67
|
const isValid = validate && validate(obj[propName])
|
|
68
|
-
|
|
69
|
-
if (!isValid) {
|
|
70
|
-
return false
|
|
71
|
-
}
|
|
68
|
+
if (!isValid) return false
|
|
72
69
|
}
|
|
73
70
|
|
|
74
71
|
return true
|
|
@@ -183,23 +180,32 @@ function _xpr(expr, target, kind, isLambda, navPrefix = []) {
|
|
|
183
180
|
if (inExpr) res.push(`(${inExpr})`)
|
|
184
181
|
i += 1
|
|
185
182
|
} else if (_isLambda(cur, expr[i + 1])) {
|
|
183
|
+
// Process 'exists' expression
|
|
184
|
+
|
|
186
185
|
const { where } = expr[i + 1].ref.at(-1)
|
|
187
|
-
const
|
|
186
|
+
const navSegments = expr[i + 1].ref.map(ref => ref?.id ?? ref)
|
|
187
|
+
const nav = navSegments.join('/')
|
|
188
188
|
|
|
189
189
|
if (kind === 'odata-v2') {
|
|
190
190
|
// odata-v2 does not support lambda expressions but successfactors allows filter like for to-one assocs
|
|
191
191
|
cds.log('remote').info(`OData V2 does not support lambda expressions. Using path expression as best effort.`)
|
|
192
192
|
isLambda = false
|
|
193
193
|
res.push(_xpr(where, target, kind, isLambda, [...navPrefix, nav]))
|
|
194
|
-
} else if (
|
|
195
|
-
|
|
196
|
-
|
|
194
|
+
} else if (res.at(-1) === 'not' && where.length === 2 && where[0] === 'not' && where[1].xpr) {
|
|
195
|
+
// Convert double negation 'not exists where not' to 'all'
|
|
196
|
+
// > reverting the mirrored transformation performed in grammar.peggy/lambda
|
|
197
|
+
|
|
198
|
+
res.pop()
|
|
199
|
+
res.push(`${nav}/all(${LAMBDA_VARIABLE}:${_xpr(where[1].xpr, target, kind, true, navPrefix)})`)
|
|
200
|
+
} else if (where) {
|
|
197
201
|
res.push(
|
|
198
202
|
`${nav}/any(${LAMBDA_VARIABLE}:${_xpr(where, target?.elements[nav]._target, kind, true, navPrefix)})`
|
|
199
203
|
)
|
|
204
|
+
} else {
|
|
205
|
+
res.push(`${nav}/any()`)
|
|
200
206
|
}
|
|
201
207
|
|
|
202
|
-
i
|
|
208
|
+
i += 1
|
|
203
209
|
} else {
|
|
204
210
|
res.push(OPERATORS[cur] || cur.toLowerCase())
|
|
205
211
|
}
|