@sap/cds 7.9.1 → 7.9.3

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,12 +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
+
8
+ ## Version 7.9.3 - 2024-06-27
9
+
10
+ ### Fixed
11
+
12
+ - `cds compile --to serviceinfo` returns the correct URL path for Java applications.
13
+ - Prevent HANA deadlocks when processing outbox table
14
+ - Invalid cache for `SiblingEntity` requests
15
+ - `cds.test` recommends version 7 of `chai-as-promised`. Version 8 is ESM-only and does not work with `cds.test` at the moment.
16
+ - Loading of `cds.plugins` now respects the (internal!) property `cds.env.plugins` again.
17
+ - `req.data` and `req.INSERT.entries` were not pointing to same object if it contains more than one entry.
18
+
19
+ ## Version 7.9.2 - 2024-05-22
20
+
21
+ ### Fixed
22
+
23
+ - Server crash in case of certain errors in Cloud SDK
24
+ - Bug in restriction of entities modeled as composition of aspects
25
+ - `$search`: resolve an exception accessing `req.query.elements`
26
+ - Ignore flattened associations in projection on remote entities
27
+ - Falsy keys in `cds.ql` were ignored in usage like `SELECT.from(Books, 0)`
28
+
7
29
  ## Version 7.9.1 - 2024-05-13
8
30
 
9
31
  ### Fixed
10
32
 
11
33
  - `cds.compile.to.sql` doesn't fail for older compiler versions if `postgres` keywords aren't defined
12
- - `cds compile --to serviceinfo` no longer detects a Java project if there is a poml.xml file in a subfolder of `app/`
34
+ - `cds compile --to serviceinfo` no longer detects a Java project if there is a pom.xml file in a subfolder of `app/`
13
35
  - `acquireTimeoutMillis` is ensured if custom pool config is provided
14
36
 
15
37
  ## 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]) {
@@ -142,6 +142,7 @@ const _databases = {
142
142
  },
143
143
 
144
144
  "hana": {
145
+ '[legacy-hana]': { impl: `${_runtime}/hana/Service.js` },
145
146
  '[better-hana]': { impl: '@cap-js/hana' },
146
147
  impl: `${_runtime}/hana/Service.js`,
147
148
  },
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 !
package/lib/ql/DELETE.js CHANGED
@@ -10,7 +10,7 @@ module.exports = class Query extends Whereable {
10
10
 
11
11
  from(entity, key) {
12
12
  this.DELETE.from = this._target4 (...arguments) // supporting tts
13
- if (key) this.byKey(key)
13
+ if (key !== undefined) this.byKey(key)
14
14
  return this
15
15
  }
16
16
 
package/lib/ql/SELECT.js CHANGED
@@ -76,7 +76,7 @@ module.exports = class Query extends Whereable {
76
76
 
77
77
  from (target, second, third) {
78
78
  this.SELECT.from = target === '*' || this._target_ref4 (...arguments)
79
- if (!target.raw && second) {
79
+ if (!target.raw && second !== undefined) {
80
80
  if (third) {
81
81
  this.byKey(second)
82
82
  this.columns(third)
package/lib/ql/UPDATE.js CHANGED
@@ -11,7 +11,7 @@ module.exports = class Query extends Whereable {
11
11
 
12
12
  entity (e, key) {
13
13
  this.UPDATE.entity = this._target4 (...arguments) // supporting tts
14
- if (key) this.byKey(key)
14
+ if (key !== undefined) this.byKey(key)
15
15
  return this
16
16
  }
17
17
 
@@ -41,7 +41,7 @@ class Query extends require('./Query') {
41
41
  }
42
42
 
43
43
  byKey(key) {
44
- if (typeof key !== 'object') key = { [Object.keys(this._target.keys||{ID:1})[0]]: key }
44
+ if (typeof key !== 'object' || key === null) key = { [Object.keys(this._target.keys||{ID:1})[0]]: key }
45
45
  if (this.SELECT) this.SELECT.one = true
46
46
  if (cds.env.features.keys_into_where) return this.where(key)
47
47
  if (this.UPDATE) { this.UPDATE.entity = { ref: [{ id: cds.env.ql.quirks_mode ? this.UPDATE.entity : this.UPDATE.entity.ref.at(-1), where: predicate4([key]) }] }; return this }
@@ -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
  }
@@ -9,6 +9,54 @@ const restrictHandler = require('./restrict')
9
9
  const restrictExpandHandler = require('./expand')
10
10
 
11
11
  module.exports = cds.service.impl(function authorization() {
12
+ // REVISIT: general approach to dependent auth:
13
+ // add restrictions to auth-dependent entities as if modeled to allow static access during request processing
14
+ // // TODO: where to do?
15
+ // // add restrictions to auth-dependent entities
16
+ // const defs = this.model.definitions
17
+ // const deps = []
18
+ // for (const each of this.entities) {
19
+ // for (const k in each.compositions) {
20
+ // const c = each.compositions[k]
21
+ // const ct = defs[c.target]
22
+ // if (defs[ct?.elements.up_?.target] === each && !ct['@requires'] && !ct['@restrict']) {
23
+ // deps.push(c.target)
24
+ // }
25
+ // }
26
+ // }
27
+ // for (const each of deps) {
28
+ // const e = defs[each]
29
+ // let rstr
30
+ // let cur = defs[e.elements.up_.target]
31
+ // while (cur && !rstr) {
32
+ // rstr = cur['@requires'] || cur['@restrict']
33
+ // cur = defs[cur.elements.up_?.target]
34
+ // }
35
+ // if (rstr) {
36
+ // // TODO: normalize restriction to @restrict syntax
37
+ // // TODO: add rewrite paths in instance-based auth
38
+ // e['@restrict'] = rstr
39
+ // }
40
+ // }
41
+
42
+ // mark entities that depend on ancestor for auth with that ancestor
43
+ const defs = this.model.definitions
44
+ for (const each of this.entities) {
45
+ for (const k in each.compositions) {
46
+ const c = each.compositions[k]
47
+ const ct = defs[c.target]
48
+ if (defs[ct?.elements.up_?.target] === each && !ct['@requires'] && !ct['@restrict']) {
49
+ let rstr
50
+ let cur = defs[ct.elements.up_.target]
51
+ while (!rstr && cur) {
52
+ if (cur['@requires'] || cur['@restrict']) rstr = cur
53
+ cur = defs[cur.elements.up_?.target]
54
+ }
55
+ if (rstr) Object.defineProperty(ct, '_auth_depends_on', { value: rstr })
56
+ }
57
+ }
58
+ }
59
+
12
60
  /*
13
61
  * @requires
14
62
  */
@@ -140,9 +140,10 @@ const resolveUserAttrs = (restrict, req) => {
140
140
  return restrict
141
141
  }
142
142
 
143
- const _authDependsOnParent = (entity, annotations) => {
143
+ const _authDependsOnAncestor = (entity, annotations) => {
144
144
  // @cds.autoexposed and not @cds.autoexpose -> not explicitly exposed by modeling
145
145
  return (
146
+ entity._auth_depends_on ||
146
147
  entity.name.match(/\.DraftAdministrativeData$/) ||
147
148
  (entity['@cds.autoexposed'] && !entity['@cds.autoexpose'] && !annotations.some(a => a in entity))
148
149
  )
@@ -159,7 +160,10 @@ const cqnFrom = req => {
159
160
 
160
161
  const getAuthRelevantEntity = (req, model, annotations) => {
161
162
  if (!req.target || !(req.event in CRUD_EVENTS)) return
162
- if (!_authDependsOnParent(req.target, annotations)) return req.target
163
+
164
+ const it = _authDependsOnAncestor(req.target, annotations)
165
+ if (!it) return req.target
166
+ if (it?.kind === 'entity' && req.subject.ref?.length === 1) return it
163
167
 
164
168
  let cqn = cqnFrom(req)
165
169
 
@@ -188,7 +192,7 @@ const getAuthRelevantEntity = (req, model, annotations) => {
188
192
  let authRelevantEntity
189
193
  for (let i = segments.length - 1; i >= 0; i--) {
190
194
  const segment = segments[i]
191
- if (segment.kind === 'entity' && !_authDependsOnParent(segment, annotations)) {
195
+ if (segment.kind === 'entity' && !_authDependsOnAncestor(segment, annotations)) {
192
196
  authRelevantEntity = segment
193
197
  break
194
198
  }
@@ -33,6 +33,9 @@ const _inverseTransition = transition => {
33
33
 
34
34
  const ref0 = value.ref[0]
35
35
  if (value.ref.length > 1) {
36
+ // ignore flattened columns like author.name
37
+ if (transition.target.elements[ref0].isAssociation) continue
38
+
36
39
  const nested = inverseTransition.mapping.get(ref0) || {}
37
40
  if (!nested.transition) nested.transition = { mapping: new Map() }
38
41
  let current = nested.transition.mapping
@@ -2,20 +2,44 @@ 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) {
15
34
  if (typeof req.query === 'string') return
16
35
 
17
- // invoke req.subject before it gets modified
36
+ // invoke req.subject and req.query.elements before it gets modified
18
37
  req.subject
38
+ try {
39
+ req.query.elements
40
+ } catch {
41
+ // ignore potential errors (no x4 support in req.query.elements)
42
+ }
19
43
 
20
44
  if (!this.model) {
21
45
  // best-effort rewrite of path in from
@@ -25,14 +49,17 @@ function handler(req) {
25
49
 
26
50
  const _streaming = cds.env.features.stream_compat && req.query._streaming
27
51
 
52
+ // for restore link to req.data
53
+ const linked = _isLinked(req)
54
+
28
55
  // convert to sql cqn
29
56
  req.query = cqn2cqn4sql(req.query, this.model, { service: this })
30
57
 
58
+ // REVISIT: should not be necessary
31
59
  // restore link to req.data
32
- _restoreLink(req)
60
+ if (linked) _restoreLink(req)
33
61
 
34
62
  if (_streaming) req.query._streaming = _streaming
35
-
36
63
  generateAliases(req.query)
37
64
  }
38
65
 
@@ -132,7 +132,7 @@ const _addAliasToElement = (expr, alias) => {
132
132
  return { ...expr, args }
133
133
  }
134
134
 
135
- if (expr.SELECT && expr.SELECT.where) {
135
+ if (expr?.SELECT?.where) {
136
136
  // special case in lambda functions
137
137
  _addParentAlias(expr.SELECT.where, alias)
138
138
  }
@@ -205,8 +205,10 @@ const _getSanitizedError = (e, reqOptions, options = { suppressRemoteResponseBod
205
205
  }
206
206
 
207
207
  // AxiosError's toJSON() method doesn't include the request and response objects
208
- e.toJSON = function () {
209
- return { ...this.__proto__.toJSON(), request: this.request, response: this.response }
208
+ if (e.__proto__.toJSON) {
209
+ e.toJSON = function () {
210
+ return { ...this.__proto__.toJSON(), request: this.request, response: this.response }
211
+ }
210
212
  }
211
213
 
212
214
  return e
@@ -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.1",
3
+ "version": "7.9.3",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [
@@ -37,10 +37,5 @@
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
  }