@sap/cds 8.9.0 → 8.9.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,14 @@
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 8.9.1 - 2025-04-03
8
+
9
+ ### Fixed
10
+
11
+ - `cds.env` merging for `null` values
12
+ - Best-effort mechanisms for lambda support on OData V2 remote services (usage of functions in lambda expressions)
13
+ - Use extended model in `enterprise-messaging` inbound handlers
14
+
7
15
  ## Version 8.9.0 - 2025-03-31
8
16
 
9
17
  ### Added
@@ -470,7 +470,7 @@ function _merge (dst, src, _profiles) {
470
470
  else _merge (dst[p], v, _profiles)
471
471
  continue
472
472
  }
473
- else if (typeof v === 'string' && typeof dst[p] === 'object' && dst[p].kind) {
473
+ else if (typeof v === 'string' && typeof dst[p] === 'object' && dst[p]?.kind) {
474
474
  dst[p].kind = v // requires.db = 'foo' -> requires.db.kind = 'foo'
475
475
  }
476
476
  else if (v !== undefined) dst[p] = v
@@ -51,7 +51,7 @@ class AMQPWebhookMessaging extends MessagingService {
51
51
  if (!msg._) msg._ = {}
52
52
  msg._.topic = _topic
53
53
  try {
54
- await this.tx({ user: cds.User.privileged, tenant: msg.tenant, _: msg._ }, tx => tx.emit(msg))
54
+ await this.processInboundMsg({ tenant: msg.tenant, _: msg._ }, msg)
55
55
  done()
56
56
  } catch (e) {
57
57
  // In case of AMQP and Solace, the `failed` callback must be called
@@ -22,8 +22,7 @@ const normalizeIncomingMessage = message => {
22
22
 
23
23
  return {
24
24
  data,
25
- headers,
26
- inbound: true
25
+ headers
27
26
  }
28
27
  }
29
28
 
@@ -54,12 +54,12 @@ class FileBasedMessaging extends MessagingService {
54
54
  if (this.subscribedTopics.has(topic)) {
55
55
  const event = this.subscribedTopics.get(topic)
56
56
  if (!event) return
57
- this.tx(tx =>
58
- tx.emit({ event, ...json, inbound: true }).catch(e => {
59
- e.message = 'ERROR occurred in asynchronous event processing: ' + e.message
60
- this.LOG.error(e)
61
- })
62
- )
57
+ try {
58
+ await this.processInboundMsg({}, { event, ...json })
59
+ } catch (e) {
60
+ e.message = 'ERROR occurred in asynchronous event processing: ' + e.message
61
+ this.LOG.error(e)
62
+ }
63
63
  } else other.push(each + '\n')
64
64
  }
65
65
  } catch {
@@ -131,7 +131,7 @@ class KafkaService extends cds.MessagingService {
131
131
  msg.tenant = raw.message.headers['x-sap-cap-tenant-id']
132
132
  if (!msg.event) return
133
133
 
134
- await this.tx({ user: cds.User.privileged, tenant: msg.tenant }, tx => tx.emit(msg))
134
+ await this.processInboundMsg({ tenant: msg.tenant }, msg)
135
135
  } catch (e) {
136
136
  if (e.code === 'NO_HANDLER_FOUND') return // consume
137
137
  this.LOG.error('ERROR occured in asynchronous event processing:', e)
@@ -169,8 +169,7 @@ function _normalizeIncomingMessage(message) {
169
169
 
170
170
  return {
171
171
  data,
172
- headers,
173
- inbound: true
172
+ headers
174
173
  }
175
174
  }
176
175
 
@@ -77,7 +77,7 @@ class RedisMessaging extends cds.MessagingService {
77
77
  const msg = normalizeIncomingMessage(message)
78
78
  msg.event = topic
79
79
  try {
80
- await this.tx({ user: cds.User.privileged }, tx => tx.emit(msg))
80
+ await this.processInboundMsg({}, msg)
81
81
  } catch (e) {
82
82
  e.message = 'ERROR occurred in asynchronous event processing: ' + e.message
83
83
  this.LOG.error(e)
@@ -78,15 +78,22 @@ class MessagingService extends cds.Service {
78
78
 
79
79
  async handle(msg) {
80
80
  if (msg.inbound) {
81
- if (cds.model) {
82
- const ctx = cds.context
83
- ctx.model = await ExtendedModels.model4(ctx.tenant, ctx.features)
84
- }
85
81
  return super.handle(this.message4(msg))
86
82
  }
87
83
  return super.handle(msg)
88
84
  }
89
85
 
86
+ async processInboundMsg(ctx, msg) {
87
+ msg.inbound = true
88
+ if (!cds.context) cds.context = {}
89
+ if (ctx.tenant) cds.context.tenant = ctx.tenant
90
+ if (!ctx.user) ctx.user = cds.User.privileged
91
+ // this.tx expects cds.context.model
92
+ if (cds.model && (cds.env.requires.extensibility || cds.env.requires.toggles))
93
+ cds.context.model = await ExtendedModels.model4(ctx.tenant, ctx.features || {})
94
+ return await this.tx(ctx, tx => tx.emit(msg))
95
+ }
96
+
90
97
  on(event, cb) {
91
98
  const _event = _warnAndStripTopicPrefix(event)
92
99
  // save all subscribed topics (not needed for local-messaging)
@@ -74,7 +74,7 @@ function hasValidProps(obj, ...names) {
74
74
  return true
75
75
  }
76
76
 
77
- function _args(args, func) {
77
+ function _args(args, func, navPrefix) {
78
78
  const res = []
79
79
 
80
80
  for (const cur of args) {
@@ -84,9 +84,9 @@ function _args(args, func) {
84
84
  }
85
85
 
86
86
  if (hasValidProps(cur, 'func', 'args')) {
87
- res.push(`${cur.func}(${_args(cur.args, cur.func)})`)
87
+ res.push(`${cur.func}(${_args(cur.args, cur.func, navPrefix)})`)
88
88
  } else if (hasValidProps(cur, 'ref')) {
89
- res.push(_format(cur))
89
+ res.push(_format(cur, null, null, null, null, null, navPrefix))
90
90
  } else if (hasValidProps(cur, 'val')) {
91
91
  res.push(_format(cur, null, null, null, null, func))
92
92
  }
@@ -95,38 +95,44 @@ function _args(args, func) {
95
95
  return res.join(',')
96
96
  }
97
97
 
98
- const _in = (column, /* in */ collection, target, kind, isLambda) => {
99
- const ref = _format(column, null, target, kind, isLambda)
98
+ const _in = (column, /* in */ collection, target, kind, isLambda, navPrefix) => {
99
+ const ref = _format(column, null, target, kind, isLambda, null, navPrefix)
100
100
  // { list: [ { val: 1}, { val: 2}, { val: 3} ] }
101
101
  const values = collection.list
102
102
  if (values && values.length) {
103
103
  // REVISIT: what about OData `in` operator?
104
- const expressions = values.map(value => `${ref}%20eq%20${_format(value, ref, target, kind, isLambda)}`)
104
+ const expressions = values.map(
105
+ value => `${ref}%20eq%20${_format(value, ref, target, kind, isLambda, null, navPrefix)}`
106
+ )
105
107
  return expressions.join('%20or%20')
106
108
  }
107
109
  }
108
110
 
109
- const _odataV2Func = (func, args) => {
111
+ const _odataV2Func = (func, args, navPrefix) => {
110
112
  switch (func) {
111
113
  case 'contains':
112
114
  // this doesn't support the contains signature with two collections as args, introduced in odata v4.01
113
- return `substringof(${_args([args[1], args[0]])})`
115
+ return `substringof(${_args([args[1], args[0]], null, navPrefix)})`
114
116
  default:
115
- return `${func}(${_args(args, func)})`
117
+ return `${func}(${_args(args, func, navPrefix)})`
116
118
  }
117
119
  }
118
120
 
119
- const _format = (cur, elementName, target, kind, isLambda, func) => {
121
+ const _format = (cur, elementName, target, kind, isLambda, func, navPrefix = []) => {
120
122
  if (typeof cur !== 'object') return encodeURIComponent(formatVal(cur, elementName, target, kind))
121
123
  if (hasValidProps(cur, 'ref'))
122
- return encodeURIComponent(isLambda ? [LAMBDA_VARIABLE, ...cur.ref].join('/') : cur.ref[0].id || cur.ref.join('/'))
124
+ return encodeURIComponent(
125
+ isLambda ? [LAMBDA_VARIABLE, ...cur.ref].join('/') : cur.ref[0].id || [...navPrefix, ...cur.ref].join('/')
126
+ )
123
127
  if (hasValidProps(cur, 'val'))
124
128
  return encodeURIComponent(formatVal(cur.val, elementName, target, kind, func, cur.literal))
125
- if (hasValidProps(cur, 'xpr')) return `(${_xpr(cur.xpr, target, kind, isLambda)})`
129
+ if (hasValidProps(cur, 'xpr')) return `(${_xpr(cur.xpr, target, kind, isLambda, navPrefix)})`
126
130
  // REVISIT: How to detect the types for all functions?
127
131
  if (hasValidProps(cur, 'func')) {
128
132
  if (cur.args?.length) {
129
- return kind === 'odata-v2' ? _odataV2Func(cur.func, cur.args) : `${cur.func}(${_args(cur.args, cur.func)})`
133
+ return kind === 'odata-v2'
134
+ ? _odataV2Func(cur.func, cur.args, navPrefix)
135
+ : `${cur.func}(${_args(cur.args, cur.func)})`
130
136
  }
131
137
  return `${cur.func}()`
132
138
  }
@@ -138,7 +144,7 @@ const _isLambda = (cur, next) => {
138
144
  return last && hasValidProps(last, 'id')
139
145
  }
140
146
 
141
- function _xpr(expr, target, kind, isLambda) {
147
+ function _xpr(expr, target, kind, isLambda, navPrefix = []) {
142
148
  const res = []
143
149
  const openBrackets = []
144
150
 
@@ -165,10 +171,10 @@ function _xpr(expr, target, kind, isLambda) {
165
171
  const between = [expr[i - 1], 'gt', expr[i + 1], 'and', expr[i - 1], 'lt', expr[i + 3]]
166
172
  // cleanup previous ref
167
173
  res.pop()
168
- res.push(`(${_xpr(between, target, kind, isLambda)})`)
174
+ res.push(`(${_xpr(between, target, kind, isLambda, navPrefix)})`)
169
175
  i += 3
170
176
  } else if (cur === 'in') {
171
- const inExpr = _in(expr[i - 1], expr[i + 1], target, kind, isLambda)
177
+ const inExpr = _in(expr[i - 1], expr[i + 1], target, kind, isLambda, navPrefix)
172
178
  // cleanup previous ref
173
179
  res.pop()
174
180
  // when sending a where clause with "col in []" we currently ignore the where clause
@@ -179,20 +185,33 @@ function _xpr(expr, target, kind, isLambda) {
179
185
  } else if (_isLambda(cur, expr[i + 1])) {
180
186
  const { where } = expr[i + 1].ref.at(-1)
181
187
  const nav = expr[i + 1].ref.map(ref => ref?.id ?? ref).join('/')
182
- // odata-v2 does not support lambda expressions but successfactors allows filter like for to-one assocs
188
+
183
189
  if (kind === 'odata-v2') {
190
+ // odata-v2 does not support lambda expressions but successfactors allows filter like for to-one assocs
184
191
  cds.log('remote').info(`OData V2 does not support lambda expressions. Using path expression as best effort.`)
185
192
  isLambda = false
186
- res.push(`${nav}%2F${_xpr(where, target, kind)}`)
187
- } else if (!where) res.push(`${nav}/any()`)
188
- else res.push(`${nav}/any(${LAMBDA_VARIABLE}:${_xpr(where, target, kind, true)})`)
193
+ res.push(_xpr(where, target, kind, isLambda, [...navPrefix, nav]))
194
+ } else if (!where) {
195
+ res.push(`${nav}/any()`)
196
+ } else {
197
+ res.push(`${nav}/any(${LAMBDA_VARIABLE}:${_xpr(where, target, kind, true, navPrefix)})`)
198
+ }
199
+
189
200
  i++
190
201
  } else {
191
202
  res.push(OPERATORS[cur] || cur.toLowerCase())
192
203
  }
193
204
  } else {
194
205
  const ref = expr[i - 2]
195
- const formatted = _format(cur, ref?.ref && (ref.ref.length ? ref.ref : ref.ref[0]), target, kind, isLambda)
206
+ const formatted = _format(
207
+ cur,
208
+ ref?.ref && (ref.ref.length ? ref.ref : ref.ref[0]),
209
+ target,
210
+ kind,
211
+ isLambda,
212
+ null,
213
+ navPrefix
214
+ )
196
215
  if (formatted !== undefined) res.push(formatted)
197
216
  }
198
217
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "8.9.0",
3
+ "version": "8.9.1",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [