@sap/cds 8.2.2 → 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 +49 -3
- package/bin/serve.js +3 -0
- package/bin/test.js +1 -1
- package/lib/compile/etc/csv.js +1 -1
- package/lib/dbs/cds-deploy.js +10 -5
- package/lib/env/cds-requires.js +0 -13
- package/lib/linked/validate.js +5 -3
- 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/streamProp.js +1 -1
- package/libx/_runtime/common/utils/templateProcessor.js +3 -0
- package/libx/_runtime/fiori/lean-draft.js +34 -13
- 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 +19 -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/libx/odata/utils/result.js +3 -1
- 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,52 @@
|
|
|
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
|
+
|
|
39
|
+
## Version 8.2.3 - 2024-09-20
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- All annotations in input data are skipped and removed from the input by `cds.validate()` - as we did in legacy OData adapter
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- Unmanaged associations are excluded from `@mandatory` checks
|
|
48
|
+
- Properly reject direct requests to `DraftAdministrativeData`
|
|
49
|
+
- Virtual elements annotated with `@Core.MediaType`
|
|
50
|
+
- OData Requests targeting a specific instance and custom handler returns empty array
|
|
51
|
+
- `cds-serve` and `cds-deploy` now set `cds.cli` information
|
|
52
|
+
|
|
7
53
|
## Version 8.2.2 - 2024-09-13
|
|
8
54
|
|
|
9
55
|
### Fixed
|
|
@@ -40,7 +86,9 @@
|
|
|
40
86
|
|
|
41
87
|
### Changed
|
|
42
88
|
|
|
43
|
-
- 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`.
|
|
44
92
|
|
|
45
93
|
### Fixed
|
|
46
94
|
|
|
@@ -62,7 +110,6 @@
|
|
|
62
110
|
[...linked.definitions].map(d => d.name)
|
|
63
111
|
```
|
|
64
112
|
|
|
65
|
-
|
|
66
113
|
## Version 8.1.1 - 2024-08-08
|
|
67
114
|
|
|
68
115
|
### Fixed
|
|
@@ -392,7 +439,6 @@
|
|
|
392
439
|
|
|
393
440
|
### Changed
|
|
394
441
|
|
|
395
|
-
- 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`.
|
|
396
442
|
- The index page now lists all service endpoints, which is important for services that are exposed through multiple protocols.
|
|
397
443
|
- `cds.deploy` improves error diagnostics with deeper `Query` object inspection.
|
|
398
444
|
- Slightly changed the default export for ESM compatibility. This fixed failing ESM imports in Vitest tests.
|
package/bin/serve.js
CHANGED
|
@@ -158,6 +158,9 @@ async function serve (all=[], o={}) {
|
|
|
158
158
|
if (o.watch) return _watch.call(this, o.project,o) // cds serve --watch <project>
|
|
159
159
|
if (o.project) _chdir_to (o.project) // cds run --project <project>
|
|
160
160
|
|
|
161
|
+
// let plugins know about the CLI
|
|
162
|
+
cds.cli = { command: 'serve', argv: all, options: o }
|
|
163
|
+
|
|
161
164
|
// Ensure loading plugins before calling cds.env!
|
|
162
165
|
await cds.plugins
|
|
163
166
|
|
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)
|
|
@@ -384,6 +387,7 @@ const _entity4 = (file, csn) => {
|
|
|
384
387
|
|
|
385
388
|
/** CLI used as via cds-deploy as deployer for PostgreSQL */
|
|
386
389
|
if (!module.parent) (async function CLI () {
|
|
390
|
+
cds.cli = { command: 'deploy', argv: process.argv.slice(2), options: {} }
|
|
387
391
|
await cds.plugins // IMPORTANT: that has to go before any call to cds.env, like through cds.deploy or cds.requires below
|
|
388
392
|
let db = cds.requires.db
|
|
389
393
|
try {
|
|
@@ -400,6 +404,7 @@ if (!module.parent) (async function CLI () {
|
|
|
400
404
|
if (o.username) (db.credentials ??= {}).username = o.username
|
|
401
405
|
if (o.password) (db.credentials ??= {}).password = o.password
|
|
402
406
|
}
|
|
407
|
+
cds.cli.options = o
|
|
403
408
|
db = await cds.connect.to(db);
|
|
404
409
|
db = await cds.deploy('*',o).to(db)
|
|
405
410
|
} finally {
|
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/linked/validate.js
CHANGED
|
@@ -46,7 +46,8 @@ class Validation {
|
|
|
46
46
|
return filter.length ? `(${filter})` : `[${index}]`
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
unknown(e,d) {
|
|
49
|
+
unknown(e,d,input) {
|
|
50
|
+
if (e.startsWith('@')) return delete input[e] //> skip all annotations, like @odata.Type
|
|
50
51
|
d['@open'] || cds.error (`Property "${e}" does not exist in ${d.name}`, {status:400})
|
|
51
52
|
}
|
|
52
53
|
}
|
|
@@ -190,12 +191,13 @@ class struct extends $any {
|
|
|
190
191
|
if (each.name in skip) continue // skip uplinks in deep inserts -> see Composition.validate()
|
|
191
192
|
if (each.$struct in data) continue // got struct for flattened element/fk, e.g. {author:{ID:1}}
|
|
192
193
|
if (each.elements || each.foreignKeys) continue // skip struct-likes as we check flat payloads above, and deep payloads via struct.validate()
|
|
194
|
+
if (each.isAssociation) continue // unmanaged associations are always ignored (no value like)
|
|
193
195
|
else ctx.error ('ASSERT_NOT_NULL', path_, each.name) // ASSERT_NOT_NULL should be ASSERT_REQUIRED
|
|
194
196
|
}
|
|
195
197
|
// check values of given data
|
|
196
198
|
for (let each in data) { // will work for structured payloads as well as flattened ones with universal CSN
|
|
197
199
|
let /** @type {$any} */ d = elements[each]
|
|
198
|
-
if (!d
|
|
200
|
+
if (!d) ctx.unknown (each, this, data)
|
|
199
201
|
else if (ctx.cleanse && d._is_readonly()) delete data[each]
|
|
200
202
|
else if (d['@cds.validate'] !== false) d.validate (data[each], path_, ctx)
|
|
201
203
|
}
|
|
@@ -310,4 +312,4 @@ $.LargeBinary.prototype .type_check = v => Buffer.isBuffer(v) || typeof v === 's
|
|
|
310
312
|
$.LargeString.prototype .type_check = v => Buffer.isBuffer(v) || typeof v === 'string' || v instanceof Readable
|
|
311
313
|
|
|
312
314
|
// Mixin above class extensions to cds.linked.classes
|
|
313
|
-
$.mixin ( Decimal, string, $any, action, array, struct, entity, Association, Composition )
|
|
315
|
+
$.mixin ( Decimal, string, $any, action, array, struct, entity, Association, Composition )
|
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
|