@sap/cds 8.9.0 → 8.9.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 +15 -0
- package/lib/compile/for/lean_drafts.js +1 -1
- package/lib/env/cds-env.js +1 -1
- package/lib/ql/SELECT.js +1 -1
- package/libx/_runtime/messaging/AMQPWebhookMessaging.js +1 -1
- package/libx/_runtime/messaging/common-utils/normalizeIncomingMessage.js +1 -2
- package/libx/_runtime/messaging/file-based.js +6 -6
- package/libx/_runtime/messaging/kafka.js +2 -3
- package/libx/_runtime/messaging/redis-messaging.js +1 -1
- package/libx/_runtime/messaging/service.js +11 -4
- package/libx/odata/parse/cqn2odata.js +40 -21
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@
|
|
|
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.2 - 2025-04-14
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- `forUpdate` will not consider `wait` if `ignoreLocked` is set
|
|
12
|
+
- Do not crash in case of custom `DraftAdministrativeData` table
|
|
13
|
+
|
|
14
|
+
## Version 8.9.1 - 2025-04-03
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- `cds.env` merging for `null` values
|
|
19
|
+
- Best-effort mechanisms for lambda support on OData V2 remote services (usage of functions in lambda expressions)
|
|
20
|
+
- Use extended model in `enterprise-messaging` inbound handlers
|
|
21
|
+
|
|
7
22
|
## Version 8.9.0 - 2025-03-31
|
|
8
23
|
|
|
9
24
|
### Added
|
|
@@ -144,7 +144,7 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
|
|
|
144
144
|
}
|
|
145
145
|
if (e.name === 'DraftAdministrativeData') {
|
|
146
146
|
// redirect to DraftAdministrativeData service entity
|
|
147
|
-
_redirect(newEl, active._service.entities.DraftAdministrativeData)
|
|
147
|
+
if (active._service?.entities.DraftAdministrativeData) _redirect(newEl, active._service.entities.DraftAdministrativeData)
|
|
148
148
|
}
|
|
149
149
|
Object.defineProperty (newEl,'parent',{value:draft,enumerable:false, configurable: true, writable: true})
|
|
150
150
|
|
package/lib/env/cds-env.js
CHANGED
|
@@ -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]
|
|
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
|
package/lib/ql/SELECT.js
CHANGED
|
@@ -152,8 +152,8 @@ class SELECT extends Whereable {
|
|
|
152
152
|
forUpdate ({ of, wait = cds.env.sql.lock_acquire_timeout || -1, ignoreLocked } = {}) {
|
|
153
153
|
const sfu = this.SELECT.forUpdate = {}
|
|
154
154
|
if (of) sfu.of = of.map (c => ({ref:c.split('.')}))
|
|
155
|
-
if (wait >= 0) sfu.wait = wait
|
|
156
155
|
if (ignoreLocked) sfu.ignoreLocked = true
|
|
156
|
+
else if (wait >= 0) sfu.wait = wait
|
|
157
157
|
return this
|
|
158
158
|
}
|
|
159
159
|
|
|
@@ -51,7 +51,7 @@ class AMQPWebhookMessaging extends MessagingService {
|
|
|
51
51
|
if (!msg._) msg._ = {}
|
|
52
52
|
msg._.topic = _topic
|
|
53
53
|
try {
|
|
54
|
-
await this.
|
|
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
|
|
@@ -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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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.
|
|
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.
|
|
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(
|
|
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(
|
|
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'
|
|
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
|
-
|
|
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(
|
|
187
|
-
} else if (!where)
|
|
188
|
-
|
|
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(
|
|
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
|
}
|