@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +35 -3
  2. package/bin/test.js +1 -1
  3. package/lib/compile/etc/csv.js +1 -1
  4. package/lib/dbs/cds-deploy.js +8 -5
  5. package/lib/env/cds-requires.js +0 -13
  6. package/lib/log/cds-error.js +10 -7
  7. package/lib/plugins.js +8 -3
  8. package/lib/srv/middlewares/errors.js +5 -3
  9. package/lib/srv/protocols/index.js +4 -4
  10. package/lib/srv/srv-methods.js +1 -0
  11. package/lib/utils/cds-test.js +2 -1
  12. package/lib/utils/cds-utils.js +14 -1
  13. package/lib/utils/colors.js +45 -44
  14. package/libx/_runtime/common/composition/data.js +4 -2
  15. package/libx/_runtime/common/composition/index.js +1 -2
  16. package/libx/_runtime/common/composition/tree.js +1 -24
  17. package/libx/_runtime/common/generic/auth/restrict.js +29 -4
  18. package/libx/_runtime/common/generic/auth/restrictions.js +29 -36
  19. package/libx/_runtime/common/i18n/messages.properties +1 -1
  20. package/libx/_runtime/common/utils/cqn.js +0 -26
  21. package/libx/_runtime/common/utils/csn.js +0 -14
  22. package/libx/_runtime/common/utils/differ.js +1 -0
  23. package/libx/_runtime/common/utils/resolveView.js +28 -9
  24. package/libx/_runtime/common/utils/templateProcessor.js +3 -0
  25. package/libx/_runtime/fiori/lean-draft.js +30 -12
  26. package/libx/_runtime/types/api.js +1 -1
  27. package/libx/_runtime/ucl/Service.js +2 -2
  28. package/libx/common/utils/path.js +1 -4
  29. package/libx/odata/ODataAdapter.js +6 -0
  30. package/libx/odata/middleware/batch.js +7 -9
  31. package/libx/odata/middleware/create.js +4 -2
  32. package/libx/odata/middleware/delete.js +3 -1
  33. package/libx/odata/middleware/operation.js +7 -5
  34. package/libx/odata/middleware/read.js +14 -10
  35. package/libx/odata/middleware/service-document.js +1 -1
  36. package/libx/odata/middleware/stream.js +1 -0
  37. package/libx/odata/middleware/update.js +5 -3
  38. package/libx/odata/parse/afterburner.js +37 -49
  39. package/libx/odata/utils/postProcess.js +3 -8
  40. package/package.json +1 -1
  41. package/libx/_runtime/cds-services/services/utils/compareJson.js +0 -2
  42. 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(' ') } \\)`
@@ -44,7 +44,7 @@ function parse (csv) {
44
44
  }
45
45
  else { // normal char
46
46
  if (val === undefined) val = ''
47
- val += c === '\\' ? '\\\\' : c
47
+ val += c
48
48
  }
49
49
  }
50
50
 
@@ -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 && console.log(after)
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 (o.dry) {
112
- console.log(); for (let each of drops) console.log(each)
113
- console.log(); for (let each of creas) console.log(each, '\n')
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)
@@ -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
@@ -1,17 +1,17 @@
1
- const { format } = require('util'), _formatted = v => format(v)
1
+ const { format, inspect } = require('../utils/cds-utils')
2
2
 
3
3
 
4
4
  /**
5
- * This is the implementation of cds.error().
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
- * let e = new cds.error(...) //> will not throw
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` it throws immediately. The latter is
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) => String.raw(strings,...values.map(_formatted))
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
- const expected = exports.expected = ([,type], arg) => {
59
+ exports.expected = ([,type], arg) => {
57
60
  const [ name, value ] = Object.entries(arg)[0]
58
- return error (`Expected argument '${name}'${type}, but got: ${require('util').inspect(value,{depth:11})}`, undefined, expected)
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
- await Promise.all (Object.entries(plugins) .map (async ([ plugin, conf ]) => {
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 ('../..'), LOG = cds.log('error')
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
- let { path: prefix } = this[kind] || cds.error `Unknown protocol: ${kind}`
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 = prefix+'/'+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
  }
@@ -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
 
@@ -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 ('<next>', ()=>{}) //> to signal the start of a test file
224
+ // suite was introduced in Node 22
225
+ suite?.('<next>', ()=>{}) //> to signal the start of a test file
225
226
 
226
227
  }
227
228
 
@@ -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() { return super.inspect = require('util').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'
@@ -1,49 +1,50 @@
1
- const none = process.stdout.isTTY && !process.env.NO_COLOR || process.env.FORCE_COLOR ? null : ''
1
+ const enabled = process.stdout.isTTY && !process.env.NO_COLOR || process.env.FORCE_COLOR
2
2
  const colors = {
3
- RESET: none ?? '\x1b[0m',
4
- BOLD: none ?? '\x1b[1m',
5
- BRIGHT: none ?? '\x1b[1m',
6
- DIMMED: none ?? '\x1b[2m',
7
- ITALIC: none ?? '\x1b[3m',
8
- UNDER: none ?? '\x1b[4m',
9
- BLINK: none ?? '\x1b[5m',
10
- FLASH: none ?? '\x1b[6m',
11
- INVERT: none ?? '\x1b[7m',
12
- BLACK: none ?? '\x1b[30m',
13
- RED: none ?? '\x1b[31m',
14
- GREEN: none ?? '\x1b[32m',
15
- YELLOW: none ?? '\x1b[33m',
16
- BLUE: none ?? '\x1b[34m',
17
- PINK: none ?? '\x1b[35m',
18
- CYAN: none ?? '\x1b[36m',
19
- LIGHT_GRAY: none ?? '\x1b[37m',
20
- DEFAULT: none ?? '\x1b[39m',
21
- GRAY: none ?? '\x1b[90m',
22
- LIGHT_RED: none ?? '\x1b[91m',
23
- LIGHT_GREEN: none ?? '\x1b[92m',
24
- LIGHT_YELLOW: none ?? '\x1b[93m',
25
- LIGHT_BLUE: none ?? '\x1b[94m',
26
- LIGHT_PINK: none ?? '\x1b[95m',
27
- LIGHT_CYAN: none ?? '\x1b[96m',
28
- WHITE: none ?? '\x1b[97m',
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: none ?? '\x1b[40m',
31
- RED: none ?? '\x1b[41m',
32
- GREEN: none ?? '\x1b[42m',
33
- YELLOW: none ?? '\x1b[43m',
34
- BLUE: none ?? '\x1b[44m',
35
- PINK: none ?? '\x1b[45m',
36
- CYAN: none ?? '\x1b[46m',
37
- WHITE: none ?? '\x1b[47m',
38
- DEFAULT: none ?? '\x1b[49m',
39
- LIGHT_GRAY: none ?? '\x1b[100m',
40
- LIGHT_RED: none ?? '\x1b[101m',
41
- LIGHT_GREEN: none ?? '\x1b[102m',
42
- LIGHT_YELLOW: none ?? '\x1b[103m',
43
- LIGHT_BLUE: none ?? '\x1b[104m',
44
- LIGHT_PINK: none ?? '\x1b[105m',
45
- LIGHT_CYAN: none ?? '\x1b[106m',
46
- LIGHT_WHITE: none ?? '\x1b[107m',
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
- const where = sqlQuery.UPDATE.where || []
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, getCompositionRoot } = require('./tree')
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
- const restrictedCount = await _getRestrictedCount(req, this.model, resolvedApplicables)
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