@sap/cds 8.0.3 → 8.1.0

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/_i18n/i18n_bg.properties +113 -0
  3. package/_i18n/i18n_el.properties +113 -0
  4. package/_i18n/i18n_he.properties +113 -0
  5. package/_i18n/i18n_hr.properties +113 -0
  6. package/_i18n/i18n_kk.properties +113 -0
  7. package/_i18n/i18n_sh.properties +113 -0
  8. package/_i18n/i18n_sk.properties +113 -0
  9. package/_i18n/i18n_sl.properties +113 -0
  10. package/_i18n/i18n_uk.properties +113 -0
  11. package/lib/compile/etc/_localized.js +8 -20
  12. package/lib/dbs/cds-deploy.js +1 -0
  13. package/lib/env/cds-requires.js +1 -0
  14. package/lib/env/defaults.js +1 -1
  15. package/lib/env/plugins.js +22 -6
  16. package/lib/linked/validate.js +1 -1
  17. package/lib/log/cds-log.js +2 -2
  18. package/lib/srv/protocols/hcql.js +5 -5
  19. package/lib/srv/protocols/http.js +23 -11
  20. package/lib/test/expect.js +1 -1
  21. package/lib/utils/cds-test.js +4 -4
  22. package/libx/_runtime/common/composition/insert.js +1 -1
  23. package/libx/_runtime/common/error/utils.js +2 -1
  24. package/libx/_runtime/common/generic/input.js +2 -5
  25. package/libx/_runtime/common/generic/stream.js +18 -3
  26. package/libx/_runtime/common/utils/cqn2cqn4sql.js +5 -2
  27. package/libx/_runtime/db/query/read.js +18 -9
  28. package/libx/_runtime/fiori/lean-draft.js +2 -2
  29. package/libx/_runtime/hana/customBuilder/CustomReferenceBuilder.js +1 -1
  30. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
  31. package/libx/_runtime/messaging/event-broker.js +76 -24
  32. package/libx/common/assert/utils.js +1 -57
  33. package/libx/odata/middleware/batch.js +86 -40
  34. package/libx/odata/middleware/body-parser.js +2 -3
  35. package/libx/odata/middleware/operation.js +11 -11
  36. package/libx/odata/middleware/read.js +1 -1
  37. package/libx/odata/parse/grammar.peggy +6 -1
  38. package/libx/odata/parse/parser.js +1 -1
  39. package/libx/odata/utils/metadata.js +18 -44
  40. package/libx/rest/middleware/error.js +9 -2
  41. package/libx/rest/middleware/parse.js +1 -1
  42. package/package.json +1 -1
  43. package/libx/common/assert/index.js +0 -228
  44. package/libx/common/assert/type-relaxed.js +0 -39
@@ -13,8 +13,6 @@ const HTTP_METHODS = { GET: 1, POST: 1, PUT: 1, PATCH: 1, DELETE: 1 }
13
13
  const CT = { JSON: 'application/json', MULTIPART: 'multipart/mixed' }
14
14
  const CRLF = '\r\n'
15
15
 
16
- const CONTINUE_ON_ERROR = /odata\.continue-on-error/
17
-
18
16
  /*
19
17
  * common
20
18
  */
@@ -99,16 +97,17 @@ const _validateBatch = body => {
99
97
  _validateProperty('dependent request ID', dependsOnId, 'string')
100
98
 
101
99
  const dependency = ids[dependsOnId]
102
- if (!dependency)
103
- throw _deserializationError(`Request ID '${dependsOnId}' used in dependsOn has not been defined before.`)
104
-
105
- const dependencyAtomicityGroup = dependency.atomicityGroup
106
- if (
107
- dependencyAtomicityGroup &&
108
- dependencyAtomicityGroup !== atomicityGroup &&
109
- !request.dependsOn.includes(dependencyAtomicityGroup)
110
- )
111
- request.dependsOn.push(dependencyAtomicityGroup)
100
+ if (!dependency) {
101
+ throw _deserializationError(
102
+ `"${dependsOnId}" does not match the id or atomicity group of any preceding request`
103
+ )
104
+ }
105
+
106
+ // automatically add the atomicityGroup of the dependency as a dependency (actually a client error)
107
+ const dag = dependency.atomicityGroup
108
+ if (dag && dag !== atomicityGroup && !request.dependsOn.includes(dag)) {
109
+ request.dependsOn.push(dag)
110
+ }
112
111
  })
