@sap/cds 8.2.3 → 8.3.1
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 +44 -3
- package/bin/test.js +1 -1
- package/lib/compile/etc/_localized.js +1 -0
- package/lib/compile/etc/csv.js +1 -1
- package/lib/compile/for/lean_drafts.js +5 -0
- package/lib/dbs/cds-deploy.js +8 -5
- package/lib/env/cds-requires.js +0 -13
- package/lib/linked/validate.js +11 -9
- package/lib/log/cds-error.js +10 -7
- package/lib/plugins.js +8 -3
- package/lib/srv/middlewares/cds-context.js +1 -1
- 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/error/frontend.js +18 -4
- 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/index.js +3 -2
- 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,47 @@
|
|
|
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.1 - 2024-10-08
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Erroneous caching in `cds.validate`
|
|
12
|
+
- Precedence of request headers for `cds.context.id`
|
|
13
|
+
- For `quoted` names, overwrite `@cds.persistence.name` for drafts and localized views properly
|
|
14
|
+
- Do not use hana error code as http status code
|
|
15
|
+
|
|
16
|
+
## Version 8.3.0 - 2024-09-30
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- `cds.deploy` can now also write its DDL statements to a separate log
|
|
21
|
+
- Symlinks are followed in `cds test`
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Unknown protocols in `@protocol` annotations formerly prevented server starts; they are merely ignored now with a warning in the logs.
|
|
26
|
+
- Deprecated configuration flag `cds.env.features.keys_in_data_compat` because of incompatibility with data validation in new OData adapter.
|
|
27
|
+
- `@cds.api.ignore` doesn't suppress an association, the annotation is propagated to the (generated) foreign keys.
|
|
28
|
+
- Where clauses of restrictions for bound actions and functions defined by `@restrict` are now enforced and no longer ignored.
|
|
29
|
+
- `@cap-js/telemetry` is now loaded before other plugins to allow better instrumentation.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- When modifying active children of of draft-enabled entities directly (`bypass_draft`), the error message was misleading.
|
|
34
|
+
- Cleaning up drafts calls `CANCEL` handlers
|
|
35
|
+
- Allow to call `CANCEL` on draft entities programmatically
|
|
36
|
+
- Encoding of `@odata.nextLink` path
|
|
37
|
+
- Computed fields are ignored in projections
|
|
38
|
+
- Consider `id` in a `ref` step for mapping of service elements to their name on the db.
|
|
39
|
+
- Feature toggles with new OData adapter.
|
|
40
|
+
- Target entity was incorrectly calculated for some actions in new OData adapter.
|
|
41
|
+
- `req.diff()` does not manipulate existing queries anymore.
|
|
42
|
+
- New OData adapter: normalize on commit error in `/$batch`
|
|
43
|
+
|
|
44
|
+
### Removed
|
|
45
|
+
|
|
46
|
+
- Alpha support for SAP Event Broker-based messaging (kind `event-broker`). Use CDS plugin `@cap-js/event-broker` instead.
|
|
47
|
+
|
|
7
48
|
## Version 8.2.3 - 2024-09-20
|
|
8
49
|
|
|
9
50
|
### Changed
|
|
@@ -54,7 +95,9 @@
|
|
|
54
95
|
|
|
55
96
|
### Changed
|
|
56
97
|
|
|
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
|
|
98
|
+
- 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.
|
|
99
|
+
- 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.
|
|
100
|
+
- 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
101
|
|
|
59
102
|
### Fixed
|
|
60
103
|
|
|
@@ -76,7 +119,6 @@
|
|
|
76
119
|
[...linked.definitions].map(d => d.name)
|
|
77
120
|
```
|
|
78
121
|
|
|
79
|
-
|
|
80
122
|
## Version 8.1.1 - 2024-08-08
|
|
81
123
|
|
|
82
124
|
### Fixed
|
|
@@ -406,7 +448,6 @@
|
|
|
406
448
|
|
|
407
449
|
### Changed
|
|
408
450
|
|
|
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
451
|
- The index page now lists all service endpoints, which is important for services that are exposed through multiple protocols.
|
|
411
452
|
- `cds.deploy` improves error diagnostics with deeper `Query` object inspection.
|
|
412
453
|
- 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(' ') } \\)`
|
|
@@ -83,6 +83,7 @@ function unfold_csn (m) { // NOSONAR
|
|
|
83
83
|
function _add_proxy4 (d, name, callback) {
|
|
84
84
|
if (name in m.definitions) return DEBUG && DEBUG ('NOT overriding existing:', name)
|
|
85
85
|
const x = {__proto__:d, name }; DEBUG && DEBUG ('adding proxy:', x)
|
|
86
|
+
if (d['@cds.persistence.name']) x['@cds.persistence.name'] = `localized.${d['@cds.persistence.name']}`
|
|
86
87
|
Object.defineProperty (m.definitions, name, {value:x,writable:true,configurable:true})
|
|
87
88
|
if (callback) callback(x)
|
|
88
89
|
}
|
package/lib/compile/etc/csv.js
CHANGED
|
@@ -64,6 +64,11 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
|
|
|
64
64
|
elements: { ...active.elements, ...Draft.elements },
|
|
65
65
|
query: undefined
|
|
66
66
|
}
|
|
67
|
+
|
|
68
|
+
// for quoted names, we need to overwrite the cds.persistence.name of the derived, draft entity
|
|
69
|
+
if (active['@cds.persistence.name'])
|
|
70
|
+
draft['@cds.persistence.name'] = active['@cds.persistence.name'] + '_drafts'
|
|
71
|
+
|
|
67
72
|
Object.defineProperty(model.definitions, _draftEntity, { value: draft })
|
|
68
73
|
Object.defineProperty(active, 'drafts', { value: draft })
|
|
69
74
|
Object.defineProperty(active, 'actives', { value: active })
|
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/linked/validate.js
CHANGED
|
@@ -241,14 +241,16 @@ class Association extends struct {
|
|
|
241
241
|
/** Compositions are like nested entities, validating deep input against their target entity definitions. */
|
|
242
242
|
class Composition extends entity {
|
|
243
243
|
validate (data, path, ctx) { if (!data) return
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
244
|
+
const _validate = this.own('_validate', () => {
|
|
245
|
+
const elements = this._target.elements
|
|
246
|
+
const uplinks = {} // statically determine the uplinks for this composition
|
|
247
|
+
if (this.on) for (let {ref} of this.on) if (ref?.[0] === this.name) {
|
|
248
|
+
const fk = ref[1], fk_ = fk+'_'; uplinks[fk] = true
|
|
249
|
+
for (let e in elements) if (e.startsWith(fk_)) uplinks[e] = true
|
|
250
|
+
}
|
|
251
|
+
return (data, path, ctx) => super.validate (data, path, ctx, elements, uplinks)
|
|
252
|
+
})
|
|
253
|
+
_validate (data, path, ctx)
|
|
252
254
|
}
|
|
253
255
|
}
|
|
254
256
|
|
|
@@ -312,4 +314,4 @@ $.LargeBinary.prototype .type_check = v => Buffer.isBuffer(v) || typeof v === 's
|
|
|
312
314
|
$.LargeString.prototype .type_check = v => Buffer.isBuffer(v) || typeof v === 'string' || v instanceof Readable
|
|
313
315
|
|
|
314
316
|
// Mixin above class extensions to cds.linked.classes
|
|
315
|
-
$.mixin ( Decimal, string, $any, action, array, struct, entity, Association, Composition )
|
|
317
|
+
$.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
|
}
|
|
@@ -9,7 +9,7 @@ const { EventContext } = cds
|
|
|
9
9
|
module.exports = () => {
|
|
10
10
|
/** @type { import('express').Handler } */
|
|
11
11
|
return function cds_context (req, res, next) {
|
|
12
|
-
const id = req.headers[corr_id] ??= req.headers[
|
|
12
|
+
const id = req.headers[corr_id] ??= req.headers[crippled_corr_id] || req.headers[req_id] || req.headers[vr_id] || uuid()
|
|
13
13
|
const ctx = EventContext.for ({ id, http: { req, res } })
|
|
14
14
|
res.set ('X-Correlation-ID', id) // Note: we use capitalized style here as that's common standard in HTTP world
|
|
15
15
|
cds._context.run (ctx, next)
|
|
@@ -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
|
}
|
|
@@ -51,7 +51,7 @@ const _rewriteError = error => {
|
|
|
51
51
|
(code.startsWith('SQLITE_CONSTRAINT') && (message.match(/COMMIT/) || message.match(/FOREIGN KEY/))) ||
|
|
52
52
|
(code === '155' && message.match(/fk constraint violation/))
|
|
53
53
|
) {
|
|
54
|
-
// > foreign key constaint violation
|
|
54
|
+
// > foreign key constaint violation on sqlite/ hana
|
|
55
55
|
error.code = '400'
|
|
56
56
|
error.message = 'FK_CONSTRAINT_VIOLATION'
|
|
57
57
|
return
|
|
@@ -63,17 +63,33 @@ const _rewriteError = error => {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
const _isInHttpResponseCodeRange = errorCode => errorCode >= 300 && errorCode <= 599
|
|
67
|
+
|
|
68
|
+
const BAD_REQUEST_ERRORS = new Set(['ENTITY_ALREADY_EXISTS', 'FK_CONSTRAINT_VIOLATION', 'UNIQUE_CONSTRAINT_VIOLATION'])
|
|
69
|
+
|
|
66
70
|
const _normalize = (err, locale, formatterFn = _getFiltered) => {
|
|
67
71
|
// REVISIT: code and message rewriting
|
|
68
72
|
_rewriteError(err)
|
|
69
73
|
|
|
74
|
+
const { message: originalMessage } = err
|
|
75
|
+
|
|
70
76
|
// message (i18n)
|
|
71
77
|
err.message = getErrorMessage(err, locale)
|
|
72
78
|
|
|
73
79
|
// ensure code is set and a string
|
|
74
80
|
err.code = String(err.code || 'null')
|
|
75
81
|
|
|
76
|
-
|
|
82
|
+
// determine status code from error
|
|
83
|
+
let statusCode = err.status || err.statusCode //> REVISIT: why prefer status over statusCode?
|
|
84
|
+
// well-defined bad request errors
|
|
85
|
+
if (!statusCode && BAD_REQUEST_ERRORS.has(originalMessage)) statusCode = 400
|
|
86
|
+
if (!statusCode && _isInHttpResponseCodeRange(err.code)) {
|
|
87
|
+
if ('sqlState' in err) {
|
|
88
|
+
// HANA/ database error -> don't use code as status code
|
|
89
|
+
} else {
|
|
90
|
+
statusCode = err.code
|
|
91
|
+
}
|
|
92
|
+
}
|
|
77
93
|
|
|
78
94
|
// details
|
|
79
95
|
if (err.details) {
|
|
@@ -95,8 +111,6 @@ const _normalize = (err, locale, formatterFn = _getFiltered) => {
|
|
|
95
111
|
return { error, statusCode }
|
|
96
112
|
}
|
|
97
113
|
|
|
98
|
-
const _isAllowedError = errorCode => errorCode >= 300 && errorCode < 505
|
|
99
|
-
|
|
100
114
|
// - for one unique value, we use it
|
|
101
115
|
// - if at least one 5xx exists, we use 500
|
|
102
116
|
// - else if at least one 4xx exists, we use 400
|