@sap/cds 9.4.4 → 9.5.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 +81 -1
- package/_i18n/messages_en_US_saptrc.properties +1 -1
- package/common.cds +5 -2
- package/lib/compile/cds-compile.js +1 -0
- package/lib/compile/for/assert.js +64 -0
- package/lib/compile/for/flows.js +194 -58
- package/lib/compile/for/lean_drafts.js +75 -7
- package/lib/compile/parse.js +1 -1
- package/lib/compile/to/csn.js +6 -2
- package/lib/compile/to/edm.js +1 -1
- package/lib/compile/to/yaml.js +8 -1
- package/lib/dbs/cds-deploy.js +2 -2
- package/lib/env/cds-env.js +14 -4
- package/lib/env/defaults.js +6 -1
- package/lib/i18n/localize.js +1 -1
- package/lib/index.js +7 -7
- package/lib/req/event.js +4 -0
- package/lib/req/validate.js +4 -1
- package/lib/srv/cds.Service.js +2 -1
- package/lib/srv/middlewares/auth/ias-auth.js +5 -7
- package/lib/srv/middlewares/auth/index.js +1 -1
- package/lib/srv/protocols/index.js +7 -6
- package/lib/srv/srv-handlers.js +7 -0
- package/libx/_runtime/common/Service.js +5 -1
- package/libx/_runtime/common/constants/events.js +1 -0
- package/libx/_runtime/common/generic/assert.js +220 -0
- package/libx/_runtime/common/generic/flows.js +168 -108
- package/libx/_runtime/common/generic/input.js +6 -4
- package/libx/_runtime/common/utils/cqn.js +0 -24
- package/libx/_runtime/common/utils/normalizeTimestamp.js +2 -2
- package/libx/_runtime/common/utils/resolveView.js +8 -2
- package/libx/_runtime/common/utils/templateProcessor.js +10 -1
- package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +21 -9
- package/libx/_runtime/fiori/lean-draft.js +511 -379
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +39 -35
- package/libx/_runtime/messaging/enterprise-messaging.js +2 -2
- package/libx/_runtime/remote/Service.js +4 -5
- package/libx/_runtime/ucl/Service.js +111 -15
- package/libx/common/utils/streaming.js +1 -1
- package/libx/odata/middleware/batch.js +8 -6
- package/libx/odata/middleware/create.js +2 -2
- package/libx/odata/middleware/delete.js +2 -2
- package/libx/odata/middleware/metadata.js +18 -11
- package/libx/odata/middleware/read.js +2 -2
- package/libx/odata/middleware/service-document.js +1 -1
- package/libx/odata/middleware/update.js +1 -1
- package/libx/odata/parse/afterburner.js +46 -36
- package/libx/odata/parse/cqn2odata.js +2 -6
- package/libx/odata/parse/grammar.peggy +91 -13
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/index.js +2 -2
- package/libx/odata/utils/readAfterWrite.js +2 -0
- package/libx/queue/TaskRunner.js +26 -1
- package/libx/queue/index.js +11 -1
- package/package.json +1 -1
- package/srv/ucl-service.cds +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,79 @@
|
|
|
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.5.1 - 2025-12-01
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Draft child creation in case `cds.features.new_draft_via_action` is enabled
|
|
12
|
+
- Draft messages of declarative constraints
|
|
13
|
+
- Persisted draft messages in case of on-commit errors
|
|
14
|
+
- Async UCL tenant mapping for UCL SPII v2
|
|
15
|
+
- Quoting in `cds.compile.to.yaml`
|
|
16
|
+
|
|
17
|
+
## Version 9.5.0 - 2025-11-26
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- Support for Declarative Constraints (`@assert`; beta)
|
|
22
|
+
- Status Transition Flows (`@flow`; beta):
|
|
23
|
+
+ Aspect `sap.common.FlowHistory` automatically appended to entities with a `@to: $flow.previous`
|
|
24
|
+
+ Deactivate via `cds.features.history_for_flows=false`
|
|
25
|
+
+ Experimental: Via `cds.features.history_for_flows='all'`, all entities with a flow definition get appended
|
|
26
|
+
+ Note: FlowHistory is not maintained within drafts
|
|
27
|
+
+ Support for `@flow.status: <element name>` on entity level
|
|
28
|
+
+ UI annotation generation adds action to UI automatically (Object Page and List Report) but hidden in draft mode
|
|
29
|
+
+ Experimental support for CRUD flows:
|
|
30
|
+
+ Pseudo bound actions `CREATE` and `UPDATE` events can be flow-annotated
|
|
31
|
+
+ Note: The `CREATE` event cannot have a `@from` condition
|
|
32
|
+
+ Pseudo bound actions for drafts `NEW`, `EDIT`, `PATCH`, `DISCARD`, and `SAVE` can be flow-annotated
|
|
33
|
+
+ Note: Flow annotations inside drafts (`@from`, `@to`) are processed as in standard flow transitions, with the following exceptions:
|
|
34
|
+
+ The `NEW` event cannot have a `@from` condition
|
|
35
|
+
+ The `DISCARD` event cannot have a `@to` condition
|
|
36
|
+
- Support for pseudo protocols
|
|
37
|
+
- Support for Async UCL tenant mapping notification flow
|
|
38
|
+
- `flush` on a queued service returns a Promise that resolves when immediate work (i.e., not scheduled for future) is processed
|
|
39
|
+
- For draft-enabled entities, IsActiveEntity=true can be omitted from url
|
|
40
|
+
- Support for `@Common.DraftRoot.NewAction` annotation with feature flag `cds.features.new_draft_via_action`
|
|
41
|
+
+ Generic collection bound action `draftNew` will be added to draft enabled entities
|
|
42
|
+
+ The action specified in the annotation will be rewritten into a draft `NEW` event
|
|
43
|
+
+ Active instances of draft enabled entities can be created directly via `POST`
|
|
44
|
+
- Limited support for `$compute` query option: computed properties are only supported in `$select` at the root level, not in expanded entries or other query options. Only numeric operands and operators `add`, `sub`, `-`, `mul` and `div` are supported
|
|
45
|
+
- `ias-auth`: configurable name for XSUAA fallback's `cds.requires` entry
|
|
46
|
+
- `enterprise-messaging`: Support for scenario `ias-auth` with XSUAA fallback
|
|
47
|
+
- Service configuration through `VCAP_SERVICES` can now be supplied with `VCAP_SERVICES_FILE_PATH`. Note that this is an experimental CF feature.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- Internal service `UCLService` moved into non-extensible namespace `cds.core`
|
|
52
|
+
- Status Transition Flows (`@flow`; beta):
|
|
53
|
+
+ UI annotation generation is on by default. Switch off via `cds.features.annotate_for_flows=false`.
|
|
54
|
+
+ Feature flag `cds.features.compile_for_flows` renamed to `cds.features.annotate_for_flows`
|
|
55
|
+
- `DELETE` requests during draft edit, that do not use containment will cause persisted draft messages to be cleared
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
|
|
59
|
+
- Correctly format values in a where clause send to an external OData service, when the expression order is: value, operator, reference
|
|
60
|
+
- cds.ql: tolerate extra spaces after in; parse RHS arrays of values as list
|
|
61
|
+
- CRUD-style API: `cds.read()` et al. used without `await` do not throw if there is no database connected
|
|
62
|
+
- Unnecessary compilation of model for edmx generation in multitenancy cases
|
|
63
|
+
- Using `req.notify`, `req.warn` and `req.info` in custom draft handlers by collecting validation errors in a dedicated collection
|
|
64
|
+
- `cds.auth` factory: passed options take precedence
|
|
65
|
+
- `cds deploy --dry` no longer produces broken SQL for DB functions like `days_between`.
|
|
66
|
+
- Read-after-write for create events during draft choreography will no longer include messages targeting siblings
|
|
67
|
+
- `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`.
|
|
68
|
+
- Escaping of JSON escape sequences during localization
|
|
69
|
+
|
|
70
|
+
## Version 9.4.5 - 2025-11-07
|
|
71
|
+
|
|
72
|
+
### Fixed
|
|
73
|
+
|
|
74
|
+
- Custom error message for `@assert.range`
|
|
75
|
+
- For hierarchy requests with `$filter`, properly remove inner `where` clause
|
|
76
|
+
- Calling a parameterless function with parameter
|
|
77
|
+
- Aligned error handling for path navigation and `$expand`
|
|
78
|
+
- Input validation immediately rejects for `@mandatory`
|
|
79
|
+
|
|
7
80
|
## Version 9.4.4 - 2025-10-23
|
|
8
81
|
|
|
9
82
|
### Fixed
|
|
@@ -109,7 +182,7 @@
|
|
|
109
182
|
+ For `@sap/xssec`-based authentication strategies, `cds.context.user.authInfo` is an instance of `@sap/xssec`'s `SecurityContext`
|
|
110
183
|
- Support for status transition flows (`@flow`; alpha):
|
|
111
184
|
+ Generic handlers for validating entry (`@from`) and exit (`@to`) states
|
|
112
|
-
+ Automatic addition of necessary annotations for Fiori UIs (`@Common.SideEffects` and `@Core.OperationAvailable`) during compile to EDMX with feature flag `cds.features.compile_for_flows=true`
|
|
185
|
+
+ Automatic addition of necessary annotations for Fiori UIs (`@Common.SideEffects` and `@Core.OperationAvailable`) during compile to EDMX with feature flag `cds.features.compile_for_flows = true`
|
|
113
186
|
- Experimental support for consuming remote HCQL services (`cds.requires.<remote>.kind = 'hcql'`)
|
|
114
187
|
- Infrastructure for implementing the tenant mapping notification of Unified Customer Landscape's (UCL) Service Provider Integration Interface (SPII) API
|
|
115
188
|
+ Bootstrap the `UCLService` via `cds.requires.ucl = true` and implement the assign and unassign operations like so:
|
|
@@ -350,6 +423,13 @@
|
|
|
350
423
|
- Deprecated stripping of unnecessary topic prefix `topic:` in messaging
|
|
351
424
|
- Deprecated messaging `Outbox` class. Please use config or `cds.outboxed(srv)` to outbox your service.
|
|
352
425
|
|
|
426
|
+
## Version 8.9.7 - 2025-11-07
|
|
427
|
+
|
|
428
|
+
### Fixed
|
|
429
|
+
|
|
430
|
+
- Reject navigations in `$expand` without parsing the navigation path
|
|
431
|
+
- Aligned error handling for path navigation and `$expand`
|
|
432
|
+
|
|
353
433
|
## Version 8.9.6 - 2025-07-29
|
|
354
434
|
|
|
355
435
|
### Fixed
|
|
@@ -191,4 +191,4 @@ SINGLETON_NOT_NULLABLE=The singleton entity is not nullable
|
|
|
191
191
|
#XMSG: Action "acceptTravel" requires "travelStatus" to be "Open".
|
|
192
192
|
INVALID_FLOW_TRANSITION_SINGLE=35OYYAR7qBeFqZojBKMD7w_Action "{0}" requires "{1}" to be "{2}".
|
|
193
193
|
#XMSG: Action "cancelTravel" requires "travelStatus" to be one of the following values: Open,Accepted.
|
|
194
|
-
INVALID_FLOW_TRANSITION_MULTI=mqoLKfqWIZ4EX7lQDYHTzw_Action "{0}" requires "{1}" to be one of the following values: {2}.
|
|
194
|
+
INVALID_FLOW_TRANSITION_MULTI=mqoLKfqWIZ4EX7lQDYHTzw_Action "{0}" requires "{1}" to be one of the following values: {2}.
|
package/common.cds
CHANGED
|
@@ -97,13 +97,16 @@ context sap.common {
|
|
|
97
97
|
key locale: Locale;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
aspect FlowHistory
|
|
100
|
+
aspect FlowHistory @(
|
|
101
|
+
cds.persistence.skip : 'if-unused'
|
|
102
|
+
) {
|
|
103
|
+
@odata.draft.enabled : false
|
|
101
104
|
transitions_ : Composition of many {
|
|
102
105
|
key timestamp : managed:createdAt;
|
|
103
106
|
user : managed:createdBy;
|
|
104
107
|
status : String;
|
|
105
108
|
comment : String;
|
|
106
|
-
}
|
|
109
|
+
};
|
|
107
110
|
}
|
|
108
111
|
}
|
|
109
112
|
|
|
@@ -8,6 +8,7 @@ const compile = module.exports = Object.assign (cds_compile, {
|
|
|
8
8
|
get nodejs() { return super.nodejs = require('./for/nodejs') }
|
|
9
9
|
get lean_drafts() { return super.lean_drafts = require('./for/lean_drafts') }
|
|
10
10
|
get flows() { return super.flows = require('./for/flows') }
|
|
11
|
+
get assert() { return super.assert = require('./for/assert') }
|
|
11
12
|
get odata() { return super.odata = require('./for/odata') }
|
|
12
13
|
get sql() { return super.sql = require('./for/sql') }
|
|
13
14
|
},
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const $collected = Symbol.for('collected')
|
|
2
|
+
|
|
3
|
+
module.exports = function cds_compile_for_assert(csn) {
|
|
4
|
+
function _asserts4element(element, name, base) {
|
|
5
|
+
let xpr = element?.['@assert']?.xpr
|
|
6
|
+
if (!xpr) return
|
|
7
|
+
|
|
8
|
+
const inherited = base.elements[name]?.['@assert']?.xpr
|
|
9
|
+
if (inherited) {
|
|
10
|
+
// dedupe by splitting into "when ... then ..." blocks and filtering out own blocks already in @assert of projection target
|
|
11
|
+
const _inherited = _extract_cases(inherited).map(c => JSON.stringify(c))
|
|
12
|
+
const own = _extract_cases(xpr).filter(c => !_inherited.includes(JSON.stringify(c)))
|
|
13
|
+
if (own.length)
|
|
14
|
+
xpr = ['case', ...inherited.slice(1, -1), ...own.reduce((acc, cur) => (acc.push(...cur), acc), []), 'end']
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return xpr
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function _asserts4entity(entity) {
|
|
21
|
+
if (entity[$collected]) return
|
|
22
|
+
|
|
23
|
+
// buttom up collection of asserts -> process base entity first
|
|
24
|
+
projection: if (entity.projection || entity.query) {
|
|
25
|
+
// during compile for java, model is never linked -> no prototype chain
|
|
26
|
+
let base = (entity.projection || entity.query.SELECT)?.from?.ref?.[0]
|
|
27
|
+
if (!base) throw new Error(`Unable to determine base entity of ${entity.name}`)
|
|
28
|
+
|
|
29
|
+
base = csn.definitions[base]
|
|
30
|
+
|
|
31
|
+
// REVISIT: when compiling extensions, base may not be in the model -> OK to abort?
|
|
32
|
+
if (!base) break projection
|
|
33
|
+
|
|
34
|
+
_asserts4entity(base)
|
|
35
|
+
|
|
36
|
+
for (const each in entity.elements) {
|
|
37
|
+
const element = entity.elements[each]
|
|
38
|
+
if (element['@assert']) {
|
|
39
|
+
element['@assert']._xpr = element['@assert'].xpr
|
|
40
|
+
element['@assert'].xpr = _asserts4element(element, each, base)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
entity[$collected] = true
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const each in csn.definitions) {
|
|
49
|
+
const entity = csn.definitions[each]
|
|
50
|
+
if (entity.kind !== 'entity') continue
|
|
51
|
+
if (!entity.projection && !entity.query) continue
|
|
52
|
+
|
|
53
|
+
_asserts4entity(entity)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function _extract_cases(xpr) {
|
|
58
|
+
const cases = []
|
|
59
|
+
for (const each of xpr.slice(1, -1)) {
|
|
60
|
+
if (each === 'when') cases.push([])
|
|
61
|
+
cases.at(-1).push(each)
|
|
62
|
+
}
|
|
63
|
+
return cases
|
|
64
|
+
}
|
package/lib/compile/for/flows.js
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
// REVISIT: to be moved to cds-compiler later
|
|
2
|
-
|
|
3
1
|
const cds = require('../../..')
|
|
4
2
|
|
|
3
|
+
const { WELL_KNOWN_EVENTS } = require('../../req/event')
|
|
4
|
+
|
|
5
5
|
const FLOW_STATUS = '@flow.status'
|
|
6
6
|
const FROM = '@from'
|
|
7
7
|
const TO = '@to'
|
|
8
|
-
// backwards compat
|
|
9
|
-
const FLOW_FROM = '@flow.from'
|
|
10
|
-
const FLOW_TO = '@flow.to'
|
|
11
8
|
const FLOW_PREVIOUS = '$flow.previous'
|
|
12
9
|
|
|
13
10
|
const getFrom = action => {
|
|
14
|
-
let from = action[FROM]
|
|
11
|
+
let from = action[FROM]
|
|
15
12
|
return Array.isArray(from) ? from : [from]
|
|
16
13
|
}
|
|
17
14
|
|
|
@@ -19,7 +16,7 @@ function addOperationAvailableToActions(actions, statusEnum, statusElementName)
|
|
|
19
16
|
for (const action of Object.values(actions)) {
|
|
20
17
|
const fromList = getFrom(action)
|
|
21
18
|
const conditions = fromList.map(from => {
|
|
22
|
-
const value = from['#'] ?
|
|
19
|
+
const value = from['#'] ? statusEnum[from['#']]?.val ?? from['#'] : from
|
|
23
20
|
return `$self.${statusElementName} = ${typeof value === 'string' ? `'${value}'` : value}`
|
|
24
21
|
})
|
|
25
22
|
const condition = `(${conditions.join(' OR ')})`
|
|
@@ -51,6 +48,44 @@ function addSideEffectToActions(actions, statusElementName) {
|
|
|
51
48
|
}
|
|
52
49
|
}
|
|
53
50
|
|
|
51
|
+
function addActionsToTarget(targetAnnotation, entity, actions) {
|
|
52
|
+
const identification = (entity[targetAnnotation] ??= [])
|
|
53
|
+
|
|
54
|
+
for (const item of identification) {
|
|
55
|
+
if (
|
|
56
|
+
item.$Type === 'UI.DataFieldForAction' &&
|
|
57
|
+
!Object.hasOwn(item, '@UI.Hidden') &&
|
|
58
|
+
entity['@odata.draft.enabled'] === true
|
|
59
|
+
) {
|
|
60
|
+
item['@UI.Hidden'] = {
|
|
61
|
+
'=': true,
|
|
62
|
+
xpr: [{ ref: ['$self', 'IsActiveEntity'] }, '=', { val: false }]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const existingActionNames =
|
|
68
|
+
identification?.filter(item => item.$Type === 'UI.DataFieldForAction').map(item => item.Action.split('.').pop()) ??
|
|
69
|
+
[]
|
|
70
|
+
|
|
71
|
+
actions.forEach(action => {
|
|
72
|
+
const actionName = action.name
|
|
73
|
+
if (!existingActionNames.includes(actionName)) {
|
|
74
|
+
identification.push({
|
|
75
|
+
$Type: 'UI.DataFieldForAction',
|
|
76
|
+
Action: `${entity._service.name}.${actionName}`,
|
|
77
|
+
Label: action["@Common.Label"] ?? action["@title"] ?? `{i18n>${actionName}}`,
|
|
78
|
+
...(entity['@odata.draft.enabled'] && {
|
|
79
|
+
'@UI.Hidden': {
|
|
80
|
+
'=': true,
|
|
81
|
+
xpr: [{ ref: ['$self', 'IsActiveEntity'] }, '=', { val: false }]
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
54
89
|
function resolveStatusEnum(csn, codeElem) {
|
|
55
90
|
if (codeElem.enum !== undefined) return codeElem.enum
|
|
56
91
|
if (codeElem.type) {
|
|
@@ -71,10 +106,12 @@ function enhanceCSNwithFlowAnnotations4FE(csn) {
|
|
|
71
106
|
const fromActions = []
|
|
72
107
|
const toActions = []
|
|
73
108
|
for (const action of Object.values(entity.actions)) {
|
|
74
|
-
if (action[FROM]
|
|
75
|
-
if (action[TO]
|
|
109
|
+
if (action[FROM]) fromActions.push(action)
|
|
110
|
+
if (action[TO]) toActions.push(action)
|
|
76
111
|
}
|
|
77
112
|
if (fromActions.length === 0 && toActions.length === 0) continue
|
|
113
|
+
addActionsToTarget('@UI.Identification', entity, toActions)
|
|
114
|
+
addActionsToTarget('@UI.LineItem', entity, toActions)
|
|
78
115
|
if (element.enum) {
|
|
79
116
|
// Element is an enum directly
|
|
80
117
|
addSideEffectToActions(toActions, elemName)
|
|
@@ -86,8 +123,10 @@ function enhanceCSNwithFlowAnnotations4FE(csn) {
|
|
|
86
123
|
const codeElem = targetDef.elements.code
|
|
87
124
|
const statusEnum = resolveStatusEnum(csn, codeElem)
|
|
88
125
|
if (statusEnum) {
|
|
89
|
-
|
|
90
|
-
|
|
126
|
+
// REVISIT: is there no way to know from the CSN?
|
|
127
|
+
const statusElementName = csn._4java ? elemName + '.code' : elemName + '_code'
|
|
128
|
+
addSideEffectToActions(toActions, statusElementName)
|
|
129
|
+
addOperationAvailableToActions(fromActions, statusEnum, statusElementName)
|
|
91
130
|
}
|
|
92
131
|
}
|
|
93
132
|
} else if (element['@odata.foreignKey4']) {
|
|
@@ -103,66 +142,163 @@ function enhanceCSNwithFlowAnnotations4FE(csn) {
|
|
|
103
142
|
}
|
|
104
143
|
|
|
105
144
|
module.exports = function cds_compile_for_flows(csn) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
for (const name in csn.definitions) {
|
|
109
|
-
const def = csn.definitions[name]
|
|
145
|
+
const { history_for_flows } = cds.env.features
|
|
110
146
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
147
|
+
const _requires_history = !history_for_flows
|
|
148
|
+
? () => false
|
|
149
|
+
: history_for_flows === 'all'
|
|
150
|
+
? def => {
|
|
151
|
+
for (const each in def.elements) {
|
|
152
|
+
if (def.elements[each]['@flow.status']) {
|
|
153
|
+
return true
|
|
154
|
+
}
|
|
119
155
|
}
|
|
120
156
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
157
|
+
: def => {
|
|
158
|
+
for (const each in def.actions) {
|
|
159
|
+
const action = def.actions[each]
|
|
160
|
+
if (action && action[TO]?.['='] === FLOW_PREVIOUS) {
|
|
161
|
+
return true
|
|
162
|
+
}
|
|
127
163
|
}
|
|
128
164
|
}
|
|
165
|
+
|
|
166
|
+
/*
|
|
167
|
+
* 1. propagate flows for well-known actions from extensions to definitions
|
|
168
|
+
*/
|
|
169
|
+
if (csn.extensions) {
|
|
170
|
+
for (const ext of csn.extensions) {
|
|
171
|
+
if (!ext.actions) continue
|
|
172
|
+
const def = csn.definitions[ext.annotate]
|
|
173
|
+
if (!def || !def.kind || def.kind !== 'entity') continue
|
|
174
|
+
for (const each in ext.actions) {
|
|
175
|
+
if (!(each in WELL_KNOWN_EVENTS)) continue
|
|
176
|
+
def.actions ??= {}
|
|
177
|
+
def.actions[each] ??= { kind: 'action' }
|
|
178
|
+
Object.assign(def.actions[each], ext.actions[each])
|
|
179
|
+
}
|
|
129
180
|
}
|
|
130
|
-
|
|
181
|
+
}
|
|
131
182
|
|
|
132
|
-
|
|
133
|
-
|
|
183
|
+
const extensions = new Set()
|
|
184
|
+
const exclusions = new Set()
|
|
134
185
|
|
|
135
|
-
|
|
186
|
+
for (const name in csn.definitions) {
|
|
187
|
+
const def = csn.definitions[name]
|
|
136
188
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
189
|
+
/*
|
|
190
|
+
* 2. propagate @flow.status to respective element and make it @readonly
|
|
191
|
+
*/
|
|
192
|
+
if (def['@flow.status']?.['=']) {
|
|
193
|
+
const element = def.elements?.[def['@flow.status']['=']]
|
|
194
|
+
if (element) {
|
|
195
|
+
element['@flow.status'] = true
|
|
196
|
+
if (!('@readonly' in element)) element['@readonly'] = true
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!def.kind || def.kind !== 'entity' || !def.actions) continue
|
|
201
|
+
|
|
202
|
+
/*
|
|
203
|
+
* 3. normalize @from and @to annotations
|
|
204
|
+
*/
|
|
205
|
+
for (const each in def.actions) {
|
|
206
|
+
const action = def.actions[each]
|
|
207
|
+
if (action['@flow.from']) action['@from'] = action['@flow.from']
|
|
208
|
+
if (action['@flow.to']) action['@to'] = action['@flow.to']
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/*
|
|
212
|
+
* 4. automatically apply aspect FlowHistory if needed and not present yet
|
|
213
|
+
*/
|
|
214
|
+
if (!_requires_history(def)) continue
|
|
215
|
+
|
|
216
|
+
const projections = _get_projection_stack(name, csn)
|
|
217
|
+
const base_name = projections.pop()
|
|
218
|
+
const base = csn.definitions[base_name]
|
|
219
|
+
if (base.elements?.transitions_) continue //> manually added -> don't interfere
|
|
220
|
+
|
|
221
|
+
// add aspect FlowHistory to db entity
|
|
222
|
+
extensions.add(base_name)
|
|
223
|
+
|
|
224
|
+
// hack for "excludes" not possible via extensions
|
|
225
|
+
if (projections.length) projections.forEach(p => exclusions.add(p))
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (extensions.size) {
|
|
229
|
+
// REVISIT: ensure sap.common.FlowHistory is there
|
|
230
|
+
csn.definitions['sap.common.FlowHistory'] ??= JSON.parse(FlowHistory)
|
|
231
|
+
|
|
232
|
+
const _extensions = [...extensions].map(extend => ({ extend, includes: ['sap.common.FlowHistory'] }))
|
|
233
|
+
csn = cds.extend(csn).with({ extensions: _extensions })
|
|
234
|
+
|
|
235
|
+
// REVISIT: annotate all generated X.transitions_ with @cds.autoexpose: false
|
|
236
|
+
for (const each of extensions) csn.definitions[`${each}.transitions_`]['@cds.autoexpose'] = false
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (exclusions.size) {
|
|
240
|
+
for (const proj of exclusions) {
|
|
241
|
+
delete csn.definitions[proj].elements.transitions_
|
|
242
|
+
delete csn.definitions[`${proj}.transitions_`]
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// REVISIT: annotate all X.transitions_ with @odata.draft.enabled: false
|
|
247
|
+
for (const name in csn.definitions)
|
|
248
|
+
if (name.endsWith('.transitions_')) csn.definitions[name]['@odata.draft.enabled'] = false
|
|
249
|
+
|
|
250
|
+
return csn
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function _get_projection_stack(name, csn, stack = []) {
|
|
254
|
+
stack.push(name)
|
|
255
|
+
const def = csn.definitions[name]
|
|
256
|
+
if (def.projection || def.query) {
|
|
257
|
+
const base = (def.projection || def.query.SELECT)?.from?.ref?.[0]
|
|
258
|
+
if (!base) throw new Error(`Unable to determine base entity of ${name}`)
|
|
259
|
+
return _get_projection_stack(base, csn, stack)
|
|
260
|
+
}
|
|
261
|
+
return stack
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const FlowHistory = `{
|
|
265
|
+
"kind": "aspect",
|
|
266
|
+
"@cds.persistence.skip": "if-unused",
|
|
267
|
+
"elements": {
|
|
268
|
+
"transitions_": {
|
|
269
|
+
"@odata.draft.enabled": false,
|
|
140
270
|
"type": "cds.Composition",
|
|
141
271
|
"cardinality": { "max": "*" },
|
|
142
|
-
"targetAspect": {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
"
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
272
|
+
"targetAspect": {
|
|
273
|
+
"elements": {
|
|
274
|
+
"timestamp": {
|
|
275
|
+
"@cds.on.insert": { "=": "$now" },
|
|
276
|
+
"@UI.HiddenFilter": true,
|
|
277
|
+
"@UI.ExcludeFromNavigationContext": true,
|
|
278
|
+
"@Core.Immutable": true,
|
|
279
|
+
"@title": "{i18n>CreatedAt}",
|
|
280
|
+
"@readonly": true,
|
|
281
|
+
"key": true,
|
|
282
|
+
"type": "cds.Timestamp"
|
|
283
|
+
},
|
|
284
|
+
"user": {
|
|
285
|
+
"@cds.on.insert": { "=": "$user" },
|
|
286
|
+
"@UI.HiddenFilter": true,
|
|
287
|
+
"@UI.ExcludeFromNavigationContext": true,
|
|
288
|
+
"@Core.Immutable": true,
|
|
289
|
+
"@title": "{i18n>CreatedBy}",
|
|
290
|
+
"@readonly": true,
|
|
291
|
+
"@description": "{i18n>UserID.Description}",
|
|
292
|
+
"type": "cds.String",
|
|
293
|
+
"length": 255
|
|
294
|
+
},
|
|
295
|
+
"status": { "type": "cds.String" },
|
|
296
|
+
"comment": { "type": "cds.String" }
|
|
297
|
+
}
|
|
162
298
|
}
|
|
163
|
-
}
|
|
299
|
+
}
|
|
164
300
|
}
|
|
165
|
-
}
|
|
301
|
+
}`
|
|
166
302
|
|
|
167
303
|
module.exports.enhanceCSNwithFlowAnnotations4FE = enhanceCSNwithFlowAnnotations4FE
|
|
168
304
|
module.exports.getFrom = getFrom
|