@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +81 -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 +4 -1
  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/generic/input.js +6 -4
  29. package/libx/_runtime/common/utils/cqn.js +0 -24
  30. package/libx/_runtime/common/utils/normalizeTimestamp.js +2 -2
  31. package/libx/_runtime/common/utils/resolveView.js +8 -2
  32. package/libx/_runtime/common/utils/templateProcessor.js +10 -1
  33. package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +21 -9
  34. package/libx/_runtime/fiori/lean-draft.js +511 -379
  35. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +39 -35
  36. package/libx/_runtime/messaging/enterprise-messaging.js +2 -2
  37. package/libx/_runtime/remote/Service.js +4 -5
  38. package/libx/_runtime/ucl/Service.js +111 -15
  39. package/libx/common/utils/streaming.js +1 -1
  40. package/libx/odata/middleware/batch.js +8 -6
  41. package/libx/odata/middleware/create.js +2 -2
  42. package/libx/odata/middleware/delete.js +2 -2
  43. package/libx/odata/middleware/metadata.js +18 -11
  44. package/libx/odata/middleware/read.js +2 -2
  45. package/libx/odata/middleware/service-document.js +1 -1
  46. package/libx/odata/middleware/update.js +1 -1
  47. package/libx/odata/parse/afterburner.js +46 -36
  48. package/libx/odata/parse/cqn2odata.js +2 -6
  49. package/libx/odata/parse/grammar.peggy +91 -13
  50. package/libx/odata/parse/parser.js +1 -1
  51. package/libx/odata/utils/index.js +2 -2
  52. package/libx/odata/utils/readAfterWrite.js +2 -0
  53. package/libx/queue/TaskRunner.js +26 -1
  54. package/libx/queue/index.js +11 -1
  55. package/package.json +1 -1
  56. 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
+ }
@@ -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