@sap/cds 9.6.0 → 9.6.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 CHANGED
@@ -4,6 +4,23 @@
4
4
  - The format is based on [Keep a Changelog](https://keepachangelog.com/).
5
5
  - This project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## Version 9.6.2 - 2026-01-08
8
+
9
+ ### Fixed
10
+
11
+ - Consider `@mandatory.message` for the error message, when a mandatory action / function parameter is not provided
12
+ - Respond with error code `400` when receiving requests that use `any()` / `all()` filters on an association to one
13
+ - Error message of unknown property check will no longer include `undefined` to identify structs without a name
14
+ - `enterprise-messaging`: Type error in check for unofficial XSUAA fallback
15
+ - Status Transition Flows: Exclusions for multiple projections
16
+
17
+ ## Version 9.6.1 - 2025-12-18
18
+
19
+ ### Fixed
20
+
21
+ - Status check in case of non-existing subject
22
+ - Gracefully handle bad value when calculating `@Core.OperationAvailable` from `@from`
23
+
7
24
  ## Version 9.6.0 - 2025-12-16
8
25
 
9
26
  ### Added
@@ -13,12 +13,18 @@ const getFrom = action => {
13
13
  }
14
14
 
15
15
  function addOperationAvailableToActions(actions, statusEnum, statusElementName) {
16
- for (const action of Object.values(actions)) {
16
+ action: for (const action of Object.values(actions)) {
17
17
  const fromList = getFrom(action)
18
- const conditions = fromList.map(from => {
18
+ const conditions = []
19
+ for (const from of fromList) {
19
20
  const value = from['#'] ? statusEnum[from['#']]?.val ?? from['#'] : from
20
- return `$self.${statusElementName} = ${typeof value === 'string' ? `'${value}'` : value}`
21
- })
21
+ if (typeof value !== 'string') {
22
+ const msg = `Error while constructing @Core.OperationAvailable for action "${action.name}" of "${action.parent.name}". Value of @from must either be an enum symbol or a raw string.`
23
+ cds.log('cds|edmx').warn(msg)
24
+ continue action
25
+ }
26
+ conditions.push(`$self.${statusElementName} = '${value}'`)
27
+ }
22
28
  const condition = `(${conditions.join(' OR ')})`
23
29
  const parsedXpr = cds.parse.expr(condition)
24
30
  action['@Core.OperationAvailable'] ??= {
@@ -74,7 +80,7 @@ function addActionsToTarget(targetAnnotation, entity, actions) {
74
80
  identification.push({
75
81
  $Type: 'UI.DataFieldForAction',
76
82
  Action: `${entity._service.name}.${actionName}`,
77
- Label: action["@Common.Label"] ?? action["@title"] ?? `{i18n>${actionName}}`,
83
+ Label: action['@Common.Label'] ?? action['@title'] ?? `{i18n>${actionName}}`,
78
84
  ...(entity['@odata.draft.enabled'] && {
79
85
  '@UI.Hidden': {
80
86
  '=': true,
@@ -180,8 +186,7 @@ module.exports = function cds_compile_for_flows(csn) {
180
186
  }
181
187
  }
182
188
 
183
- const extensions = new Set()
184
- const exclusions = new Set()
189
+ const to_be_extended = new Set()
185
190
 
186
191
  for (const name in csn.definitions) {
187
192
  const def = csn.definitions[name]
@@ -219,28 +224,35 @@ module.exports = function cds_compile_for_flows(csn) {
219
224
  if (base.elements?.transitions_) continue //> manually added -> don't interfere
220
225
 
221
226
  // 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))
227
+ to_be_extended.add(base_name)
226
228
  }
227
229
 
228
- if (extensions.size) {
230
+ if (to_be_extended.size) {
229
231
  // REVISIT: ensure sap.common.FlowHistory is there
230
232
  csn.definitions['sap.common.FlowHistory'] ??= JSON.parse(FlowHistory)
231
233
 
232
- const _extensions = [...extensions].map(extend => ({ extend, includes: ['sap.common.FlowHistory'] }))
233
- csn = cds.extend(csn).with({ extensions: _extensions })
234
+ const extensions = [...to_be_extended].map(extend => ({ extend, includes: ['sap.common.FlowHistory'] }))
235
+ const dsn = cds.extend(csn).with({ extensions })
234
236
 
235
237
  // 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
+ for (const each of to_be_extended) dsn.definitions[`${each}.transitions_`]['@cds.autoexpose'] = false
238
239
 
239
- if (exclusions.size) {
240
- for (const proj of exclusions) {
241
- delete csn.definitions[proj].elements.transitions_
242
- delete csn.definitions[`${proj}.transitions_`]
240
+ // hack for "excludes" not possible via extensions
241
+ for (const name in dsn.definitions) {
242
+ const _new = dsn.definitions[name]
243
+ if (
244
+ _new.kind !== 'entity' ||
245
+ to_be_extended.has(name.replace(/\.transitions_$/, '')) ||
246
+ (!name.match(/\.transitions_$/) && !_new.elements.transitions_)
247
+ ) {
248
+ continue
249
+ }
250
+ const _old = csn.definitions[name]
251
+ if (!_old) delete dsn.definitions[name]
252
+ else if (_new.elements.transitions_ && !_old.elements.transitions_) delete _new.elements.transitions_
243
253
  }
254
+
255
+ csn = dsn
244
256
  }
245
257
 
246
258
  // REVISIT: annotate all X.transitions_ with @odata.draft.enabled: false
@@ -28,14 +28,18 @@ class Validation {
28
28
  this.cleanse = options.cleanse !== false
29
29
  }
30
30
 
31
+ _targetFromPath (p,n) {
32
+ if (n === undefined) return p
33
+ if (n.row) return p + this.filter4(n) // > some/entity(ID=1)...
34
+ if (typeof n === 'number') return p + `[${n}]` // > some/array[1]...
35
+ if (p && n) return p+'/'+n // > some/element...
36
+ return n
37
+ }
38
+
31
39
  error (code, path, leaf, i18n, ...args) {
32
40
  const err = (this.errors ??= new ValidationErrors).add (code)
33
41
  if (this.options.path) path = [ this.options.path, ...path ] // e.g. used to prefix 'in/' for actions
34
- if (path) err.target = (!leaf ? path : path.concat(leaf)).reduce?.((p,n)=> (
35
- n?.row ? p + this.filter4(n) : //> some/entity(ID=1)...
36
- typeof n === 'number' ? p + `[${n}]` : //> some/array[1]...
37
- p && n ? p+'/'+n : n //> some/element...
38
- ),'')
42
+ if (path) err.target = (!leaf ? path : path.concat(leaf)).reduce?.((p, n) => this._targetFromPath(p, n),'')
39
43
  if (typeof i18n === 'string') err.i18n = i18n
40
44
  if (args.length) err.args = args
41
45
  return err
@@ -50,12 +54,16 @@ class Validation {
50
54
  else if (typeof v === 'string' && !entity.elements[k].isUUID || entity.elements[k]['@odata.Type'] === 'Edm.String') v = `'${v}'`
51
55
  filter.push (`${k}=${v}`)
52
56
  }
53
- return filter.length ? `(${filter})` : `[${index}]`
57
+ if (filter.length) return `(${filter})`
58
+ if (index !== undefined) return `[${index}]`
59
+ return ''
54
60
  }
55
61
 
56
- unknown(e,d,input) {
62
+ unknown(e,d,input, path) {
57
63
  if (this.protocol === 'odata' && e.match(/^\w*@\w+\.\w+$/)) return delete input[e] //> skip all annotations, like @odata.Type (according to OData spec annotations contain an "@" and a ".")
58
- d['@open'] || cds.error (`Property "${e}" does not exist in ${d.name}`, {status:400})
64
+ if (d['@open']) return
65
+ const target = d.name ?? path.reduce((p, n) => this._targetFromPath(p, n), '')
66
+ cds.error (`Property "${e}" does not exist in ${target}`, { status: 400, target })
59
67
  }
60
68
  }
61
69
 
@@ -209,12 +217,12 @@ class struct extends $any {
209
217
  if (each.$struct in data) continue // got struct for flattened element/fk, e.g. {author:{ID:1}}
210
218
  if ((each.elements && each.kind !== 'param' ) || each.foreignKeys) continue // skip struct-likes as we check flat payloads above, and deep payloads via struct.validate(), parameters don't have flat elements
211
219
  if (each.isAssociation) continue // unmanaged associations are always ignored (no value like)
212
- else ctx.error (ASSERT_MANDATORY, path_, each.name)
220
+ else ctx.error (ASSERT_MANDATORY, path_, each.name, each['@mandatory.message'] || each['@mandatory'])
213
221
  }
214
222
  // check values of given data
215
223
  for (let each in data) { // will work for structured payloads as well as flattened ones with universal CSN
216
224
  let /** @type {$any} */ d = Object.hasOwn(elements, each) && elements[each]
217
- if (!d || (d['@cds.api.ignore'] && ctx.rejectIgnore)) ctx.unknown (each, this, data)
225
+ if (!d || (d['@cds.api.ignore'] && ctx.rejectIgnore)) ctx.unknown (each, this, data, path_)
218
226
  else if (ctx.cleanse && d._is_readonly() && !d.key) delete data[each]
219
227
  else if (ctx.cleanse && d._is_immutable() && !ctx.insert && !path) delete data[each] // @Core.Immutable processed only for root, children are handled when knowing db state
220
228
  else if (d['@cds.validate'] !== false) d.validate (data[each], path_, ctx)
@@ -9,24 +9,20 @@ const FLOW_PREVIOUS = '$flow.previous'
9
9
 
10
10
  const $transitions_ = Symbol.for('transitions_')
11
11
 
12
- function buildAllowedCondition(action, statusElementName, statusEnum) {
12
+ function isCurrentStatusInFrom(result, action, statusElementName, statusEnum) {
13
13
  const fromList = getFrom(action)
14
- const conditions = fromList.map(from => {
14
+ const allowed = fromList.filter(from => {
15
15
  const value = from['#'] ? (statusEnum[from['#']]?.val ?? statusEnum[from['#']]['$path'].at(-1)) : from
16
- return `${statusElementName} = ${typeof value === 'string' ? `'${value}'` : value}`
16
+ return result[statusElementName] === value
17
17
  })
18
- return `(${conditions.join(' OR ')})`
19
- }
20
-
21
- async function isCurrentStatusInFrom(subject, action, statusElementName, statusEnum) {
22
- const cond = buildAllowedCondition(action, statusElementName, statusEnum)
23
- const parsedXpr = cds.parse.expr(cond)
24
- const dbEntity = await SELECT.one.from(subject).where(parsedXpr)
25
- return dbEntity !== undefined
18
+ return allowed.length
26
19
  }
27
20
 
28
21
  async function checkStatus(subject, action, statusElementName, statusEnum) {
29
- const allowed = await isCurrentStatusInFrom(subject, action, statusElementName, statusEnum)
22
+ const result = await SELECT.one.from(subject)
23
+ if (!result) cds.error(404)
24
+
25
+ const allowed = isCurrentStatusInFrom(result, action, statusElementName, statusEnum)
30
26
  if (!allowed) {
31
27
  const from = getFrom(action)
32
28
  const fromValues = JSON.stringify(from.flatMap(el => Object.values(el)))
@@ -29,7 +29,7 @@ class EndpointRegistry {
29
29
  cds.app.use(basePath, cds.middlewares.context())
30
30
  if (_IS_SECURED) {
31
31
  // unofficial XSUAA fallback
32
- if (cds.env.requires.messaging.xsuaa) {
32
+ if (cds.env.requires.messaging?.xsuaa) {
33
33
  cds.app.use(basePath, _xsuaa_fallback())
34
34
  } else {
35
35
  cds.app.use(basePath, cds.middlewares.auth())
@@ -56,7 +56,7 @@ const _normalize = (err, req, keep,
56
56
  const status = err.status || err.statusCode || _status4(err) || _reduce(details)
57
57
 
58
58
  // Determine error code and message
59
- const key = err.message || err.code || status
59
+ const key = err.message || err.code || status // REVISIT: Should we use the message over the code every time?
60
60
  const msg = i18n.messages.at (key, '', err.args) // lookup messages for log output from factory default texts
61
61
  if (msg && msg !== key) {
62
62
  if (typeof err.code !== 'string') err.code = key
@@ -85,7 +85,7 @@ function _addDefaultParams(ref, view) {
85
85
  }
86
86
 
87
87
  function getResolvedElement(entity, { ref }) {
88
- const element = entity.elements[ref[0]]
88
+ const element = entity.elements[ref[0]?.id ?? ref[0]]
89
89
 
90
90
  if (element && element.isAssociation && ref.length > 1) {
91
91
  return getResolvedElement(element._target, { ref: ref.slice(1) })
@@ -782,7 +782,9 @@ function _validateXpr(xpr, target, isOne, model, aliases = []) {
782
782
  .map(element => element.name)
783
783
  const newFoundAliases = []
784
784
 
785
- for (const x of xpr) {
785
+ for (let i = 0; i < xpr.length; i++) {
786
+ const x = xpr[i]
787
+
786
788
  if (x.as) newFoundAliases.push(x.as)
787
789
 
788
790
  if (x.xpr) {
@@ -795,9 +797,7 @@ function _validateXpr(xpr, target, isOne, model, aliases = []) {
795
797
 
796
798
  if (x.ref[0].where) {
797
799
  const element = target.elements[refName]
798
- if (!element) {
799
- _doesNotExistError(true, refName, target.name)
800
- }
800
+ if (!element) _doesNotExistError(true, refName, target.name)
801
801
  _validateXpr(x.ref[0].where, element._target ?? element.items, isOne, model)
802
802
  }
803
803
 
@@ -818,28 +818,34 @@ function _validateXpr(xpr, target, isOne, model, aliases = []) {
818
818
  }
819
819
  }
820
820
 
821
+ if (xpr[i - 1] === 'exists') {
822
+ const element = getResolvedElement(target, x)
823
+ if (!element.isAssociation)
824
+ cds.error({ status: 400, message: 'Invalid use of lambda any / all on a non-association' })
825
+ if (element.is2one)
826
+ cds.error({ status: 400, message: 'Invalid use of lambda any / all on an association to one' })
827
+ }
828
+
821
829
  if (x.expand) {
822
830
  let element = target.elements[refName]
831
+
823
832
  if (element.kind === 'element' && element.elements) {
824
833
  // structured
825
834
  _validateXpr([{ ref: x.ref.slice(1) }], element, isOne, model)
826
835
  element = _structProperty(x.ref.slice(1), element)
827
836
  }
837
+
828
838
  const target_service = _get_service_of(target)
839
+
829
840
  if (!element._target || element._target._service !== target_service) {
830
- _doesNotExistError(
831
- true,
832
- refName,
833
- target_service ? target.name.replace(target_service.name + '.', '') : target.name
834
- )
841
+ const targetName = target_service ? target.name.replace(target_service.name + '.', '') : target.name
842
+ _doesNotExistError(true, refName, targetName)
835
843
  }
844
+
836
845
  _validateXpr(x.expand, element._target, false, model)
837
- if (x.where) {
838
- _validateXpr(x.where, element._target, false, model)
839
- }
840
- if (x.orderBy) {
841
- _validateXpr(x.orderBy, element._target, false, model)
842
- }
846
+
847
+ if (x.where) _validateXpr(x.where, element._target, false, model)
848
+ if (x.orderBy) _validateXpr(x.orderBy, element._target, false, model)
843
849
  }
844
850
  }
845
851
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "9.6.0",
3
+ "version": "9.6.2",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [