@sap/cds 7.9.2 → 7.9.4

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,28 @@
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 7.9.4 - 2024-07-18
9
+
10
+ ### Fixed
11
+
12
+ - View resolving for `cds.features.lean_draft`
13
+ - Error in `enterprise-messaging` deploy script
14
+ - Properly forward path expression infront of lamda functions for `odata-v2` remote services
15
+ - OData queries selecting the same column with `$count=true`
16
+ - Closed higher end of version range for dependency on `cds-types`
17
+
18
+ ## Version 7.9.3 - 2024-06-27
19
+
20
+ ### Fixed
21
+
22
+ - `cds compile --to serviceinfo` returns the correct URL path for Java applications.
23
+ - Prevent HANA deadlocks when processing outbox table
24
+ - Invalid cache for `SiblingEntity` requests
25
+ - `cds.test` recommends version 7 of `chai-as-promised`. Version 8 is ESM-only and does not work with `cds.test` at the moment.
26
+ - Loading of `cds.plugins` now respects the (internal!) property `cds.env.plugins` again.
27
+ - `req.data` and `req.INSERT.entries` were not pointing to same object if it contains more than one entry.
28
+
7
29
  ## Version 7.9.2 - 2024-05-22
8
30
 
9
31
  ### Fixed
@@ -19,7 +41,7 @@
19
41
  ### Fixed
20
42
 
21
43
  - `cds.compile.to.sql` doesn't fail for older compiler versions if `postgres` keywords aren't defined
22
- - `cds compile --to serviceinfo` no longer detects a Java project if there is a poml.xml file in a subfolder of `app/`
44
+ - `cds compile --to serviceinfo` no longer detects a Java project if there is a pom.xml file in a subfolder of `app/`
23
45
  - `acquireTimeoutMillis` is ensured if custom pool config is provided
24
46
 
25
47
  ## Version 7.9.0 - 2024-04-30
@@ -3,7 +3,14 @@ const LOG = cds.log('auth')
3
3
 
4
4
  // _require for better error message
5
5
  const _require = require('../../libx/_runtime/common/utils/require')
6
- const xssec = _require('@sap/xssec')
6
+
7
+ let xssec = _require('@sap/xssec')
8
+ let xssec4 = false
9
+ // use v3 compat api
10
+ if (xssec.v3) {
11
+ xssec = xssec.v3
12
+ xssec4 = true
13
+ }
7
14
 
8
15
  // getter function extracted to show deprecation warning only once
9
16
  const _getTokenInfo = tokenInfo => tokenInfo
