@sap/cds 5.9.3 → 5.9.6
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 +50 -3
- package/lib/serve/Service-methods.js +19 -0
- package/libx/_runtime/auth/index.js +16 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +14 -19
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/MultipartReader.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ContextURLFactory.js +6 -5
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ServiceJsonSerializer.js +1 -1
- package/libx/_runtime/cds-services/adapter/rest/utils/header-checks.js +5 -1
- package/libx/_runtime/common/aspects/utils.js +8 -2
- package/libx/_runtime/common/error/frontend.js +23 -24
- package/libx/_runtime/common/error/utils.js +24 -0
- package/libx/_runtime/common/generic/auth/expand.js +1 -0
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +4 -9
- package/libx/_runtime/common/utils/require.js +2 -1
- package/libx/_runtime/hana/driver.js +1 -1
- package/libx/_runtime/hana/execute.js +17 -19
- package/libx/_runtime/hana/pool.js +4 -1
- package/libx/_runtime/remote/Service.js +3 -3
- package/libx/rest/RestAdapter.js +2 -2
- package/libx/rest/middleware/content.js +5 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,13 +4,60 @@
|
|
|
4
4
|
- The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
5
5
|
- This project adheres to [Semantic Versioning](http://semver.org/).
|
|
6
6
|
|
|
7
|
+
|
|
8
|
+
## Version 5.9.6 - 2022-05-24
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Ignored requests in batch requests
|
|
13
|
+
- `pool` module for logger facade is separated from `hana` database logger. Timeout error by acquiring client from pool is now enhanced with `_poolState` providing current pool attibutes.
|
|
14
|
+
- Multiple errors did not have correct HTTP response status code
|
|
15
|
+
- `POST|PUT|PATCH` requests with `charset` directive in `Content-Type` header (e.g. `Content-Type: application/json; charset=utf-8`) no longer issues an error "Invalid content type" in REST adapters
|
|
16
|
+
- Call hana procedure:
|
|
17
|
+
+ accepted are any symbols in a procedure name if it is delimited with a double quotation (`"`)
|
|
18
|
+
+ fixed results for table output parameters when using `@sap/hana-client`; **limitation**: output parameters in a `CALL` statement must follow the same order used in a stored procedure definition
|
|
19
|
+
- `@odata.context` considers `cds.env.odata.contextAbsoluteUrl` when requesting an OData Service
|
|
20
|
+
|
|
21
|
+
## Version 5.9.5 - 2022-05-09
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- `HDB_TCP_KEEP_ALIVE_IDLE` config
|
|
26
|
+
- A combination of `!=` operator and `or` in `where` clauses of `@restrict` annotations or when adjusting `req.query` in custom handlers (OData services only)
|
|
27
|
+
- Programmatic calls to bound actions/functions do have keys in `req.data` again if compat flag `cds.env.features.keys_in_data_compat` is set
|
|
28
|
+
|
|
29
|
+
## Version 5.9.4 - 2022-05-02
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- Error messages are improved if no `passport` module was found or if no `xsuaa` service binding is available
|
|
34
|
+
- Issue fixed for `srv.get()`. It was returning `TypeError` in plain REST usage for external services, e.g. `srv.get('/some/arbitrary/path/111')`
|
|
35
|
+
- Allow unrestricted services to run unauthenticated, removing the `Unable to require required package/file "passport"` error. Totally not recommended in production. Note that though this restores pre 5.9.0 behavior, this will come again starting in 6.0.
|
|
36
|
+
- Audit logging of sensitive data in a composition child if its parent is annotated with `@PersonalData.EntitySemantics: 'Other'` and has no data privacy annotations other than `@PersonalData.FieldSemantics: 'DataSubjectID'` annotating a corresponding composition, for example:
|
|
37
|
+
```js
|
|
38
|
+
annotate Customers with @PersonalData : {
|
|
39
|
+
DataSubjectRole : 'Address',
|
|
40
|
+
EntitySemantics : 'Other'
|
|
41
|
+
} {
|
|
42
|
+
addresses @PersonalData.FieldSemantics: 'DataSubjectID';
|
|
43
|
+
}
|
|
44
|
+
annotate CustomerPostalAddress with @PersonalData : {
|
|
45
|
+
DataSubjectRole : 'Address',
|
|
46
|
+
EntitySemantics : 'DataSubject'
|
|
47
|
+
} {
|
|
48
|
+
ID @PersonalData.FieldSemantics : 'DataSubjectID';
|
|
49
|
+
street @PersonalData.IsPotentiallyPersonal;
|
|
50
|
+
town @PersonalData.IsPotentiallySensitive;
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
7
54
|
## Version 5.9.3 - 2022-04-25
|
|
8
55
|
|
|
9
56
|
### Fixed
|
|
10
57
|
|
|
11
|
-
- Since 5.8.2 `req.target` for requests like `srv.put('/MyService.entity')` is defined, but `req.query` undefined (before `req.target` was also undefined). This was leading to accessing undefined, which has been fixed.
|
|
12
|
-
- Custom actions with names conflicting with methods from service base classes, e.g. `run()`, could lead to hard-to-detect errors. This is now detected and avoided with a warning.
|
|
13
|
-
- Typed methods for custom actions were erroneously applied to `cds.db` service, which led to server crashes, e.g. when the action was named `deploy()`.
|
|
58
|
+
- Since 5.8.2 `req.target` for requests like `srv.put('/MyService.entity')` is defined, but `req.query` undefined (before `req.target` was also undefined). This was leading to accessing undefined, which has been fixed.
|
|
59
|
+
- Custom actions with names conflicting with methods from service base classes, e.g. `run()`, could lead to hard-to-detect errors. This is now detected and avoided with a warning.
|
|
60
|
+
- Typed methods for custom actions were erroneously applied to `cds.db` service, which led to server crashes, e.g. when the action was named `deploy()`.
|
|
14
61
|
- Invalid batch requests were further processed after error response was already sent to client, leading to an InternalServerError
|
|
15
62
|
- Full support of `SELECT` queries with operator expressions (`xpr`)
|
|
16
63
|
|
|
@@ -62,6 +62,25 @@ const add_handler_for = (srv, def) => {
|
|
|
62
62
|
}
|
|
63
63
|
const {params} = target ? target.actions[event] : def
|
|
64
64
|
if (params) req.data = _named(args,params) || _positional(args,params)
|
|
65
|
+
|
|
66
|
+
// ensure legacy compat, keys in req.data
|
|
67
|
+
if(cds.env.features.keys_in_data_compat && target) {
|
|
68
|
+
// named/positional variant of keys
|
|
69
|
+
const named = req.params.length === 1 && typeof req.params[0] === 'object'
|
|
70
|
+
|
|
71
|
+
let pos = 0 //> counter for key in positional variant
|
|
72
|
+
for (const k in target.keys) {
|
|
73
|
+
if (req.data[k]) {
|
|
74
|
+
LOG._warn && LOG.warn(`
|
|
75
|
+
${target.name} has defined ${k} as key and action parameter.
|
|
76
|
+
Key will be used in req.data.
|
|
77
|
+
`)
|
|
78
|
+
}
|
|
79
|
+
req.data[k] = named ? req.params[0][k] : req.params[pos++]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
}
|
|
83
|
+
|
|
65
84
|
return this.send (req)
|
|
66
85
|
}
|
|
67
86
|
stub._is_stub = true
|
|
@@ -44,7 +44,10 @@ const _initializers = {
|
|
|
44
44
|
// REVISIT: compat, remove with cds^6
|
|
45
45
|
passport.use(new XSUAAStrategy(uaa.credentials))
|
|
46
46
|
} else {
|
|
47
|
-
throw Object.assign(
|
|
47
|
+
throw Object.assign(
|
|
48
|
+
new Error('No or malformed credentials for auth kind "xsuaa". Make sure to bind the app to an "xsuaa" service'),
|
|
49
|
+
{ credentials }
|
|
50
|
+
)
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
53
|
}
|
|
@@ -178,6 +181,18 @@ module.exports = (srv, app, options) => {
|
|
|
178
181
|
// > dummy or mock authentication (for development/testing)
|
|
179
182
|
_mountMockAuth(srv, app, strategy, config)
|
|
180
183
|
} else {
|
|
184
|
+
// if no restriction and no binding, don't mount passport middleware
|
|
185
|
+
if (!restricted && !config.credentials) {
|
|
186
|
+
if (!logged) {
|
|
187
|
+
const msg = `Service ${srv.name} is unrestricted`
|
|
188
|
+
if (process.env.NODE_ENV !== 'production') LOG._debug && LOG.debug(msg)
|
|
189
|
+
else LOG._info && LOG.info(`${msg}. This is not recommended in production.`)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// no auth wanted > return
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
181
196
|
// > passport authentication
|
|
182
197
|
_mountPassportAuth(srv, app, strategy, config)
|
|
183
198
|
}
|
|
@@ -28,11 +28,7 @@ const _read = require('./handlers/read')
|
|
|
28
28
|
const _action = require('./handlers/action')
|
|
29
29
|
const { normalizeError, isClientError } = require('../../../common/error/frontend')
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
const i18n = (...args) => {
|
|
33
|
-
if (!_i18n) _i18n = require('../../../common/i18n')
|
|
34
|
-
return _i18n(...args)
|
|
35
|
-
}
|
|
31
|
+
const { getErrorMessage } = require('../../../common/error/utils')
|
|
36
32
|
|
|
37
33
|
function _log(level, arg) {
|
|
38
34
|
const { params } = arg
|
|
@@ -51,31 +47,30 @@ function _log(level, arg) {
|
|
|
51
47
|
if (!obj.level) obj.level = arg.level
|
|
52
48
|
if (!obj.timestamp) obj.timestamp = arg.timestamp
|
|
53
49
|
|
|
54
|
-
// replace messages in toLog with developer texts (i.e., undefined locale) iff level === 'error' (cf. req.reject() etc.)
|
|
55
|
-
const _message = obj.message
|
|
56
|
-
const _details = obj.details
|
|
57
50
|
if (level === 'error') {
|
|
58
|
-
obj.message = i18n(obj.message || obj.code, undefined, obj.args) || obj.message
|
|
59
|
-
if (obj.details) {
|
|
60
|
-
const details = []
|
|
61
|
-
for (const d of obj.details) {
|
|
62
|
-
details.push(Object.assign({}, d, { message: i18n(d.message || d.code, undefined, d.args) || d.message }))
|
|
63
|
-
}
|
|
64
|
-
obj.details = details
|
|
65
|
-
}
|
|
66
|
-
|
|
67
51
|
// reduce 4xx to warning
|
|
68
52
|
if (isClientError(obj)) {
|
|
69
53
|
if (!LOG._warn) {
|
|
70
54
|
// restore
|
|
71
|
-
obj.message = _message
|
|
72
|
-
if (_details) obj.details = _details
|
|
73
55
|
return
|
|
74
56
|
}
|
|
75
57
|
level = 'warn'
|
|
76
58
|
}
|
|
77
59
|
}
|
|
78
60
|
|
|
61
|
+
// replace messages in toLog with developer texts (i.e., undefined locale) (cf. req.reject() etc.)
|
|
62
|
+
const _message = obj.message
|
|
63
|
+
const _details = obj.details
|
|
64
|
+
|
|
65
|
+
obj.message = getErrorMessage(obj)
|
|
66
|
+
if (obj.details) {
|
|
67
|
+
const details = []
|
|
68
|
+
for (const d of obj.details) {
|
|
69
|
+
details.push(Object.assign({}, d, { message: getErrorMessage(d) }))
|
|
70
|
+
}
|
|
71
|
+
obj.details = details
|
|
72
|
+
}
|
|
73
|
+
|
|
79
74
|
// log it
|
|
80
75
|
LOG[level](obj)
|
|
81
76
|
|
|
@@ -243,7 +243,7 @@ class MultipartParser extends Reader {
|
|
|
243
243
|
// the nested multipart is finished.
|
|
244
244
|
|
|
245
245
|
if (this._stopPattern) {
|
|
246
|
-
if (cache.
|
|
246
|
+
if (cache._length - cache.getReadPos() < this._stopPattern.length) {
|
|
247
247
|
return true
|
|
248
248
|
}
|
|
249
249
|
if (cache.indexOf(this._stopPattern, cache.getSearchPosition()) === cache.getSearchPosition()) {
|
|
@@ -28,14 +28,12 @@ class ContextURLFactory {
|
|
|
28
28
|
createContextURL(uriInfo, expand, representationKind, providedKeyMap = new Map(), edm, request, logger) {
|
|
29
29
|
const pathSegments = uriInfo.getPathSegments()
|
|
30
30
|
const lastSegment = pathSegments[pathSegments.length - 1]
|
|
31
|
-
|
|
32
|
-
if (lastSegment.getKind() === ResourceKind.SERVICE) return '$metadata'
|
|
33
|
-
if (lastSegment.getKind() === ResourceKind.METADATA) return ''
|
|
34
|
-
|
|
35
31
|
const contextUrlInfo = this._parseSegments(pathSegments, providedKeyMap, edm)
|
|
36
|
-
|
|
37
32
|
const contextUrlPrefix = this._buildContextUrlPrefix(contextUrlInfo, pathSegments, request, logger)
|
|
38
33
|
|
|
34
|
+
if (lastSegment.getKind() === ResourceKind.SERVICE) return `${contextUrlPrefix}$metadata`
|
|
35
|
+
if (lastSegment.getKind() === ResourceKind.METADATA) return ''
|
|
36
|
+
|
|
39
37
|
const finalEdmType = uriInfo.getFinalEdmType()
|
|
40
38
|
const structuredType =
|
|
41
39
|
finalEdmType && (finalEdmType.getKind() === EdmTypeKind.ENTITY || finalEdmType.getKind() === EdmTypeKind.COMPLEX)
|
|
@@ -93,6 +91,9 @@ class ContextURLFactory {
|
|
|
93
91
|
|
|
94
92
|
const kind = segment.getKind()
|
|
95
93
|
switch (kind) {
|
|
94
|
+
case ResourceKind.SERVICE:
|
|
95
|
+
case ResourceKind.METADATA:
|
|
96
|
+
return { result: '', isOnlyTyped, isEntity, hasReferencedSegment }
|
|
96
97
|
case ResourceKind.ENTITY:
|
|
97
98
|
target = segment.getEntitySet()
|
|
98
99
|
result.unshift(
|
|
@@ -34,7 +34,7 @@ class ServiceJsonSerializer {
|
|
|
34
34
|
serialize (data) {
|
|
35
35
|
try {
|
|
36
36
|
let outputJson = {
|
|
37
|
-
[JsonAnnotations.CONTEXT]: '$metadata',
|
|
37
|
+
[JsonAnnotations.CONTEXT]: data[MetaProperties.CONTEXT] || '$metadata',
|
|
38
38
|
[JsonAnnotations.METADATA_ETAG]:
|
|
39
39
|
data[MetaProperties.ETAG] === null || data[MetaProperties.ETAG] === undefined
|
|
40
40
|
? undefined
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
const getError = require('../../../../common/error')
|
|
2
2
|
|
|
3
3
|
const contentTypeCheck = req => {
|
|
4
|
-
|
|
4
|
+
const contentType = req.headers['content-type'] && req.headers['content-type'].split(';')
|
|
5
|
+
if (
|
|
6
|
+
contentType &&
|
|
7
|
+
(!contentType[0].match(/^application\/json$/) || (typeof contentType[1] === 'string' && !contentType[1]))
|
|
8
|
+
) {
|
|
5
9
|
throw getError(415, 'INVALID_CONTENT_TYPE_ONLY_JSON')
|
|
6
10
|
}
|
|
7
11
|
}
|
|
@@ -44,7 +44,9 @@ const hasPersonalData = entity => {
|
|
|
44
44
|
for (const ele in entity.elements) {
|
|
45
45
|
if (
|
|
46
46
|
entity.elements[ele]['@PersonalData.IsPotentiallyPersonal'] ||
|
|
47
|
-
entity.elements[ele]['@PersonalData.IsPotentiallySensitive']
|
|
47
|
+
entity.elements[ele]['@PersonalData.IsPotentiallySensitive'] ||
|
|
48
|
+
(entity.elements[ele]['@PersonalData.FieldSemantics'] &&
|
|
49
|
+
entity.elements[ele]['@PersonalData.FieldSemantics'] === 'DataSubjectID')
|
|
48
50
|
) {
|
|
49
51
|
val = true
|
|
50
52
|
break
|
|
@@ -58,7 +60,11 @@ const hasSensitiveData = entity => {
|
|
|
58
60
|
let val
|
|
59
61
|
if (entity['@PersonalData.DataSubjectRole'] && entity['@PersonalData.EntitySemantics']) {
|
|
60
62
|
for (const ele in entity.elements) {
|
|
61
|
-
if (
|
|
63
|
+
if (
|
|
64
|
+
entity.elements[ele]['@PersonalData.IsPotentiallySensitive'] ||
|
|
65
|
+
(entity.elements[ele]['@PersonalData.FieldSemantics'] &&
|
|
66
|
+
entity.elements[ele]['@PersonalData.FieldSemantics'] === 'DataSubjectID')
|
|
67
|
+
) {
|
|
62
68
|
val = true
|
|
63
69
|
break
|
|
64
70
|
}
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* Error responses MAY contain annotations in any of its JSON objects.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
const localeFrom = require('../../../../lib/req/locale')
|
|
10
|
+
const { getErrorMessage } = require('./utils')
|
|
11
|
+
|
|
9
12
|
let _i18n
|
|
10
13
|
const i18n = (...args) => {
|
|
11
14
|
if (!_i18n) _i18n = require('../i18n')
|
|
@@ -60,7 +63,7 @@ const _rewrite = error => {
|
|
|
60
63
|
|
|
61
64
|
const _normalize = (err, locale, inner = false) => {
|
|
62
65
|
// message (i18n)
|
|
63
|
-
err.message =
|
|
66
|
+
err.message = getErrorMessage(err, locale)
|
|
64
67
|
|
|
65
68
|
// only allowed properties
|
|
66
69
|
const error = _getFiltered(err)
|
|
@@ -68,31 +71,37 @@ const _normalize = (err, locale, inner = false) => {
|
|
|
68
71
|
// ensure code is set and a string
|
|
69
72
|
error.code = String(error.code || 'null')
|
|
70
73
|
|
|
74
|
+
// REVISIT: code and message rewriting
|
|
75
|
+
_rewrite(error)
|
|
76
|
+
|
|
77
|
+
let statusCode = err.status || err.statusCode || (_isAllowedError(error.code) && error.code)
|
|
78
|
+
|
|
71
79
|
// details
|
|
72
80
|
if (!inner && err.details) {
|
|
73
|
-
|
|
81
|
+
const childErrorCodes = new Set()
|
|
82
|
+
error.details = err.details.map(ele => {
|
|
83
|
+
const { error: childError, statusCode: childStatusCode } = _normalize(ele, locale, true)
|
|
84
|
+
childErrorCodes.add(childStatusCode)
|
|
85
|
+
return childError
|
|
86
|
+
})
|
|
87
|
+
statusCode = statusCode || _statusCodeFromDetails(childErrorCodes)
|
|
74
88
|
}
|
|
75
89
|
|
|
76
|
-
//
|
|
77
|
-
|
|
90
|
+
// make sure it's a number, if it's an inner error don't set to 500, as will be set on root level if no other inner error has a statusCode
|
|
91
|
+
statusCode = statusCode ? Number(statusCode) : inner ? undefined : 500
|
|
78
92
|
|
|
79
|
-
return error
|
|
93
|
+
return { error, statusCode }
|
|
80
94
|
}
|
|
81
95
|
|
|
82
96
|
const _isAllowedError = errorCode => {
|
|
83
97
|
return errorCode >= 300 && errorCode < 505
|
|
84
98
|
}
|
|
85
99
|
|
|
86
|
-
const localeFrom = require('../../../../lib/req/locale')
|
|
87
|
-
|
|
88
100
|
// - for one unique value, we use it
|
|
89
101
|
// - if at least one 5xx exists, we use 500
|
|
90
102
|
// - else if at least one 4xx exists, we use 400
|
|
91
103
|
// - else we use 500
|
|
92
|
-
const _statusCodeFromDetails =
|
|
93
|
-
const uniqueStatusCodes = new Set(
|
|
94
|
-
details.map(d => d.status || d.statusCode || d.code).map(c => (!isNaN(c) && Number(c)) || c)
|
|
95
|
-
)
|
|
104
|
+
const _statusCodeFromDetails = uniqueStatusCodes => {
|
|
96
105
|
if (uniqueStatusCodes.size === 1) return uniqueStatusCodes.values().next().value
|
|
97
106
|
if ([...uniqueStatusCodes].some(s => s >= 500)) return 500
|
|
98
107
|
if ([...uniqueStatusCodes].some(s => s >= 400)) return 400
|
|
@@ -101,17 +110,7 @@ const _statusCodeFromDetails = details => {
|
|
|
101
110
|
|
|
102
111
|
const normalizeError = (err, req) => {
|
|
103
112
|
const locale = req.locale || (req.locale = localeFrom(req))
|
|
104
|
-
const error = _normalize(err, locale)
|
|
105
|
-
|
|
106
|
-
// derive status code from err status OR root code OR matching detail codes
|
|
107
|
-
let statusCode = err.status || err.statusCode || (_isAllowedError(error.code) && error.code)
|
|
108
|
-
if (!statusCode && error.details) {
|
|
109
|
-
const detailsCode = _statusCodeFromDetails(error.details)
|
|
110
|
-
if (_isAllowedError(detailsCode)) statusCode = detailsCode
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// make sure it's a number
|
|
114
|
-
statusCode = statusCode ? Number(statusCode) : 500
|
|
113
|
+
const { error, statusCode } = _normalize(err, locale)
|
|
115
114
|
|
|
116
115
|
// REVISIT: make === 500 in cds^6
|
|
117
116
|
// error[SKIP_SANITIZATION] is not an official API!!!
|
|
@@ -138,7 +137,7 @@ const _ensureSeverity = arg => {
|
|
|
138
137
|
}
|
|
139
138
|
|
|
140
139
|
const _normalizeMessage = (message, locale) => {
|
|
141
|
-
const normalized = _normalize(message, locale)
|
|
140
|
+
const { error: normalized } = _normalize(message, locale)
|
|
142
141
|
|
|
143
142
|
// numericSeverity without @Common
|
|
144
143
|
normalized.numericSeverity = _ensureSeverity(message.numericSeverity)
|
|
@@ -164,7 +163,7 @@ const getSapMessages = (messages, req) => {
|
|
|
164
163
|
|
|
165
164
|
const isClientError = e => {
|
|
166
165
|
// e.code may be undefined, string, number, ... -> NaN -> not a client error
|
|
167
|
-
const numericCode = e.statusCode || Number(e.code)
|
|
166
|
+
const numericCode = e.statusCode || e.status || Number(e.code)
|
|
168
167
|
return numericCode >= 400 && numericCode < 500
|
|
169
168
|
}
|
|
170
169
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
let _i18n
|
|
2
|
+
// REVISIT does it really make sense to delay i18n require here?
|
|
3
|
+
const i18n = (...args) => {
|
|
4
|
+
if (!_i18n) _i18n = require('../i18n')
|
|
5
|
+
return _i18n(...args)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Gets the localized error message for an error based on message, code, statusCode or status
|
|
10
|
+
* @param {*} error
|
|
11
|
+
* @param {*} locale can be undefined for default language
|
|
12
|
+
* @returns localized error message
|
|
13
|
+
*/
|
|
14
|
+
function getErrorMessage(error, locale) {
|
|
15
|
+
return (
|
|
16
|
+
i18n(error.message || error.code || error.status || error.statusCode, locale, error.args) ||
|
|
17
|
+
error.message ||
|
|
18
|
+
`${error.code || error.status || error.statusCode}`
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
getErrorMessage
|
|
24
|
+
}
|
|
@@ -487,16 +487,16 @@ const _convertNotEqual = (container, partName = 'where') => {
|
|
|
487
487
|
const where = container[partName]
|
|
488
488
|
|
|
489
489
|
if (where) {
|
|
490
|
-
let
|
|
491
|
-
|
|
490
|
+
for (let index = 0; index < where.length; index++) {
|
|
491
|
+
const el = where[index]
|
|
492
492
|
if (el === '!=') {
|
|
493
493
|
const refIndex = _getRefIndex(where, index)
|
|
494
494
|
if (refIndex !== undefined) {
|
|
495
495
|
where[index - 1] = {
|
|
496
496
|
xpr: [where[index - 1], el, where[index + 1], 'or', where[refIndex], '=', { val: null }]
|
|
497
497
|
}
|
|
498
|
-
where
|
|
499
|
-
|
|
498
|
+
where.splice(index, 2)
|
|
499
|
+
--index
|
|
500
500
|
}
|
|
501
501
|
}
|
|
502
502
|
|
|
@@ -504,11 +504,6 @@ const _convertNotEqual = (container, partName = 'where') => {
|
|
|
504
504
|
if (el.SELECT) _convertNotEqual(el.SELECT, partName)
|
|
505
505
|
if (el.xpr) _convertNotEqual(el, 'xpr')
|
|
506
506
|
}
|
|
507
|
-
})
|
|
508
|
-
|
|
509
|
-
// delete undefined values
|
|
510
|
-
if (changed) {
|
|
511
|
-
container[partName] = where.filter(el => el)
|
|
512
507
|
}
|
|
513
508
|
}
|
|
514
509
|
|
|
@@ -37,7 +37,7 @@ function _connectHdb(creds, tenant) {
|
|
|
37
37
|
// tls keep alive
|
|
38
38
|
if (process.env.HDB_TCP_KEEP_ALIVE_IDLE) {
|
|
39
39
|
const num = Number(process.env.HDB_TCP_KEEP_ALIVE_IDLE)
|
|
40
|
-
creds.tcpKeepAliveIdle = Number.
|
|
40
|
+
creds.tcpKeepAliveIdle = Number.isNaN(num) ? false : num
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
const hdbClient = this.createClient(creds)
|
|
@@ -49,13 +49,10 @@ function _getBinaries(stmt) {
|
|
|
49
49
|
|
|
50
50
|
const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}={2})$/
|
|
51
51
|
|
|
52
|
-
function _isProcedureCall(sql) {
|
|
53
|
-
return sql.trim().match(/^call \s*"{0,1}\w*/i)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
52
|
function _getProcedureName(sql) {
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
// name delimited with "" allows any character
|
|
54
|
+
const match = sql.trim().match(/^call \s*(("(?<delimited>.+)")|(?<undelimited>\w+))\s*\(/i)
|
|
55
|
+
return match && (match.groups.undelimited || match.groups.delimited)
|
|
59
56
|
}
|
|
60
57
|
|
|
61
58
|
function _hdbGetResultForProcedure(rows, args, outParameters) {
|
|
@@ -83,14 +80,16 @@ function _hcGetResultForProcedure(stmt, resultSet, outParameters) {
|
|
|
83
80
|
}
|
|
84
81
|
}
|
|
85
82
|
// merge table output params into scalar params
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
const params = Array.isArray(outParameters) && outParameters.filter(md => !(md.PARAMETER_NAME in result))
|
|
84
|
+
if (params && params.length) {
|
|
88
85
|
let i = 0
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
86
|
+
do {
|
|
87
|
+
const parameterName = params[i++].PARAMETER_NAME
|
|
88
|
+
result[parameterName] = []
|
|
89
|
+
while (resultSet.next()) {
|
|
90
|
+
result[parameterName].push(resultSet.getValues())
|
|
91
|
+
}
|
|
92
|
+
} while (resultSet.nextResult())
|
|
94
93
|
}
|
|
95
94
|
return result
|
|
96
95
|
}
|
|
@@ -132,10 +131,9 @@ function _executeAsPreparedStatement(dbc, sql, values, reject, resolve) {
|
|
|
132
131
|
|
|
133
132
|
// procedure call metadata
|
|
134
133
|
let outParameters
|
|
135
|
-
const
|
|
136
|
-
if (
|
|
134
|
+
const procedureName = _getProcedureName(sql)
|
|
135
|
+
if (procedureName) {
|
|
137
136
|
try {
|
|
138
|
-
const procedureName = _getProcedureName(sql)
|
|
139
137
|
outParameters = await _getProcedureMetadata(procedureName, dbc)
|
|
140
138
|
} catch (e) {
|
|
141
139
|
LOG._warn && LOG.warn('Unable to fetch procedure metadata due to error:', e)
|
|
@@ -143,7 +141,7 @@ function _executeAsPreparedStatement(dbc, sql, values, reject, resolve) {
|
|
|
143
141
|
}
|
|
144
142
|
|
|
145
143
|
// on @sap/hana-client, we need to use execQuery in case of calling procedures
|
|
146
|
-
stmt[
|
|
144
|
+
stmt[procedureName && dbc.name !== 'hdb' ? 'execQuery' : 'exec'](values, function (err, rows, ...args) {
|
|
147
145
|
if (err) {
|
|
148
146
|
stmt.drop(() => {})
|
|
149
147
|
err.query = sql
|
|
@@ -152,7 +150,7 @@ function _executeAsPreparedStatement(dbc, sql, values, reject, resolve) {
|
|
|
152
150
|
}
|
|
153
151
|
|
|
154
152
|
let result
|
|
155
|
-
if (
|
|
153
|
+
if (procedureName) {
|
|
156
154
|
result =
|
|
157
155
|
dbc.name === 'hdb'
|
|
158
156
|
? _hdbGetResultForProcedure(rows, args, outParameters)
|
|
@@ -183,7 +181,7 @@ function _executeSimpleSQL(dbc, sql, values) {
|
|
|
183
181
|
values = Object.values(values)
|
|
184
182
|
}
|
|
185
183
|
// ensure that stored procedure with parameters is always executed as prepared
|
|
186
|
-
if (_hasValues(values) ||
|
|
184
|
+
if (_hasValues(values) || !!_getProcedureName(sql)) {
|
|
187
185
|
_executeAsPreparedStatement(dbc, sql, values, reject, resolve)
|
|
188
186
|
} else {
|
|
189
187
|
dbc.exec(sql, function (err, result) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const cds = require('../cds')
|
|
2
|
-
const LOG = cds.log('
|
|
2
|
+
const LOG = cds.log('pool|db')
|
|
3
3
|
|
|
4
4
|
const { pool } = require('@sap/cds-foss')
|
|
5
5
|
const hana = require('./driver')
|
|
@@ -225,6 +225,9 @@ async function resilientAcquire(pool, attempts = 1) {
|
|
|
225
225
|
err.message =
|
|
226
226
|
'Acquiring client from pool timed out. Please review your system setup, transaction handling, and pool configuration.'
|
|
227
227
|
err._attempts = attempt
|
|
228
|
+
const { borrowed, pending, size, available, max } = pool
|
|
229
|
+
err._poolState = { borrowed, pending, size, available, max }
|
|
230
|
+
LOG._debug && LOG.debug(err)
|
|
228
231
|
throw err
|
|
229
232
|
}
|
|
230
233
|
|
|
@@ -41,7 +41,7 @@ const _buildPartialUrlFunctions = (url, data, params, kind = 'odata-v4') => {
|
|
|
41
41
|
const funcParams = []
|
|
42
42
|
const queryOptions = []
|
|
43
43
|
// REVISIT: take params from params after importer fix (the keys should not be part of params)
|
|
44
|
-
for (const param in data) {
|
|
44
|
+
for (const param in _extractParamsFromData(data, params)) {
|
|
45
45
|
if (kind === 'odata-v2') {
|
|
46
46
|
funcParams.push(`${param}=${_setCorrectValue(param, data, params, kind)}`)
|
|
47
47
|
} else {
|
|
@@ -54,7 +54,7 @@ const _buildPartialUrlFunctions = (url, data, params, kind = 'odata-v4') => {
|
|
|
54
54
|
: `${url}(${funcParams.join(',')})?${queryOptions.join('&')}`
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
const _extractParamsFromData = (data, params) => {
|
|
57
|
+
const _extractParamsFromData = (data, params = {}) => {
|
|
58
58
|
return Object.keys(data).reduce((res, el) => {
|
|
59
59
|
if (params[el]) Object.assign(res, { [el]: data[el] })
|
|
60
60
|
return res
|
|
@@ -118,7 +118,7 @@ const _handleV2BoundActionFunction = (srv, def, req, event, kind) => {
|
|
|
118
118
|
const params = []
|
|
119
119
|
const data = req.data
|
|
120
120
|
// REVISIT: take params from def.params, after importer fix (the keys should not be part of params)
|
|
121
|
-
for (const param in req.data) {
|
|
121
|
+
for (const param in _extractParamsFromData(req.data, def.params)) {
|
|
122
122
|
params.push(`${param}=${formatVal(data[param], param, { elements: def.params }, kind)}`)
|
|
123
123
|
}
|
|
124
124
|
const keys = _buildKeys(req, this.kind)
|
package/libx/rest/RestAdapter.js
CHANGED
|
@@ -22,14 +22,14 @@ class RestAdapter extends express.Router {
|
|
|
22
22
|
|
|
23
23
|
alias2ref(srv)
|
|
24
24
|
|
|
25
|
-
this.use(express.json())
|
|
26
|
-
|
|
27
25
|
// pass srv-reated stuff to middlewares via req
|
|
28
26
|
this.use('/', (req, res, next) => {
|
|
29
27
|
req._srv = srv
|
|
30
28
|
next()
|
|
31
29
|
})
|
|
32
30
|
|
|
31
|
+
this.use(express.json())
|
|
32
|
+
|
|
33
33
|
// check @requires as soon as possible (DoS)
|
|
34
34
|
this.use('/', auth)
|
|
35
35
|
|
|
@@ -3,7 +3,11 @@ const UPDATE = { PUT: 1, PATCH: 1 }
|
|
|
3
3
|
|
|
4
4
|
module.exports = (req, res, next) => {
|
|
5
5
|
if (PPP[req.method]) {
|
|
6
|
-
|
|
6
|
+
const contentType = req.headers['content-type'] && req.headers['content-type'].split(';')
|
|
7
|
+
if (
|
|
8
|
+
contentType &&
|
|
9
|
+
(!contentType[0].match(/^application\/json$/) || (typeof contentType[1] === 'string' && !contentType[1]))
|
|
10
|
+
) {
|
|
7
11
|
throw { statusCode: 415, code: '415', message: 'INVALID_CONTENT_TYPE_ONLY_JSON' }
|
|
8
12
|
}
|
|
9
13
|
if (UPDATE[req.method] && Array.isArray(req.body)) {
|