@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 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
- const nsPattern = pattern + '.'
142
- if (allowListEntry.kind === 'service') {
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 (!entities.some(entity => entity.name === name && entity.kind === 'entity')) {
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 (!services.some(service => service.name === name && service.kind === 'service')) {
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 hash => {
37
- this._extMap.delete(hash)
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
- 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
 
@@ -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.8",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [