@sap/cds 7.6.1 → 7.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,18 @@
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.2 - 2024-02-09
8
+
9
+ ### Fixed
10
+
11
+ - Introduce i18n `BATCH_TOO_MANY_REQ` key for error message: "Batch request contains too many requests"
12
+ - Properly handle `$orderby` in lean draft
13
+ - View resolving in combination with `@cap-js/cds-db`
14
+ - Allow `cds.requires.someService.outbox` to be a string
15
+ - `cds.log`: errors, when not the first argument, were considered objects carrying custom fields
16
+ - `accept` header parsing for OData requests if quality factor `q` is included
17
+ - Broken links on index page if multiple protocols are configured
18
+
7
19
  ## Version 7.6.1 - 2024-01-30
8
20
 
9
21
  ### 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
- module.exports = { get html(){
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
- const odata = srv => srv._adapters && Object.keys(srv._adapters).find (a => a.startsWith ('odata'))
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}"> &rarr; ${l.name}</a>`)
72
- .join (' ')
61
+ .join (' ') : ''
73
62
  }
74
63
 
75
64
  function _project(){
@@ -3,6 +3,7 @@ const cds = require('../../..')
3
3
  const $remove = Symbol('remove')
4
4
 
5
5
  const _is_custom_fields = (arg, custom_fields) => {
6
+ if (!Object.keys(arg).length) return false
6
7
  for (const k in arg) if (!(k in custom_fields)) return false
7
8
  return true
8
9
  }
@@ -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)
@@ -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 occured in background job:`, err); return err }
58
+ const _error = (err, cds) => { cds.log().error(`ERROR occurred in background job:`, err); return err }
@@ -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, message: 'Batch request contains too many requests'})
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
 
@@ -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, isSubSelect) => {
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 (isSubSelect && mapped && newRef.length === 1) {
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, isSubselect)
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 = _cleanseWhere(cqn.orderBy, {}, DRAFT_ELEMENTS)
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
- this.LOG.error('ERROR occured in asynchronous event processing:', e)
60
+ e.message = 'ERROR occurred in asynchronous event processing: ' + e.message
61
+ this.LOG.error(e)
61
62
  }
62
63
  })
63
64
  }
@@ -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
- .emit({ event, ...json, inbound: true })
60
- .catch(e => this.LOG.error('ERROR occured in asynchronous event processing:', e))
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
- this.LOG.error('ERROR occured in asynchronous event processing:', e)
83
+ e.message = 'ERROR occurred in asynchronous event processing: ' + e.message
84
+ this.LOG.error(e)
84
85
  }
85
86
  })
86
87
  }
@@ -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 cds.requires.outbox === 'object' && cds.requires.outbox) || {},
251
- (typeof srv.options?.outbox === 'object' && srv.options.outbox) || {},
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
- // REVISIT: Also allow maxAttempts for in-memory outbox?
271
- context.on('succeeded', async () => {
272
- try {
273
- if (req.reply) await originalSrv.send(req)
274
- else await originalSrv.emit(req)
275
- } catch (e) {
276
- LOG.error('Emit failed', { event: req.event, cause: e })
277
- if (isStandardError(e)) cds.exit(1)
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "7.6.1",
3
+ "version": "7.6.2",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [