@sap/cds 9.4.5 → 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.
Files changed (55) hide show
  1. package/CHANGELOG.md +71 -1
  2. package/_i18n/messages_en_US_saptrc.properties +1 -1
  3. package/common.cds +5 -2
  4. package/lib/compile/cds-compile.js +1 -0
  5. package/lib/compile/for/assert.js +64 -0
  6. package/lib/compile/for/flows.js +194 -58
  7. package/lib/compile/for/lean_drafts.js +75 -7
  8. package/lib/compile/parse.js +1 -1
  9. package/lib/compile/to/csn.js +6 -2
  10. package/lib/compile/to/edm.js +1 -1
  11. package/lib/compile/to/yaml.js +8 -1
  12. package/lib/dbs/cds-deploy.js +2 -2
  13. package/lib/env/cds-env.js +14 -4
  14. package/lib/env/defaults.js +6 -1
  15. package/lib/i18n/localize.js +1 -1
  16. package/lib/index.js +7 -7
  17. package/lib/req/event.js +4 -0
  18. package/lib/req/validate.js +3 -0
  19. package/lib/srv/cds.Service.js +2 -1
  20. package/lib/srv/middlewares/auth/ias-auth.js +5 -7
  21. package/lib/srv/middlewares/auth/index.js +1 -1
  22. package/lib/srv/protocols/index.js +7 -6
  23. package/lib/srv/srv-handlers.js +7 -0
  24. package/libx/_runtime/common/Service.js +5 -1
  25. package/libx/_runtime/common/constants/events.js +1 -0
  26. package/libx/_runtime/common/generic/assert.js +220 -0
  27. package/libx/_runtime/common/generic/flows.js +168 -108
  28. package/libx/_runtime/common/utils/cqn.js +0 -24
  29. package/libx/_runtime/common/utils/normalizeTimestamp.js +2 -2
  30. package/libx/_runtime/common/utils/resolveView.js +8 -2
  31. package/libx/_runtime/common/utils/templateProcessor.js +10 -1
  32. package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +21 -9
  33. package/libx/_runtime/fiori/lean-draft.js +511 -379
  34. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +39 -35
  35. package/libx/_runtime/messaging/enterprise-messaging.js +2 -2
  36. package/libx/_runtime/remote/Service.js +4 -5
  37. package/libx/_runtime/ucl/Service.js +111 -15
  38. package/libx/common/utils/streaming.js +1 -1
  39. package/libx/odata/middleware/batch.js +8 -6
  40. package/libx/odata/middleware/create.js +2 -2
  41. package/libx/odata/middleware/delete.js +2 -2
  42. package/libx/odata/middleware/metadata.js +18 -11
  43. package/libx/odata/middleware/read.js +2 -2
  44. package/libx/odata/middleware/service-document.js +1 -1
  45. package/libx/odata/middleware/update.js +1 -1
  46. package/libx/odata/parse/afterburner.js +24 -25
  47. package/libx/odata/parse/cqn2odata.js +2 -6
  48. package/libx/odata/parse/grammar.peggy +90 -12
  49. package/libx/odata/parse/parser.js +1 -1
  50. package/libx/odata/utils/index.js +2 -2
  51. package/libx/odata/utils/readAfterWrite.js +2 -0
  52. package/libx/queue/TaskRunner.js +26 -1
  53. package/libx/queue/index.js +11 -1
  54. package/package.json +1 -1
  55. package/srv/ucl-service.cds +2 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,69 @@
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
+
7
70
  ## Version 9.4.5 - 2025-11-07
8
71
 
9
72
  ### Fixed
@@ -119,7 +182,7 @@
119
182
  + For `@sap/xssec`-based authentication strategies, `cds.context.user.authInfo` is an instance of `@sap/xssec`'s `SecurityContext`
120
183
  - Support for status transition flows (`@flow`; alpha):
121
184
  + 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`
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`
123
186
  - Experimental support for consuming remote HCQL services (`cds.requires.<remote>.kind = 'hcql'`)
124
187
  - Infrastructure for implementing the tenant mapping notification of Unified Customer Landscape's (UCL) Service Provider Integration Interface (SPII) API
125
188
  + Bootstrap the `UCLService` via `cds.requires.ucl = true` and implement the assign and unassign operations like so:
@@ -360,6 +423,13 @@
360
423
  - Deprecated stripping of unnecessary topic prefix `topic:` in messaging
361
424
  - Deprecated messaging `Outbox` class. Please use config or `cds.outboxed(srv)` to outbox your service.
362
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
+
363
433
  ## Version 8.9.6 - 2025-07-29
364
434
 
365
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
+ }
@@ -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] ?? action[FLOW_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['#'] ? (statusEnum[from['#']]?.val ?? from['#']) : 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] || action[FLOW_FROM]) fromActions.push(action)
75
- if (action[TO] || action[FLOW_TO]) toActions.push(action)
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
- addSideEffectToActions(toActions, elemName + '.code')
90
- addOperationAvailableToActions(fromActions, statusEnum, elemName + '.code')
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
- let flowHistory
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
- if (!def.kind || def.kind !== 'entity' || !def.actions || def.elements?.transitions_) continue
112
-
113
- let history
114
- if (cds.env.features.history_for_flows === 'all') {
115
- for (const each in def.elements) {
116
- if (def.elements[each]['@flow.status']) {
117
- history = true
118
- break
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
- } else {
122
- for (const each in def.actions) {
123
- const action = def.actions[each]
124
- if (action && (action[TO]?.['='] === FLOW_PREVIOUS || action[FLOW_TO]?.['='] === FLOW_PREVIOUS)) {
125
- history = true
126
- break
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
- if (!history) continue
181
+ }
131
182
 
132
- flowHistory ??= csn.definitions['sap.common.FlowHistory']
133
- if (!flowHistory) cds.error('Cannot find definition sap.common.FlowHistory')
183
+ const extensions = new Set()
184
+ const exclusions = new Set()
134
185
 
135
- const { elements: aspectElements } = flowHistory.elements.transitions_.targetAspect
186
+ for (const name in csn.definitions) {
187
+ const def = csn.definitions[name]
136
188
 
137
- def.includes ??= []
138
- def.includes.push('sap.common.FlowHistory')
139
- def.elements.transitions_ = JSON.parse(`{
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": { "elements": ${JSON.stringify(aspectElements)} },
143
- "target": "${name}.transitions_",
144
- "on": [{ "ref": ["transitions_", "up_"] }, "=", { "ref": ["$self"] }]
145
- }`)
146
-
147
- const def_keys = Object.keys(def.elements)
148
- .filter(k => def.elements[k].key)
149
- .map(k => ({ ref: [k] }))
150
- csn.definitions[`${name}.transitions_`] = JSON.parse(`{
151
- "kind": "entity",
152
- "elements": {
153
- "up_": {
154
- "key": true,
155
- "type": "cds.Association",
156
- "cardinality": { "min": 1, "max": 1 },
157
- "target": "${name}",
158
- "keys": ${JSON.stringify(def_keys)},
159
- "notNull": true
160
- },
161
- ${JSON.stringify(aspectElements).slice(1, -1)}
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: { ...active.elements, ...Draft.elements }, enumerable: true },
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 (active, _draftEntity)
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
- Object.defineProperty (newEl,'parent',{value:draft,enumerable:false, configurable: true, writable: true})
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
  }