@sap/cds 5.9.5 → 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,6 +4,20 @@
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
+
7
21
  ## Version 5.9.5 - 2022-05-09
8
22
 
9
23
  ### Fixed
@@ -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
  }
@@ -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
+ }
@@ -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
 
@@ -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.5",
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": [