@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.
- package/CHANGELOG.md +34 -1
- package/bin/serve.js +9 -2
- package/lib/auth/ias-auth.js +4 -1
- package/lib/auth/jwt-auth.js +4 -1
- package/lib/compile/cdsc.js +1 -1
- package/lib/compile/extend.js +23 -23
- package/lib/compile/to/srvinfo.js +3 -1
- package/lib/{linked → core}/classes.js +8 -6
- package/lib/{linked/models.js → core/linked-csn.js} +4 -0
- package/lib/env/defaults.js +4 -1
- package/lib/i18n/localize.js +2 -2
- package/lib/index.js +43 -59
- package/lib/log/cds-error.js +21 -21
- package/lib/ql/cds-ql.js +5 -5
- package/lib/req/cds-context.js +5 -0
- package/lib/req/context.js +2 -2
- package/lib/req/locale.js +25 -21
- package/lib/srv/cds-serve.js +1 -1
- package/lib/srv/middlewares/errors.js +20 -7
- package/lib/srv/protocols/hcql.js +106 -43
- package/lib/srv/protocols/http.js +2 -2
- package/lib/srv/protocols/index.js +14 -10
- package/lib/srv/protocols/odata-v4.js +2 -26
- package/lib/srv/protocols/okra.js +24 -0
- package/lib/srv/srv-models.js +6 -8
- package/lib/{utils → test}/cds-test.js +5 -5
- package/lib/utils/check-version.js +8 -15
- package/lib/utils/extend.js +20 -0
- package/lib/utils/lazify.js +33 -0
- package/lib/utils/tar.js +39 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +0 -1
- package/libx/_runtime/common/generic/auth/restrict.js +1 -3
- package/libx/_runtime/common/generic/sorting.js +1 -1
- package/libx/_runtime/common/utils/compareJson.js +139 -53
- package/libx/_runtime/common/utils/compareJsonOLD.js +280 -0
- package/libx/_runtime/common/utils/differ.js +9 -1
- package/libx/_runtime/common/utils/resolveView.js +19 -23
- package/libx/_runtime/fiori/lean-draft.js +2 -2
- package/libx/_runtime/messaging/kafka.js +7 -1
- package/libx/_runtime/remote/utils/data.js +30 -24
- package/libx/odata/ODataAdapter.js +17 -7
- package/libx/odata/middleware/batch.js +4 -1
- package/libx/odata/middleware/error.js +6 -0
- package/libx/odata/middleware/operation.js +8 -0
- package/libx/odata/parse/afterburner.js +5 -6
- package/libx/odata/parse/grammar.peggy +3 -4
- package/libx/odata/parse/multipartToJson.js +60 -10
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/metadata.js +31 -1
- package/libx/outbox/index.js +5 -1
- package/package.json +3 -4
- package/server.js +18 -0
- package/lib/lazy.js +0 -51
- package/lib/test/index.js +0 -2
- /package/lib/{linked → core}/entities.js +0 -0
- /package/lib/{linked → core}/types.js +0 -0
- /package/lib/{linked → req}/validate.js +0 -0
- /package/lib/{utils → test}/axios.js +0 -0
- /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
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
value
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
type === 'cds.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
}
|
|
94
|
-
|
|
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
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
request.
|
|
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
|
-
|
|
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(
|
|
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
|
+
}
|