113
112
  }
114
113
 
@@ -227,7 +226,7 @@ const _getNextForLookalike = lookalike => {
227
226
  const _transaction = async srv => {
228
227
  return new Promise(res => {
229
228
  const ret = {}
230
- const _tx = srv.tx(
229
+ const _tx = (ret._tx = srv.tx(
231
230
  async () =>
232
231
  (ret.promise = new Promise((resolve, reject) => {
233
232
  const proms = []
@@ -253,10 +252,49 @@ const _transaction = async srv => {
253
252
  }
254
253
  res(ret)
255
254
  }))
256
- )
255
+ ))
257
256
  })
258
257
  }
259
258
 
259
+ const _tx_done = async (tx, responses, isJson) => {
260
+ let rejected
261
+ try {
262
+ rejected = await tx.done()
263
+ } catch (e) {
264
+ // here, the commit was rejected even though all requests were successful (e.g., by custom handler or db consistency check)
265
+ rejected = 'rejected'
266
+ // construct commit error
267
+ let statusCode = e.statusCode || e.status || (e.code && Number(e.code))
268
+ if (isNaN(statusCode)) statusCode = 500
269
+ const code = String(e.code || statusCode)
270
+ const message = e.message || 'Internal Server Error'
271
+ const error = { error: { ...e, code, message } }
272
+ // replace all responses with commit error
273
+ for (const res of responses) {
274
+ res.status = 'fail'
275
+ // REVISIT: should error go through any error middleware/ customization logic?
276
+ if (isJson) {
277
+ let txt = ''
278
+ for (let i = 0; i < res.txt.length; i++) txt += Buffer.isBuffer(res.txt[i]) ? res.txt[i].toString() : res.txt[i]
279
+ txt = JSON.parse(txt)
280
+ txt.status = statusCode
281
+ txt.body = error
282
+ // REVISIT: content-length needed? not there in multipart case...
283
+ delete txt.headers['content-length']
284
+ res.txt = [JSON.stringify(txt)]
285
+ } else {
286
+ let txt = res.txt[0]
287
+ txt = txt.replace(/HTTP\/1\.1 \d\d\d \w+/, `HTTP/1.1 ${statusCode} ${message}`)
288
+ txt = txt.split(/\r\n/)
289
+ txt.splice(-1, 1, JSON.stringify(error))
290
+ txt = txt.join('\r\n')
291
+ res.txt = [txt]
292
+ }
293
+ }
294
+ }
295
+ return rejected
296
+ }
297
+
260
298
  const _processBatch = async (srv, router, req, res, next, body, ct, boundary) => {
261
299
  body ??= req.body
262
300
  ct ??= 'JSON'
@@ -268,6 +306,14 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
268
306
  }
269
307
  const _formatResponse = isJson ? _formatResponseJson : _formatResponseMultipart
270
308
 
309
+ // continue-on-error defaults to true in json batch
310
+ let continue_on_error = req.headers.prefer?.match(/odata\.continue-on-error(=(\w+))?/)
311
+ if (!continue_on_error) {
312
+ continue_on_error = isJson ? true : false
313
+ } else {
314
+ continue_on_error = continue_on_error[2] === 'false' ? false : true
315
+ }
316
+
271
317
  try {
272
318
  const ids = _validateBatch(body) // REVISIT: we will not be able to validate the whole once we stream
273
319
 
@@ -304,13 +350,13 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
304
350
  if (tx) {
305
351
  // Each change in `atomicityGroup` results in a new transaction. We execute them in sequence to avoid too many database connections.
306
352
  // In the future, we might make this configurable (e.g. allow X parallel connections per HTTP request).
307
- const rejected = await tx.done()
353
+ const rejected = await _tx_done(tx, responses, isJson)
308
354
  if (tx.failed?.res.statusCode === 401 && req._login) return req._login()
309
355
  else sendPreludeOnce()
310
356
  isJson
311
357
  ? _writeResponseJson(responses, res)
312
358
  : _writeResponseMultipart(responses, res, rejected, previousAtomicityGroup, boundary)
313
- if (rejected && !CONTINUE_ON_ERROR.test(req.headers.prefer)) {
359
+ if (rejected && !continue_on_error) {
314
360
  tx = null
315
361
  break
316
362
  }
@@ -318,7 +364,7 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
318
364
 
319
365
  responses = []
320
366
  tx = await _transaction(srv)
321
- if (atomicityGroup) ids[atomicityGroup].promise = tx.promise
367
+ if (atomicityGroup) ids[atomicityGroup].promise = tx._tx
322
368
  }
323
369
 
324
370
  tx.add(() => {
@@ -329,12 +375,13 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
329
375
  const results = await Promise.allSettled(dependencies)
330
376
  const dependendOnFailed = results.some(({ status }) => status === 'rejected')
331
377
  if (dependendOnFailed) {
378
+ tx.id = request.id
332
379
  tx.res = {
333
380
  getHeaders: () => {},
334
- statusCode: 404,
381
+ statusCode: 424,
335
382
  _chunk: JSON.stringify({
336
- status: 404,
337
- message: `Dependency for Request ${request.id} failed to execute`
383
+ code: '424',
384
+ message: 'Failed Dependency'
338
385
  })
339
386
  }
340
387
  throw tx
@@ -375,14 +422,14 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
375
422
  const resp = { status: 'ok' }
376
423
  if (separator) resp.separator = separator
377
424
  else separator = isJson ? Buffer.from(',') : Buffer.from(CRLF)
378
- resp.txt = _formatResponse(req, boundary)
425
+ resp.txt = _formatResponse(req, atomicityGroup)
379
426
  responses.push(resp)
380
427
  })
381
428
  .catch(failedReq => {
382
429
  const resp = { status: 'fail' }
383
430
  if (separator) resp.separator = separator
384
431
  else separator = isJson ? Buffer.from(',') : Buffer.from(CRLF)
385
- resp.txt = _formatResponse(failedReq, boundary)
432
+ resp.txt = _formatResponse(failedReq, atomicityGroup)
386
433
  tx.failed = failedReq
387
434
  responses.push(resp)
388
435
  })
@@ -392,7 +439,7 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
392
439
 
393
440
  if (tx) {
394
441
  // The last open transaction must be finished
395
- const rejected = await tx.done()
442
+ const rejected = await _tx_done(tx, responses, isJson)
396
443
  if (tx.failed?.res.statusCode === 401 && req._login) return req._login()
397
444
  else sendPreludeOnce()
398
445
  isJson
@@ -479,19 +526,19 @@ const _formatStatics = {
479
526
  close: Buffer.from('}')
480
527
  }
481
528
 
482
- const _formatResponseJson = request => {
529
+ const _formatResponseJson = (request, atomicityGroup) => {
483
530
  const { id, res: response } = request
484
531
 
485
- const raw = Buffer.from(
486
- JSON.stringify({
487
- id,
488
- status: response.statusCode,
489
- headers: {
490
- ...response.getHeaders(),
491
- 'content-type': 'application/json' //> REVISIT: why?
492
- }
493
- })
494
- )
532
+ const chunk = {
533
+ id,
534
+ status: response.statusCode,
535
+ headers: {
536
+ ...response.getHeaders(),
537
+ 'content-type': 'application/json' //> REVISIT: why?
538
+ }
539
+ }
540
+ if (atomicityGroup) chunk.atomicityGroup = atomicityGroup
541
+ const raw = Buffer.from(JSON.stringify(chunk))
495
542
 
496
543
  // body?
497
544
  if (!response._chunk) return [raw]
@@ -506,12 +553,11 @@ const _formatResponseJson = request => {
506
553
  */
507
554
 
508
555
  module.exports = adapter => {
509
- const { options: config, router, service } = adapter
510
- const { max_content_length } = config //> max_content_length is unofficial config
511
-
512
- const options = { type: '*/*' }
513
- if (max_content_length) options.limit = max_content_length
514
- const textBodyParser = express.text(options)
556
+ const { router, service } = adapter
557
+ const textBodyParser = express.text({
558
+ ...adapter.body_parser_options,
559
+ type: '*/*' // REVISIT: why do we need to override type here?
560
+ })
515
561
 
516
562
  return function odata_batch(req, res, next) {
517
563
  if (req.headers['content-type'].includes('application/json')) {
@@ -3,9 +3,8 @@ const express = require('express')
3
3
  // basically express.json() with string representation of body stored in req._raw for recovery
4
4
  // REVISIT: why do we need our own body parser? Only because of req._raw?
5
5
  module.exports = function bodyParser4(adapter, options = {}) {
6
- if (!options.type) options.type = 'json'
7
- const { max_content_length } = adapter.options //> max_content_length is unofficial config
8
- if (!options.limit && max_content_length) options.limit = max_content_length
6
+ Object.assign(options, adapter.body_parser_options)
7
+ options.type ??= 'json' // REVISIT: why do we need to override type here?
9
8
  const textParser = express.text(options)
10
9
  return function http_body_parser(req, res, next) {
11
10
  if (typeof req.body === 'object') {
@@ -51,9 +51,6 @@ module.exports = adapter => {
51
51
  let { operation, args } = req._query.SELECT?.from.ref?.slice(-1)[0] || {}
52
52
  if (!operation) return next() //> create or read
53
53
 
54
- // REVISIT: should not be necessary
55
- const _originalQuery = JSON.parse(JSON.stringify(req._query))
56
-
57
54
  // unbound vs. bound
58
55
  let entity, params
59
56
  if (service.model.definitions[operation]) {
@@ -102,7 +99,6 @@ module.exports = adapter => {
102
99
 
103
100
  if (operation.returns._type?.match?.(/^cds\./)) {
104
101
  const context = `${'../'.repeat(query?.SELECT?.from?.ref?.length)}$metadata#${cds2edm[operation.returns._type]}`
105
-
106
102
  result = { '@odata.context': context, value: result }
107
103
  return res.send(result)
108
104
  }
@@ -121,15 +117,19 @@ module.exports = adapter => {
121
117
  if (operation.returns.type !== 'sap.esh.SearchResult') {
122
118
  const isCollection = !!operation.returns.items
123
119
  const _target = operation.returns.items ?? operation.returns
124
- // REVISIT: when is edmName needed?
125
- const edmName = _opResultName({ service, operation, returnType: _target })
126
- const metadata = getODataMetadata(
127
- { SELECT: { from: _originalQuery?.SELECT?.from, one: !isCollection }, _target },
128
-
129
- { result, isCollection, edmName }
130
- )
120
+ const options = { result, isCollection }
121
+ if (!_target.name) {
122
+ // case: return inline type def
123
+ options.edmName = _opResultName({ service, operation, returnType: _target })
124
+ }
125
+ const SELECT = {
126
+ from: query ? { ref: [...query.SELECT.from.ref, { operation: operation.name }] } : {},
127
+ one: !isCollection
128
+ }
129
+ const metadata = getODataMetadata({ SELECT, _target }, options)
131
130
  result = getODataResult(result, metadata, { isCollection })
132
131
  }
132
+
133
133
  res.send(result)
134
134
  })
135
135
  .catch(err => {
@@ -109,7 +109,7 @@ const _count = result => {
109
109
  ? result.reduce((acc, val) => {
110
110
  return acc + (val?.$count ?? val?._counted_ ?? (Array.isArray(val) && _count(val))) || 0
111
111
  }, 0)
112
- : result.$count ?? result._counted_ ?? 0
112
+ : (result.$count ?? result._counted_ ?? 0)
113
113
  }
114
114
 
115
115
  // REVISIT: integrate with default handler
@@ -402,6 +402,7 @@
402
402
  "$skip=" o val:skip { _setLimitOffset(val) } /
403
403
  "$search=" o s:search_expand { if (s) SELECT.search = s } /
404
404
  "$count=" o count /
405
+ "$apply=" &{ return cds.env.features.skip_apply_parsing } o [^&]* { return null } /
405
406
  "$apply=" o trafos:transformations { return trafos } /
406
407
  // Workaround to support empty expand even if not OData compliant old adapter supported it and did not crash
407
408
  "$expand=" {return null}
@@ -486,7 +487,11 @@
486
487
  / o // Do not add search property for space only
487
488
 
488
489
  search_clause
489
- = val:$( [^&]+ ) { return [{ val }] }
490
+ = val:$(($[ ]* (
491
+ [^"&]+
492
+ / ('"' ("\\\\" / "\\\"" / [^"])+ '"' [ ]*)+
493
+ ))+)
494
+ { return [{ val }] }
490
495
 
491
496
  search_expand
492
497
  = val:$( [^;)]+ ) { return [{ val }] }