@sap/cds 5.9.4 → 5.9.7

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,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.7 - 2022-06-13
8
+
9
+ ### Fixed
10
+
11
+ - Deleting a parent will delete all compositions, also texts
12
+ - Views with aliased elements in `orderBy`
13
+
14
+ ## Version 5.9.6 - 2022-05-24
15
+
16
+ ### Fixed
17
+
18
+ - Ignored requests in batch requests
19
+ - `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.
20
+ - Multiple errors did not have correct HTTP response status code
21
+ - `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
22
+ - Call hana procedure:
23
+ + accepted are any symbols in a procedure name if it is delimited with a double quotation (`"`)
24
+ + 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
25
+ - `@odata.context` considers `cds.env.odata.contextAbsoluteUrl` when requesting an OData Service
26
+
27
+ ## Version 5.9.5 - 2022-05-09
28
+
29
+ ### Fixed
30
+
31
+ - `HDB_TCP_KEEP_ALIVE_IDLE` config
32
+ - A combination of `!=` operator and `or` in `where` clauses of `@restrict` annotations or when adjusting `req.query` in custom handlers (OData services only)
33
+ - 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
34
+
7
35
  ## Version 5.9.4 - 2022-05-02
8
36
 
9
37
  ### Fixed
@@ -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
@@ -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
  }
@@ -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
- // Since `>2.5.2` compiler generates constraints for compositions of one like for annotations
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 = 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
+ }
@@ -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 => ({ by: { '=': keyName.ref[0] }, desc: keyName.sort === 'desc' }))
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
 
@@ -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
 
@@ -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.4",
3
+ "version": "5.9.7",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [