@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 +23 -1
- package/lib/auth/ias-auth.js +15 -1
- package/lib/auth/jwt-auth.js +16 -2
- package/lib/compile/to/srvinfo.js +1 -1
- package/lib/env/cds-requires.js +1 -0
- package/lib/plugins.js +1 -1
- package/lib/ql/DELETE.js +1 -1
- package/lib/ql/SELECT.js +1 -1
- package/lib/ql/UPDATE.js +1 -1
- package/lib/ql/Whereable.js +1 -1
- package/lib/utils/cds-test.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/edm/AbstractEdmStructuredType.js +1 -1
- package/libx/_runtime/common/generic/auth/index.js +48 -0
- package/libx/_runtime/common/generic/auth/utils.js +7 -3
- package/libx/_runtime/common/utils/resolveView.js +3 -0
- package/libx/_runtime/db/generic/rewrite.js +32 -5
- package/libx/_runtime/db/utils/generateAliases.js +1 -1
- package/libx/_runtime/remote/utils/client.js +4 -2
- package/libx/outbox/index.js +1 -1
- package/package.json +1 -6
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
|
|
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
|
package/lib/auth/ias-auth.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/lib/auth/jwt-auth.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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,'
|
|
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/env/cds-requires.js
CHANGED
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 =
|
|
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
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
package/lib/ql/Whereable.js
CHANGED
|
@@ -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 }
|
package/lib/utils/cds-test.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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' && !
|
|
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
|
-
|
|
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
|
|
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
|
|
|
@@ -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
|
|
209
|
-
|
|
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
|
package/libx/outbox/index.js
CHANGED
|
@@ -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.
|
|
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
|
}
|