@sap/cds 9.0.4 → 9.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 +25 -0
- package/lib/compile/for/lean_drafts.js +29 -7
- package/lib/dbs/cds-deploy.js +5 -3
- package/lib/env/cds-requires.js +1 -1
- package/lib/env/defaults.js +0 -11
- package/lib/env/schemas/cds-rc.js +214 -6
- package/lib/req/request.js +1 -1
- package/lib/req/validate.js +1 -2
- package/lib/srv/middlewares/auth/xssec.js +1 -1
- package/lib/utils/inflect.js +2 -2
- package/lib/utils/tar.js +60 -23
- package/libx/_runtime/common/generic/crud.js +1 -3
- package/libx/_runtime/common/generic/input.js +2 -2
- package/libx/_runtime/common/generic/temporal.js +0 -6
- package/libx/_runtime/fiori/lean-draft.js +487 -141
- package/libx/_runtime/remote/utils/client.js +1 -0
- package/libx/odata/ODataAdapter.js +47 -43
- package/libx/odata/middleware/batch.js +0 -1
- package/libx/odata/middleware/error.js +7 -0
- package/libx/odata/middleware/operation.js +15 -21
- package/libx/odata/parse/afterburner.js +22 -8
- package/libx/odata/parse/grammar.peggy +182 -133
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/index.js +0 -35
- 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 +5 -2
- package/package.json +1 -1
|
@@ -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, ')')
|
|
@@ -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))
|
|
@@ -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?
|
|
@@ -30,6 +30,13 @@ exports.normalizeError = (err, req, cleanse = ODATA_PROPERTIES) => {
|
|
|
30
30
|
return err
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// TODO: This should go somewhere else ...
|
|
34
|
+
exports.getLocalizedMessages = (messages, req) => {
|
|
35
|
+
const locale = cds.i18n.locale.from(req)
|
|
36
|
+
for (let m of messages) _normalize(m, locale, SAP_MSG_PROPERTIES)
|
|
37
|
+
return messages
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
exports.getSapMessages = (messages, req) => {
|
|
34
41
|
const locale = cds.i18n.locale.from(req)
|
|
35
42
|
for (let m of messages) _normalize(m, locale, SAP_MSG_PROPERTIES)
|
|
@@ -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
|
})
|
|
@@ -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
|