@sap/cds 9.4.5 → 9.5.2
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 +79 -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 +3 -0
- 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 +236 -0
- package/libx/_runtime/common/generic/flows.js +168 -108
- 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 +24 -25
- package/libx/odata/parse/cqn2odata.js +2 -6
- package/libx/odata/parse/grammar.peggy +90 -12
- 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,77 @@
|
|
|
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.2 - 2025-12-09
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Usage of associations in `@assert`
|
|
12
|
+
- Usage of `@assert` on child entities
|
|
13
|
+
- Mocking of remote services using `srv.post('<url>')`
|
|
14
|
+
|
|
15
|
+
## Version 9.5.1 - 2025-12-01
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Draft child creation in case `cds.features.new_draft_via_action` is enabled
|
|
20
|
+
- Draft messages of declarative constraints
|
|
21
|
+
- Persisted draft messages in case of on-commit errors
|
|
22
|
+
- Async UCL tenant mapping for UCL SPII v2
|
|
23
|
+
- Quoting in `cds.compile.to.yaml`
|
|
24
|
+
|
|
25
|
+
## Version 9.5.0 - 2025-11-26
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- Support for Declarative Constraints (`@assert`; beta)
|
|
30
|
+
- Status Transition Flows (`@flow`; beta):
|
|
31
|
+
+ Aspect `sap.common.FlowHistory` automatically appended to entities with a `@to: $flow.previous`
|
|
32
|
+
+ Deactivate via `cds.features.history_for_flows=false`
|
|
33
|
+
+ Experimental: Via `cds.features.history_for_flows='all'`, all entities with a flow definition get appended
|
|
34
|
+
+ Note: FlowHistory is not maintained within drafts
|
|
35
|
+
+ Support for `@flow.status: <element name>` on entity level
|
|
36
|
+
+ UI annotation generation adds action to UI automatically (Object Page and List Report) but hidden in draft mode
|
|
37
|
+
+ Experimental support for CRUD flows:
|
|
38
|
+
+ Pseudo bound actions `CREATE` and `UPDATE` events can be flow-annotated
|
|
39
|
+
+ Note: The `CREATE` event cannot have a `@from` condition
|
|
40
|
+
+ Pseudo bound actions for drafts `NEW`, `EDIT`, `PATCH`, `DISCARD`, and `SAVE` can be flow-annotated
|
|
41
|
+
+ Note: Flow annotations inside drafts (`@from`, `@to`) are processed as in standard flow transitions, with the following exceptions:
|
|
42
|
+
+ The `NEW` event cannot have a `@from` condition
|
|
43
|
+
+ The `DISCARD` event cannot have a `@to` condition
|
|
44
|
+
- Support for pseudo protocols
|
|
45
|
+
- Support for Async UCL tenant mapping notification flow
|
|
46
|
+
- `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
|
+
- Support for `@Common.DraftRoot.NewAction` annotation with feature flag `cds.features.new_draft_via_action`
|
|
49
|
+
+ Generic collection bound action `draftNew` will be added to draft enabled entities
|
|
50
|
+
+ The action specified in the annotation will be rewritten into a draft `NEW` event
|
|
51
|
+
+ Active instances of draft enabled entities can be created directly via `POST`
|
|
52
|
+
- 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
|
|
53
|
+
- `ias-auth`: configurable name for XSUAA fallback's `cds.requires` entry
|
|
54
|
+
- `enterprise-messaging`: Support for scenario `ias-auth` with XSUAA fallback
|
|
55
|
+
- Service configuration through `VCAP_SERVICES` can now be supplied with `VCAP_SERVICES_FILE_PATH`. Note that this is an experimental CF feature.
|
|
56
|
+
|
|
57
|
+
### Changed
|
|
58
|
+
|
|
59
|
+
- Internal service `UCLService` moved into non-extensible namespace `cds.core`
|
|
60
|
+
- Status Transition Flows (`@flow`; beta):
|
|
61
|
+
+ UI annotation generation is on by default. Switch off via `cds.features.annotate_for_flows=false`.
|
|
62
|
+
+ Feature flag `cds.features.compile_for_flows` renamed to `cds.features.annotate_for_flows`
|
|
63
|
+
- `DELETE` requests during draft edit, that do not use containment will cause persisted draft messages to be cleared
|
|
64
|
+
|
|
65
|
+
### Fixed
|
|
66
|
+
|
|
67
|
+
- Correctly format values in a where clause send to an external OData service, when the expression order is: value, operator, reference
|
|
68
|
+
- cds.ql: tolerate extra spaces after in; parse RHS arrays of values as list
|
|
69
|
+
- CRUD-style API: `cds.read()` et al. used without `await` do not throw if there is no database connected
|
|
70
|
+
- Unnecessary compilation of model for edmx generation in multitenancy cases
|
|
71
|
+
- Using `req.notify`, `req.warn` and `req.info` in custom draft handlers by collecting validation errors in a dedicated collection
|
|
72
|
+
- `cds.auth` factory: passed options take precedence
|
|
73
|
+
- `cds deploy --dry` no longer produces broken SQL for DB functions like `days_between`.
|
|
74
|
+
- Read-after-write for create events during draft choreography will no longer include messages targeting siblings
|
|
75
|
+
- `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
|
+
- Escaping of JSON escape sequences during localization
|
|
77
|
+
|
|
7
78
|
## Version 9.4.5 - 2025-11-07
|
|
8
79
|
|
|
9
80
|
### Fixed
|
|
@@ -119,7 +190,7 @@
|
|
|
119
190
|
+ For `@sap/xssec`-based authentication strategies, `cds.context.user.authInfo` is an instance of `@sap/xssec`'s `SecurityContext`
|
|
120
191
|
- Support for status transition flows (`@flow`; alpha):
|
|
121
192
|
+ Generic handlers for validating entry (`@from`) and exit (`@to`) states
|
|
122
|
-
+ 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`
|
|
193
|
+
+ 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`
|
|
123
194
|
- Experimental support for consuming remote HCQL services (`cds.requires.<remote>.kind = 'hcql'`)
|
|
124
195
|
- Infrastructure for implementing the tenant mapping notification of Unified Customer Landscape's (UCL) Service Provider Integration Interface (SPII) API
|
|
125
196
|
+ Bootstrap the `UCLService` via `cds.requires.ucl = true` and implement the assign and unassign operations like so:
|
|
@@ -360,6 +431,13 @@
|
|
|
360
431
|
- Deprecated stripping of unnecessary topic prefix `topic:` in messaging
|
|
361
432
|
- Deprecated messaging `Outbox` class. Please use config or `cds.outboxed(srv)` to outbox your service.
|
|
362
433
|
|
|
434
|
+
## Version 8.9.7 - 2025-11-07
|
|
435
|
+
|
|
436
|
+
### Fixed
|
|
437
|
+
|
|
438
|
+
- Reject navigations in `$expand` without parsing the navigation path
|
|
439
|
+
- Aligned error handling for path navigation and `$expand`
|
|
440
|
+
|
|
363
441
|
## Version 8.9.6 - 2025-07-29
|
|
364
442
|
|
|
365
443
|
### 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
|
|
@@ -19,7 +19,6 @@ function _isCompositionBacklink(e) {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
23
22
|
// NOTE: Keep outside of the function to avoid calling the parser repeatedly
|
|
24
23
|
const { Draft } = cds.linked(`
|
|
25
24
|
entity ActiveEntity { key ID: UUID; }
|
|
@@ -42,9 +41,20 @@ const { Draft } = cds.linked(`
|
|
|
42
41
|
`).definitions
|
|
43
42
|
|
|
44
43
|
function DraftEntity4(active, name = active.name + '.drafts') {
|
|
44
|
+
// skip compositions with @odata.draft.enabled: false
|
|
45
|
+
const active_elements = {}
|
|
46
|
+
for (const each in active.elements) {
|
|
47
|
+
const element = active.elements[each]
|
|
48
|
+
if (element.isComposition && element['@odata.draft.enabled'] === false) {
|
|
49
|
+
// exclude, i.e., do nothing
|
|
50
|
+
} else {
|
|
51
|
+
active_elements[each] = element
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
45
55
|
const draft = Object.create(active, {
|
|
46
56
|
name: { value: name }, // REVISIT: lots of things break if we do that!
|
|
47
|
-
elements: { value: { ...
|
|
57
|
+
elements: { value: { ...active_elements, ...Draft.elements }, enumerable: true },
|
|
48
58
|
actives: { value: active },
|
|
49
59
|
query: { value: undefined }, // to not inherit that from active
|
|
50
60
|
// drafts: { value: undefined }, // to not inherit that from active -> doesn't work yet as the coding in lean-draft.js uses .drafts to identify both active and draft entities
|
|
@@ -58,6 +68,51 @@ function DraftEntity4(active, name = active.name + '.drafts') {
|
|
|
58
68
|
return draft
|
|
59
69
|
}
|
|
60
70
|
|
|
71
|
+
function addNewActionAnnotation(def) {
|
|
72
|
+
// Skip if a new action was defined manually
|
|
73
|
+
if (def.own('@Common.DraftRoot.NewAction')) return
|
|
74
|
+
|
|
75
|
+
// Skip for non draft roots
|
|
76
|
+
if (!def.own('@Common.DraftRoot.ActivationAction')) return
|
|
77
|
+
|
|
78
|
+
// TODO: This is perhaps THE ugliest way to automatically add a 'draftNew' action:
|
|
79
|
+
// TODO: > Instead, this should happen in cds-compiler/lib/transfrom/draft/odata.js
|
|
80
|
+
// TODO: > Within generateDrafts -> generateDraftForOData
|
|
81
|
+
// TODO: > Unfortunately, the 'createAction' utility does not currently allow creating collection bound actions
|
|
82
|
+
|
|
83
|
+
def['@Common.DraftRoot.NewAction'] = `${def._service.name}.draftNew`
|
|
84
|
+
|
|
85
|
+
// TODO: Find a better way than this:
|
|
86
|
+
// TODO: > By rewriting `draftNew` into a `NEW` req in draftHandle, action input validation is skipped
|
|
87
|
+
// TODO: > This causes issues if the action has parameters derived from key fields that should be mandatory
|
|
88
|
+
// TODO: > This will bubble up a NOT NULL CONSTRAINT error instead of raising a proper client error
|
|
89
|
+
// TODO: > This behavior also occurs for regular custom actions
|
|
90
|
+
|
|
91
|
+
// Format a list of cds action parameters, based on the entities key fields
|
|
92
|
+
// > E.g.: [ 'dayKey: Integer', 'nameKey: String', ...]
|
|
93
|
+
// > UUID keys are skipped as they are generated
|
|
94
|
+
const idParameters = Object.values(def.keys)
|
|
95
|
+
.filter(el => el.key && !el.virtual && el._type !== 'cds.UUID') // TODO: Ignore @UI.Hidden keys?
|
|
96
|
+
.map(el => `${el.name}: ${el._type}`)
|
|
97
|
+
|
|
98
|
+
// Use cds.linked to create a valid action definition
|
|
99
|
+
const { draftNew } = cds.linked(`
|
|
100
|
+
service Service {
|
|
101
|
+
entity ActiveEntity { } actions {
|
|
102
|
+
action draftNew(in: many $self, ${idParameters.join(', ')}) returns ActiveEntity;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
`).definitions['Service.ActiveEntity'].actions
|
|
106
|
+
|
|
107
|
+
draftNew.name = 'draftNew'
|
|
108
|
+
draftNew.returns = Object.create(def)
|
|
109
|
+
draftNew.returns.type = def.name
|
|
110
|
+
draftNew.parent = { name: def.name}
|
|
111
|
+
delete draftNew['$location']
|
|
112
|
+
|
|
113
|
+
def.actions['draftNew'] = draftNew
|
|
114
|
+
}
|
|
115
|
+
|
|
61
116
|
module.exports = function cds_compile_for_lean_drafts(csn) {
|
|
62
117
|
function _redirect(assoc, target) {
|
|
63
118
|
assoc.target = target.name
|
|
@@ -78,7 +133,7 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
|
|
|
78
133
|
if (d) return d
|
|
79
134
|
// We need to construct a fake draft entity definition
|
|
80
135
|
// We cannot use new cds.entity because runtime aspects would be missing
|
|
81
|
-
const draft = new DraftEntity4
|
|
136
|
+
const draft = new DraftEntity4(active, _draftEntity)
|
|
82
137
|
Object.defineProperty(model.definitions, _draftEntity, { value: draft })
|
|
83
138
|
Object.defineProperty(active, 'drafts', { value: draft })
|
|
84
139
|
|
|
@@ -123,6 +178,7 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
|
|
|
123
178
|
let _2manies
|
|
124
179
|
for (const each in draft.elements) {
|
|
125
180
|
const e = draft.elements[each]
|
|
181
|
+
|
|
126
182
|
// add @odata.draft.enclosed to filtered compositions
|
|
127
183
|
if (e.$enclosed) {
|
|
128
184
|
e['@odata.draft.enclosed'] = true
|
|
@@ -130,6 +186,7 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
|
|
|
130
186
|
_2manies ??= Object.keys(draft.elements).map(k => draft.elements[k]).filter(c => c.isComposition && c.is2many)
|
|
131
187
|
if (_2manies.find(c => c.name !== e.name && c.target.replace(/\.drafts$/, '') === e.target)) e['@odata.draft.enclosed'] = true
|
|
132
188
|
}
|
|
189
|
+
|
|
133
190
|
const newEl = Object.create(e)
|
|
134
191
|
if (
|
|
135
192
|
e.isComposition ||
|
|
@@ -139,19 +196,21 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
|
|
|
139
196
|
if (e._target['@odata.draft.enabled'] === false) continue // happens for texts if @fiori.draft.enabled is not set
|
|
140
197
|
_redirect(newEl, addDraftEntity(e._target, model))
|
|
141
198
|
}
|
|
199
|
+
|
|
142
200
|
if (e.name === 'DraftAdministrativeData') {
|
|
143
201
|
// redirect to DraftAdministrativeData service entity
|
|
144
202
|
if (active._service?.entities.DraftAdministrativeData) _redirect(newEl, active._service.entities.DraftAdministrativeData)
|
|
145
203
|
}
|
|
146
|
-
|
|
204
|
+
|
|
205
|
+
Object.defineProperty(newEl, 'parent', { value: draft, enumerable: false, configurable: true, writable: true })
|
|
147
206
|
|
|
148
207
|
for (const key in newEl) {
|
|
149
208
|
if (
|
|
150
209
|
key === '@mandatory' ||
|
|
151
|
-
key === '@Common.FieldControl' && newEl[key]?.['#'] === 'Mandatory' ||
|
|
210
|
+
(key === '@Common.FieldControl' && newEl[key]?.['#'] === 'Mandatory') ||
|
|
152
211
|
// key === '@Core.Immutable': Not allowed via UI anyway -> okay to cleanse them in PATCH
|
|
153
212
|
// REVISIT: Remove feature flag dependency: If active, validation errors will be degraded to messages and stored in draft admin data
|
|
154
|
-
(!active._service?.entities.DraftAdministrativeData.elements.DraftMessages && key.startsWith('@assert')) ||
|
|
213
|
+
(!active._service?.entities.DraftAdministrativeData.elements.DraftMessages && key.startsWith('@assert')) ||
|
|
155
214
|
key.startsWith('@PersonalData')
|
|
156
215
|
)
|
|
157
216
|
newEl[key] = undefined
|
|
@@ -182,12 +241,18 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
|
|
|
182
241
|
|
|
183
242
|
for (const name in csn.definitions) {
|
|
184
243
|
const def = csn.definitions[name]
|
|
244
|
+
|
|
245
|
+
// Do nothing for entities that are not draft-enabled
|
|
185
246
|
if (!_isDraft(def) || def['@cds.external']) continue
|
|
247
|
+
|
|
248
|
+
// Mark elements as virtual as required
|
|
186
249
|
def.elements.IsActiveEntity.virtual = true
|
|
187
250
|
def.elements.HasDraftEntity.virtual = true
|
|
188
251
|
def.elements.HasActiveEntity.virtual = true
|
|
189
|
-
if (def.elements.DraftAdministrativeData_DraftUUID) def.elements.DraftAdministrativeData_DraftUUID.virtual = true
|
|
190
252
|
def.elements.DraftAdministrativeData.virtual = true
|
|
253
|
+
if (def.elements.DraftAdministrativeData_DraftUUID) def.elements.DraftAdministrativeData_DraftUUID.virtual = true
|
|
254
|
+
|
|
255
|
+
// For Hierarchies: Exclude recursive compoisitions from draft tree
|
|
191
256
|
if (def.elements.LimitedDescendantCount) {
|
|
192
257
|
// for hierarchies: make sure recursive compositions are not part of the draft tree
|
|
193
258
|
for (const c in def.compositions) {
|
|
@@ -195,7 +260,10 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
|
|
|
195
260
|
if (comp.target === def.name) comp['@odata.draft.ignore'] = true
|
|
196
261
|
}
|
|
197
262
|
}
|
|
263
|
+
|
|
198
264
|
// will insert drafts entities, so that others can use `.drafts` even without incoming draft requests
|
|
199
265
|
addDraftEntity(def, csn)
|
|
266
|
+
|
|
267
|
+
if (cds.env.fiori.draft_new_action) addNewActionAnnotation(def)
|
|
200
268
|
}
|
|
201
269
|
}
|