@sap/cds 8.7.2 → 8.8.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 +33 -0
- package/_i18n/i18n.properties +3 -0
- package/_i18n/i18n_cs.properties +6 -6
- package/_i18n/i18n_de.properties +3 -0
- package/_i18n/i18n_en.properties +3 -0
- package/_i18n/i18n_es.properties +3 -0
- package/_i18n/i18n_fr.properties +3 -0
- package/_i18n/i18n_it.properties +3 -0
- package/_i18n/i18n_ja.properties +3 -0
- package/_i18n/i18n_pl.properties +7 -4
- package/_i18n/i18n_pt.properties +3 -0
- package/_i18n/i18n_ru.properties +3 -0
- package/app/index.js +2 -30
- package/lib/env/cds-env.js +1 -1
- package/lib/env/cds-requires.js +16 -9
- package/lib/env/schemas/cds-package.js +1 -1
- package/lib/env/schemas/cds-rc.js +17 -4
- package/lib/index.js +1 -1
- package/lib/ql/SELECT.js +6 -1
- package/lib/req/request.js +5 -2
- package/lib/req/validate.js +3 -1
- package/lib/srv/bindings.js +31 -20
- package/lib/srv/cds-connect.js +1 -1
- package/lib/srv/middlewares/auth/mocked-users.js +1 -0
- package/lib/srv/protocols/okra.js +5 -7
- package/lib/srv/srv-dispatch.js +0 -5
- package/lib/test/cds-test.js +34 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -0
- package/libx/_runtime/common/generic/auth/service.js +2 -2
- package/libx/_runtime/common/generic/input.js +1 -1
- package/libx/_runtime/common/utils/binary.js +1 -35
- package/libx/_runtime/common/utils/rewriteAsterisks.js +5 -8
- package/libx/_runtime/fiori/lean-draft.js +2 -4
- package/libx/common/utils/path.js +1 -5
- package/libx/common/utils/streaming.js +76 -0
- package/libx/odata/middleware/create.js +1 -1
- package/libx/odata/middleware/delete.js +1 -1
- package/libx/odata/middleware/operation.js +48 -4
- package/libx/odata/middleware/read.js +1 -1
- package/libx/odata/middleware/stream.js +29 -101
- package/libx/odata/middleware/update.js +1 -1
- package/libx/odata/parse/afterburner.js +20 -1
- package/libx/odata/parse/grammar.peggy +108 -26
- package/libx/odata/parse/parser.js +1 -1
- package/libx/rest/RestAdapter.js +2 -16
- package/libx/rest/middleware/operation.js +38 -18
- package/libx/rest/middleware/parse.js +5 -25
- package/libx/rest/post-processing.js +33 -0
- package/libx/rest/pre-processing.js +38 -0
- package/package.json +1 -1
- package/libx/common/utils/index.js +0 -5
package/libx/rest/RestAdapter.js
CHANGED
|
@@ -8,8 +8,7 @@ const deleet = require('./middleware/delete')
|
|
|
8
8
|
const operation = require('./middleware/operation')
|
|
9
9
|
const error = require('./middleware/error')
|
|
10
10
|
|
|
11
|
-
const {
|
|
12
|
-
const getTemplate = require('../_runtime/common/utils/template')
|
|
11
|
+
const { postProcessData } = require('./post-processing')
|
|
13
12
|
|
|
14
13
|
const HttpAdapter = require('../../lib/srv/protocols/http')
|
|
15
14
|
const bodyParser4 = require('../odata/middleware/body-parser')
|
|
@@ -22,17 +21,6 @@ class RestRequest extends NoaRequest {
|
|
|
22
21
|
}
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
const _processorFn = elementInfo => {
|
|
26
|
-
const { row, key, plain } = elementInfo
|
|
27
|
-
if (plain.categories.includes('@cds.api.ignore')) delete row[key]
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const _pick = element => {
|
|
31
|
-
const categories = []
|
|
32
|
-
if (element['@cds.api.ignore'] && !element.isAssociation) categories.push('@cds.api.ignore')
|
|
33
|
-
if (categories.length) return { categories }
|
|
34
|
-
}
|
|
35
|
-
|
|
36
24
|
class RestAdapter extends HttpAdapter {
|
|
37
25
|
request4(args) {
|
|
38
26
|
return new RestRequest(args)
|
|
@@ -127,9 +115,7 @@ class RestAdapter extends HttpAdapter {
|
|
|
127
115
|
srv.model.definitions[definition] ||
|
|
128
116
|
srv.model.definitions[definition.split(':$:')[0]].actions[definition.split(':$:')[1]]
|
|
129
117
|
if (result && srv && definition) {
|
|
130
|
-
|
|
131
|
-
const template = getTemplate('process_result', srv, definition, { pick: _pick })
|
|
132
|
-
template.process(result, _processorFn)
|
|
118
|
+
postProcessData(result, srv, definition)
|
|
133
119
|
}
|
|
134
120
|
|
|
135
121
|
if (status && res.statusCode === 200) res.status(status) //> only set status if not yet modified
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
const cds = require('../../_runtime/cds')
|
|
2
|
+
const getError = require('../../_runtime/common/error')
|
|
2
3
|
|
|
3
4
|
const { checkStatic } = require('../../_runtime/cds-services/util/assert')
|
|
4
|
-
const
|
|
5
|
+
const { Readable } = require('node:stream')
|
|
5
6
|
|
|
6
7
|
// REVISIT: strict or relaxed type checkers?
|
|
7
8
|
const typeCheckers = require('../../common/assert/type-strict')
|
|
9
|
+
const streaming = require('../../common/utils/streaming')
|
|
8
10
|
|
|
9
11
|
// REVISIT: use i18n
|
|
10
12
|
const _enrichErrorDetails = (isPrimitive, error) => {
|
|
@@ -74,21 +76,40 @@ module.exports = adapter => {
|
|
|
74
76
|
|
|
75
77
|
return async function operation(req, res) {
|
|
76
78
|
const { _query: query, _operation: operation, _data: data, _params: params } = req
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
query
|
|
84
|
-
? { query, event: operation.name }
|
|
85
|
-
: { event: operation.name.replace(`${service.definition.name}.`, '') },
|
|
86
|
-
{ data, params, req, res }
|
|
87
|
-
)
|
|
79
|
+
const request = adapter.request4(
|
|
80
|
+
Object.assign(
|
|
81
|
+
query // REVISIT: when is a query given, and when not? -> bound vs unbound? -> obsolete with req.subject? -> deprecated?
|
|
82
|
+
? { event: operation.name, query } // REVISIT: why don't we need a similar .replace as below here?
|
|
83
|
+
: { event: operation.name.replace(`${service.definition.name}.`, '') }, // REVISIT: why is this .replace needed?
|
|
84
|
+
{ data, params, req, res } // REVISIT: req and res are not part of the public API !!!
|
|
88
85
|
)
|
|
89
86
|
)
|
|
90
87
|
|
|
91
|
-
|
|
88
|
+
let result = await service.dispatch(request)
|
|
89
|
+
|
|
90
|
+
if (!operation.returns || result == null) return { status: 204 }
|
|
91
|
+
|
|
92
|
+
const _isStream = result instanceof Readable || (operation.returns._type === 'cds.LargeBinary' && 'value' in result)
|
|
93
|
+
if (_isStream) {
|
|
94
|
+
const stream = streaming.getReadable(result)
|
|
95
|
+
if (!stream) return res.sendStatus(204)
|
|
96
|
+
|
|
97
|
+
const { mimetype, filename, disposition } = streaming.collectStreamMetadata(result, operation, query)
|
|
98
|
+
streaming.validateMimetypeIsAcceptedOrThrow(req.headers, mimetype)
|
|
99
|
+
|
|
100
|
+
if (mimetype && !res.get('Content-Type'))
|
|
101
|
+
res.set('Content-Type', mimetype)
|
|
102
|
+
|
|
103
|
+
if (filename && !res.get('Content-Disposition'))
|
|
104
|
+
res.set('Content-Disposition', `${disposition}; filename="${filename}"`)
|
|
105
|
+
// NOTE: We should NOT use encodeURIComponent here, as it is not needed for the filename in the Content-Disposition header
|
|
106
|
+
// and it would break the filename in the header if it contains special characters like
|
|
107
|
+
// e.g. German umlauts (ä, ö, ü) or spaces
|
|
108
|
+
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
|
|
109
|
+
|
|
110
|
+
return stream.pipe(res) && {} // REVISIT: strange caller expects this
|
|
111
|
+
}
|
|
112
|
+
|
|
92
113
|
|
|
93
114
|
// REVISIT: do not use from old rest adapter
|
|
94
115
|
// REVISIT: new impl should return instead of throwing to avoid try catch
|
|
@@ -97,11 +118,10 @@ module.exports = adapter => {
|
|
|
97
118
|
// set content-type header to text/plain for returned primitive data types, except for boolean
|
|
98
119
|
const returnType = operation.returns._type
|
|
99
120
|
if (
|
|
100
|
-
!res.get('content-type') &&
|
|
101
|
-
!operation.returns.items &&
|
|
102
|
-
returnType &&
|
|
103
121
|
cds.builtin.types[returnType] &&
|
|
104
|
-
|
|
122
|
+
!operation.returns.items &&
|
|
123
|
+
returnType !== 'cds.Boolean' &&
|
|
124
|
+
!res.get('content-type')
|
|
105
125
|
) {
|
|
106
126
|
res.set('Content-Type', 'text/plain')
|
|
107
127
|
}
|
|
@@ -112,7 +132,7 @@ module.exports = adapter => {
|
|
|
112
132
|
res.set('Content-Type', 'application/json')
|
|
113
133
|
}
|
|
114
134
|
|
|
115
|
-
// REVISIT: still needed?
|
|
135
|
+
// REVISIT: still needed? // REVISIT: indeed!
|
|
116
136
|
if (!operation.returns.items && Array.isArray(result)) result = result[0]
|
|
117
137
|
|
|
118
138
|
if (result === undefined) return { status: 204 }
|
|
@@ -1,31 +1,11 @@
|
|
|
1
1
|
const cds = require('../../_runtime/cds')
|
|
2
2
|
const { INSERT, SELECT, UPDATE, DELETE } = cds.ql
|
|
3
3
|
|
|
4
|
-
const { getKeysAndParamsFromPath } = require('../../common/utils')
|
|
4
|
+
const { getKeysAndParamsFromPath } = require('../../common/utils/path')
|
|
5
5
|
|
|
6
|
-
const {
|
|
6
|
+
const { preProcessData } = require('../pre-processing')
|
|
7
7
|
const { convertStructured } = require('../../_runtime/common/utils/ucsn')
|
|
8
|
-
const getTemplate = require('../../_runtime/common/utils/template')
|
|
9
8
|
|
|
10
|
-
const { checkStaticElementByKey } = require('../../_runtime/cds-services/util/assert')
|
|
11
|
-
|
|
12
|
-
const _processorFn = errors => {
|
|
13
|
-
return ({ row, key, plain: categories, target }) => {
|
|
14
|
-
// REVISIT move validation to generic asserter => see PR 717
|
|
15
|
-
if (categories['static_validation'] && row[key] != null) {
|
|
16
|
-
const validations = checkStaticElementByKey(target, key, row[key])
|
|
17
|
-
errors.push(...validations)
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const _picker = element => {
|
|
23
|
-
const categories = {}
|
|
24
|
-
if (Array.isArray(element)) return
|
|
25
|
-
if (element._isStructured || element.isAssociation || element.items) return
|
|
26
|
-
categories['static_validation'] = true
|
|
27
|
-
return categories
|
|
28
|
-
}
|
|
29
9
|
|
|
30
10
|
const _cache = req => `rest-input;skip-key-validation:${req.method !== 'POST'}`
|
|
31
11
|
|
|
@@ -149,14 +129,14 @@ module.exports = adapter => {
|
|
|
149
129
|
convertStructured(service, operation || definition, payload, {
|
|
150
130
|
cleanupStruct: cds.env.features.rest_struct_data
|
|
151
131
|
})
|
|
152
|
-
|
|
153
|
-
template.process(payload, _processorFn(errs))
|
|
132
|
+
preProcessData(payload, service, definition, _cache, errs)
|
|
154
133
|
if (errs.length) {
|
|
155
134
|
if (errs.length === 1) throw errs[0]
|
|
156
135
|
throw Object.assign(new Error('MULTIPLE_ERRORS'), { statusCode: 400, details: errs })
|
|
157
136
|
}
|
|
137
|
+
} else {
|
|
138
|
+
preProcessData(payload, service, definition)
|
|
158
139
|
}
|
|
159
|
-
base64ToBuffer(payload, service, definition)
|
|
160
140
|
req._data = payload
|
|
161
141
|
}
|
|
162
142
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const getTemplate = require('../_runtime/common/utils/template')
|
|
2
|
+
const _picker = element => {
|
|
3
|
+
const categories = []
|
|
4
|
+
if (Array.isArray(element)) return
|
|
5
|
+
if (element.type === 'cds.Binary' || element.type === 'cds.LargeBinary') categories.push('convert_binary')
|
|
6
|
+
if (element['@cds.api.ignore'] && !element.isAssociation) categories.push('@cds.api.ignore')
|
|
7
|
+
if (categories.length) return { categories }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const _processorFn = elementInfo => {
|
|
11
|
+
const { row, key, plain } = elementInfo
|
|
12
|
+
if (typeof row !== 'object') return
|
|
13
|
+
for (const category of plain.categories) {
|
|
14
|
+
switch (category) {
|
|
15
|
+
case 'convert_binary':
|
|
16
|
+
if (row[key] != null && Buffer.isBuffer(row[key])) row[key] = row[key].toString('base64')
|
|
17
|
+
break
|
|
18
|
+
case '@cds.api.ignore':
|
|
19
|
+
delete row[key]
|
|
20
|
+
break
|
|
21
|
+
// no default
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const postProcessData = (data, srv, definition) => {
|
|
27
|
+
const template = getTemplate('rest-post-process', srv, definition, { pick: _picker })
|
|
28
|
+
template.process(data, _processorFn)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
postProcessData
|
|
33
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const getTemplate = require('../_runtime/common/utils/template')
|
|
2
|
+
const { checkStaticElementByKey } = require('../_runtime/cds-services/util/assert')
|
|
3
|
+
const _picker = element => {
|
|
4
|
+
const categories = []
|
|
5
|
+
if (Array.isArray(element)) return
|
|
6
|
+
if (element.type === 'cds.Binary' || element.type === 'cds.LargeBinary') categories.push('convert_binary')
|
|
7
|
+
if (!(element._isStructured || element.isAssociation || element.items)) categories.push('static_validation')
|
|
8
|
+
if (categories.length) return { categories }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const _processorFn = errors => elementInfo => {
|
|
12
|
+
const { row, key, plain, target } = elementInfo
|
|
13
|
+
if (typeof row !== 'object') return
|
|
14
|
+
if (row[key] == null) return
|
|
15
|
+
for (const category of plain.categories) {
|
|
16
|
+
switch (category) {
|
|
17
|
+
case 'convert_binary':
|
|
18
|
+
if (typeof row[key] === 'string') row[key] = Buffer.from(row[key], 'base64')
|
|
19
|
+
break
|
|
20
|
+
case 'static_validation':
|
|
21
|
+
if (errors) { //> errors collector is only provided in case of !cds.env.features.cds_validate
|
|
22
|
+
// REVISIT move validation to generic asserter => see PR 717
|
|
23
|
+
const validations = checkStaticElementByKey(target, key, row[key])
|
|
24
|
+
errors.push(...validations)
|
|
25
|
+
}
|
|
26
|
+
break
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const preProcessData = (data, srv, definition, usecase = 'rest-pre-process', errors) => {
|
|
32
|
+
const template = getTemplate(usecase, srv, definition, { pick: _picker })
|
|
33
|
+
template.process(data, _processorFn(errors))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = {
|
|
37
|
+
preProcessData
|
|
38
|
+
}
|
package/package.json
CHANGED