@sap/cds 8.3.1 → 8.4.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/bin/serve.js +9 -2
  3. package/lib/auth/ias-auth.js +4 -1
  4. package/lib/auth/jwt-auth.js +4 -1
  5. package/lib/compile/cdsc.js +1 -1
  6. package/lib/compile/extend.js +23 -23
  7. package/lib/compile/to/srvinfo.js +3 -1
  8. package/lib/{linked → core}/classes.js +8 -6
  9. package/lib/{linked/models.js → core/linked-csn.js} +4 -0
  10. package/lib/env/defaults.js +4 -1
  11. package/lib/i18n/localize.js +2 -2
  12. package/lib/index.js +43 -59
  13. package/lib/log/cds-error.js +21 -21
  14. package/lib/ql/cds-ql.js +5 -5
  15. package/lib/req/cds-context.js +5 -0
  16. package/lib/req/context.js +2 -2
  17. package/lib/req/locale.js +25 -21
  18. package/lib/srv/cds-serve.js +1 -1
  19. package/lib/srv/middlewares/errors.js +20 -7
  20. package/lib/srv/protocols/hcql.js +106 -43
  21. package/lib/srv/protocols/http.js +2 -2
  22. package/lib/srv/protocols/index.js +14 -10
  23. package/lib/srv/protocols/odata-v4.js +2 -26
  24. package/lib/srv/protocols/okra.js +24 -0
  25. package/lib/srv/srv-models.js +6 -8
  26. package/lib/{utils → test}/cds-test.js +5 -5
  27. package/lib/utils/check-version.js +8 -15
  28. package/lib/utils/extend.js +20 -0
  29. package/lib/utils/lazify.js +33 -0
  30. package/lib/utils/tar.js +39 -1
  31. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +0 -1
  32. package/libx/_runtime/common/generic/auth/restrict.js +1 -3
  33. package/libx/_runtime/common/generic/sorting.js +1 -1
  34. package/libx/_runtime/common/utils/compareJson.js +139 -53
  35. package/libx/_runtime/common/utils/compareJsonOLD.js +280 -0
  36. package/libx/_runtime/common/utils/differ.js +9 -1
  37. package/libx/_runtime/common/utils/resolveView.js +19 -23
  38. package/libx/_runtime/fiori/lean-draft.js +2 -2
  39. package/libx/_runtime/messaging/kafka.js +7 -1
  40. package/libx/_runtime/remote/utils/data.js +30 -24
  41. package/libx/odata/ODataAdapter.js +17 -7
  42. package/libx/odata/middleware/batch.js +4 -1
  43. package/libx/odata/middleware/error.js +6 -0
  44. package/libx/odata/middleware/operation.js +8 -0
  45. package/libx/odata/parse/afterburner.js +5 -6
  46. package/libx/odata/parse/grammar.peggy +3 -4
  47. package/libx/odata/parse/multipartToJson.js +60 -10
  48. package/libx/odata/parse/parser.js +1 -1
  49. package/libx/odata/utils/metadata.js +31 -1
  50. package/libx/outbox/index.js +5 -1
  51. package/package.json +3 -4
  52. package/server.js +18 -0
  53. package/lib/lazy.js +0 -51
  54. package/lib/test/index.js +0 -2
  55. /package/lib/{linked → core}/entities.js +0 -0
  56. /package/lib/{linked → core}/types.js +0 -0
  57. /package/lib/{linked → req}/validate.js +0 -0
  58. /package/lib/{utils → test}/axios.js +0 -0
  59. /package/lib/{utils → test}/data.js +0 -0
