@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 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(new Error('No or malformed credentials for auth kind "xsuaa"'), { credentials })
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
- let _i18n
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.length - cache.getReadPos() < this._stopPattern.length) {
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
- if (req.headers['content-type'] && req.headers['content-type'] !== 'application/json') {
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 (entity.elements[ele]['@PersonalData.IsPotentiallySensitive']) {
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 = i18n(err.message || err.code, locale, err.args) || err.message || `${err.code}`
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
- error.details = err.details.map(ele => _normalize(ele, locale, true))
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
- // REVISIT: code and message rewriting
77
- _rewrite(error)
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 = details => {
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
+ }
@@ -41,6 +41,7 @@ const _getRestrictedExpand = (columns, target, definitions) => {
41
41
  }
42
42
 
43
43
  function handler(req) {
44
+ if (!req.query) return
44
45
  const restricted = _getRestrictedExpand(
45
46
  req.query.SELECT && req.query.SELECT.columns,
46
47
  req.target,
@@ -487,16 +487,16 @@ const _convertNotEqual = (container, partName = 'where') => {
487
487
  const where = container[partName]
488
488
 
489
489
  if (where) {
490
- let changed
491
- where.forEach((el, index) => {
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[index] = where[index + 1] = undefined
499
- changed = true
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
 
@@ -3,6 +3,7 @@ module.exports = name => {
3
3
  try {
4
4
  return require(name)
5
5
  } catch (e) {
6
- throw new Error(`Unable to require required package/file "${name}"`)
6
+ e.message = `Cannot find module '${name}'. Make sure to install it with 'npm i ${name}'\n` + e.message
7
+ throw e
7
8
  }
8
9
  }
@@ -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.NaN(num) ? false : num
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
- const match = sql.trim().match(/^call \s*"{0,1}(\w*)/i)
58
- return match && match[1]
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
- if (outParameters && outParameters.length) {
87
- const params = outParameters.filter(md => !(md.PARAMETER_NAME in result))
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
- while (resultSet.next()) {
90
- result[params[i].PARAMETER_NAME] = [resultSet.getValues()]
91
- resultSet.nextResult()
92
- i++
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 isProcedureCall = _isProcedureCall(sql)
136
- if (isProcedureCall) {
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[isProcedureCall && dbc.name !== 'hdb' ? 'execQuery' : 'exec'](values, function (err, rows, ...args) {
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 (isProcedureCall) {
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) || _isProcedureCall(sql)) {
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('hana|db|sql')
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)
@@ -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
- if (req.headers['content-type'] && req.headers['content-type'] !== 'application/json') {
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)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "5.9.3",
3
+ "version": "5.9.6",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [