@sap/cds 7.6.1 → 7.6.3
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 +19 -0
- package/app/index.js +5 -16
- package/lib/compile/to/srvinfo.js +18 -13
- package/lib/log/format/aspects/als.js +1 -0
- package/lib/log/format/json.js +5 -1
- package/lib/req/cds-context.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/batch/BatchProcessor.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/http/HttpHeaderReader.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/TrustedResourceJsonSerializer.js +5 -6
- package/libx/_runtime/common/i18n/messages.properties +3 -0
- package/libx/_runtime/common/utils/resolveView.js +4 -10
- package/libx/_runtime/fiori/lean-draft.js +1 -1
- package/libx/_runtime/messaging/AMQPWebhookMessaging.js +2 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -3
- package/libx/_runtime/messaging/file-based.js +4 -3
- package/libx/_runtime/messaging/redis-messaging.js +2 -1
- package/libx/outbox/index.js +27 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,25 @@
|
|
|
4
4
|
- The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
5
5
|
- This project adheres to [Semantic Versioning](http://semver.org/).
|
|
6
6
|
|
|
7
|
+
## Version 7.6.3 - 2024-02-13
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Event Mesh webhooks now add standard `before` middlewares in case of custom authorization
|
|
12
|
+
- `compile.to.serviceinfo` no longer fails for services marked with `@protocol:'none'`. Such internal services are not shown in the output.
|
|
13
|
+
|
|
14
|
+
## Version 7.6.2 - 2024-02-09
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Introduce i18n `BATCH_TOO_MANY_REQ` key for error message: "Batch request contains too many requests"
|
|
19
|
+
- Properly handle `$orderby` in lean draft
|
|
20
|
+
- View resolving in combination with `@cap-js/cds-db`
|
|
21
|
+
- Allow `cds.requires.someService.outbox` to be a string
|
|
22
|
+
- `cds.log`: errors, when not the first argument, were considered objects carrying custom fields
|
|
23
|
+
- `accept` header parsing for OData requests if quality factor `q` is included
|
|
24
|
+
- Broken links on index page if multiple protocols are configured
|
|
25
|
+
|
|
7
26
|
## Version 7.6.1 - 2024-01-30
|
|
8
27
|
|
|
9
28
|
### Fixed
|
package/app/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
const cds = require('../lib')
|
|
2
2
|
const { find, path, fs } = cds.utils
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
const odata = srv => srv.endpoints?.[0]?.kind.startsWith('odata')
|
|
5
|
+
const metadata = srv => odata(srv) ? ` / <a href="${srv.path}/$metadata">$metadata</a>` : ``
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
const metadata = srv => odata(srv) ? ` / <a href="${srv.path}/$metadata">$metadata</a>` : ``
|
|
7
|
+
module.exports = { get html(){
|
|
8
8
|
|
|
9
9
|
let html = fs.readFileSync(path.join(__dirname,'index.html'),'utf-8')
|
|
10
10
|
// .replace ('{{subtitle}}', 'Version ' + cds.version)
|
|
@@ -26,17 +26,6 @@ module.exports = { get html(){
|
|
|
26
26
|
</ul>
|
|
27
27
|
`).join(''))
|
|
28
28
|
|
|
29
|
-
// add /graphql
|
|
30
|
-
if (cds.env.features.graphql) {
|
|
31
|
-
html = html.replace(/\n\s*<footer>/, `
|
|
32
|
-
|
|
33
|
-
<h3>
|
|
34
|
-
<a href="/graphql">/graphql</a>
|
|
35
|
-
</h3>
|
|
36
|
-
|
|
37
|
-
<footer>`)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
29
|
Object.defineProperty (this,'html',{value:html})
|
|
41
30
|
return html
|
|
42
31
|
|
|
@@ -64,12 +53,12 @@ function _entities_in (service) {
|
|
|
64
53
|
}
|
|
65
54
|
|
|
66
55
|
function _moreLinks (srv, entity) {
|
|
67
|
-
return (srv.$linkProviders || [])
|
|
56
|
+
return odata(srv) ? (srv.$linkProviders || [])
|
|
68
57
|
.map (linkProv => linkProv(entity))
|
|
69
58
|
.filter (l => l && l.href && l.name)
|
|
70
59
|
.sort ((l1, l2) => l1.name.localeCompare(l2))
|
|
71
60
|
.map (l => ` <a class="preview" href="${l.href}" title="${l.title||l.name}"> → ${l.name}</a>`)
|
|
72
|
-
.join (' ')
|
|
61
|
+
.join (' ') : ''
|
|
73
62
|
}
|
|
74
63
|
|
|
75
64
|
function _project(){
|
|
@@ -11,17 +11,20 @@ module.exports = (model, options={}) => {
|
|
|
11
11
|
const javaPrefix = _javaPrefix()
|
|
12
12
|
const isJavaProject = !!javaPrefix
|
|
13
13
|
|
|
14
|
-
cds.linked(model) .all ('service')
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (
|
|
14
|
+
cds.linked(model) .all ('service')
|
|
15
|
+
.filter(service => service['@protocol'] !== 'none') // 'none' means internal service
|
|
16
|
+
.forEach (service => {
|
|
17
|
+
if (isJavaProject) {
|
|
18
|
+
result.push(_makeJava(service))
|
|
19
|
+
if (isNodeProject) { // could be a node project as well (hybrid)
|
|
20
|
+
result.push(_makeNode(service))
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
else { // assume this is node
|
|
18
24
|
result.push(_makeNode(service))
|
|
19
25
|
}
|
|
20
26
|
}
|
|
21
|
-
|
|
22
|
-
result.push(_makeNode(service))
|
|
23
|
-
}
|
|
24
|
-
})
|
|
27
|
+
)
|
|
25
28
|
|
|
26
29
|
return result
|
|
27
30
|
|
|
@@ -46,11 +49,13 @@ module.exports = (model, options={}) => {
|
|
|
46
49
|
|
|
47
50
|
// the URL path that is *likely* effective at runtime
|
|
48
51
|
function _url4 (p) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
if (p) {
|
|
53
|
+
p = p.replace(/\\/g, '/') // handle Windows
|
|
54
|
+
.replace(/^\/+/, '') // strip leading
|
|
55
|
+
.replace(/\/+$/, '') // strip trailing
|
|
56
|
+
p += '/' // end with /
|
|
57
|
+
return p
|
|
58
|
+
}
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
function _javaPath (service) {
|
package/lib/log/format/json.js
CHANGED
|
@@ -65,10 +65,14 @@ module.exports = function format(module, level, ...args) {
|
|
|
65
65
|
}
|
|
66
66
|
toLog.timestamp = new Date()
|
|
67
67
|
|
|
68
|
+
// start message with leading string args (if any)
|
|
69
|
+
const i = args.findIndex(arg => typeof arg === 'object' && arg.message)
|
|
70
|
+
if (i > 0 && args.slice(0, i).every(arg => typeof arg === 'string')) toLog.msg = args.splice(0, i).join(' ')
|
|
71
|
+
|
|
68
72
|
// merge toLog with passed Error (or error-like object)
|
|
69
73
|
if (args.length && typeof args[0] === 'object' && args[0].message) {
|
|
70
74
|
const err = args.shift()
|
|
71
|
-
toLog.msg = err.message
|
|
75
|
+
toLog.msg = `${toLog.msg ? toLog.msg + ' ' : ''}${err.message}`
|
|
72
76
|
if (typeof err.stack === 'string' && !_is4xx(err)) toLog.stacktrace = err.stack.split(/\s*\r?\n\s*/)
|
|
73
77
|
if (Array.isArray(err.details))
|
|
74
78
|
for (const d of err.details)
|
package/lib/req/cds-context.js
CHANGED
|
@@ -55,4 +55,4 @@ module.exports = new class extends AsyncLocalStorage {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
const _error = (err, cds) => { cds.log().error(`ERROR
|
|
58
|
+
const _error = (err, cds) => { cds.log().error(`ERROR occurred in background job:`, err); return err }
|
package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/batch/BatchProcessor.js
CHANGED
|
@@ -36,7 +36,7 @@ class BatchProcessor {
|
|
|
36
36
|
* @returns {Promise} the overall result
|
|
37
37
|
*/
|
|
38
38
|
process () {
|
|
39
|
-
if(cds.env.odata.batch_limit < this._batchContext.getRequestList().length) return Promise.reject({statusCode: 429,
|
|
39
|
+
if(cds.env.odata.batch_limit < this._batchContext.getRequestList().length) return Promise.reject({statusCode: 429, code: 'BATCH_TOO_MANY_REQ'})
|
|
40
40
|
const request = this._batchContext.getRequest()
|
|
41
41
|
const componentManager = request.getService().getComponentManager()
|
|
42
42
|
|
package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/http/HttpHeaderReader.js
CHANGED
|
@@ -9,7 +9,7 @@ const HeaderInfo = require('./HeaderInfo')
|
|
|
9
9
|
const AcceptTypeInfo = require('../format/AcceptTypeInfo')
|
|
10
10
|
const CharsetInfo = require('../format/CharsetInfo')
|
|
11
11
|
|
|
12
|
-
const Q_PATTERN = new RegExp('^(?:(?:0(?:\\.\\d{0,3})?)|(?:1(?:\\.0{0,3})?))$')
|
|
12
|
+
const Q_PATTERN = new RegExp('^(?:(?:0(?:\\.\\d{0,3})?)|(?:1(?:\\.0{0,3})?)|(?:0?\\.\\d{0,3})|\\bq=\\.\\d{0,3}\\b)$')
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Reads header values as defined in RFCs 7231, 7230, 7240, and 5234.
|
|
@@ -303,6 +303,11 @@ class TrustedResourceJsonSerializer {
|
|
|
303
303
|
* @private
|
|
304
304
|
*/
|
|
305
305
|
_serializeStructure (result, type, data, expandItems, odataPath, structurePath) {
|
|
306
|
+
// enterprise search result? -> simply return what was provided
|
|
307
|
+
if (type._fqn?.name === 'sap_esh_SearchResult') {
|
|
308
|
+
return Object.assign(result, data)
|
|
309
|
+
}
|
|
310
|
+
|
|
306
311
|
for (const entityProp in data) {
|
|
307
312
|
// draft-enabled entities in odata v2 have own property DraftAdministrativeData_DraftUUID -> preserve if in data
|
|
308
313
|
if (entityProp === 'DraftAdministrativeData_DraftUUID') {
|
|
@@ -337,12 +342,6 @@ class TrustedResourceJsonSerializer {
|
|
|
337
342
|
const isCollection = Array.isArray(propertyValue)
|
|
338
343
|
const edmProperty = type.getStructuralProperty(entityProp)
|
|
339
344
|
|
|
340
|
-
// enterprise search result? -> simple return what was provided
|
|
341
|
-
if (type._fqn?.name === 'sap_esh_SearchResult') {
|
|
342
|
-
result[entityProp] = propertyValue
|
|
343
|
-
continue
|
|
344
|
-
}
|
|
345
|
-
|
|
346
345
|
if (edmProperty) {
|
|
347
346
|
let propertyType = edmProperty.getType()
|
|
348
347
|
|
|
@@ -81,6 +81,9 @@ INVALID_PATCH=PATCH is only allowed on a specific resource
|
|
|
81
81
|
INVALID_DELETE=DELETE is only supported on a specific resource
|
|
82
82
|
CRUD_VIA_NAVIGATION_NOT_SUPPORTED=CRUD via navigations is not yet supported
|
|
83
83
|
|
|
84
|
+
# OData protocol adapter
|
|
85
|
+
BATCH_TOO_MANY_REQ=Batch request contains too many requests
|
|
86
|
+
|
|
84
87
|
# draft
|
|
85
88
|
DRAFT_ALREADY_EXISTS=A draft for this entity already exists
|
|
86
89
|
DRAFT_NOT_EXISTING=No draft for this entity exists
|
|
@@ -234,7 +234,7 @@ const _newInsertColumns = (columns = [], transition) => {
|
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
// REVISIT: this hard-coding on ref indexes does not support path expressions
|
|
237
|
-
const _newWhereRef = (newWhereElement, transition, alias, tableName
|
|
237
|
+
const _newWhereRef = (newWhereElement, transition, alias, tableName) => {
|
|
238
238
|
const newRef = Array.isArray(newWhereElement.ref) ? [...newWhereElement.ref] : [newWhereElement.ref]
|
|
239
239
|
|
|
240
240
|
if (newRef.length > 1 && newRef[0] === alias) {
|
|
@@ -244,15 +244,9 @@ const _newWhereRef = (newWhereElement, transition, alias, tableName, isSubSelect
|
|
|
244
244
|
newRef[0] = transition.target.name
|
|
245
245
|
const mapped = transition.mapping.get(newRef[1])
|
|
246
246
|
if (mapped) newRef[1] = mapped.ref.join('_')
|
|
247
|
-
} else {
|
|
247
|
+
} else if (newRef.length === 1) {
|
|
248
248
|
const mapped = transition.mapping.get(newRef[0])
|
|
249
|
-
if (
|
|
250
|
-
// Add a table alias prefix only for not-yet-qualified refs
|
|
251
|
-
newRef.unshift(transition.target.name)
|
|
252
|
-
newRef[1] = mapped.ref[0]
|
|
253
|
-
} else {
|
|
254
|
-
if (mapped) newRef[0] = mapped.ref[0]
|
|
255
|
-
}
|
|
249
|
+
if (mapped) newRef[0] = mapped.ref.join('_')
|
|
256
250
|
}
|
|
257
251
|
|
|
258
252
|
newWhereElement.ref = newRef
|
|
@@ -280,7 +274,7 @@ const _newWhere = (where = [], transition, tableName, alias, isSubselect = false
|
|
|
280
274
|
}
|
|
281
275
|
|
|
282
276
|
if (newWhereElement.ref) {
|
|
283
|
-
_newWhereRef(newWhereElement, transition, alias, tableName
|
|
277
|
+
_newWhereRef(newWhereElement, transition, alias, tableName)
|
|
284
278
|
return newWhereElement
|
|
285
279
|
}
|
|
286
280
|
|
|
@@ -972,7 +972,7 @@ function _cleansed(query, model) {
|
|
|
972
972
|
}
|
|
973
973
|
|
|
974
974
|
if (target.drafts && cqn.where) cqn.where = _cleanseWhere(cqn.where, draftParams, DRAFT_ELEMENTS)
|
|
975
|
-
if (target.drafts && cqn.orderBy) cqn.orderBy =
|
|
975
|
+
if (target.drafts && cqn.orderBy) cqn.orderBy = _cleanseCols(cqn.orderBy, DRAFT_ELEMENTS, target) // allowed to reuse
|
|
976
976
|
if (cqn.columns) cqn.columns = _cleanseCols(cqn.columns, DRAFT_ELEMENTS, target)
|
|
977
977
|
return q
|
|
978
978
|
}
|
|
@@ -57,7 +57,8 @@ class AMQPWebhookMessaging extends MessagingService {
|
|
|
57
57
|
// In case of AMQP and Solace, the `failed` callback must be called
|
|
58
58
|
// with an error, otherwise there are problems with the redelivery count.
|
|
59
59
|
failed(new Error('processing failed'))
|
|
60
|
-
|
|
60
|
+
e.message = 'ERROR occurred in asynchronous event processing: ' + e.message
|
|
61
|
+
this.LOG.error(e)
|
|
61
62
|
}
|
|
62
63
|
})
|
|
63
64
|
}
|
|
@@ -17,9 +17,7 @@ class EndpointRegistry {
|
|
|
17
17
|
if (isSecured()) {
|
|
18
18
|
if (cds.requires.auth.impl) {
|
|
19
19
|
if (cds.env.requires.middlewares !== false) {
|
|
20
|
-
|
|
21
|
-
const custom_auth = require('../../../../lib/auth/index.js')
|
|
22
|
-
paths.forEach(path => cds.app.use(path, custom_auth()))
|
|
20
|
+
paths.forEach(path => cds.app.use(path, cds.middlewares.before)) // contains auth, trace, context
|
|
23
21
|
} else {
|
|
24
22
|
const impl = _require(cds.resolve(cds.requires.auth.impl))
|
|
25
23
|
paths.forEach(path => cds.app.use(path, impl))
|
|
@@ -55,9 +55,10 @@ class FileBasedMessaging extends MessagingService {
|
|
|
55
55
|
const event = this.subscribedTopics.get(topic)
|
|
56
56
|
if (!event) return
|
|
57
57
|
this.tx(tx =>
|
|
58
|
-
tx
|
|
59
|
-
.
|
|
60
|
-
|
|
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
|
+
})
|
|
61
62
|
)
|
|
62
63
|
} else other.push(each + '\n')
|
|
63
64
|
}
|
|
@@ -80,7 +80,8 @@ class RedisMessaging extends cds.MessagingService {
|
|
|
80
80
|
try {
|
|
81
81
|
await this.tx({ user: cds.User.privileged }, tx => tx.emit(msg))
|
|
82
82
|
} catch (e) {
|
|
83
|
-
|
|
83
|
+
e.message = 'ERROR occurred in asynchronous event processing: ' + e.message
|
|
84
|
+
this.LOG.error(e)
|
|
84
85
|
}
|
|
85
86
|
})
|
|
86
87
|
}
|
package/libx/outbox/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const cdsUser = 'cds.internal.user'
|
|
|
13
13
|
const $messageProcessorRegistered = Symbol('message processor registered')
|
|
14
14
|
const $outboxed = Symbol('outboxed')
|
|
15
15
|
const $unboxed = Symbol('unboxed')
|
|
16
|
+
const $stored_reqs = Symbol('stored_reqs')
|
|
16
17
|
|
|
17
18
|
const _get100NanosecondTimestampISOString = () => {
|
|
18
19
|
const [now, nanoseconds] = [new Date(), process.hrtime()[1]]
|
|
@@ -245,13 +246,20 @@ function outboxed(srv, customOpts) {
|
|
|
245
246
|
|
|
246
247
|
if (!new.target) Object.defineProperty(srv, $outboxed, { value: outboxedSrv })
|
|
247
248
|
|
|
249
|
+
let requiresOpts = cds.requires.outbox
|
|
250
|
+
let serviceOpts = srv.options?.outbox
|
|
251
|
+
|
|
252
|
+
if (typeof requiresOpts === 'string') requiresOpts = { kind: requiresOpts }
|
|
253
|
+
if (typeof serviceOpts === 'string') serviceOpts = { kind: serviceOpts }
|
|
254
|
+
|
|
248
255
|
const outboxOpts = Object.assign(
|
|
249
256
|
{},
|
|
250
|
-
(typeof
|
|
251
|
-
(typeof
|
|
257
|
+
(typeof requiresOpts === 'object' && requiresOpts) || {},
|
|
258
|
+
(typeof serviceOpts === 'object' && serviceOpts) || {},
|
|
252
259
|
customOpts || {}
|
|
253
260
|
)
|
|
254
261
|
|
|
262
|
+
|
|
255
263
|
outboxedSrv.handle = async function (req) {
|
|
256
264
|
const context = req.context || cds.context
|
|
257
265
|
if (outboxOpts.kind === 'persistent-outbox' && hasPersistentOutbox(context.tenant)) {
|
|
@@ -267,16 +275,23 @@ function outboxed(srv, customOpts) {
|
|
|
267
275
|
await writeInOutbox(srv.name, req, context)
|
|
268
276
|
return
|
|
269
277
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
278
|
+
if (!context[$stored_reqs]) {
|
|
279
|
+
context[$stored_reqs] = []
|
|
280
|
+
context.on('succeeded', async () => {
|
|
281
|
+
// REVISIT: Also allow maxAttempts for in-memory outbox?
|
|
282
|
+
for (const _req of context[$stored_reqs]) {
|
|
283
|
+
try {
|
|
284
|
+
if (req.reply) await originalSrv.send(req)
|
|
285
|
+
else await originalSrv.emit(req)
|
|
286
|
+
} catch (e) {
|
|
287
|
+
LOG.error('Emit failed', { event: req.event, cause: e })
|
|
288
|
+
if (isStandardError(e)) cds.exit(1)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
delete context[$stored_reqs]
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
context[$stored_reqs].push(req)
|
|
280
295
|
}
|
|
281
296
|
|
|
282
297
|
return outboxedSrv
|