@sap/cds 8.2.3 → 8.3.0
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 +35 -3
- package/bin/test.js +1 -1
- package/lib/compile/etc/csv.js +1 -1
- package/lib/dbs/cds-deploy.js +8 -5
- package/lib/env/cds-requires.js +0 -13
- package/lib/log/cds-error.js +10 -7
- package/lib/plugins.js +8 -3
- package/lib/srv/middlewares/errors.js +5 -3
- package/lib/srv/protocols/index.js +4 -4
- package/lib/srv/srv-methods.js +1 -0
- package/lib/utils/cds-test.js +2 -1
- package/lib/utils/cds-utils.js +14 -1
- package/lib/utils/colors.js +45 -44
- package/libx/_runtime/common/composition/data.js +4 -2
- package/libx/_runtime/common/composition/index.js +1 -2
- package/libx/_runtime/common/composition/tree.js +1 -24
- package/libx/_runtime/common/generic/auth/restrict.js +29 -4
- package/libx/_runtime/common/generic/auth/restrictions.js +29 -36
- package/libx/_runtime/common/i18n/messages.properties +1 -1
- package/libx/_runtime/common/utils/cqn.js +0 -26
- package/libx/_runtime/common/utils/csn.js +0 -14
- package/libx/_runtime/common/utils/differ.js +1 -0
- package/libx/_runtime/common/utils/resolveView.js +28 -9
- package/libx/_runtime/common/utils/templateProcessor.js +3 -0
- package/libx/_runtime/fiori/lean-draft.js +30 -12
- package/libx/_runtime/types/api.js +1 -1
- package/libx/_runtime/ucl/Service.js +2 -2
- package/libx/common/utils/path.js +1 -4
- package/libx/odata/ODataAdapter.js +6 -0
- package/libx/odata/middleware/batch.js +7 -9
- package/libx/odata/middleware/create.js +4 -2
- package/libx/odata/middleware/delete.js +3 -1
- package/libx/odata/middleware/operation.js +7 -5
- package/libx/odata/middleware/read.js +14 -10
- package/libx/odata/middleware/service-document.js +1 -1
- package/libx/odata/middleware/stream.js +1 -0
- package/libx/odata/middleware/update.js +5 -3
- package/libx/odata/parse/afterburner.js +37 -49
- package/libx/odata/utils/postProcess.js +3 -8
- package/package.json +1 -1
- package/libx/_runtime/cds-services/services/utils/compareJson.js +0 -2
- package/libx/_runtime/messaging/event-broker.js +0 -317
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,38 @@
|
|
|
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.3.0 - 2024-09-30
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `cds.deploy` can now also write its DDL statements to a separate log
|
|
12
|
+
- Symlinks are followed in `cds test`
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Unknown protocols in `@protocol` annotations formerly prevented server starts; they are merely ignored now with a warning in the logs.
|
|
17
|
+
- Deprecated configuration flag `cds.env.features.keys_in_data_compat` because of incompatibility with data validation in new OData adapter.
|
|
18
|
+
- `@cds.api.ignore` doesn't suppress an association, the annotation is propagated to the (generated) foreign keys.
|
|
19
|
+
- Where clauses of restrictions for bound actions and functions defined by `@restrict` are now enforced and no longer ignored.
|
|
20
|
+
- `@cap-js/telemetry` is now loaded before other plugins to allow better instrumentation.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- When modifying active children of of draft-enabled entities directly (`bypass_draft`), the error message was misleading.
|
|
25
|
+
- Cleaning up drafts calls `CANCEL` handlers
|
|
26
|
+
- Allow to call `CANCEL` on draft entities programmatically
|
|
27
|
+
- Encoding of `@odata.nextLink` path
|
|
28
|
+
- Computed fields are ignored in projections
|
|
29
|
+
- Consider `id` in a `ref` step for mapping of service elements to their name on the db.
|
|
30
|
+
- Feature toggles with new OData adapter.
|
|
31
|
+
- Target entity was incorrectly calculated for some actions in new OData adapter.
|
|
32
|
+
- `req.diff()` does not manipulate existing queries anymore.
|
|
33
|
+
- New OData adapter: normalize on commit error in `/$batch`
|
|
34
|
+
|
|
35
|
+
### Removed
|
|
36
|
+
|
|
37
|
+
- Alpha support for SAP Event Broker-based messaging (kind `event-broker`). Use CDS plugin `@cap-js/event-broker` instead.
|
|
38
|
+
|
|
7
39
|
## Version 8.2.3 - 2024-09-20
|
|
8
40
|
|
|
9
41
|
### Changed
|
|
@@ -54,7 +86,9 @@
|
|
|
54
86
|
|
|
55
87
|
### Changed
|
|
56
88
|
|
|
57
|
-
- Revert workaround from 8.1.0 for server startup message `WARNING: Package '@sap/cds' was loaded from different installations`. This is now addressed in `@sap/cds-mtxs` 2.0.5
|
|
89
|
+
- Revert workaround from 8.1.0 for server startup message `WARNING: Package '@sap/cds' was loaded from different installations`. This is now addressed in `@sap/cds-mtxs` 2.0.5.
|
|
90
|
+
- When parsing CSV files, `cds.deploy` no longer doubles a literal `\` character (backslash) with a second backslash (`\\`), but retains it as-is. This caused unwanted data changes.
|
|
91
|
+
- Optimized handling of large binaries (BLOBs) in case of drafts. Unchanged BLOBs are not copied into the draft entity. If those BLOBs from draft entities are requested, the unchanged BLOBs will be fetched from the corresponding active entity. Note that this change may require adjustment of custom logic, if large binaries from draft entities are requested (for example, using `ql.SELECT` statement). To restore previous behavior use `cds.features.binary_draft_compat`.
|
|
58
92
|
|
|
59
93
|
### Fixed
|
|
60
94
|
|
|
@@ -76,7 +110,6 @@
|
|
|
76
110
|
[...linked.definitions].map(d => d.name)
|
|
77
111
|
```
|
|
78
112
|
|
|
79
|
-
|
|
80
113
|
## Version 8.1.1 - 2024-08-08
|
|
81
114
|
|
|
82
115
|
### Fixed
|
|
@@ -406,7 +439,6 @@
|
|
|
406
439
|
|
|
407
440
|
### Changed
|
|
408
441
|
|
|
409
|
-
- Optimized handling of large binaries (BLOBs) in case of drafts. Unchanged BLOBs are not copied into the draft entity. If those BLOBs from draft entities are requested, the unchanged BLOBs will be fetched from the corresponding active entity. Note that this change may require adjustment of custom logic, if large binaries from draft entities are requested (for example, using `ql.SELECT` statement). To restore previous behavior use `cds.features.binary_draft_compat`.
|
|
410
442
|
- The index page now lists all service endpoints, which is important for services that are exposed through multiple protocols.
|
|
411
443
|
- `cds.deploy` improves error diagnostics with deeper `Query` object inspection.
|
|
412
444
|
- Slightly changed the default export for ESM compatibility. This fixed failing ESM imports in Vitest tests.
|
package/bin/test.js
CHANGED
|
@@ -67,7 +67,7 @@ async function find (argv,o,recent) {
|
|
|
67
67
|
if (files.length && !roots.length && !includes.length) return files //> all files resolved
|
|
68
68
|
|
|
69
69
|
// Prepare UNIX find command to fetch matching files
|
|
70
|
-
let find = `find ${roots.join(' ')||'.'} -type f`
|
|
70
|
+
let find = `find -L ${roots.join(' ')||'.'} -type f`
|
|
71
71
|
if (patterns.length) find += ` \\( ${ patterns.map (p=>`-name "${p.replace(/^([^*])/,'*$1')}"`).join(' -o ') } \\)`
|
|
72
72
|
if (includes.length) find += ` \\( ${ includes.map (p=>`-regex .*${p.replace(/\./g,'\\\\.')}.*`).join(' -o ') } \\)`
|
|
73
73
|
if (excludes.length) find += ` \\( ${ excludes.map (x=>`! -regex .*${x.replace(/\./g,'\\\\.')}.*`).join(' ') } \\)`
|
package/lib/compile/etc/csv.js
CHANGED
package/lib/dbs/cds-deploy.js
CHANGED
|
@@ -60,6 +60,9 @@ const deploy = module.exports = function cds_deploy (model, options, csvs) {
|
|
|
60
60
|
deploy.schema = async function (db, csn = db.model, o) {
|
|
61
61
|
|
|
62
62
|
if (!o.to || o.to === db.options.kind) o = { ...db.options, ...o }
|
|
63
|
+
let schema_log
|
|
64
|
+
if (Array.isArray(o.schema_log)) schema_log = { log: (...args) => args.length ? o.schema_log.push(...args) : o.schema_log.push('') }
|
|
65
|
+
else if (o.dry) schema_log = console
|
|
63
66
|
|
|
64
67
|
let drops, creas
|
|
65
68
|
let schevo = (o.kind === 'postgres' && o.schema_evolution !== false)
|
|
@@ -82,7 +85,7 @@ deploy.schema = async function (db, csn = db.model, o) {
|
|
|
82
85
|
}
|
|
83
86
|
o.schema_evolution = 'auto' // for INSERT_from4 below
|
|
84
87
|
// cds deploy --model-only > fills in table cds_model above
|
|
85
|
-
if (o['model-only']) return o.dry &&
|
|
88
|
+
if (o['model-only']) return o.dry && schema_log.log(after)
|
|
86
89
|
// cds deploy -- with auto schema evolution > upgrade by applying delta to former model
|
|
87
90
|
creas = createsAndAlters
|
|
88
91
|
drops = d
|
|
@@ -108,11 +111,11 @@ deploy.schema = async function (db, csn = db.model, o) {
|
|
|
108
111
|
|
|
109
112
|
if (!drops.length && !creas.length) return !o.dry
|
|
110
113
|
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return
|
|
114
|
+
if (schema_log) {
|
|
115
|
+
schema_log.log(); for (let each of drops) schema_log.log(each)
|
|
116
|
+
schema_log.log(); for (let each of creas) schema_log.log(each, '\n')
|
|
115
117
|
}
|
|
118
|
+
if (o.dry) return
|
|
116
119
|
|
|
117
120
|
await db.run(drops)
|
|
118
121
|
await db.run(creas)
|
package/lib/env/cds-requires.js
CHANGED
|
@@ -231,19 +231,6 @@ const _messaging = {
|
|
|
231
231
|
vcap: { label: "enterprise-messaging" },
|
|
232
232
|
outbox: true
|
|
233
233
|
},
|
|
234
|
-
"event-broker": {
|
|
235
|
-
impl: `${_runtime}/messaging/event-broker.js`,
|
|
236
|
-
format: 'cloudevents',
|
|
237
|
-
vcap: {
|
|
238
|
-
label: "event-broker"
|
|
239
|
-
}
|
|
240
|
-
},
|
|
241
|
-
"event-broker-internal": {
|
|
242
|
-
kind: "event-broker",
|
|
243
|
-
vcap: {
|
|
244
|
-
label: "eventmesh-sap2sap-internal"
|
|
245
|
-
}
|
|
246
|
-
},
|
|
247
234
|
'message-queuing': {
|
|
248
235
|
impl: `${_runtime}/messaging/message-queuing.js`,
|
|
249
236
|
outbox: true
|
package/lib/log/cds-error.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
const { format } = require('
|
|
1
|
+
const { format, inspect } = require('../utils/cds-utils')
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Constructs and optionally throws an Error object.
|
|
6
6
|
* Usage variants:
|
|
7
7
|
*
|
|
8
8
|
* cds.error `Message with formatted: ${{foo:'bar'}}`
|
|
9
9
|
* cds.error ({ message, code, ... })
|
|
10
10
|
* cds.error (message, { code, ... })
|
|
11
|
-
*
|
|
11
|
+
* cds.error (status, message, { code, ... })
|
|
12
12
|
*
|
|
13
13
|
* Calling `cds.error()` with `new` returns the newly created Error,
|
|
14
|
-
* while calling it without `new`
|
|
14
|
+
* while calling it without `new` throws immediately. The latter is
|
|
15
15
|
* useful for usages like that:
|
|
16
16
|
*
|
|
17
17
|
* let x = y || cds.error `Argument 'y' must not be null`
|
|
@@ -19,6 +19,7 @@ const { format } = require('util'), _formatted = v => format(v)
|
|
|
19
19
|
const error = exports = module.exports = function cds_error ( message, details, caller ) {
|
|
20
20
|
let e
|
|
21
21
|
if (message.raw) [ message, details, caller ] = [ error.message(...arguments) ]
|
|
22
|
+
if (typeof message === 'number') [ message, details ] = [ details, {status:message} ]
|
|
22
23
|
if (typeof message === 'string') {
|
|
23
24
|
e = new Error(message)
|
|
24
25
|
} else {
|
|
@@ -41,7 +42,9 @@ const error = exports = module.exports = function cds_error ( message, details,
|
|
|
41
42
|
* //> x = A sample message with a string and [object Object], and 1,2,3
|
|
42
43
|
* //> y = with a string, { an: 'object' }, and [ 1, 2, 3 ]
|
|
43
44
|
*/
|
|
44
|
-
exports.message = (strings,...values) =>
|
|
45
|
+
exports.message = (strings,...values) => {
|
|
46
|
+
return String.raw(strings,...values.map(v => format(v)))
|
|
47
|
+
}
|
|
45
48
|
|
|
46
49
|
|
|
47
50
|
/**
|
|
@@ -53,9 +56,9 @@ exports.message = (strings,...values) => String.raw(strings,...values.map(_forma
|
|
|
53
56
|
* typeof x === 'string' || cds.error.expected `${{x}} to be a string`
|
|
54
57
|
* //> Error: Expected argument 'x' to be a string, but got: { foo: 'bar' }
|
|
55
58
|
*/
|
|
56
|
-
|
|
59
|
+
exports.expected = ([,type], arg) => {
|
|
57
60
|
const [ name, value ] = Object.entries(arg)[0]
|
|
58
|
-
return error (`Expected argument '${name}'${type}, but got: ${
|
|
61
|
+
return error (`Expected argument '${name}'${type}, but got: ${inspect(value)}`, undefined, error.expected)
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
|
package/lib/plugins.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
2
1
|
const cds = require('.')
|
|
2
|
+
const prio_plugins = {
|
|
3
|
+
'@cap-js/telemetry': true // to allow better instrumentation.
|
|
4
|
+
}
|
|
3
5
|
|
|
4
6
|
exports.require = require
|
|
5
7
|
|
|
@@ -31,7 +33,7 @@ exports.activate = async function () {
|
|
|
31
33
|
const DEBUG = cds.debug ('plugins', {label:'cds'})
|
|
32
34
|
DEBUG?.time ('[cds] - loaded plugins in')
|
|
33
35
|
const { plugins } = cds.env, { local } = cds.utils
|
|
34
|
-
|
|
36
|
+
const loadPlugin = async ([plugin, conf]) => {
|
|
35
37
|
DEBUG?.(`loading plugin ${plugin}:`, { impl: local(conf.impl) })
|
|
36
38
|
// TODO: support ESM plugins. But see cap/cds/pull/1838#issuecomment-1177200 !
|
|
37
39
|
const p = require (conf.impl)
|
|
@@ -43,7 +45,10 @@ exports.activate = async function () {
|
|
|
43
45
|
await p.activate(conf)
|
|
44
46
|
}
|
|
45
47
|
return p
|
|
46
|
-
}
|
|
48
|
+
}
|
|
49
|
+
const all = Object.entries(plugins)
|
|
50
|
+
await Promise.all (all .filter(([name]) => prio_plugins[name]) .map (loadPlugin))
|
|
51
|
+
await Promise.all (all .filter(([name]) => !prio_plugins[name]) .map (loadPlugin))
|
|
47
52
|
DEBUG?.timeEnd ('[cds] - loaded plugins in')
|
|
48
53
|
return plugins
|
|
49
54
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const production = process.env.NODE_ENV === 'production'
|
|
2
|
-
const cds = require ('../..')
|
|
2
|
+
const cds = require ('../..')
|
|
3
|
+
const LOG = cds.log('error')
|
|
4
|
+
const { inspect } = cds.utils
|
|
3
5
|
|
|
4
6
|
module.exports = () => {
|
|
5
7
|
return function http_error (error, req, res, _next) { // eslint-disable-line no-unused-vars
|
|
@@ -15,9 +17,9 @@ module.exports = () => {
|
|
|
15
17
|
if (!production && error.stack) error.stack = error.stack.replace(/\n {4}at .*(?:node_modules\/express|node:internal).*/g,'')
|
|
16
18
|
|
|
17
19
|
if (400 <= status && status < 500) {
|
|
18
|
-
LOG.warn (status, '>', error)
|
|
20
|
+
LOG.warn (status, '>', inspect(error))
|
|
19
21
|
} else {
|
|
20
|
-
LOG.error (status, '>', error)
|
|
22
|
+
LOG.error (status, '>', inspect(error))
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
// Expose as little information as possible in production, and as much as possible in development
|
|
@@ -56,7 +56,7 @@ class Protocols {
|
|
|
56
56
|
// app.disable('x-powered-by')
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
for (let { kind, path } of endpoints) {
|
|
59
|
+
if (endpoints) for (let { kind, path } of endpoints) {
|
|
60
60
|
|
|
61
61
|
// construct adapter instance from resolved implementation
|
|
62
62
|
let adapter = cached[kind]; if (!adapter) {
|
|
@@ -117,11 +117,11 @@ class Protocols {
|
|
|
117
117
|
// canonicalize to { kind, path } objects
|
|
118
118
|
const endpoints = annos.map (each => {
|
|
119
119
|
let { kind = each['='] || each, path } = each
|
|
120
|
-
|
|
120
|
+
if (!(kind in this)) return cds.log('adapters').warn ('ignoring unknown protocol:', kind)
|
|
121
121
|
if (typeof path !== 'string') path = o?.at || o?.path || def['@path'] || _slugified(srv.name)
|
|
122
|
-
if (path[0] !== '/') path =
|
|
122
|
+
if (path[0] !== '/') path = this[kind].path + '/' + path // prefix with protocol path
|
|
123
123
|
return { kind, path }
|
|
124
|
-
})
|
|
124
|
+
}) .filter (e => e) //> skipping unknown protocols
|
|
125
125
|
|
|
126
126
|
return endpoints.length && endpoints
|
|
127
127
|
}
|
package/lib/srv/srv-methods.js
CHANGED
|
@@ -71,6 +71,7 @@ const add_handler_for = (srv, def) => {
|
|
|
71
71
|
|
|
72
72
|
// ensure legacy compat, keys in req.data
|
|
73
73
|
if(cds.env.features.keys_in_data_compat && target) {
|
|
74
|
+
cds.utils.deprecated({ old: 'flag cds.env.features.keys_in_data_compat'})
|
|
74
75
|
// named/positional variant of keys
|
|
75
76
|
const named = req.params.length === 1 && typeof req.params[0] === 'object'
|
|
76
77
|
|
package/lib/utils/cds-test.js
CHANGED
|
@@ -221,7 +221,8 @@ let _expect = undefined
|
|
|
221
221
|
global.beforeEach = beforeEach
|
|
222
222
|
global.afterEach = afterEach
|
|
223
223
|
global.expect = _expect = require('../test/expect')
|
|
224
|
-
suite
|
|
224
|
+
// suite was introduced in Node 22
|
|
225
|
+
suite?.('<next>', ()=>{}) //> to signal the start of a test file
|
|
225
226
|
|
|
226
227
|
}
|
|
227
228
|
|
package/lib/utils/cds-utils.js
CHANGED
|
@@ -2,15 +2,27 @@ const cwd = process.env._original_cwd || process.cwd()
|
|
|
2
2
|
const cds = require('../index')
|
|
3
3
|
|
|
4
4
|
module.exports = exports = new class {
|
|
5
|
+
get colors() { return super.colors = require('./colors') }
|
|
5
6
|
get inflect() { return super.inflect = require('./inflect') }
|
|
6
|
-
get inspect() {
|
|
7
|
+
get inspect() {
|
|
8
|
+
const options = { depth: 11, colors: this.colors.enabled }
|
|
9
|
+
const {inspect} = require('node:util')
|
|
10
|
+
return super.inspect = v => inspect(v,options)
|
|
11
|
+
}
|
|
12
|
+
get format() {
|
|
13
|
+
const {format} = require('node:util')
|
|
14
|
+
return super.format = format
|
|
15
|
+
}
|
|
7
16
|
get uuid() { return super.uuid = require('crypto').randomUUID }
|
|
8
17
|
get yaml() { return super.yaml = require('@sap/cds-foss').yaml }
|
|
9
18
|
get pool() { return super.pool = require('@sap/cds-foss').pool }
|
|
10
19
|
get tar() { return super.tar = require('./tar') }
|
|
11
20
|
}
|
|
12
21
|
|
|
22
|
+
/** @type {import('node:path')} */
|
|
13
23
|
const path = exports.path = require('path'), { dirname, extname, join, resolve, relative } = path
|
|
24
|
+
|
|
25
|
+
/** @type {import('node:fs')} */
|
|
14
26
|
const fs = exports.fs = Object.assign (exports,require('fs')) //> for compatibility
|
|
15
27
|
|
|
16
28
|
|
|
@@ -232,6 +244,7 @@ exports.find = function find (base, patterns='*', filter=()=>true) {
|
|
|
232
244
|
return files
|
|
233
245
|
}
|
|
234
246
|
|
|
247
|
+
|
|
235
248
|
exports.deprecated = (fn, { kind = 'Method', old = fn.name+'()', use } = {}) => {
|
|
236
249
|
const yellow = '\x1b[33m'
|
|
237
250
|
const reset = '\x1b[0m'
|
package/lib/utils/colors.js
CHANGED
|
@@ -1,49 +1,50 @@
|
|
|
1
|
-
const
|
|
1
|
+
const enabled = process.stdout.isTTY && !process.env.NO_COLOR || process.env.FORCE_COLOR
|
|
2
2
|
const colors = {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
3
|
+
enabled,
|
|
4
|
+
RESET: enabled ? '\x1b[0m' : '',
|
|
5
|
+
BOLD: enabled ? '\x1b[1m' : '',
|
|
6
|
+
BRIGHT: enabled ? '\x1b[1m' : '',
|
|
7
|
+
DIMMED: enabled ? '\x1b[2m' : '',
|
|
8
|
+
ITALIC: enabled ? '\x1b[3m' : '',
|
|
9
|
+
UNDER: enabled ? '\x1b[4m' : '',
|
|
10
|
+
BLINK: enabled ? '\x1b[5m' : '',
|
|
11
|
+
FLASH: enabled ? '\x1b[6m' : '',
|
|
12
|
+
INVERT: enabled ? '\x1b[7m' : '',
|
|
13
|
+
BLACK: enabled ? '\x1b[30m' : '',
|
|
14
|
+
RED: enabled ? '\x1b[31m' : '',
|
|
15
|
+
GREEN: enabled ? '\x1b[32m' : '',
|
|
16
|
+
YELLOW: enabled ? '\x1b[33m' : '',
|
|
17
|
+
BLUE: enabled ? '\x1b[34m' : '',
|
|
18
|
+
PINK: enabled ? '\x1b[35m' : '',
|
|
19
|
+
CYAN: enabled ? '\x1b[36m' : '',
|
|
20
|
+
LIGHT_GRAY: enabled ? '\x1b[37m' : '',
|
|
21
|
+
DEFAULT: enabled ? '\x1b[39m' : '',
|
|
22
|
+
GRAY: enabled ? '\x1b[90m' : '',
|
|
23
|
+
LIGHT_RED: enabled ? '\x1b[91m' : '',
|
|
24
|
+
LIGHT_GREEN: enabled ? '\x1b[92m' : '',
|
|
25
|
+
LIGHT_YELLOW: enabled ? '\x1b[93m' : '',
|
|
26
|
+
LIGHT_BLUE: enabled ? '\x1b[94m' : '',
|
|
27
|
+
LIGHT_PINK: enabled ? '\x1b[95m' : '',
|
|
28
|
+
LIGHT_CYAN: enabled ? '\x1b[96m' : '',
|
|
29
|
+
WHITE: enabled ? '\x1b[97m' : '',
|
|
29
30
|
bg: {
|
|
30
|
-
BLACK:
|
|
31
|
-
RED:
|
|
32
|
-
GREEN:
|
|
33
|
-
YELLOW:
|
|
34
|
-
BLUE:
|
|
35
|
-
PINK:
|
|
36
|
-
CYAN:
|
|
37
|
-
WHITE:
|
|
38
|
-
DEFAULT:
|
|
39
|
-
LIGHT_GRAY:
|
|
40
|
-
LIGHT_RED:
|
|
41
|
-
LIGHT_GREEN:
|
|
42
|
-
LIGHT_YELLOW:
|
|
43
|
-
LIGHT_BLUE:
|
|
44
|
-
LIGHT_PINK:
|
|
45
|
-
LIGHT_CYAN:
|
|
46
|
-
LIGHT_WHITE:
|
|
31
|
+
BLACK: enabled ? '\x1b[40m' : '',
|
|
32
|
+
RED: enabled ? '\x1b[41m' : '',
|
|
33
|
+
GREEN: enabled ? '\x1b[42m' : '',
|
|
34
|
+
YELLOW: enabled ? '\x1b[43m' : '',
|
|
35
|
+
BLUE: enabled ? '\x1b[44m' : '',
|
|
36
|
+
PINK: enabled ? '\x1b[45m' : '',
|
|
37
|
+
CYAN: enabled ? '\x1b[46m' : '',
|
|
38
|
+
WHITE: enabled ? '\x1b[47m' : '',
|
|
39
|
+
DEFAULT: enabled ? '\x1b[49m' : '',
|
|
40
|
+
LIGHT_GRAY: enabled ? '\x1b[100m' : '',
|
|
41
|
+
LIGHT_RED: enabled ? '\x1b[101m' : '',
|
|
42
|
+
LIGHT_GREEN: enabled ? '\x1b[102m' : '',
|
|
43
|
+
LIGHT_YELLOW: enabled ? '\x1b[103m' : '',
|
|
44
|
+
LIGHT_BLUE: enabled ? '\x1b[104m' : '',
|
|
45
|
+
LIGHT_PINK: enabled ? '\x1b[105m' : '',
|
|
46
|
+
LIGHT_CYAN: enabled ? '\x1b[106m' : '',
|
|
47
|
+
LIGHT_WHITE: enabled ? '\x1b[107m' : '',
|
|
47
48
|
},
|
|
48
49
|
}
|
|
49
50
|
module.exports = colors
|
|
@@ -326,6 +326,7 @@ const selectDeepUpdateData = (service, model, req, selectAllColumns = false) =>
|
|
|
326
326
|
const query = req.query
|
|
327
327
|
|
|
328
328
|
// REVISIT this should be done somewhere before, so it is not done twice for deep updates
|
|
329
|
+
// REVISIT: this is done (better) in the new db-services
|
|
329
330
|
const sqlQuery = cqn2cqn4sql(query, model)
|
|
330
331
|
|
|
331
332
|
if (req && _isSameEntity(sqlQuery, req)) {
|
|
@@ -334,10 +335,11 @@ const selectDeepUpdateData = (service, model, req, selectAllColumns = false) =>
|
|
|
334
335
|
|
|
335
336
|
const from = getEntityNameFromUpdateCQN(sqlQuery)
|
|
336
337
|
const alias = sqlQuery.UPDATE.entity.as
|
|
337
|
-
|
|
338
|
+
// if parts of (another) query are re-used make sure to get your own copy
|
|
339
|
+
const where = cds.clone(sqlQuery.UPDATE.where) || []
|
|
338
340
|
const entityName = ensureNoDraftsSuffix(from)
|
|
339
341
|
const draft = entityName !== from
|
|
340
|
-
const orderBy = req?.target?.query?.SELECT?.orderBy
|
|
342
|
+
const orderBy = req?.target?.query?.SELECT?.orderBy ? cds.clone(req?.target?.query?.SELECT?.orderBy) : null
|
|
341
343
|
_resolveOrderBy(orderBy, sqlQuery.UPDATE._transitions)
|
|
342
344
|
const data = Object.assign({}, sqlQuery.UPDATE.data || {}, query.UPDATE.with || {})
|
|
343
345
|
const compositionTree = getCompositionTree({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { getCompositionTree
|
|
1
|
+
const { getCompositionTree } = require('./tree')
|
|
2
2
|
const { hasDeepInsert, getDeepInsertCQNs } = require('./insert')
|
|
3
3
|
const { hasDeepUpdate, getDeepUpdateCQNs } = require('./update')
|
|
4
4
|
const { hasDeepDelete, getDeepDeleteCQNs, getSetNullParentForeignKeyCQNs } = require('./delete')
|
|
@@ -7,7 +7,6 @@ const { selectDeepUpdateData } = require('./data')
|
|
|
7
7
|
module.exports = {
|
|
8
8
|
// tree
|
|
9
9
|
getCompositionTree,
|
|
10
|
-
getCompositionRoot,
|
|
11
10
|
// insert
|
|
12
11
|
hasDeepInsert,
|
|
13
12
|
getDeepInsertCQNs,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
2
|
|
|
3
|
-
const { ensureNoDraftsSuffix } = require('../utils/draft')
|
|
4
3
|
const { getTransition, getDBTable } = require('../utils/resolveView')
|
|
5
4
|
|
|
6
5
|
const { prefixForStruct } = require('../../common/utils/csn')
|
|
@@ -259,27 +258,6 @@ const _memoizeGetCompositionTree = fn => {
|
|
|
259
258
|
* exports
|
|
260
259
|
*/
|
|
261
260
|
|
|
262
|
-
const getCompositionRoot = (definitions, entity) => {
|
|
263
|
-
const associationElements = Object.keys(entity.elements)
|
|
264
|
-
.map(key => entity.elements[key])
|
|
265
|
-
.filter(element => element._isAssociationStrict)
|
|
266
|
-
|
|
267
|
-
for (const { target } of associationElements) {
|
|
268
|
-
const parentEntity = definitions[target]
|
|
269
|
-
for (const parentElementName in parentEntity.elements) {
|
|
270
|
-
const parentElement = parentEntity.elements[parentElementName]
|
|
271
|
-
if (
|
|
272
|
-
parentElement.isComposition &&
|
|
273
|
-
parentElement.target === entity.name &&
|
|
274
|
-
parentElement.target !== ensureNoDraftsSuffix(parentElement.parent.name)
|
|
275
|
-
) {
|
|
276
|
-
return getCompositionRoot(definitions, parentEntity)
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
return entity
|
|
281
|
-
}
|
|
282
|
-
|
|
283
261
|
/**
|
|
284
262
|
* Provides tree of all compositions. (Cached)
|
|
285
263
|
*
|
|
@@ -291,6 +269,5 @@ const getCompositionRoot = (definitions, entity) => {
|
|
|
291
269
|
const getCompositionTree = _memoizeGetCompositionTree(_getCompositionTree)
|
|
292
270
|
|
|
293
271
|
module.exports = {
|
|
294
|
-
getCompositionTree
|
|
295
|
-
getCompositionRoot
|
|
272
|
+
getCompositionTree
|
|
296
273
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
const cds = require('../../../cds')
|
|
1
|
+
const cds = require('../../../cds'),
|
|
2
|
+
LOG = cds.log('auth')
|
|
2
3
|
|
|
3
4
|
const { reject, getRejectReason, resolveUserAttrs, getAuthRelevantEntity } = require('./utils')
|
|
4
5
|
const { DRAFT_EVENTS, MOD_EVENTS } = require('./constants')
|
|
@@ -245,11 +246,12 @@ async function check_roles(req) {
|
|
|
245
246
|
return
|
|
246
247
|
}
|
|
247
248
|
|
|
249
|
+
//Instance based authorization for bound actions /functions
|
|
250
|
+
await restrictBoundActionFunctions(req, resolvedApplicables, definition, this)
|
|
251
|
+
|
|
248
252
|
// no modification -> nothing more to do
|
|
249
253
|
if (!MOD_EVENTS[req.event]) return
|
|
250
254
|
|
|
251
|
-
// REVISIT: selected data could be used for etag check, diff, etc.
|
|
252
|
-
|
|
253
255
|
/*
|
|
254
256
|
* Here we check if UPDATE/DELETE requests add additional restrictions
|
|
255
257
|
* Note: Needs to happen sequentially because of side effects
|
|
@@ -258,13 +260,36 @@ async function check_roles(req) {
|
|
|
258
260
|
const unrestrictedCount = await _getUnrestrictedCount(req)
|
|
259
261
|
if (unrestrictedCount === 0) req.reject(404)
|
|
260
262
|
|
|
261
|
-
|
|
263
|
+
// REVISIT: selected data could be used for etag check, diff, etc.
|
|
262
264
|
|
|
265
|
+
const restrictedCount = await _getRestrictedCount(req, this.model, resolvedApplicables)
|
|
263
266
|
if (restrictedCount < unrestrictedCount) {
|
|
264
267
|
reject(req, getRejectReason(req, '@restrict', definition, restrictedCount, unrestrictedCount))
|
|
265
268
|
}
|
|
266
269
|
}
|
|
267
270
|
|
|
271
|
+
const isBoundToCollection = action =>
|
|
272
|
+
action['@cds.odata.bindingparameter.collection'] ||
|
|
273
|
+
(action.params && Object.values(action.params).some(param => param?.items?.type === '$self'))
|
|
274
|
+
|
|
275
|
+
const restrictBoundActionFunctions = async (req, resolvedApplicables, definition, srv) => {
|
|
276
|
+
if (req.target?.actions?.[req.event] && !isBoundToCollection(req.target.actions[req.event])) {
|
|
277
|
+
//Clone to avoid target modification, which would cause a different query
|
|
278
|
+
const query = cds.ql.clone(req.query) ?? SELECT.from(req.subject)
|
|
279
|
+
_addRestrictionsToRead({ query: query, target: req.target }, cds.model, resolvedApplicables)
|
|
280
|
+
const result = await srv.run(query)
|
|
281
|
+
if (!result || result.length === 0) {
|
|
282
|
+
// If we got a result, we don't need to check for the existence, hence only in this special case we must determine if `404` or `403`.
|
|
283
|
+
const unrestrictedCount = await _getUnrestrictedCount(req)
|
|
284
|
+
if (unrestrictedCount === 0) req.reject(404)
|
|
285
|
+
|
|
286
|
+
if (LOG._debug) LOG.debug(`Restricted access on action ${req.event}`)
|
|
287
|
+
reject(req, getRejectReason(req, '@restrict', definition))
|
|
288
|
+
}
|
|
289
|
+
req._auth_query_result = result
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
268
293
|
check_roles._initial = true
|
|
269
294
|
|
|
270
295
|
module.exports = check_roles
|