@sap/cds 5.9.5 → 5.9.8
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 +28 -0
- package/bin/build/provider/mtx/index.js +8 -13
- package/bin/version.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +2 -2
- 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/composition/delete.js +9 -12
- package/libx/_runtime/common/error/frontend.js +23 -24
- package/libx/_runtime/common/error/utils.js +24 -0
- package/libx/_runtime/common/generic/sorting.js +4 -1
- package/libx/_runtime/hana/execute.js +17 -19
- package/libx/_runtime/hana/pool.js +4 -1
- 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,6 +4,34 @@
|
|
|
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
|
+
## Version 5.9.8 - 2022-06-24
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Application model is now again properly updated after extension activation
|
|
12
|
+
- Avoid crashes during `cds version` when `folders.db` or `folders.srv` are array-valued instead of strings
|
|
13
|
+
- `cds build` correctly validates MTX extension allow lists and doesn't log false positive warning messages
|
|
14
|
+
|
|
15
|
+
## Version 5.9.7 - 2022-06-13
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Deleting a parent will delete all compositions, also texts
|
|
20
|
+
- Views with aliased elements in `orderBy`
|
|
21
|
+
|
|
22
|
+
## Version 5.9.6 - 2022-05-24
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Ignored requests in batch requests
|
|
27
|
+
- `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.
|
|
28
|
+
- Multiple errors did not have correct HTTP response status code
|
|
29
|
+
- `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
|
|
30
|
+
- Call hana procedure:
|
|
31
|
+
+ accepted are any symbols in a procedure name if it is delimited with a double quotation (`"`)
|
|
32
|
+
+ 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
|
|
33
|
+
- `@odata.context` considers `cds.env.odata.contextAbsoluteUrl` when requesting an OData Service
|
|
34
|
+
|
|
7
35
|
## Version 5.9.5 - 2022-05-09
|
|
8
36
|
|
|
9
37
|
### Fixed
|
|
@@ -127,26 +127,21 @@ class MtxModuleBuilder extends BuildTaskHandlerEdmx {
|
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
function isValid(e, pattern, nsPattern, kind) {
|
|
131
|
+
return e && e.name && (e.name === pattern || (nsPattern && e.name.startsWith(nsPattern))) && (!kind || e.kind === kind)
|
|
132
|
+
}
|
|
133
|
+
|
|
130
134
|
if (extensionAllowlist || entityWhitelist || serviceWhitelist) {
|
|
131
135
|
const invalidEntries = new Set()
|
|
132
136
|
const reflected = this.cds.reflect(model)
|
|
133
|
-
const services = reflected.services
|
|
134
|
-
const entities = Object.values(reflected.entities)
|
|
135
137
|
|
|
136
138
|
if (Array.isArray(extensionAllowlist)) {
|
|
137
139
|
extensionAllowlist.forEach(allowListEntry => {
|
|
138
140
|
if (Array.isArray(allowListEntry.for)) {
|
|
139
141
|
allowListEntry.for.forEach(pattern => {
|
|
140
142
|
if (pattern !== '*') {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (!services.some(service => service.name === pattern || service.name.startsWith(nsPattern))) {
|
|
144
|
-
invalidEntries.add(pattern)
|
|
145
|
-
}
|
|
146
|
-
} else {
|
|
147
|
-
if (!entities.some(entity => entity.name === pattern || entity.name.startsWith(nsPattern))) {
|
|
148
|
-
invalidEntries.add(pattern)
|
|
149
|
-
}
|
|
143
|
+
if (!reflected.find(e => isValid(e, pattern, pattern + '.', allowListEntry.kind))) {
|
|
144
|
+
invalidEntries.add(pattern)
|
|
150
145
|
}
|
|
151
146
|
}
|
|
152
147
|
})
|
|
@@ -159,14 +154,14 @@ class MtxModuleBuilder extends BuildTaskHandlerEdmx {
|
|
|
159
154
|
// validate whitelist entries
|
|
160
155
|
if (Array.isArray(entityWhitelist)) {
|
|
161
156
|
entityWhitelist.forEach(name => {
|
|
162
|
-
if (!
|
|
157
|
+
if (!reflected.find(e => isValid(e, name, null, "entity"))) {
|
|
163
158
|
invalidEntries.add(name)
|
|
164
159
|
}
|
|
165
160
|
})
|
|
166
161
|
}
|
|
167
162
|
if (Array.isArray(serviceWhitelist)) {
|
|
168
163
|
serviceWhitelist.forEach(name => {
|
|
169
|
-
if (!
|
|
164
|
+
if (!reflected.find(e => isValid(e, name, null, "service"))) {
|
|
170
165
|
invalidEntries.add(name)
|
|
171
166
|
}
|
|
172
167
|
})
|
package/bin/version.js
CHANGED
|
@@ -159,7 +159,7 @@ function _findMTX() {
|
|
|
159
159
|
|
|
160
160
|
// mtx still not found via cds.env? Try looking in well-known subdirectories
|
|
161
161
|
const folders = cds.env.folders
|
|
162
|
-
? [cds.env.folders.db, cds.env.folders.srv].filter(d => d)
|
|
162
|
+
? [cds.env.folders.db, cds.env.folders.srv].flat().filter(d => d)
|
|
163
163
|
: []
|
|
164
164
|
let i = 0
|
|
165
165
|
while(res[cdsmtx] === undefined && i < folders.length) {
|
|
@@ -33,8 +33,8 @@ class Dispatcher {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
if (cds._mtxEnabled) {
|
|
36
|
-
cds.mtx.eventEmitter.on(cds.mtx.events.TENANT_UPDATED, async
|
|
37
|
-
this._extMap.delete(
|
|
36
|
+
cds.mtx.eventEmitter.on(cds.mtx.events.TENANT_UPDATED, async tenant => {
|
|
37
|
+
this._extMap.delete(getModelHash(tenant))
|
|
38
38
|
})
|
|
39
39
|
}
|
|
40
40
|
}
|
|
@@ -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
|
}
|
|
@@ -135,20 +135,17 @@ function _getStaticWhere(allBackLinks, entity1) {
|
|
|
135
135
|
}, [])
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
const _is2oneComposition = (element, model) => {
|
|
139
|
-
const csnElement = element.target && model.definitions[element.target].elements[element.name]
|
|
140
|
-
return csnElement && csnElement.is2one && csnElement._isCompositionEffective
|
|
141
|
-
}
|
|
142
|
-
|
|
143
138
|
const _addToCQNs = (cqns, subCQN, element, model, level) => {
|
|
139
|
+
// REVISIT:
|
|
140
|
+
// The compiler generates foreign-key constraints (except if !cds.env.features._db_foreign_key_constraints)
|
|
141
|
+
// and enables DELETE CASCADE. For these cases, the runtime doesn't need to delete compositions
|
|
142
|
+
// manually, it's done by the database itself.
|
|
143
|
+
// However, there are cases (unmanaged compositions), where this doesn't happen.
|
|
144
|
+
// As a first step, the runtime will delete _all_ compositions regardless of the database.
|
|
145
|
+
// In the future, the runtime can enable the deletion of only those compositions
|
|
146
|
+
// which wouldn't be deleted by the database.
|
|
144
147
|
cqns[level] = cqns[level] || []
|
|
145
|
-
|
|
146
|
-
// Thus only single 2one case (`$self`-managed composition) has DELETE CASCADE
|
|
147
|
-
// Here it's ignored to simplify i.e. handle all "2ones" in a same manner
|
|
148
|
-
// REVISIT: why is _db_foreign_key_constraints necessary?
|
|
149
|
-
if (!cds.env.features._db_foreign_key_constraints || _is2oneComposition(element, model)) {
|
|
150
|
-
cqns[level].push(subCQN)
|
|
151
|
-
}
|
|
148
|
+
cqns[level].push(subCQN)
|
|
152
149
|
}
|
|
153
150
|
|
|
154
151
|
// unofficial config!
|
|
@@ -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
|
+
}
|
|
@@ -27,7 +27,10 @@ const _getStaticOrders = req => {
|
|
|
27
27
|
|
|
28
28
|
if (entity.query && entity.query.SELECT && entity.query.SELECT.orderBy) {
|
|
29
29
|
const orderBy = entity.query.SELECT.orderBy
|
|
30
|
-
const ordersFromView = orderBy.map(keyName => ({
|
|
30
|
+
const ordersFromView = orderBy.map(keyName => ({
|
|
31
|
+
by: { '=': keyName.ref[keyName.ref.length - 1] },
|
|
32
|
+
desc: keyName.sort === 'desc'
|
|
33
|
+
}))
|
|
31
34
|
return [...ordersFromView, ...defaultOrders, ...ordersFromKeys]
|
|
32
35
|
}
|
|
33
36
|
|
|
@@ -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
|
|
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)) {
|