@@ -209,12 +209,18 @@ function _getKeyFn(topicOrEvent) {
209
209
 
210
210
  async function _getConfig(srv) {
211
211
  const caCerts = await _getCaCerts(srv)
212
+
213
+ const allBrokers =
214
+ srv.options.credentials.cluster?.['brokers.client_ssl'] ||
215
+ srv.options.credentials['cluster.public']?.['brokers.client_ssl']
216
+ const brokers = allBrokers.split(',')
217
+
212
218
  return {
213
219
  clientId: srv.appId,
214
220
  // logLevel: 4,
215
221
  connectionTimeout: 15000,
216
222
  authenticationTimeout: 15000,
217
- brokers: srv.options.credentials.cluster?.['brokers.client_ssl'].split(','),
223
+ brokers,
218
224
  ssl: {
219
225
  rejectUnauthorized: true,
220
226
  ca: caCerts,
@@ -1,4 +1,5 @@
1
1
  const { big } = require('@sap/cds-foss')
2
+ const cds = require('../../cds')
2
3
 
3
4
  // Code adopted from @sap/cds-odata-v2-adapter-proxy
4
5
  // https://www.w3.org/TR/xmlschema11-2/#nt-duDTFrag
@@ -68,31 +69,36 @@ const _convertValue = (ieee754Compatible, exponentialDecimals) => (value, elemen
68
69
  if (value == null) return value
69
70
 
70
71
  const type = _elementType(element)
71
- if (type === 'cds.Boolean') {
72
- if (value === 'true') {
73
- value = true
74
- } else if (value === 'false') {
75
- value = false
76
- }
77
- } else if (type === 'cds.Integer' || type === 'cds.UInt8' || type === 'cds.Int16' || type === 'cds.Int32') {
78
- value = parseInt(value, 10)
79
- } else if (
80
- type === 'cds.Decimal' ||
81
- type === 'cds.DecimalFloat' ||
82
- type === 'cds.Integer64' ||
83
- type === 'cds.Int64'
84
- ) {
85
- const bigValue = big(value)
86
- if (ieee754Compatible) {
87
- // TODO test with arrayed => element.items.scale?
88
- value = exponentialDecimals ? bigValue.toExponential(element.scale) : bigValue.toFixed(element.scale)
89
- } else {
90
- // OData V2 does not even mention ieee754Compatible, but V4 requires JSON number if ieee754Compatible=false
91
- value = bigValue.toNumber()
72
+
73
+ if (cds.env.features.odata_v2_result_conversion) {
74
+ cds.utils.deprecated({ old: 'flag cds.env.features.odata_v2_result_conversion' })
75
+ if (type === 'cds.Boolean') {
76
+ if (value === 'true') {
77
+ value = true
78
+ } else if (value === 'false') {
79
+ value = false
80
+ }
81
+ } else if (type === 'cds.Integer' || type === 'cds.UInt8' || type === 'cds.Int16' || type === 'cds.Int32') {
82
+ value = parseInt(value, 10)
83
+ } else if (
84
+ type === 'cds.Decimal' ||
85
+ type === 'cds.DecimalFloat' ||
86
+ type === 'cds.Integer64' ||
87
+ type === 'cds.Int64'
88
+ ) {
89
+ const bigValue = big(value)
90
+ if (ieee754Compatible) {
91
+ // TODO test with arrayed => element.items.scale?
92
+ value = exponentialDecimals ? bigValue.toExponential(element.scale) : bigValue.toFixed(element.scale)
93
+ } else {
94
+ // OData V2 does not even mention ieee754Compatible, but V4 requires JSON number if ieee754Compatible=false
95
+ value = bigValue.toNumber()
96
+ }
97
+ } else if (type === 'cds.Double') {
98
+ value = parseFloat(value)
92
99
  }
93
- } else if (type === 'cds.Double') {
94
- value = parseFloat(value)
95
- } else if (type === 'cds.Time') {
100
+ }
101
+ if (type === 'cds.Time') {
96
102
  const match = value.match(DurationRegex)
97
103
 
98
104
  if (match) {
@@ -73,13 +73,6 @@ class ODataAdapter extends HttpAdapter {
73
73
  if (req.method === 'POST' && req.headers['content-type']?.match(/multipart\/mixed/)) {
74
74
  return next()
75
75
  }
76
- if (req.method in { POST: 1, PUT: 1, PATCH: 1 } && req.headers['content-type']) {
77
- const parts = req.headers['content-type'].split(';')
78
- // header ending with semicolon is not allowed
79
- if (!parts[0].match(/^application\/json$/) || parts[1] === '') {
80
- throw cds.error('415', { statusCode: 415, code: '415' }) // FIXME: use res.status
81
- }
82
- }
83
76
  // POST with empty body is allowed by actions
84
77
  if (req.method in { PUT: 1, PATCH: 1 }) {
85
78
  if (req.headers['content-length'] === '0') {
@@ -87,6 +80,18 @@ class ODataAdapter extends HttpAdapter {
87
80
  return
88
81
  }
89
82
  }
83
+ if (req.method in { POST: 1, PUT: 1, PATCH: 1 }) {
84
+ const contentType = req.headers['content-type'] ?? ''
85
+ let contentLength = req.headers['content-length']
86
+ contentLength = contentLength ? parseInt(contentLength) : 0
87
+
88
+ const parts = contentType.split(';')
89
+ // header ending with semicolon is not allowed
90
+ if ((contentLength && !parts[0].match(/^application\/json$/)) || parts[1] === '') {
91
+ res.status(415).json({ error: { message: 'Unsupported Media Type', statusCode: 415, code: '415' } })
92
+ return
93
+ }
94
+ }
90
95
 
91
96
  return jsonBodyParser(req, res, next)
92
97
  })
@@ -97,6 +102,11 @@ class ODataAdapter extends HttpAdapter {
97
102
  .head('*', (_, res) => res.sendStatus(405))
98
103
  .post('*', operation4(this), create4(this))
99
104
  .get('*', operation4(this), stream4(this), read4(this))
105
+ .use('*', (req, res, next) => {
106
+ // operations must have been handled above (POST or GET)
107
+ const { operation } = req._query.SELECT?.from.ref?.slice(-1)[0] || {}
108
+ next(operation ? { code: 405 } : undefined)
109
+ })
100
110
  .put('*', update4(this), create4(this, 'upsert'))
101
111
  .patch('*', update4(this), create4(this, 'upsert'))
102
112
  .delete('*', delete4(this))
@@ -342,6 +342,8 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
342
342
  : {}
343
343
  }
344
344
 
345
+ request.headers['content-type'] ??= req.headers['content-type']
346
+
345
347
  const { atomicityGroup } = request
346
348
 
347
349
  if (!atomicityGroup || atomicityGroup !== previousAtomicityGroup) {
@@ -459,12 +461,13 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
459
461
 
460
462
  const _multipartBatch = async (srv, router, req, res, next) => {
461
463
  const boundary = getBoundary(req)
462
- if (!boundary) return next(cds.error('No boundary found in Content-Type header', { code: 400 }))
464
+ if (!boundary) return next(new cds.error('No boundary found in Content-Type header', { code: 400 }))
463
465
 
464
466
  try {
465
467
  const { requests } = await multipartToJson(req.body, boundary)
466
468
  _processBatch(srv, router, req, res, next, { requests }, 'MULTIPART', boundary)
467
469
  } catch (e) {
470
+ // REVISIT: (how) handle multipart accepts?
468
471
  next(e)
469
472
  }
470
473
  }
@@ -3,11 +3,17 @@ const cds = require('../../../lib')
3
3
  const _log = require('../../_runtime/common/error/log')
4
4
 
5
5
  const { normalizeError, unwrapMultipleErrors } = require('../../_runtime/common/error/frontend')
6
+ const { isStandardError } = require('../../_runtime/common/error/standardError')
6
7
 
7
8
  module.exports = () => {
8
9
  return function odata_error(err, req, res, next) {
9
10
  if (err == 401 || err.code == 401) return next(err) // speed up logins, at least temporary until we reviewed and eliminated overhead that may be involved below
10
11
 
12
+ // if error already has statusCode, it comes from express, don't throw
13
+ if (!err.statusCode && isStandardError(err) && cds.env.server.shutdown_on_uncaught_errors) {
14
+ return next(err)
15
+ }
16
+
11
17
  // REVISIT: keep?
12
18
  // log the error (4xx -> warn)
13
19
  _log(err)
@@ -70,6 +70,14 @@ module.exports = adapter => {
70
70
  params = keysAndParams.params
71
71
  }
72
72
 
73
+ // validate method
74
+ if (
75
+ (operation.kind === 'action' && req.method !== 'POST') ||
76
+ (operation.kind === 'function' && req.method !== 'GET')
77
+ ) {
78
+ return next({ code: 405 })
79
+ }
80
+
73
81
  // payload & params
74
82
  const data = args || req.body
75
83
 
@@ -504,13 +504,12 @@ function _addKeys(columns, target) {
504
504
  function _removeDuplicateAsterisk(columns) {
505
505
  let hasExpandStar = false
506
506
 
507
- for (let i = columns.length - 1; i > 0; i--) {
508
- const column = columns[i]
509
- if (!hasExpandStar && !column.ref && column?.expand?.[0] === '*') hasExpandStar = true
510
- else if (hasExpandStar && !column.ref && column?.expand[0] === '*') {
511
- columns.splice(i, 1)
507
+ columns.forEach((column, i) => {
508
+ if (!column.ref && column.expand?.[0] === '*') {
509
+ if (hasExpandStar) columns.splice(i, 1)
510
+ hasExpandStar = true
512
511
  }
513
- }
512
+ })
514
513
  }
515
514
 
516
515
  const _structProperty = (ref, target) => {
@@ -255,6 +255,7 @@
255
255
  // remove the prefix identifier
256
256
  if (e.ref && e.ref[0] === prefix) e.ref.shift()
257
257
  if (e.func) _removeLambdaPrefix(prefix, e.args)
258
+ if (e.xpr) _removeLambdaPrefix(prefix, e.xpr)
258
259
  }
259
260
  return elements
260
261
  }
@@ -551,7 +552,7 @@
551
552
 
552
553
  inner_lambda =
553
554
  p:( n:NOT? { return n ? [n] : [] } )(
554
- OPEN xpr:inner_lambda CLOSE { p.push('(', ...xpr, ')') }
555
+ OPEN xpr:inner_lambda CLOSE { p.push({xpr}) }
555
556
  / comp:comparison { p.push(...comp) }
556
557
  / func:function { p.push(func) }
557
558
  / lambda:lambda { p.push(...lambda)}
@@ -561,9 +562,7 @@
561
562
  { return p }
562
563
 
563
564
  lambda_clause =
564
- prefix:identifier ":" inner:inner_lambda {
565
- return _removeLambdaPrefix(prefix, inner)
566
- }
565
+ prefix:identifier ":" inner:inner_lambda { return _removeLambdaPrefix(prefix, inner) }
567
566
 
568
567
  any =
569
568
  "any" OPEN p:lambda_clause? CLOSE { return p }
@@ -1,11 +1,38 @@
1
+ const cds = require('../../../')
2
+ const LOG = cds.log('odata')
3
+
1
4
  const { parsers, freeParser, HTTPParser } = require('_http_common')
2
5
  const { PassThrough, Readable } = require('stream')
3
6
  const streamConsumers = require('stream/consumers')
7
+
4
8
  const { getBoundary } = require('../utils')
5
9
 
6
10
  const CRLF = '\r\n'
7
11
 
8
- const parseStream = async function* (body, boundary) {
12
+ let MAX_BATCH_HEADER_SIZE
13
+
14
+ const _normalizeSize = size => {
15
+ const match = size.match(/^([0-9]+)([\w ]+)$/i)
16
+ if (!match) return
17
+ let [, val, unit] = match
18
+ unit = unit.toLowerCase().trim()
19
+ switch (unit) {
20
+ case 'b':
21
+ return val
22
+ case 'kb':
23
+ return val * 1000
24
+ case 'kib':
25
+ return val * 1024
26
+ case 'mb':
27
+ return val * 1000 * 1000
28
+ case 'mib':
29
+ return val * 1024 * 1024
30
+ default:
31
+ return
32
+ }
33
+ }
34
+
35
+ const _parseStream = async function* (body, boundary) {
9
36
  const parser = parsers.alloc()
10
37
 
11
38
  try {
@@ -42,16 +69,17 @@ const parseStream = async function* (body, boundary) {
42
69
  body: streamConsumers.json(wrapper).catch(() => {})
43
70
  }
44
71
 
45
- const dependencies = [...req.url.matchAll(/\$(\d+)/g)]
72
+ const dependencies = [...req.url.matchAll(/^\/?\$([\d.\-_~a-zA-Z]+)/g)]
46
73
  if (dependencies.length) {
47
74
  request.dependsOn = []
48
75
  for (const dependency of dependencies) {
49
- const index = Number.parseInt(dependency[1], 10)
50
- if (Number.isNaN(index)) continue
51
- const i = requests.findIndex(r => r.content_id == index) //> prefer content-id
52
- const id = requests[i > -1 ? i : index - 1].id
53
- request.dependsOn.push(id)
54
- request.url = request.url.replace(`$${index}`, `$${id}`)
76
+ const dependencyId = dependency[1]
77
+ const dependsOnRequest = requests.findLast(r => r.content_id == dependencyId) //> prefer content-id
78
+ if (!dependsOnRequest) {
79
+ continue
80
+ }
81
+ request.dependsOn.push(dependsOnRequest.id)
82
+ request.url = request.url.replace(`$${dependencyId}`, `$${dependsOnRequest.id}`)
55
83
  }
56
84
  if (request.url[1] === '$') request.url = request.url.slice(1)
57
85
  }
@@ -62,7 +90,24 @@ const parseStream = async function* (body, boundary) {
62
90
  requests.push(request)
63
91
  }
64
92
 
65
- parser.initialize(HTTPParser.REQUEST, { type: 'HTTPINCOMINGMESSAGE' })
93
+ if (MAX_BATCH_HEADER_SIZE == null) {
94
+ MAX_BATCH_HEADER_SIZE = cds.env.odata.max_batch_header_size
95
+ if (typeof MAX_BATCH_HEADER_SIZE === 'string') {
96
+ // eslint-disable-next-line no-extra-boolean-cast
97
+ MAX_BATCH_HEADER_SIZE = !!Number(MAX_BATCH_HEADER_SIZE)
98
+ ? Number(MAX_BATCH_HEADER_SIZE)
99
+ : _normalizeSize(MAX_BATCH_HEADER_SIZE)
100
+ }
101
+ if (typeof MAX_BATCH_HEADER_SIZE !== 'number') {
102
+ LOG._warn &&
103
+ LOG.warn(
104
+ `Invalid value "${cds.env.odata.max_batch_header_size}" for configuration 'cds.odata.max_batch_header_size'. Using default value of 64 KiB.`
105
+ )
106
+ MAX_BATCH_HEADER_SIZE = 64 * 1024
107
+ }
108
+ }
109
+
110
+ parser.initialize(HTTPParser.REQUEST, { type: 'HTTPINCOMINGMESSAGE' }, MAX_BATCH_HEADER_SIZE)
66
111
 
67
112
  if (typeof body === 'string') body = [body]
68
113
 
@@ -153,9 +198,14 @@ module.exports = async (body, boundary) => {
153
198
 
154
199
  // This logic would ultimately be inside the json batch processor
155
200
  // for await supports both async iterator and normal iterators (e.g. any Array)
156
- for await (const request of Readable.from(parseStream(body, boundary))) {
201
+ for await (const request of Readable.from(_parseStream(body, boundary))) {
157
202
  ret.requests.push(request)
158
203
  }
159
204
 
160
205
  return ret
161
206
  }
207
+
208
+ module.exports._normalizeSize = _normalizeSize
209
+ module.exports._clearMaxBatchHeaderSize = () => {
210
+ MAX_BATCH_HEADER_SIZE = null
211
+ }