@sap/cds 9.6.1 → 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,16 @@
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
+
7
17
  ## Version 9.6.1 - 2025-12-18
8
18
 
9
19
  ### Fixed
@@ -186,8 +186,7 @@ module.exports = function cds_compile_for_flows(csn) {
186
186
  }
187
187
  }
188
188
 
189
- const extensions = new Set()
190
- const exclusions = new Set()
189
+ const to_be_extended = new Set()
191
190
 
192
191
  for (const name in csn.definitions) {
193
192
  const def = csn.definitions[name]
@@ -225,28 +224,35 @@ module.exports = function cds_compile_for_flows(csn) {
225
224
  if (base.elements?.transitions_) continue //> manually added -> don't interfere
226
225
 
227
226
  // add aspect FlowHistory to db entity
228
- extensions.add(base_name)
229
-
230
- // hack for "excludes" not possible via extensions
231
- if (projections.length) projections.forEach(p => exclusions.add(p))
227
+ to_be_extended.add(base_name)
232
228
  }
233
229
 
234
- if (extensions.size) {
230
+ if (to_be_extended.size) {
235
231
  // REVISIT: ensure sap.common.FlowHistory is there
236
232
  csn.definitions['sap.common.FlowHistory'] ??= JSON.parse(FlowHistory)
237
233
 
238
- const _extensions = [...extensions].map(extend => ({ extend, includes: ['sap.common.FlowHistory'] }))
239
- 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 })
240
236
 
241
237
  // REVISIT: annotate all generated X.transitions_ with @cds.autoexpose: false
242
- for (const each of extensions) csn.definitions[`${each}.transitions_`]['@cds.autoexpose'] = false
243
- }
238
+ for (const each of to_be_extended) dsn.definitions[`${each}.transitions_`]['@cds.autoexpose'] = false
244
239
 
245
- if (exclusions.size) {
246
- for (const proj of exclusions) {
247
- delete csn.definitions[proj].elements.transitions_
248
- 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_
249
253
  }
254
+
255
+ csn = dsn
250
256
  }
251
257
 
252
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)
@@ -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.1",
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": [