@sap/cds 8.1.0 → 8.1.1

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,21 @@
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 8.1.1 - 2024-08-08
8
+
9
+ ### Fixed
10
+
11
+ - For `accept-language`, ignore additional options
12
+ - Global `describe`, `before`, `beforeAll`, `afterAll` hooks are now writable again. They were accidentally made read-only in 8.0.0.
13
+ - Expand to `DraftAdministrativeData` for active instances of draft-enabled entities over drafts
14
+ - Deduplication of columns for certain on conditions for the legacy database driver
15
+ - For legacy-sqlite/-hana: Add keys to expands with only non-key elements to ensure not returning null for expand.
16
+ - New parser was to restrictive regarding an empty line at the end of batch body.
17
+ - Error target for operations with complex parameters
18
+ - Remote services: JWT gets found in authorization header
19
+ - Search with invalid characters
20
+ - Invoke `srv.on('error')` for each failing batch subrequest
21
+
7
22
  ## Version 8.1.0 - 2024-07-26
8
23
 
9
24
  ### Added
package/lib/index.js CHANGED
@@ -137,8 +137,9 @@ extend (global) .with (class {
137
137
 
138
138
  // ensure cds.test is loaded for running tests w/ node --test
139
139
  'describe' in global || ['describe','before','beforeAll','afterAll'].forEach (p => {
140
- Object.defineProperty (global,p, { configurable:1, set(v){Object.defineProperty(global,p,{value:v})},
141
- get(){ return cds.test, global[p] }
140
+ Object.defineProperty (global,p, { configurable:1,
141
+ set(v){ Object.defineProperty (global,p,{ value:v, writable:true }) },
142
+ get(){ cds.test; return global[p] }
142
143
  })
143
144
  })
144
145
 
@@ -26,15 +26,16 @@ class Validation {
26
26
  const err = (this.errors ??= new ValidationErrors).add (code)
27
27
  if (this.options.path) path = [ this.options.path, ...path ] // e.g. used to prefic 'in/' for actions
28
28
  if (path) err.target = (!leaf ? path : path.concat(leaf)).reduce?.((p,n)=> (
29
- n?.row ? p + this.filter4(n) : //> some/entity(ID=1)...
29
+ n?.row ? p + this.filter4(n) : //> some/entity(ID=1)...
30
30
  typeof n === 'number' ? p + `[${n}]` : //> some/array[1]...
31
- p && n ? p+'/'+n : n //> some/element/...
31
+ p && n ? p+'/'+n : n //> some/element...
32
32
  ),'')
33
33
  if (val) err.args = [ val, ...args ]
34
34
  return err
35
35
  }
36
36
 
37
37
  filter4 ({ def, row, index }) {
38
+ if (this.target.kind in { 'action': 1, 'function': 1 }) return '' //> no filter for operations
38
39
  const entity = def._target || def, filter=[]
39
40
  for (let k in entity.keys) {
40
41
  let v = row[k]
@@ -132,6 +132,7 @@ class EventContext {
132
132
  if (h) super.headers = h
133
133
  }
134
134
  get headers() {
135
+ // REVISIT: isn't "this._.req?.headers" deprecated? shouldn't it be "this.http?.req?.headers"?
135
136
  let headers = this._.req?.headers
136
137
  if (!headers) { headers={}
137
138
  const outer = this._propagated.headers
package/lib/req/locale.js CHANGED
@@ -22,7 +22,7 @@ const from_req = req => req && (
22
22
 
23
23
  function req_locale (req) {
24
24
  const locale = from_req(req); if (!locale) return i18n.default_language
25
- const loc = locale.replace(/-/g,'_')
25
+ const loc = locale.replace(/-/g,'_').match(/^[^,; ]*/)[0]
26
26
  return INCLUDE_LIST[loc]
27
27
  || /^([a-z]+)/i.test(loc) && RegExp.$1.toLowerCase()
28
28
  || i18n.default_language
package/lib/srv/srv-tx.js CHANGED
@@ -96,6 +96,7 @@ class Transaction {
96
96
  * synchronous modification of passed error only
97
97
  * err is undefined if nested tx (cf. "root.before ('failed', ()=> this.rollback())")
98
98
  */
99
+ // FIXME: with noa, this.context === cds.context and not the individual cds.Request
99
100
  if (err) for (const each of this._handlers._error) each.handler.call(this, err, this.context)
100
101
 
101
102
  if (this.ready) { //> nothing to do if no transaction started at all
@@ -760,7 +760,16 @@ const _convertSelect = (query, model, _options) => {
760
760
  query.SELECT.search = query.SELECT.search[0].val
761
761
  .match(/("")|("(?:[^"]|\\")*(?:[^\\]|\\\\)")|(\S*)/g)
762
762
  .filter(el => el.length)
763
- .map(el => (el.match(/^["](.*)["]$/) ? JSON.parse(el) : el))
763
+ .map(el => {
764
+ if (el[0] === '"' && el.at(-1) === '"') {
765
+ try {
766
+ return JSON.parse(el)
767
+ } catch {
768
+ return el
769
+ }
770
+ }
771
+ return el
772
+ })
764
773
  .reduce((arr, val, i) => {
765
774
  if (i > 0) arr.push('and')
766
775
  arr.push({ val })
@@ -779,6 +779,20 @@ class JoinCQNFromExpanded {
779
779
  })
780
780
 
781
781
  const targetEntity = this._getEntityForTable(target)
782
+
783
+ // ignore structured keys for now
784
+ let targetKeys = entity_keys(targetEntity).filter(key => !targetEntity.keys[key]._isStructured)
785
+ // ignore groupBy for now
786
+ if (targetKeys.length > 0 && !readToOneCQN.groupBy) {
787
+ const notOnlyExpandInColumns = !givenColumns.some(col => col.expand)
788
+ if (notOnlyExpandInColumns) {
789
+ const missingKeys = targetKeys.filter(keyName => !givenColumns.some(col => keyName === col.ref?.[0]))
790
+ if (missingKeys.length) {
791
+ givenColumns.push(...missingKeys.map(keyName => ({ ref: [keyName] })))
792
+ }
793
+ }
794
+ }
795
+
782
796
  if (
783
797
  'IsActiveEntity' in targetEntity.elements &&
784
798
  this._isNotIncludedIn(givenColumns)('IsActiveEntity') &&
@@ -1365,11 +1379,28 @@ class JoinCQNFromExpanded {
1365
1379
  const columns = []
1366
1380
  const outerColumns = []
1367
1381
 
1382
+ const _sameRef = (col1, col2) => {
1383
+ if (!col1.ref || !col2.ref) return false // only handle refs
1384
+ if (col1.ref.length !== col2.ref.length) return false
1385
+ if (col1.as !== col2.as) return false
1386
+ for (let i = 0; i < col1.ref.length; i++) {
1387
+ if (col1.ref[i] !== col2.ref[i]) return false
1388
+ }
1389
+ return true
1390
+ }
1391
+
1368
1392
  for (const entry of on) {
1369
1393
  if (entry.xpr) {
1370
1394
  const { columns: cols, outerColumns: outerCols } = this._getFilterColumns(readToOneCQN, entry.xpr, parentAlias)
1371
- columns.push(...cols)
1372
- outerColumns.push(...outerCols)
1395
+
1396
+ // de-duplicate
1397
+ for (const col of cols) {
1398
+ if (!columns.some(c => _sameRef(c, col))) columns.push(col)
1399
+ }
1400
+ for (const col of outerCols) {
1401
+ if (!outerColumns.some(c => _sameRef(c, col))) outerColumns.push(col)
1402
+ }
1403
+
1373
1404
  continue
1374
1405
  }
1375
1406
 
@@ -1081,14 +1081,23 @@ function _cleansed(query, model) {
1081
1081
  }
1082
1082
  cds.infer(draftsQuery, model.definitions)
1083
1083
  // draftsQuery._target = draftsQuery._target?.drafts || draftsQuery._target
1084
- if (query.SELECT.columns && query._target.drafts)
1085
- draftsQuery.SELECT.columns = _cleanseCols(query.SELECT.columns, REDUCED_DRAFT_ELEMENTS, draft)
1084
+ if (query.SELECT.columns && query._target.drafts) {
1085
+ if (draftsQuery._target.isDraft)
1086
+ draftsQuery.SELECT.columns = _cleanseCols(query.SELECT.columns, REDUCED_DRAFT_ELEMENTS, draft)
1087
+ else draftsQuery.SELECT.columns = _cleanseCols(query.SELECT.columns, DRAFT_ELEMENTS, draft)
1088
+ }
1086
1089
 
1087
- if (query.SELECT.where && query._target.drafts)
1088
- draftsQuery.SELECT.where = _cleanseWhere(query.SELECT.where, {}, DRAFT_ELEMENTS_WITHOUT_HASACTIVE)
1090
+ if (query.SELECT.where && query._target.drafts) {
1091
+ if (draftsQuery._target.isDraft)
1092
+ draftsQuery.SELECT.where = _cleanseWhere(query.SELECT.where, {}, DRAFT_ELEMENTS_WITHOUT_HASACTIVE)
1093
+ else draftsQuery.SELECT.where = _cleanseWhere(query.SELECT.where, {}, DRAFT_ELEMENTS)
1094
+ }
1089
1095
 
1090
- if (query.SELECT.orderBy && query._target.drafts)
1091
- draftsQuery.SELECT.orderBy = _cleanseWhere(query.SELECT.orderBy, {}, REDUCED_DRAFT_ELEMENTS)
1096
+ if (query.SELECT.orderBy && query._target.drafts) {
1097
+ if (draftsQuery._target.isDraft)
1098
+ draftsQuery.SELECT.orderBy = _cleanseWhere(query.SELECT.orderBy, {}, REDUCED_DRAFT_ELEMENTS)
1099
+ else draftsQuery.SELECT.orderBy = _cleanseWhere(query.SELECT.orderBy, {}, DRAFT_ELEMENTS)
1100
+ }
1092
1101
 
1093
1102
  if (draftsQuery._target.name.endsWith('.DraftAdministrativeData')) {
1094
1103
  draftsQuery.SELECT.columns = _tweakAdminCols(draftsQuery.SELECT.columns)
@@ -261,7 +261,9 @@ class RemoteService extends cds.Service {
261
261
  const returnType = req._returnType
262
262
  const additionalOptions = { destination, kind, resolvedTarget, returnType, destinationOptions }
263
263
 
264
- const jwt = req?.context?.headers?.authorization?.split(/^bearer /i)[1]
264
+ // REVISIT: i don't believe req.context.headers is an official API
265
+ let jwt = req?.context?.headers?.authorization?.split(/^bearer /i)[1]
266
+ if (!jwt) jwt = req?.context?.http?.req?.headers?.authorization?.split(/^bearer /i)[1]
265
267
  if (jwt) additionalOptions.jwt = jwt
266
268
 
267
269
  // hidden compat flag in order to suppress logging response body of failed request
@@ -85,6 +85,11 @@ module.exports = (adapter, isUpsert) => {
85
85
  .catch(err => {
86
86
  handleSapMessages(cdsReq, req, res)
87
87
 
88
+ // REVISIT: invoke service.on('error') for failed batch subrequests
89
+ if (cdsReq.http.req.path.startsWith('/$batch') && service._handlers._error.length) {
90
+ for (const each of service._handlers._error) each.handler.call(service, err, cdsReq)
91
+ }
92
+
88
93
  next(err)
89
94
  })
90
95
  }
@@ -62,6 +62,11 @@ module.exports = adapter => {
62
62
  .catch(err => {
63
63
  handleSapMessages(cdsReq, req, res)
64
64
 
65
+ // REVISIT: invoke service.on('error') for failed batch subrequests
66
+ if (cdsReq.http.req.path.startsWith('/$batch') && service._handlers._error.length) {
67
+ for (const each of service._handlers._error) each.handler.call(service, err, cdsReq)
68
+ }
69
+
65
70
  next(err)
66
71
  })
67
72
  }
@@ -7,6 +7,7 @@ const { normalizeError, unwrapMultipleErrors } = require('../../_runtime/common/
7
7
  module.exports = () => {
8
8
  return function odata_error(err, req, res, next) {
9
9
  if (err == 401 || err.code == 401) return next(err) // speed up logins, at least temporary until we reviewed and eliminated overhead that may be involved below
10
+
10
11
  // REVISIT: keep?
11
12
  // log the error (4xx -> warn)
12
13
  _log(err)
@@ -134,6 +134,12 @@ module.exports = adapter => {
134
134
  })
135
135
  .catch(err => {
136
136
  handleSapMessages(cdsReq, req, res)
137
+
138
+ // REVISIT: invoke service.on('error') for failed batch subrequests
139
+ if (cdsReq.http.req.path.startsWith('/$batch') && service._handlers._error.length) {
140
+ for (const each of service._handlers._error) each.handler.call(service, err, cdsReq)
141
+ }
142
+
137
143
  next(err)
138
144
  })
139
145
  }
@@ -164,6 +164,11 @@ const _handleArrayOfQueriesFactory = adapter => {
164
164
  .catch(err => {
165
165
  handleSapMessages(cdsReq, req, res)
166
166
 
167
+ // REVISIT: invoke service.on('error') for failed batch subrequests
168
+ if (cdsReq.http.req.path.startsWith('/$batch') && service._handlers._error.length) {
169
+ for (const each of service._handlers._error) each.handler.call(service, err, cdsReq)
170
+ }
171
+
167
172
  next(err)
168
173
  })
169
174
  }
@@ -264,9 +269,13 @@ module.exports = adapter => {
264
269
  res.send(result)
265
270
  })
266
271
  .catch(err => {
267
- // REVISIT: move error middleware -> applies to all these anti patterns
268
272
  handleSapMessages(cdsReq, req, res)
269
273
 
274
+ // REVISIT: invoke service.on('error') for failed batch subrequests
275
+ if (cdsReq.http.req.path.startsWith('/$batch') && service._handlers._error.length) {
276
+ for (const each of service._handlers._error) each.handler.call(service, err, cdsReq)
277
+ }
278
+
270
279
  next(err)
271
280
  })
272
281
  }
@@ -127,13 +127,13 @@ module.exports = adapter => {
127
127
  result = getODataResult(result, metadata, { property: _propertyAccess })
128
128
  res.send(result)
129
129
  })
130
- .catch(e => {
130
+ .catch(err => {
131
131
  handleSapMessages(cdsReq, req, res)
132
132
 
133
133
  // if UPSERT is allowed, redirect to POST
134
- const is404 = e.code === 404 || e.status === 404 || e.statusCode === 404
134
+ const is404 = err.code === 404 || err.status === 404 || err.statusCode === 404
135
135
  const isForcedInsert =
136
- (e.code === 412 || e.status === 412 || e.statusCode === 412) &&
136
+ (err.code === 412 || err.status === 412 || err.statusCode === 412) &&
137
137
  extractIfNoneMatch(req.headers?.['if-none-match']) === '*'
138
138
  if (!_propertyAccess && (is404 || isForcedInsert) && _isUpsertAllowed({ target, data, event: req.method })) {
139
139
  // PUT / PATCH with if-match header means "only if already exists" -> no insert if it does not
@@ -147,8 +147,13 @@ module.exports = adapter => {
147
147
  return next()
148
148
  }
149
149
 
150
+ // REVISIT: invoke service.on('error') for failed batch subrequests
151
+ if (cdsReq.http.req.path.startsWith('/$batch') && service._handlers._error.length) {
152
+ for (const each of service._handlers._error) each.handler.call(service, err, cdsReq)
153
+ }
154
+
150
155
  // continue with caught error
151
- next(e)
156
+ next(err)
152
157
  })
153
158
  }
154
159
  }
@@ -105,7 +105,7 @@ const parseStream = async function* (body, boundary) {
105
105
  chunk = `${leftover}${chunk}`
106
106
  const lastBoundary = chunk.lastIndexOf('--')
107
107
  const lastCRLF = chunk.lastIndexOf(CRLF)
108
- if (lastBoundary > lastCRLF) {
108
+ if (lastBoundary > lastCRLF && lastBoundary + 2 < chunk.length) {
109
109
  leftover = chunk.slice(lastBoundary)
110
110
  chunk = chunk.slice(0, lastBoundary)
111
111
  } else {
@@ -54,6 +54,7 @@ module.exports = () => {
54
54
 
55
55
  return function rest_error(err, req, res, next) {
56
56
  if (err == 401 || err.code == 401) return next(err) // speed up logins, at least temporary until we reviewed and eliminated overhead that may be involved below
57
+
57
58
  // REVISIT: keep?
58
59
  // log the error (4xx -> warn)
59
60
  _log(err)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "8.1.0",
3
+ "version": "8.1.1",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [