@sap/cds 9.5.2 → 9.6.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 +46 -1
- package/bin/args.js +3 -3
- package/bin/serve.js +18 -14
- package/lib/compile/for/flows.js +11 -5
- package/lib/compile/for/lean_drafts.js +1 -1
- package/lib/compile/for/nodejs.js +2 -0
- package/lib/compile/parse.js +1 -1
- package/lib/core/linked-csn.js +23 -13
- package/lib/env/cds-env.js +6 -0
- package/lib/env/defaults.js +3 -1
- package/lib/index.js +2 -1
- package/lib/log/format/aspects/als.js +5 -1
- package/lib/log/format/aspects/cls.js +7 -3
- package/lib/log/service/index.js +5 -1
- package/lib/req/validate.js +1 -1
- package/lib/srv/cds.Service.js +37 -5
- package/libx/_runtime/common/generic/assert.js +1 -0
- package/libx/_runtime/common/generic/flows.js +8 -12
- package/libx/_runtime/common/generic/input.js +8 -2
- package/libx/_runtime/fiori/lean-draft.js +7 -3
- package/libx/_runtime/messaging/kafka.js +1 -0
- package/libx/odata/ODataAdapter.js +2 -1
- package/libx/odata/middleware/service-document.js +1 -1
- package/libx/odata/parse/afterburner.js +6 -2
- package/libx/odata/parse/cqn2odata.js +9 -9
- package/libx/odata/parse/grammar.peggy +6 -8
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/index.js +6 -2
- package/libx/queue/index.js +127 -84
- package/package.json +2 -2
- package/srv/outbox.cds +2 -0
- package/tasks/enterprise-messaging-deploy.js +7 -5
- package/lib/srv/middlewares/sap-statistics.js +0 -13
- package/lib/utils/index.js +0 -2
- package/libx/_runtime/common/generic/stream.js +0 -21
- package/libx/_runtime/common/i18n/index.js +0 -79
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,51 @@
|
|
|
4
4
|
- The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
|
5
5
|
- This project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## Version 9.6.1 - 2025-12-18
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Status check in case of non-existing subject
|
|
12
|
+
- Gracefully handle bad value when calculating `@Core.OperationAvailable` from `@from`
|
|
13
|
+
|
|
14
|
+
## Version 9.6.0 - 2025-12-16
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- New config entry `cds.folders.apps = 'app/*'` to fetch and load all .cds files from subfolders of `./app` automatically. This eliminates the need for an `./app/index.cds` file with respective `using` directives. Can be disabled by setting `cds.folders.apps` to `false`.
|
|
19
|
+
- Support for direct CRUD on draft-enabled entities (beta)
|
|
20
|
+
+ Enable via `cds.fiori.direct_crud` (replacing `cds.features.new_draft_via_action`)
|
|
21
|
+
+ In such requests, `IsActiveEntity` is defaulted to `true`
|
|
22
|
+
- Columns `task` and `appid` to `cds.outbox.Messages`
|
|
23
|
+
- `cds.env` getter for `appid`
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Improved Event Queue Processing
|
|
28
|
+
+ For db-only message processors, the guarantee is now "exactly once" instead of "at least once".
|
|
29
|
+
+ Use default target `queue` unless specified (formerly it was the name of the service). In effect, all services with the default queue config are processed together.
|
|
30
|
+
- Cleaned-up Model Reflection APIs
|
|
31
|
+
+ `cds.entities` and `model.entities` provide access to all entities, not just the ones matching the first sources' namespace
|
|
32
|
+
+ Function usage `cds.entities()` without passing a namespace no longer offers unqualified access to the entities of the first sources' namespace
|
|
33
|
+
+ `srv.entities` returns `LinkedDefinitions` for that service
|
|
34
|
+
+ Undocumented function usages `srv.entities()`, `srv.types()`, `srv.events()`, and `srv.actions()` are officially deprecated and will be removed with `cds^10`.
|
|
35
|
+
+ For `srv.entities()`, use `cds.entities()` instead. The others have no replacement. Simply use `srv.types`, `srv.events`, and `srv.actions`.
|
|
36
|
+
+ All `.entities` variants only provide `.texts` entities if `cds.features.compat_texts_entities = true` (default until `cds^10`). Use `.texts` property of the respective entity instead (i.e., `CatalogService.Books.texts` instead of `CatalogService['Books.texts']`).
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- Actions rejected with `415` for requests with empty body
|
|
41
|
+
- `cds.parse.expr` with template literals using "param" as parameter name
|
|
42
|
+
- Kafka messaging won't listen for messages without registered on handlers
|
|
43
|
+
- Single quote escaping in remote OData requests
|
|
44
|
+
- Lambda prefix within function calls in remote OData requests
|
|
45
|
+
- Hierarchy requests with `$top` in combination with `$filter`
|
|
46
|
+
- Response format of `ASSERT_DATA_TYPE` errors with draft messages
|
|
47
|
+
- Deletion of `@assert` draft messages on SAVE
|
|
48
|
+
- Check for `process.env.VCAP_SERVICES_FILE_PATH` in logging aspects enterprise messaging tasks
|
|
49
|
+
- Remove control data from deserialized task
|
|
50
|
+
- Hierarchy draft requests with $filter
|
|
51
|
+
|
|
7
52
|
## Version 9.5.2 - 2025-12-09
|
|
8
53
|
|
|
9
54
|
### Fixed
|
|
@@ -44,7 +89,6 @@
|
|
|
44
89
|
- Support for pseudo protocols
|
|
45
90
|
- Support for Async UCL tenant mapping notification flow
|
|
46
91
|
- `flush` on a queued service returns a Promise that resolves when immediate work (i.e., not scheduled for future) is processed
|
|
47
|
-
- For draft-enabled entities, IsActiveEntity=true can be omitted from url
|
|
48
92
|
- Support for `@Common.DraftRoot.NewAction` annotation with feature flag `cds.features.new_draft_via_action`
|
|
49
93
|
+ Generic collection bound action `draftNew` will be added to draft enabled entities
|
|
50
94
|
+ The action specified in the annotation will be rewritten into a draft `NEW` event
|
|
@@ -74,6 +118,7 @@
|
|
|
74
118
|
- Read-after-write for create events during draft choreography will no longer include messages targeting siblings
|
|
75
119
|
- `before` and `after` handlers now really run in parallel. If that causes trouble, you can restore the previous behavior with `cds.features.async_handler_compat=true` until `@sap/cds@10`.
|
|
76
120
|
- Escaping of JSON escape sequences during localization
|
|
121
|
+
- Persisted draft messages in case of on-commit errors
|
|
77
122
|
|
|
78
123
|
## Version 9.4.5 - 2025-11-07
|
|
79
124
|
|
package/bin/args.js
CHANGED
|
@@ -17,12 +17,12 @@ module.exports = function _args4 (task, argv) {
|
|
|
17
17
|
}
|
|
18
18
|
// consistent production setting for NODE_ENV and CDS_ENV
|
|
19
19
|
if (process.env.NODE_ENV !== 'production') { if (process.env.CDS_ENV?.split(',').includes('production')) process.env.NODE_ENV = 'production' }
|
|
20
|
-
else process.env.CDS_ENV = Array.from(new Set([...process.env.CDS_ENV?.split(',') ?? [], 'production']))
|
|
20
|
+
else process.env.CDS_ENV = Array.from(new Set([...process.env.CDS_ENV?.split(',') ?? [], 'production'])).join(',')
|
|
21
21
|
|
|
22
22
|
function add (k,v) { options[k.slice(2)] = v || true }
|
|
23
23
|
function add_global (k,v='') {
|
|
24
|
-
if (k === '--production') return process.env.CDS_ENV = Array.from(new Set([...process.env.CDS_ENV?.split(',') ?? [], 'production']))
|
|
25
|
-
if (k === '--profile') return process.env.CDS_ENV = Array.from(new Set([...process.env.CDS_ENV?.split(',') ?? [], ...v.split(',')]))
|
|
24
|
+
if (k === '--production') return process.env.CDS_ENV = Array.from(new Set([...process.env.CDS_ENV?.split(',') ?? [], 'production'])).join(',')
|
|
25
|
+
if (k === '--profile') return process.env.CDS_ENV = Array.from(new Set([...process.env.CDS_ENV?.split(',') ?? [], ...v.split(',')])).join(',')
|
|
26
26
|
if (k === '--odata') v = { flavor:v }
|
|
27
27
|
let e=env || (env={}), path = k.slice(2).split('-')
|
|
28
28
|
while (path.length > 1) { let p = path.shift(); e = e[p]||(e[p]={}) }
|
package/bin/serve.js
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
module.exports = exports =
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
2
|
+
module.exports = exports = serve
|
|
3
|
+
|
|
4
|
+
exports.options = [
|
|
5
|
+
'--service', '--from', '--to', '--at', '--with',
|
|
6
|
+
'--port', '--workers',
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
exports.flags = [
|
|
10
|
+
'--project', '--projects',
|
|
11
|
+
'--in-memory', '--in-memory?',
|
|
12
|
+
'--mocked', '--with-mocks', '--with-bindings', '--resolve-bindings',
|
|
13
|
+
'--watch', '--with-mtx',
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
exports.shortcuts = [ '-s', undefined, '-2', '-a', '-w', undefined, undefined, '-p' ]
|
|
17
|
+
|
|
18
|
+
exports.help = `
|
|
15
19
|
# SYNOPSIS
|
|
16
20
|
|
|
17
21
|
*cds serve* [ <filenames> ] [ <options> ]
|
|
@@ -136,7 +140,7 @@ module.exports = exports = Object.assign ( serve, {
|
|
|
136
140
|
*cds watch* some/project
|
|
137
141
|
*cds watch*
|
|
138
142
|
|
|
139
|
-
`
|
|
143
|
+
`
|
|
140
144
|
|
|
141
145
|
|
|
142
146
|
const cds = require('../lib'), { exists, isfile, local, redacted, path } = cds.utils
|
package/lib/compile/for/flows.js
CHANGED
|
@@ -13,12 +13,18 @@ const getFrom = action => {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
function addOperationAvailableToActions(actions, statusEnum, statusElementName) {
|
|
16
|
-
for (const action of Object.values(actions)) {
|
|
16
|
+
action: for (const action of Object.values(actions)) {
|
|
17
17
|
const fromList = getFrom(action)
|
|
18
|
-
const conditions =
|
|
18
|
+
const conditions = []
|
|
19
|
+
for (const from of fromList) {
|
|
19
20
|
const value = from['#'] ? statusEnum[from['#']]?.val ?? from['#'] : from
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
if (typeof value !== 'string') {
|
|
22
|
+
const msg = `Error while constructing @Core.OperationAvailable for action "${action.name}" of "${action.parent.name}". Value of @from must either be an enum symbol or a raw string.`
|
|
23
|
+
cds.log('cds|edmx').warn(msg)
|
|
24
|
+
continue action
|
|
25
|
+
}
|
|
26
|
+
conditions.push(`$self.${statusElementName} = '${value}'`)
|
|
27
|
+
}
|
|
22
28
|
const condition = `(${conditions.join(' OR ')})`
|
|
23
29
|
const parsedXpr = cds.parse.expr(condition)
|
|
24
30
|
action['@Core.OperationAvailable'] ??= {
|
|
@@ -74,7 +80,7 @@ function addActionsToTarget(targetAnnotation, entity, actions) {
|
|
|
74
80
|
identification.push({
|
|
75
81
|
$Type: 'UI.DataFieldForAction',
|
|
76
82
|
Action: `${entity._service.name}.${actionName}`,
|
|
77
|
-
Label: action[
|
|
83
|
+
Label: action['@Common.Label'] ?? action['@title'] ?? `{i18n>${actionName}}`,
|
|
78
84
|
...(entity['@odata.draft.enabled'] && {
|
|
79
85
|
'@UI.Hidden': {
|
|
80
86
|
'=': true,
|
|
@@ -264,6 +264,6 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
|
|
|
264
264
|
// will insert drafts entities, so that others can use `.drafts` even without incoming draft requests
|
|
265
265
|
addDraftEntity(def, csn)
|
|
266
266
|
|
|
267
|
-
if (cds.env.fiori.
|
|
267
|
+
if (cds.env.fiori.direct_crud) addNewActionAnnotation(def)
|
|
268
268
|
}
|
|
269
269
|
}
|
|
@@ -46,6 +46,8 @@ function _compile_for_nodejs (csn, o) {
|
|
|
46
46
|
|
|
47
47
|
module.exports = function cds_compile_for_nodejs (csn,o) {
|
|
48
48
|
if ('_4nodejs' in csn) return csn._4nodejs
|
|
49
|
+
// REVISIT: remove _compat_texts_entities with cds^10
|
|
50
|
+
if (cds.env.features.compat_texts_entities) Object.defineProperty(csn, '_compat_texts_entities', { value: true, enumerable: true })
|
|
49
51
|
TRACE?.time('cds.compile 4nodejs'.padEnd(22)); try {
|
|
50
52
|
let result, next = ()=> result ??= _compile_for_nodejs (csn,o)
|
|
51
53
|
cds.emit ('compile.for.runtime', csn, o, next)
|
package/lib/compile/parse.js
CHANGED
|
@@ -87,7 +87,7 @@ exports.ttl = (parse, strings, ...values) => {
|
|
|
87
87
|
for (let k in o) {
|
|
88
88
|
const x = o[k]
|
|
89
89
|
if (!x) continue
|
|
90
|
-
if (x.param) {
|
|
90
|
+
if (x.param && x.ref) {
|
|
91
91
|
let val = values[x.ref[0]]; if (val === undefined) continue
|
|
92
92
|
let y = o[k] = cxn4(val) //; y.$ = x.ref[0]
|
|
93
93
|
if (x.cast) y.cast = x.cast
|
package/lib/core/linked-csn.js
CHANGED
|
@@ -107,22 +107,32 @@ class LinkedCSN {
|
|
|
107
107
|
return this
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
childrenOf (x,
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
110
|
+
childrenOf (x, include = ()=>true, defs = this.definitions, children = new LinkedDefinitions) {
|
|
111
|
+
const prefix = !x ? '' : typeof x === 'string' ? x+'.' : x.namespace ? x.namespace+'.' : x.name ? x.name+'.' : ''
|
|
112
|
+
for (let name in defs)
|
|
113
|
+
if (name.startsWith(prefix) && include (defs[name]))
|
|
114
|
+
children[name.slice(prefix.length)] = defs[name]
|
|
115
|
+
return children
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
get exports() {
|
|
119
|
+
const exports = this.childrenOf (this)
|
|
120
|
+
return this.set ('exports', exports)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
get entities() {
|
|
124
|
+
// REVISIT: remove _compat_texts_entities with cds^10
|
|
125
|
+
const filter = this._compat_texts_entities ? d => d.is_entity : d => d.is_entity && !d.name.endsWith('.texts')
|
|
126
|
+
const entities = ns => this.childrenOf (ns, filter)
|
|
127
|
+
for (let d of this.definitions) if (d.is_entity) entities[d.name] = d // adds all entities
|
|
128
|
+
Object.setPrototypeOf (entities, entities (this.namespace)) // adds exported entities
|
|
129
|
+
return this.set ('entities', entities)
|
|
118
130
|
}
|
|
119
131
|
|
|
120
|
-
get exports() { return this.set ('exports', this.childrenOf (this)) }
|
|
121
|
-
get entities() { return this.set ('entities', this.childrenOf (this, d => d.is_entity)) }
|
|
122
132
|
get services() {
|
|
123
|
-
|
|
124
|
-
for (let s of
|
|
125
|
-
return this.set ('services',
|
|
133
|
+
const services = this.all (d => d.is_service)
|
|
134
|
+
for (let s of services) Object.defineProperty (services, s.name, {value:s})
|
|
135
|
+
return this.set ('services', services)
|
|
126
136
|
}
|
|
127
137
|
|
|
128
138
|
/** A common cache for all kinds of things -> keeps the models clean */
|
package/lib/env/cds-env.js
CHANGED
|
@@ -92,6 +92,12 @@ class Config {
|
|
|
92
92
|
// Complete service configurations from cloud service bindings
|
|
93
93
|
this._add_cloud_service_bindings(process.env)
|
|
94
94
|
|
|
95
|
+
this.appid ??= null
|
|
96
|
+
if (typeof this.appid === 'boolean') {
|
|
97
|
+
if (this.appid) this.#import(_home, 'package.json', { get: p => ({ appid: p.name }) })
|
|
98
|
+
else this.appid = null
|
|
99
|
+
}
|
|
100
|
+
|
|
95
101
|
// Only if feature is enabled
|
|
96
102
|
if (this.features && this.features.emulate_vcap_services) {
|
|
97
103
|
this._emulate_vcap_services()
|
package/lib/env/defaults.js
CHANGED
|
@@ -47,6 +47,7 @@ module.exports = {
|
|
|
47
47
|
precise_timestamps: false,
|
|
48
48
|
ieee754compatible: undefined,
|
|
49
49
|
consistent_params: true, //> remove with cds^10
|
|
50
|
+
compat_texts_entities: true, //> remove with cds^10
|
|
50
51
|
annotate_for_flows: true,
|
|
51
52
|
history_for_flows: true,
|
|
52
53
|
compile_for_assert: undefined,
|
|
@@ -62,7 +63,7 @@ module.exports = {
|
|
|
62
63
|
draft_lock_timeout: true,
|
|
63
64
|
draft_deletion_timeout: true,
|
|
64
65
|
draft_messages: true,
|
|
65
|
-
|
|
66
|
+
direct_crud: false
|
|
66
67
|
},
|
|
67
68
|
|
|
68
69
|
ql: {
|
|
@@ -105,6 +106,7 @@ module.exports = {
|
|
|
105
106
|
db: 'db/',
|
|
106
107
|
srv: 'srv/',
|
|
107
108
|
app: 'app/',
|
|
109
|
+
apps: 'app/*',
|
|
108
110
|
},
|
|
109
111
|
|
|
110
112
|
i18n: {
|
package/lib/index.js
CHANGED
|
@@ -53,7 +53,7 @@ const cds = exports = module.exports = global.cds = new class cds extends EventE
|
|
|
53
53
|
get i18n() { return super.i18n = require('./i18n/index.js') }
|
|
54
54
|
|
|
55
55
|
// Model Reflection, Builtin types and classes
|
|
56
|
-
get entities() { return this.
|
|
56
|
+
get entities() { return (this.model||this.db?.model)?.entities }
|
|
57
57
|
get reflect() { return super.reflect = this.linked }
|
|
58
58
|
get linked() { return super.linked = require('./core/linked-csn.js') }
|
|
59
59
|
get builtin() { return super.builtin = require('./core/types.js') }
|
|
@@ -80,6 +80,7 @@ const cds = exports = module.exports = global.cds = new class cds extends EventE
|
|
|
80
80
|
get unboxed() { return this.unqueued }
|
|
81
81
|
get queued() { return super.queued = require('../libx/queue/index.js').queued }
|
|
82
82
|
get unqueued() { return super.unqueued = require('../libx/queue/index.js').unqueued }
|
|
83
|
+
get flush() { return super.flush = require('../libx/queue/index.js').cdsFlush }
|
|
83
84
|
get middlewares() { return super.middlewares = require('./srv/middlewares/index.js') }
|
|
84
85
|
get odata() { return super.odata = require('../libx/odata/index.js') }
|
|
85
86
|
get auth() { return super.auth = require('./srv/middlewares/auth/index.js') }
|
|
@@ -20,4 +20,8 @@ function als_aspect(module, level, args, toLog) {
|
|
|
20
20
|
|
|
21
21
|
als_aspect.cf = () => Object.keys({ ...cds.env.log.als_custom_fields })
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
const vcap_services = process.env.VCAP_SERVICES_FILE_PATH
|
|
24
|
+
? cds.utils.fs.readFileSync(process.env.VCAP_SERVICES_FILE_PATH, 'utf-8')
|
|
25
|
+
: process.env.VCAP_SERVICES
|
|
26
|
+
|
|
27
|
+
module.exports = vcap_services?.match(/"label":\s*"application-logs"/) ? als_aspect : () => {}
|
|
@@ -6,10 +6,14 @@ function cls_aspect(/* module, level, args, toLog */) {
|
|
|
6
6
|
|
|
7
7
|
cls_aspect.cf = () => [...cds.env.log.cls_custom_fields]
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const vcap_services = JSON.parse(
|
|
10
|
+
process.env.VCAP_SERVICES_FILE_PATH
|
|
11
|
+
? cds.utils.fs.readFileSync(process.env.VCAP_SERVICES_FILE_PATH, 'utf-8')
|
|
12
|
+
: process.env.VCAP_SERVICES || '{}'
|
|
13
|
+
)
|
|
10
14
|
|
|
11
15
|
module.exports =
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
vcap_services['cloud-logging'] ||
|
|
17
|
+
vcap_services['user-provided']?.find(e => e.tags.includes('cloud-logging') || e.tags.includes('Cloud Logging'))
|
|
14
18
|
? cls_aspect
|
|
15
19
|
: () => {}
|
package/lib/log/service/index.js
CHANGED
|
@@ -7,7 +7,11 @@ module.exports = class LogService extends cds.Service {
|
|
|
7
7
|
|
|
8
8
|
// Secure by basic auth from xsuaa in production
|
|
9
9
|
if (process.env.NODE_ENV === 'production') {
|
|
10
|
-
const { xsuaa } =
|
|
10
|
+
const { xsuaa } = JSON.parse(
|
|
11
|
+
process.env.VCAP_SERVICES_FILE_PATH
|
|
12
|
+
? cds.utils.fs.readFileSync(process.env.VCAP_SERVICES_FILE_PATH, 'utf-8')
|
|
13
|
+
: process.env.VCAP_SERVICES || '{}'
|
|
14
|
+
)
|
|
11
15
|
if (xsuaa) {
|
|
12
16
|
const { clientid, clientsecret } = xsuaa[0].credentials
|
|
13
17
|
const secret = 'Basic ' + Buffer.from (clientid + ':' + clientsecret).toString('base64')
|
package/lib/req/validate.js
CHANGED
|
@@ -201,7 +201,7 @@ class struct extends $any {
|
|
|
201
201
|
validate (data, path, /** @type {Validation} */ ctx, elements = this.elements, skip={}) {
|
|
202
202
|
if (data == null) return
|
|
203
203
|
const path_ = !path ? [] : [...path, this.name]; if (path?.row) path_.push({...path})
|
|
204
|
-
if (typeof data !== 'object') return ctx.error ('ASSERT_DATA_TYPE', path_,
|
|
204
|
+
if (typeof data !== 'object') return ctx.error ('ASSERT_DATA_TYPE', path_, null, null, data, this.target || this.type?.replace(/^cds\./,''))
|
|
205
205
|
// check for required elements in case of inserts -- note: null values are handled in the payload loop below
|
|
206
206
|
if (ctx.insert || data && path_.length && this._is_insert(data)) for (let each of this._required (elements)) {
|
|
207
207
|
if (each.name in data) continue // got value for required element
|
package/lib/srv/cds.Service.js
CHANGED
|
@@ -97,13 +97,41 @@ class ReflectionAPI extends ConsumptionAPI {
|
|
|
97
97
|
|| !this.isDatabaseService && !/\W/.test(this.name) && this.name
|
|
98
98
|
|| undefined
|
|
99
99
|
}
|
|
100
|
-
get entities() {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
get entities() {
|
|
101
|
+
if (!this.model) return super.entities = []
|
|
102
|
+
// REVISIT: remove _compat_texts_entities with cds^10
|
|
103
|
+
const filter = this.model._compat_texts_entities ? d => d.kind === 'entity' : d => d.kind === 'entity' && !d.name.endsWith('.texts')
|
|
104
|
+
const entities = this.reflect(filter)
|
|
105
|
+
return super.entities = deconstructable_and_iterable(entities, 'entities', this)
|
|
106
|
+
}
|
|
107
|
+
get events() {
|
|
108
|
+
if (!this.model) return super.events = []
|
|
109
|
+
const events = this.reflect(d => d.kind === 'event')
|
|
110
|
+
return super.events = deconstructable_and_iterable(events, 'events', this)
|
|
111
|
+
}
|
|
112
|
+
get types() {
|
|
113
|
+
if (!this.model) return super.types = []
|
|
114
|
+
const types = this.reflect(d => !d.kind || d.kind === 'type')
|
|
115
|
+
return super.types = deconstructable_and_iterable(types, 'types', this)
|
|
116
|
+
}
|
|
117
|
+
get actions() {
|
|
118
|
+
if (!this.model) return super.actions = []
|
|
119
|
+
const actions = this.reflect(d => d.kind === 'action' || d.kind === 'function')
|
|
120
|
+
return super.actions = deconstructable_and_iterable(actions, 'actions', this)
|
|
121
|
+
}
|
|
104
122
|
reflect (filter) { return this.model?.childrenOf (this.namespace, filter) || [] }
|
|
105
123
|
}
|
|
106
124
|
|
|
125
|
+
const deconstructable_and_iterable = (it, what, srv) => {
|
|
126
|
+
// REVISIT: remove deprecated function API with cds^10
|
|
127
|
+
const compat_function_api = Object.assign(compat_function_factory(what, srv, it), it)
|
|
128
|
+
// srv.* is both deconstructable and iterable
|
|
129
|
+
return Object.setPrototypeOf(compat_function_api, it)
|
|
130
|
+
}
|
|
131
|
+
const compat_function_factory = (api, srv, it) => cds.utils.deprecated (ns => {
|
|
132
|
+
return !ns || ns === srv.namespace ? it : srv.model[api]?.(ns) || {}
|
|
133
|
+
}, { kind: 'API', old: `srv.${api}()`, use: api === 'entities' ? `cds.${api}()` : undefined })
|
|
134
|
+
|
|
107
135
|
|
|
108
136
|
/**
|
|
109
137
|
* This class provides the API used by service providers to add event handlers.
|
|
@@ -111,6 +139,10 @@ class ReflectionAPI extends ConsumptionAPI {
|
|
|
111
139
|
*/
|
|
112
140
|
class Service extends ReflectionAPI {
|
|
113
141
|
|
|
142
|
+
/**
|
|
143
|
+
* @param {string} name
|
|
144
|
+
* @param {import('../core/linked-csn').LinkedCSN} model
|
|
145
|
+
*/
|
|
114
146
|
constructor (name, model, options) { super()
|
|
115
147
|
if (typeof name === 'object') [ model, options, name = _service_in(model) ] = [ name, model ]
|
|
116
148
|
this.name = name || new.target.name // i.e. when called without any arguments
|
|
@@ -124,7 +156,7 @@ class Service extends ReflectionAPI {
|
|
|
124
156
|
|
|
125
157
|
// Handler registration API
|
|
126
158
|
prepend (fn) { return this.handlers.prepend.call (this,fn) }
|
|
127
|
-
/** @typedef {( entity?,
|
|
159
|
+
/** @typedef {( event, entity?, handler:(req:import('../req/request'))=>{})=> Service} boa */
|
|
128
160
|
/** @type boa */ before (...args) { return this.handlers.register (this, 'before', ...args) }
|
|
129
161
|
/** @type boa */ on (...args) { return this.handlers.register (this, 'on', ...args) }
|
|
130
162
|
/** @type boa */ after (...args) { return this.handlers.register (this, 'after', ...args) }
|
|
@@ -9,24 +9,20 @@ const FLOW_PREVIOUS = '$flow.previous'
|
|
|
9
9
|
|
|
10
10
|
const $transitions_ = Symbol.for('transitions_')
|
|
11
11
|
|
|
12
|
-
function
|
|
12
|
+
function isCurrentStatusInFrom(result, action, statusElementName, statusEnum) {
|
|
13
13
|
const fromList = getFrom(action)
|
|
14
|
-
const
|
|
14
|
+
const allowed = fromList.filter(from => {
|
|
15
15
|
const value = from['#'] ? (statusEnum[from['#']]?.val ?? statusEnum[from['#']]['$path'].at(-1)) : from
|
|
16
|
-
return
|
|
16
|
+
return result[statusElementName] === value
|
|
17
17
|
})
|
|
18
|
-
return
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function isCurrentStatusInFrom(subject, action, statusElementName, statusEnum) {
|
|
22
|
-
const cond = buildAllowedCondition(action, statusElementName, statusEnum)
|
|
23
|
-
const parsedXpr = cds.parse.expr(cond)
|
|
24
|
-
const dbEntity = await SELECT.one.from(subject).where(parsedXpr)
|
|
25
|
-
return dbEntity !== undefined
|
|
18
|
+
return allowed.length
|
|
26
19
|
}
|
|
27
20
|
|
|
28
21
|
async function checkStatus(subject, action, statusElementName, statusEnum) {
|
|
29
|
-
const
|
|
22
|
+
const result = await SELECT.one.from(subject)
|
|
23
|
+
if (!result) cds.error(404)
|
|
24
|
+
|
|
25
|
+
const allowed = isCurrentStatusInFrom(result, action, statusElementName, statusEnum)
|
|
30
26
|
if (!allowed) {
|
|
31
27
|
const from = getFrom(action)
|
|
32
28
|
const fromValues = JSON.stringify(from.flatMap(el => Object.values(el)))
|
|
@@ -360,9 +360,15 @@ async function validate_input(req) {
|
|
|
360
360
|
|
|
361
361
|
const errs = cds.validate(req.data, req.target, assertOptions)
|
|
362
362
|
if (errs) {
|
|
363
|
+
if (errs.some(e => e.message in EARLY_REJECT_CODES)) {
|
|
364
|
+
// ensure to use the orginal req.error, not the one monkey patched for draft messages
|
|
365
|
+
// REVISIT: this is an ugly workaround -> fix in lean draft please!
|
|
366
|
+
const errorFn = cds.Request.prototype.error.bind(req)
|
|
367
|
+
errs.forEach(err => errorFn(err))
|
|
368
|
+
req.reject()
|
|
369
|
+
}
|
|
363
370
|
errs.forEach(err => req.error(err))
|
|
364
|
-
|
|
365
|
-
else return
|
|
371
|
+
return
|
|
366
372
|
}
|
|
367
373
|
|
|
368
374
|
// -------------------------------------------------
|
|
@@ -705,7 +705,7 @@ const draftHandle = async function (req) {
|
|
|
705
705
|
if (typeof rootEntityName === 'object') rootEntityName = rootEntityName.id
|
|
706
706
|
const rootEntity = this.model.definitions[rootEntityName]
|
|
707
707
|
|
|
708
|
-
const isNewDraftViaActionEnabled = cds.env.fiori.
|
|
708
|
+
const isNewDraftViaActionEnabled = cds.env.fiori.direct_crud ?? false
|
|
709
709
|
let newDraftAction = rootEntity['@Common.DraftRoot.NewAction']
|
|
710
710
|
if (typeof newDraftAction != 'string' || !newDraftAction.length) newDraftAction = false
|
|
711
711
|
else newDraftAction = newDraftAction.split('.').pop()
|
|
@@ -979,7 +979,7 @@ const draftHandle = async function (req) {
|
|
|
979
979
|
if (error.code) errors.push({ ...error })
|
|
980
980
|
if (error.details) errors.push(...error.details.map(e => ({ ...e })))
|
|
981
981
|
}
|
|
982
|
-
const nextDraftMessages = compileUpdatedDraftMessages(errors, persistedDraftMessages, {}, draftRef)
|
|
982
|
+
const nextDraftMessages = compileUpdatedDraftMessages(errors, persistedDraftMessages, _req.data || {}, draftRef)
|
|
983
983
|
await cds.tx(async () => {
|
|
984
984
|
await UPDATE('DRAFT.DraftAdministrativeData')
|
|
985
985
|
.set({ DraftMessages: nextDraftMessages })
|
|
@@ -1762,12 +1762,16 @@ function _cleansed(query, model) {
|
|
|
1762
1762
|
|
|
1763
1763
|
// set the target to null to ensure cds.infer(...) correctly infer the
|
|
1764
1764
|
// target after query modifications
|
|
1765
|
-
draftsQuery
|
|
1765
|
+
Object.defineProperty(draftsQuery, '_target', { value: null, configurable: true, writable: true })
|
|
1766
1766
|
let draftSelect = draftsQuery.SELECT
|
|
1767
1767
|
let querySelect = query.SELECT
|
|
1768
1768
|
|
|
1769
1769
|
// in the $apply scenario, only the most inner nested SELECT data structure must be cleansed
|
|
1770
1770
|
while (draftSelect.from.SELECT) {
|
|
1771
|
+
// set the target to null to ensure cds.infer(...) correctly infer the
|
|
1772
|
+
// target after query modifications
|
|
1773
|
+
if (draftSelect.from._target)
|
|
1774
|
+
Object.defineProperty(draftSelect.from, '_target', { value: null, configurable: true, writable: true })
|
|
1771
1775
|
draftSelect = draftSelect.from.SELECT
|
|
1772
1776
|
querySelect = querySelect.from.SELECT
|
|
1773
1777
|
}
|
|
@@ -94,6 +94,7 @@ class KafkaService extends cds.MessagingService {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
async startListening() {
|
|
97
|
+
if (!this._listenToAll.value && !this.subscribedTopics.size) return
|
|
97
98
|
const consumer = this.client.consumer({ groupId: this.consumerGroup })
|
|
98
99
|
await consumer.connect()
|
|
99
100
|
|
|
@@ -50,7 +50,8 @@ module.exports = class ODataAdapter extends HttpAdapter {
|
|
|
50
50
|
const isJson = type.match(/^application\/json$/) && suffix !== ''
|
|
51
51
|
|
|
52
52
|
// POST with empty body is allowed if no content-type header is set
|
|
53
|
-
if (req.method === 'POST' && (!contentLength ||
|
|
53
|
+
if (req.method === 'POST' && (!contentLength || contentLength === '0' || isJson))
|
|
54
|
+
return jsonBodyParser(req, res, next)
|
|
54
55
|
|
|
55
56
|
if (req.method in { POST: 1, PUT: 1, PATCH: 1 }) {
|
|
56
57
|
if (!isJson) {
|
|
@@ -11,6 +11,8 @@ const resolveStructured = require('../../_runtime/common/utils/resolveStructured
|
|
|
11
11
|
// Same regex as peggy parser
|
|
12
12
|
const RELAXED_UUID_REGEX = /^[0-9a-z]{8}-?[0-9a-z]{4}-?[0-9a-z]{4}-?[0-9a-z]{4}-?[0-9a-z]{12}$/i
|
|
13
13
|
|
|
14
|
+
let _isRelevantKey
|
|
15
|
+
|
|
14
16
|
function _getDefinition(definition, name, namespace) {
|
|
15
17
|
return (
|
|
16
18
|
definition?.definitions?.[name] ||
|
|
@@ -336,7 +338,7 @@ function _processSegments(from, model, namespace, cqn, protocol) {
|
|
|
336
338
|
|
|
337
339
|
ref[i] = null
|
|
338
340
|
ref[i - keyCount] = base
|
|
339
|
-
incompleteKeys = keyCount < keys.filter(
|
|
341
|
+
incompleteKeys = keyCount < keys.filter(_isRelevantKey).length
|
|
340
342
|
} else {
|
|
341
343
|
// > entity or property (incl. nested) or navigation or action or function
|
|
342
344
|
keys = null
|
|
@@ -746,7 +748,7 @@ const _checkAllKeysProvided = (params, entity) => {
|
|
|
746
748
|
|
|
747
749
|
if (!keysOfEntity) return
|
|
748
750
|
for (const keyOfEntity of keysOfEntity) {
|
|
749
|
-
if (keyOfEntity
|
|
751
|
+
if (_isRelevantKey(keyOfEntity) && !(keyOfEntity in params)) {
|
|
750
752
|
if (isView && entity.params[keyOfEntity].default) {
|
|
751
753
|
// will be added later?
|
|
752
754
|
continue
|
|
@@ -876,6 +878,8 @@ function _validateQuery(SELECT, target, isOne, model) {
|
|
|
876
878
|
module.exports = (cqn, model, namespace, protocol) => {
|
|
877
879
|
if (!model) return cqn
|
|
878
880
|
|
|
881
|
+
_isRelevantKey ??= cds.env.fiori.direct_crud ? k => k !== 'IsActiveEntity' : () => true
|
|
882
|
+
|
|
879
883
|
const from = resolveFromSelect(cqn)
|
|
880
884
|
const { ref } = from
|
|
881
885
|
|
|
@@ -71,7 +71,7 @@ function hasValidProps(obj, ...names) {
|
|
|
71
71
|
return true
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
function _args(args, func, navPrefix) {
|
|
74
|
+
function _args(args, func = null, navPrefix = [], isLambda = false) {
|
|
75
75
|
const res = []
|
|
76
76
|
|
|
77
77
|
for (const cur of args) {
|
|
@@ -81,11 +81,11 @@ function _args(args, func, navPrefix) {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
if (hasValidProps(cur, 'func', 'args')) {
|
|
84
|
-
res.push(`${cur.func}(${_args(cur.args, cur.func, navPrefix)})`)
|
|
84
|
+
res.push(`${cur.func}(${_args(cur.args, cur.func, navPrefix, isLambda)})`)
|
|
85
85
|
} else if (hasValidProps(cur, 'ref')) {
|
|
86
|
-
res.push(_format(cur, null, null, null,
|
|
86
|
+
res.push(_format(cur, null, null, null, isLambda, func, navPrefix))
|
|
87
87
|
} else if (hasValidProps(cur, 'val')) {
|
|
88
|
-
res.push(_format(cur, null, null, null,
|
|
88
|
+
res.push(_format(cur, null, null, null, isLambda, func))
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
@@ -105,13 +105,13 @@ const _in = (column, /* in */ collection, target, kind, isLambda, navPrefix) =>
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
const _odataV2Func = (func, args, navPrefix) => {
|
|
108
|
+
const _odataV2Func = (func, args, navPrefix, isLambda) => {
|
|
109
109
|
switch (func) {
|
|
110
110
|
case 'contains':
|
|
111
111
|
// this doesn't support the contains signature with two collections as args, introduced in odata v4.01
|
|
112
|
-
return `substringof(${_args([args[1], args[0]], null, navPrefix)})`
|
|
112
|
+
return `substringof(${_args([args[1], args[0]], null, navPrefix, isLambda)})`
|
|
113
113
|
default:
|
|
114
|
-
return `${func}(${_args(args, func, navPrefix)})`
|
|
114
|
+
return `${func}(${_args(args, func, navPrefix, isLambda)})`
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
@@ -128,8 +128,8 @@ const _format = (cur, elementName, target, kind, isLambda, func, navPrefix = [])
|
|
|
128
128
|
if (hasValidProps(cur, 'func')) {
|
|
129
129
|
if (cur.args?.length) {
|
|
130
130
|
return kind === 'odata-v2'
|
|
131
|
-
? _odataV2Func(cur.func, cur.args, navPrefix)
|
|
132
|
-
: `${cur.func}(${_args(cur.args, cur.func)})`
|
|
131
|
+
? _odataV2Func(cur.func, cur.args, navPrefix, isLambda)
|
|
132
|
+
: `${cur.func}(${_args(cur.args, cur.func, navPrefix, isLambda)})`
|
|
133
133
|
}
|
|
134
134
|
return `${cur.func}()`
|
|
135
135
|
}
|