@@ -46,6 +53,13 @@ module.exports = function ias_auth(config) {
46
53
 
47
54
  return (req, _, next) => {
48
55
  const token = req.headers.authorization?.split(/^bearer /i)[1]
56
+
57
+ if (!token && xssec4) {
58
+ LOG._debug && LOG.debug('No authorization header provided, continuing with default user.')
59
+ req.user = new cds.User.default()
60
+ return next()
61
+ }
62
+
49
63
  xssec.createSecurityContext(token, credentials, 'IAS', function (err, securityContext, tokenInfo) {
50
64
  // REVISIT: ias impl not as sophisticated as xsuaa impl, so we need to be more tolerant here -> xssec issue 221
51
65
  // if (err && !tokenInfo) {
@@ -3,7 +3,14 @@ const LOG = cds.log('auth')
3
3
 
4
4
  // _require for better error message
5
5
  const _require = require('../../libx/_runtime/common/utils/require')
6
- const xssec = _require('@sap/xssec')
6
+
7
+ let xssec = _require('@sap/xssec')
8
+ let xssec4 = false
9
+ // use v3 compat api
10
+ if (xssec.v3) {
11
+ xssec = xssec.v3
12
+ xssec4 = true
13
+ }
7
14
 
8
15
  // getter function extracted to show deprecation warning only once
9
16
  const _getTokenInfo = tokenInfo => tokenInfo
@@ -13,7 +20,7 @@ module.exports = function jwt_auth(config) {
13
20
 
14
21
  if (!credentials) {
15
22
  let msg = `Authentication kind "${kind}" configured, but no XSUAA instance bound to application.`
16
- msg += ' Either bind an IAS instance, or switch to an authentication kind that does not require a binding.'
23
+ msg += ' Either bind an XSUAA instance, or switch to an authentication kind that does not require a binding.'
17
24
  throw new Error(msg)
18
25
  }
19
26
 
@@ -48,6 +55,13 @@ module.exports = function jwt_auth(config) {
48
55
 
49
56
  return (req, _, next) => {
50
57
  const token = req.headers.authorization?.split(/^bearer /i)[1]
58
+
59
+ if (!token && xssec4) {
60
+ LOG._debug && LOG.debug('No authorization header provided, continuing with default user.')
61
+ req.user = new cds.User.default()
62
+ return next()
63
+ }
64
+
51
65
  xssec.createSecurityContext(token, credentials, function (err, securityContext, tokenInfo) {
52
66
  if (err && !tokenInfo) {
53
67
  // here, there is a general problem, .e.g., bad credentials -> throw the error
@@ -90,7 +90,7 @@ module.exports = (model, options={}) => {
90
90
  const javaPrefixDefault = 'odata/v4/'
91
91
  const roots = [ cds.env.folders.db, cds.env.folders.srv ].map(d => join(root, d))
92
92
  for (let r of roots) {
93
- const file = isfile (join (r,'../src/main/resources/application.yaml'))
93
+ const file = isfile (join (r,'./src/main/resources/application.yaml'))
94
94
  if (file) {
95
95
  const yaml = cds.load.yaml(file)
96
96
  for (let yamlDoc of Array.isArray(yaml) ? yaml : [yaml]) {
package/lib/plugins.js CHANGED
@@ -30,7 +30,7 @@ exports.fetch = function (DEV = process.env.NODE_ENV !== 'production') {
30
30
  exports.activate = async function () {
31
31
  const DEBUG = cds.debug ('plugins', {label:'cds'})
32
32
  DEBUG && console.time ('[cds] - loaded plugins in')
33
- const plugins = exports.fetch(), { local } = cds.utils
33
+ const { plugins } = cds.env, { local } = cds.utils
34
34
  await Promise.all (Object.entries(plugins) .map (async ([ plugin, conf ]) => {
35
35
  DEBUG?.(`loading plugin ${plugin}:`, { impl: local(conf.impl) })
36
36
  // TODO: support ESM plugins. But see cap/cds/pull/1838#issuecomment-1177200 !
@@ -170,7 +170,7 @@ class Test extends require('./axios') {
170
170
  function require (mod) { try { return module.require(mod) } catch(e) {
171
171
  if (e.code === 'MODULE_NOT_FOUND') throw new Error (`
172
172
  Failed to load required package '${mod}'. Please add it thru:
173
- npm add -D chai@4 chai-as-promised chai-subset
173
+ npm add -D chai@4 chai-as-promised@7 chai-subset
174
174
  `)}}
175
175
  }
176
176
  get assert() { return this.chai.assert }
@@ -194,7 +194,7 @@ class AbstractEdmStructuredType extends EdmType {
194
194
  if (!this._navigationProperties) {
195
195
  this._navigationProperties = new Map(this.getBaseType() ? this.getBaseType().getNavigationProperties() : [])
196
196
  for (const [name, property] of this.getOwnNavigationProperties()) {
197
- if (ignoreSibling && name === 'SiblingEntity') continue;
197
+ if (!cds.env.fiori.lean_draft && ignoreSibling && name === 'SiblingEntity') continue; // This will cause subtle caching bugs but will enable expand=* in old draft
198
198
  this._navigationProperties.set(name, property)
199
199
  }
200
200
  }
@@ -4,7 +4,7 @@ const { getCompositionTree } = require('./tree')
4
4
  const ctUtils = require('./utils')
5
5
 
6
6
  const { ensureNoDraftsSuffix } = require('../utils/draft')
7
- const { deepCopyArray } = require('../utils/copy')
7
+ const { deepCopy } = require('../utils/copy')
8
8
 
9
9
  /*
10
10
  * own utils
@@ -82,8 +82,8 @@ const hasDeepInsert = (model, cqn) => {
82
82
  const getDeepInsertCQNs = (model, cqn) => {
83
83
  const into = (cqn.INSERT.into.ref && cqn.INSERT.into.ref[0]) || cqn.INSERT.into.name || cqn.INSERT.into
84
84
  const entityName = ensureNoDraftsSuffix(into)
85
- const draft = entityName !== into
86
- const dataEntries = cqn.INSERT.entries ? deepCopyArray(cqn.INSERT.entries) : []
85
+ const draft = entityName !== into || into.endsWith('.drafts') //lean draft
86
+ const dataEntries = cqn.INSERT.entries ? deepCopy(cqn.INSERT.entries) : []
87
87
  const entity = model.definitions[entityName]
88
88
  const compositionTree = getCompositionTree({
89
89
  definitions: model.definitions,
@@ -2,13 +2,32 @@ const cds = require('../../cds')
2
2
  const { cqn2cqn4sql } = require('../../common/utils/cqn2cqn4sql')
3
3
  const { generateAliases } = require('../utils/generateAliases')
4
4
 
5
+ /**
6
+ * Restores the link of `req.data` and `req.query` in case `req.query` was overwritten.
7
+ * Only applicable for `UPDATE`s and `INSERT`s.
8
+ *
9
+ * @param { import('@sap/cds').Request } req
10
+ */
5
11
  const _restoreLink = req => {
6
12
  if (req.query.INSERT?.entries) {
7
- return (req.data = Array.isArray(req.query.INSERT.entries) ? req.query.INSERT.entries[0] : req.query.INSERT.entries)
13
+ if (Array.isArray(req.query.INSERT.entries)) req.data = req.query.INSERT.entries[0]
14
+ else req.data = req.query.INSERT.entries
15
+ } else if (req.query.UPDATE?.data) {
16
+ req.data = req.query.UPDATE.data
8
17
  }
18
+ }
19
+
20
+ const _isLinked = req => {
21
+ if (req.query.INSERT?.entries) {
22
+ if (Array.isArray(req.query.INSERT.entries)) return req.data === req.query.INSERT.entries[0]
23
+ return req.data === req.query.INSERT.entries
24
+ }
25
+
9
26
  if (req.query.UPDATE?.data) {
10
- return (req.data = req.query.UPDATE.data)
27
+ return req.data === req.query.UPDATE.data
11
28
  }
29
+
30
+ return false
12
31
  }
13
32
 
14
33
  function handler(req) {
@@ -30,14 +49,17 @@ function handler(req) {
30
49
 
31
50
  const _streaming = cds.env.features.stream_compat && req.query._streaming
32
51
 
52
+ // for restore link to req.data
53
+ const linked = _isLinked(req)
54
+
33
55
  // convert to sql cqn
34
56
  req.query = cqn2cqn4sql(req.query, this.model, { service: this })
35
57
 
58
+ // REVISIT: should not be necessary
36
59
  // restore link to req.data
37
- _restoreLink(req)
60
+ if (linked) _restoreLink(req)
38
61
 
39
62
  if (_streaming) req.query._streaming = _streaming
40
-
41
63
  generateAliases(req.query)
42
64
  }
43
65
 
@@ -60,17 +60,26 @@ const read = (executeSelectCQN, executeStreamCQN, convertStreams) => (model, dbc
60
60
 
61
61
  if (query.SELECT.count) {
62
62
  if (query.SELECT.limit) {
63
+ // IMPORTANT: do not change order!
64
+ // 1. create the count query synchronously, because it works on a copy
65
+ // 2. run the result query, duplicate names ( SELECT ID, ID ...) throw an error synchronously
66
+ // 3. run the count query
67
+ // reason is that executeSelectCQN modifies the query
63
68
  const countQuery = _createCountQuery(query)
64
- const countResultPromise = executeSelectCQN(model, dbc, countQuery, user, locale, isoTs)
65
- if (query.SELECT.limit?.rows?.val === 0) {
66
- // We don't need to perform our result query
67
- return countResultPromise.then(countResults => _arrayWithCount([], countValue(countResults)))
68
- }
69
+ const resultPromise =
70
+ query.SELECT.limit?.rows?.val === 0
71
+ ? Promise.resolve([])
72
+ : executeSelectCQN(model, dbc, query, user, locale, isoTs)
73
+ const countPromise = executeSelectCQN(model, dbc, countQuery, user, locale, isoTs)
69
74
 
70
- const resultPromise = executeSelectCQN(model, dbc, query, user, locale, isoTs)
71
- return Promise.all([countResultPromise, resultPromise]).then(([countResults, result]) =>
72
- _arrayWithCount(result, countValue(countResults))
73
- )
75
+ // use allSettled here, so all executions are awaited before we rollback
76
+ return Promise.allSettled([countPromise, resultPromise]).then(results => {
77
+ const rejection = results.find(p => p.status === 'rejected')
78
+ if (rejection) throw rejection.reason
79
+
80
+ const [{ value: countResult }, { value: result }] = results
81
+ return _arrayWithCount(result, countValue(countResult))
82
+ })
74
83
  }
75
84
 
76
85
  return executeSelectCQN(model, dbc, query, user, locale, isoTs).then(result =>
@@ -771,7 +771,7 @@ const Read = {
771
771
  else ownNewDrafts.push(draft)
772
772
  }
773
773
 
774
- const $count = ownDrafts.length + (isCount ? actives[0]?.$count : actives.$count ?? 0)
774
+ const $count = ownDrafts.length + (isCount ? actives[0]?.$count : (actives.$count ?? 0))
775
775
  if (isCount) return { $count }
776
776
 
777
777
  Read.merge(query._target, ownDrafts, [], row => {
@@ -195,10 +195,9 @@ class EventBroker extends cds.MessagingService {
195
195
  data: req.body ? req.body : undefined,
196
196
  headers: req.headers
197
197
  }
198
- cds.context = { user: cds.User.privileged }
199
- if (tenant) cds.context.tenant = tenant // TODO: In single tenant case, we don't need a tenant
200
- const tx = await this.tx()
201
- await tx.emit(msg)
198
+ const context = { user: cds.User.privileged }
199
+ if (tenant) context.tenant = tenant // TODO: In single tenant case, we don't need a tenant
200
+ await this.tx(context, tx => tx.emit(msg))
202
201
  this.LOG.debug('Event processed successfully.')
203
202
  return res.status(200).json({ message: 'OK' })
204
203
  } catch (e) {
@@ -104,7 +104,7 @@ const _count = result => {
104
104
  ? result.reduce((acc, val) => {
105
105
  return acc + (val?.$count ?? val?._counted_ ?? (Array.isArray(val) && _count(val))) || 0
106
106
  }, 0)
107
- : result.$count ?? result._counted_ ?? 0
107
+ : (result.$count ?? result._counted_ ?? 0)
108
108
  }
109
109
 
110
110
  // basically stolen from old read handler without understanding it ^^
@@ -177,13 +177,13 @@ function _xpr(expr, target, kind, isLambda) {
177
177
  if (inExpr) res.push(`(${inExpr})`)
178
178
  i += 1
179
179
  } else if (_isLambda(cur, expr[i + 1])) {
180
- const { where, id } = expr[i + 1].ref.slice(-1)[0]
181
- const nav = [...expr[i + 1].ref.slice(0, -1), id].join('/')
180
+ const { where } = expr[i + 1].ref.at(-1)
181
+ const nav = expr[i + 1].ref.map(ref => ref?.id ?? ref).join('/')
182
182
  // odata-v2 does not support lambda expressions but successfactors allows filter like for to-one assocs
183
183
  if (kind === 'odata-v2') {
184
184
  cds.log('remote').info(`OData V2 does not support lambda expressions. Using path expression as best effort.`)
185
185
  isLambda = false
186
- res.push(`${id}%2F${_xpr(where, target, kind)}`)
186
+ res.push(`${nav}%2F${_xpr(where, target, kind)}`)
187
187
  } else if (!where) res.push(`${nav}/any()`)
188
188
  else res.push(`${nav}/any(${LAMBDA_VARIABLE}:${_xpr(where, target, kind, true)})`)
189
189
  i++
@@ -61,7 +61,7 @@ const processMessages = async (service, tenant, _opts = {}) => {
61
61
  try {
62
62
  const messagesQuery = SELECT.from(messagesEntity)
63
63
  .where({ target: name })
64
- .orderBy('timestamp')
64
+ .orderBy(['timestamp', 'ID'])
65
65
  .limit(opts.chunkSize)
66
66
  .forUpdate()
67
67
  if (opts.maxAttempts) messagesQuery.where({ attempts: { '<': opts.maxAttempts } })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "7.9.2",
3
+ "version": "7.9.4",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [
@@ -33,14 +33,9 @@
33
33
  "node": ">=16"
34
34
  },
35
35
  "dependencies": {
36
- "@cap-js/cds-types": "<1",
36
+ "@cap-js/cds-types": "<=0.2.0",
37
37
  "@sap/cds-compiler": "^4",
38
38
  "@sap/cds-fiori": "^1",
39
39
  "@sap/cds-foss": "^5.0.0"
40
- },
41
- "cds": {
42
- "plugins": [
43
- "@sap/cds-fiori"
44
- ]
45
40
  }
46
41
  }
@@ -42,7 +42,7 @@ const main = async () => {
42
42
  error.code = 'DEPLOY_FAILED'
43
43
  error.target = { kind: 'DEPLOYMENT' }
44
44
  error.reason = e
45
- this.LOG.error(error)
45
+ LOG.error(error)
46
46
  throw error
47
47
  }
48
48
  }