@sap/cds 9.2.0 → 9.3.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.
- package/CHANGELOG.md +87 -1
- package/_i18n/i18n_es.properties +3 -3
- package/_i18n/i18n_es_MX.properties +3 -3
- package/_i18n/i18n_fr.properties +2 -2
- package/_i18n/messages.properties +6 -0
- package/app/index.js +0 -1
- package/bin/deploy.js +1 -1
- package/bin/serve.js +7 -20
- package/lib/compile/cdsc.js +3 -0
- package/lib/compile/for/flows.js +102 -0
- package/lib/compile/for/nodejs.js +28 -0
- package/lib/compile/to/edm.js +11 -4
- package/lib/core/classes.js +1 -1
- package/lib/core/linked-csn.js +8 -0
- package/lib/dbs/cds-deploy.js +12 -12
- package/lib/env/cds-env.js +1 -1
- package/lib/env/cds-requires.js +21 -20
- package/lib/env/defaults.js +2 -1
- package/lib/index.js +5 -6
- package/lib/log/cds-log.js +6 -5
- package/lib/log/format/aspects/cf.js +2 -2
- package/lib/plugins.js +1 -1
- package/lib/ql/UPDATE.js +3 -1
- package/lib/ql/cds-ql.js +0 -3
- package/lib/req/request.js +3 -3
- package/lib/req/response.js +12 -7
- package/lib/srv/bindings.js +17 -17
- package/lib/srv/cds-connect.js +6 -9
- package/lib/srv/cds-serve.js +74 -137
- package/lib/srv/cds.Service.js +49 -0
- package/lib/srv/factory.js +4 -4
- package/lib/srv/middlewares/auth/ias-auth.js +31 -11
- package/lib/srv/middlewares/auth/index.js +3 -2
- package/lib/srv/middlewares/auth/jwt-auth.js +19 -6
- package/lib/srv/protocols/hcql.js +16 -1
- package/lib/srv/srv-dispatch.js +1 -1
- package/lib/utils/cds-utils.js +4 -8
- package/lib/utils/csv-reader.js +27 -7
- package/libx/_runtime/cds.js +0 -6
- package/libx/_runtime/common/Service.js +5 -0
- package/libx/_runtime/common/generic/crud.js +1 -1
- package/libx/_runtime/common/generic/flows.js +106 -0
- package/libx/_runtime/common/generic/paging.js +3 -3
- package/libx/_runtime/common/utils/differ.js +5 -15
- package/libx/_runtime/common/utils/resolveView.js +2 -2
- package/libx/_runtime/common/utils/rewriteAsterisks.js +2 -2
- package/libx/_runtime/fiori/lean-draft.js +76 -40
- package/libx/_runtime/messaging/enterprise-messaging.js +1 -1
- package/libx/_runtime/messaging/file-based.js +2 -1
- package/libx/_runtime/remote/Service.js +68 -62
- package/libx/_runtime/remote/utils/client.js +29 -216
- package/libx/_runtime/remote/utils/query.js +197 -0
- package/libx/_runtime/ucl/Service.js +180 -112
- package/libx/_runtime/ucl/queries.js +61 -0
- package/libx/odata/ODataAdapter.js +1 -4
- package/libx/odata/index.js +2 -10
- package/libx/odata/middleware/error.js +8 -1
- package/libx/odata/middleware/stream.js +1 -1
- package/libx/odata/middleware/update.js +12 -2
- package/libx/odata/parse/afterburner.js +113 -20
- package/libx/odata/parse/cqn2odata.js +1 -3
- package/libx/odata/parse/grammar.peggy +4 -2
- package/libx/odata/parse/parser.js +1 -1
- package/libx/queue/index.js +1 -1
- package/libx/rest/middleware/parse.js +9 -2
- package/package.json +2 -2
- package/server.js +2 -0
- package/srv/app-service.js +1 -0
- package/srv/db-service.js +1 -0
- package/srv/msg-service.js +1 -0
- package/srv/remote-service.js +1 -0
- package/srv/ucl-service.cds +32 -0
- package/srv/ucl-service.js +1 -0
- package/lib/ql/resolve.js +0 -45
- package/libx/common/assert/type-strict.js +0 -109
- package/libx/common/assert/utils.js +0 -60
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
2
|
const LOG = cds.log('remote')
|
|
3
|
+
|
|
3
4
|
const { getCloudSdk } = require('./cloudSdkProvider')
|
|
4
|
-
const { Readable } = require('stream')
|
|
5
5
|
|
|
6
6
|
const SANITIZE_VALUES = process.env.NODE_ENV === 'production' && cds.env.log.sanitize_values !== false
|
|
7
|
-
const { convertV2ResponseData, deepSanitize
|
|
8
|
-
|
|
9
|
-
const
|
|
7
|
+
const { convertV2ResponseData, deepSanitize } = require('./data')
|
|
8
|
+
|
|
9
|
+
const _logRequest = (requestConfig, destination) => {
|
|
10
|
+
const req2log = { headers: _sanitizeHeaders({ ...requestConfig.headers }) }
|
|
11
|
+
if (requestConfig.data && Object.keys(requestConfig.data).length) {
|
|
12
|
+
// In case of auto batch (only done for `GET` requests) no data is part of batch and for debugging URL is crucial
|
|
13
|
+
if (SANITIZE_VALUES && !requestConfig._autoBatchedGet) req2log.data = deepSanitize(requestConfig.data)
|
|
14
|
+
else req2log.data = requestConfig.data
|
|
15
|
+
}
|
|
16
|
+
LOG.debug(
|
|
17
|
+
`${requestConfig.method} ${destination.url || `<${destination.destinationName}>`}${requestConfig.url}`,
|
|
18
|
+
req2log
|
|
19
|
+
)
|
|
20
|
+
}
|
|
10
21
|
|
|
11
22
|
const _sanitizeHeaders = headers => {
|
|
12
23
|
// REVISIT: is this in-place modification intended?
|
|
@@ -18,12 +29,9 @@ const _executeHttpRequest = async ({ requestConfig, destination, destinationOpti
|
|
|
18
29
|
const { executeHttpRequestWithOrigin } = getCloudSdk()
|
|
19
30
|
|
|
20
31
|
if (typeof destination === 'string') {
|
|
21
|
-
destination = {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
...{ jwt: destinationOptions?.jwt === undefined ? jwt : destinationOptions.jwt }
|
|
25
|
-
}
|
|
26
|
-
if (destination.jwt !== undefined && !destination.jwt) delete destination.jwt // don't pass any value
|
|
32
|
+
destination = { destinationName: destination, ...destinationOptions }
|
|
33
|
+
if (destination.jwt === undefined) destination.jwt = jwt
|
|
34
|
+
if (!destination.jwt && destination.jwt !== undefined) delete destination.jwt
|
|
27
35
|
} else if (destination.forwardAuthToken) {
|
|
28
36
|
destination = {
|
|
29
37
|
...destination,
|
|
@@ -32,64 +40,24 @@ const _executeHttpRequest = async ({ requestConfig, destination, destinationOpti
|
|
|
32
40
|
}
|
|
33
41
|
delete destination.forwardAuthToken
|
|
34
42
|
if (jwt) destination.headers.authorization = `Bearer ${jwt}`
|
|
35
|
-
else LOG._warn
|
|
43
|
+
else if (LOG._warn) LOG.warn('Missing JWT token for forwardAuthToken!')
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
// Cloud SDK throws error if useCache is activated and jwt is undefined
|
|
39
47
|
if (destination.jwt === undefined) destination.useCache = false
|
|
40
48
|
|
|
41
|
-
if (LOG._debug)
|
|
42
|
-
const req2log = { headers: _sanitizeHeaders({ ...requestConfig.headers }) }
|
|
43
|
-
if (requestConfig.method !== 'GET' && requestConfig.method !== 'DELETE')
|
|
44
|
-
// In case of auto batch (only done for `GET` requests) no data is part of batch and for debugging URL is crucial
|
|
45
|
-
req2log.data =
|
|
46
|
-
requestConfig.data && SANITIZE_VALUES && !requestConfig._autoBatchedGet
|
|
47
|
-
? deepSanitize(requestConfig.data)
|
|
48
|
-
: requestConfig.data
|
|
49
|
-
LOG.debug(
|
|
50
|
-
`${requestConfig.method} ${destination.url || `<${destination.destinationName}>`}${requestConfig.url}`,
|
|
51
|
-
req2log
|
|
52
|
-
)
|
|
53
|
-
}
|
|
49
|
+
if (LOG._debug) _logRequest(requestConfig, destination)
|
|
54
50
|
|
|
55
51
|
// cloud sdk requires a new mechanism to differentiate the priority of headers
|
|
56
52
|
// "custom" keeps the highest priority as before
|
|
57
|
-
const maxBodyLength = cds.env?.remote?.max_body_length
|
|
58
53
|
requestConfig = {
|
|
59
54
|
...requestConfig,
|
|
60
|
-
headers: { custom: { ...requestConfig.headers } }
|
|
61
|
-
...(maxBodyLength && { maxBodyLength })
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// set `fetchCsrfToken` to `false` because we mount a custom CSRF middleware
|
|
65
|
-
const requestOptions = { fetchCsrfToken: false }
|
|
66
|
-
|
|
67
|
-
return executeHttpRequestWithOrigin(destination, requestConfig, requestOptions)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Rest Client
|
|
72
|
-
*/
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Normalizes server path.
|
|
76
|
-
*
|
|
77
|
-
* Adds / in the beginning of the path if not exists.
|
|
78
|
-
* Removes / in the end of the path if exists.
|
|
79
|
-
*
|
|
80
|
-
* @param {*} path - to be normalized
|
|
81
|
-
*/
|
|
82
|
-
const formatPath = path => {
|
|
83
|
-
let formattedPath = path
|
|
84
|
-
if (!path.startsWith('/')) {
|
|
85
|
-
formattedPath = `/${formattedPath}`
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (path.endsWith('/')) {
|
|
89
|
-
formattedPath = formattedPath.substring(0, formattedPath.length - 1)
|
|
55
|
+
headers: { custom: { ...requestConfig.headers } }
|
|
90
56
|
}
|
|
57
|
+
const maxBodyLength = cds.env?.remote?.max_body_length
|
|
58
|
+
if (maxBodyLength) requestConfig.maxBodyLength = maxBodyLength
|
|
91
59
|
|
|
92
|
-
return
|
|
60
|
+
return executeHttpRequestWithOrigin(destination, requestConfig, { fetchCsrfToken: false })
|
|
93
61
|
}
|
|
94
62
|
|
|
95
63
|
function _defineProperty(obj, property, value) {
|
|
@@ -214,17 +182,12 @@ const _getSanitizedError = (e, reqOptions, options = { suppressRemoteResponseBod
|
|
|
214
182
|
return e
|
|
215
183
|
}
|
|
216
184
|
|
|
217
|
-
|
|
218
|
-
let response
|
|
219
|
-
|
|
185
|
+
module.exports.run = async (requestConfig, options) => {
|
|
220
186
|
const { destination, destinationOptions, jwt, suppressRemoteResponseBody } = options
|
|
187
|
+
|
|
188
|
+
let response
|
|
221
189
|
try {
|
|
222
|
-
response = await _executeHttpRequest({
|
|
223
|
-
requestConfig,
|
|
224
|
-
destination,
|
|
225
|
-
destinationOptions,
|
|
226
|
-
jwt
|
|
227
|
-
})
|
|
190
|
+
response = await _executeHttpRequest({ requestConfig, destination, destinationOptions, jwt })
|
|
228
191
|
} catch (e) {
|
|
229
192
|
// > axios received status >= 400 -> gateway error
|
|
230
193
|
const msg = e?.response?.data?.error?.message?.value ?? e?.response?.data?.error?.message ?? e.message
|
|
@@ -232,6 +195,7 @@ const run = async (requestConfig, options) => {
|
|
|
232
195
|
const sanitizedError = _getSanitizedError(e, requestConfig, { suppressRemoteResponseBody })
|
|
233
196
|
const err = Object.assign(new Error(e.message), { statusCode: 502, reason: sanitizedError })
|
|
234
197
|
cds.repl || LOG.warn(err)
|
|
198
|
+
|
|
235
199
|
throw err
|
|
236
200
|
}
|
|
237
201
|
|
|
@@ -298,154 +262,3 @@ const run = async (requestConfig, options) => {
|
|
|
298
262
|
|
|
299
263
|
return response.data
|
|
300
264
|
}
|
|
301
|
-
|
|
302
|
-
const _cqnToReqOptions = (query, service, req) => {
|
|
303
|
-
const { kind, model } = service
|
|
304
|
-
const method = req.method
|
|
305
|
-
const queryObject = cds.odata.urlify(query, { kind, model, method })
|
|
306
|
-
const reqOptions = {
|
|
307
|
-
method: queryObject.method,
|
|
308
|
-
url: queryObject.path
|
|
309
|
-
// REVISIT: remove when we can assume that number of remote services running Okra is negligible
|
|
310
|
-
// ugly workaround for Okra not allowing spaces in ( x eq 1 )
|
|
311
|
-
.replace(/\( /g, '(')
|
|
312
|
-
.replace(/ \)/g, ')')
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (queryObject.method !== 'GET' && queryObject.method !== 'HEAD') {
|
|
316
|
-
reqOptions.data = kind === 'odata-v2' ? convertV2PayloadData(queryObject.body, req.target) : queryObject.body
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
return reqOptions
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const _stringToReqOptions = (query, data, target) => {
|
|
323
|
-
const cleanQuery = query.trim()
|
|
324
|
-
const blankIndex = cleanQuery.substring(0, 8).indexOf(' ')
|
|
325
|
-
const reqOptions = {
|
|
326
|
-
method: cleanQuery.substring(0, blankIndex).toUpperCase() || 'GET',
|
|
327
|
-
url: encodeURI(formatPath(cleanQuery.substring(blankIndex, cleanQuery.length).trim()))
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
if (data && reqOptions.method !== 'GET' && reqOptions.method !== 'HEAD') {
|
|
331
|
-
reqOptions.data = this.kind === 'odata-v2' ? Object.assign({}, convertV2PayloadData(data, target)) : data
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
return reqOptions
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const _pathToReqOptions = (method, path, data, target, srvName) => {
|
|
338
|
-
let url = path
|
|
339
|
-
if (!url.startsWith('/')) {
|
|
340
|
-
// extract entity name and instance identifier (either in "()" or after "/") from fully qualified path
|
|
341
|
-
const parts = path.match(/([\w.]*)([\W.]*)(.*)/)
|
|
342
|
-
if (!parts) url = '/' + path.match(/\w*$/)[0]
|
|
343
|
-
else if (url.startsWith(srvName)) url = '/' + parts[1].replace(srvName + '.', '') + parts[2] + parts[3]
|
|
344
|
-
else url = '/' + parts[1].match(/\w*$/)[0] + parts[2] + parts[3]
|
|
345
|
-
|
|
346
|
-
// normalize in case parts[2] already starts with /
|
|
347
|
-
url = url.replace(/^\/\//, '/')
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
const reqOptions = { method, url }
|
|
351
|
-
if (data && reqOptions.method !== 'GET' && reqOptions.method !== 'HEAD') {
|
|
352
|
-
reqOptions.data = this.kind === 'odata-v2' ? Object.assign({}, convertV2PayloadData(data, target)) : data
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
return reqOptions
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
const _hasHeader = (headers, header) =>
|
|
359
|
-
Object.keys(headers || [])
|
|
360
|
-
.map(k => k.toLowerCase())
|
|
361
|
-
.includes(header)
|
|
362
|
-
|
|
363
|
-
const getReqOptions = (req, query, service) => {
|
|
364
|
-
const reqOptions =
|
|
365
|
-
typeof query === 'object'
|
|
366
|
-
? _cqnToReqOptions(query, service, req)
|
|
367
|
-
: typeof query === 'string'
|
|
368
|
-
? _stringToReqOptions(query, req.data, req.target)
|
|
369
|
-
: _pathToReqOptions(req.method, req.path, req.data, req.target, service.definition?.name || service.namespace) //> no model, no service.definition
|
|
370
|
-
|
|
371
|
-
if (service.kind === 'odata-v2' && req.event === 'READ' && reqOptions.url?.match(/(\/any\()|(\/all\()/)) {
|
|
372
|
-
req.reject(501, 'Lambda expressions are not supported in OData v2')
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
reqOptions.headers = { accept: 'application/json,text/plain' }
|
|
376
|
-
|
|
377
|
-
if (!_hasHeader(req.headers, 'accept-language')) {
|
|
378
|
-
// Forward the locale properties from the original request (including region variants or weight factors),
|
|
379
|
-
// if not given, it's taken from the user's locale (normalized and simplified)
|
|
380
|
-
const locale = req._locale
|
|
381
|
-
if (locale) reqOptions.headers['accept-language'] = locale
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// forward all dwc-* headers
|
|
385
|
-
if (service.options.forward_dwc_headers) {
|
|
386
|
-
const originalHeaders = req.context?.http.req.headers || {}
|
|
387
|
-
for (const k in originalHeaders) if (k.match(/^dwc-/)) reqOptions.headers[k] = originalHeaders[k]
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
if (
|
|
391
|
-
reqOptions.data &&
|
|
392
|
-
reqOptions.method !== 'GET' &&
|
|
393
|
-
reqOptions.method !== 'HEAD' &&
|
|
394
|
-
!(reqOptions.data instanceof Readable)
|
|
395
|
-
) {
|
|
396
|
-
if (typeof reqOptions.data === 'object' && !Buffer.isBuffer(reqOptions.data)) {
|
|
397
|
-
reqOptions.headers['content-type'] = 'application/json'
|
|
398
|
-
reqOptions.headers['content-length'] = Buffer.byteLength(JSON.stringify(reqOptions.data))
|
|
399
|
-
} else if (typeof reqOptions.data === 'string') {
|
|
400
|
-
reqOptions.headers['content-length'] = Buffer.byteLength(reqOptions.data)
|
|
401
|
-
} else if (Buffer.isBuffer(reqOptions.data)) {
|
|
402
|
-
reqOptions.headers['content-length'] = Buffer.byteLength(reqOptions.data)
|
|
403
|
-
if (!_hasHeader(req.headers, 'content-type')) reqOptions.headers['content-type'] = 'application/octet-stream'
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
reqOptions.url = formatPath(reqOptions.url)
|
|
408
|
-
|
|
409
|
-
// batch envelope if needed
|
|
410
|
-
const maxGetUrlLength = service.options.max_get_url_length ?? cds.env.remote?.max_get_url_length ?? 1028
|
|
411
|
-
if (KINDS_SUPPORTING_BATCH[service.kind] && reqOptions.method === 'GET' && reqOptions.url.length > maxGetUrlLength) {
|
|
412
|
-
LOG._debug &&
|
|
413
|
-
LOG.debug(
|
|
414
|
-
`URL of remote request exceeds the configured max length of ${maxGetUrlLength}. Converting it to a $batch request.`
|
|
415
|
-
)
|
|
416
|
-
reqOptions._autoBatchedGet = true
|
|
417
|
-
reqOptions.data = [
|
|
418
|
-
'--batch1',
|
|
419
|
-
'Content-Type: application/http',
|
|
420
|
-
'Content-Transfer-Encoding: binary',
|
|
421
|
-
'',
|
|
422
|
-
`${reqOptions.method} ${reqOptions.url.replace(/^\//, '')} HTTP/1.1`,
|
|
423
|
-
...Object.keys(reqOptions.headers).map(k => `${k}: ${reqOptions.headers[k]}`),
|
|
424
|
-
'',
|
|
425
|
-
'',
|
|
426
|
-
'--batch1--',
|
|
427
|
-
''
|
|
428
|
-
].join('\r\n')
|
|
429
|
-
reqOptions.method = 'POST'
|
|
430
|
-
reqOptions.headers.accept = 'multipart/mixed'
|
|
431
|
-
reqOptions.headers['content-type'] = 'multipart/mixed; boundary=batch1'
|
|
432
|
-
reqOptions.url = '/$batch'
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// mount resilience and csrf middlewares for SAP Cloud SDK
|
|
436
|
-
reqOptions.middleware = [service.middlewares.timeout]
|
|
437
|
-
const fetchCsrfToken = !!(reqOptions._autoBatchedGet ? service.csrfInBatch : service.csrf)
|
|
438
|
-
if (fetchCsrfToken) reqOptions.middleware.push(service.middlewares.csrf)
|
|
439
|
-
|
|
440
|
-
if (service.path) reqOptions.url = `${encodeURI(service.path)}${reqOptions.url}`
|
|
441
|
-
|
|
442
|
-
// set axios responseType to 'arraybuffer' if returning binary in rest
|
|
443
|
-
if (req._binary) reqOptions.responseType = 'arraybuffer'
|
|
444
|
-
|
|
445
|
-
return reqOptions
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
module.exports = {
|
|
449
|
-
run,
|
|
450
|
-
getReqOptions
|
|
451
|
-
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
const cds = require('../../cds')
|
|
2
|
+
const LOG = cds.log('remote')
|
|
3
|
+
|
|
4
|
+
const { convertV2PayloadData } = require('./data')
|
|
5
|
+
const { Readable } = require('stream')
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Normalizes server path.
|
|
9
|
+
*
|
|
10
|
+
* Adds / in the beginning of the path if not exists.
|
|
11
|
+
* Removes / in the end of the path if exists.
|
|
12
|
+
*
|
|
13
|
+
* @param {*} path - to be normalized
|
|
14
|
+
*/
|
|
15
|
+
const _formatPath = (path = '') => {
|
|
16
|
+
let formattedPath = path
|
|
17
|
+
|
|
18
|
+
if (!path.startsWith('/')) formattedPath = `/${formattedPath}`
|
|
19
|
+
if (path.endsWith('/')) formattedPath = formattedPath.substring(0, formattedPath.length - 1)
|
|
20
|
+
|
|
21
|
+
return formattedPath
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const _cqnWithPublicEntries = query => {
|
|
25
|
+
return Object.fromEntries(Object.entries(query).filter(([key]) => !key.startsWith('_')))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const _cqnToHcqlRequestConfig = cqn => {
|
|
29
|
+
if (cqn.SELECT && cqn.SELECT.from) return { method: 'GET', data: { SELECT: _cqnWithPublicEntries(cqn.SELECT) } }
|
|
30
|
+
if (cqn.INSERT && cqn.INSERT.into) return { method: 'POST', data: { INSERT: _cqnWithPublicEntries(cqn.INSERT) } }
|
|
31
|
+
if (cqn.UPDATE && cqn.UPDATE.entity) return { method: 'PATCH', data: { UPDATE: _cqnWithPublicEntries(cqn.UPDATE) } }
|
|
32
|
+
if (cqn.UPSERT && cqn.UPSERT.into) return { method: 'PUT', data: { UPSERT: _cqnWithPublicEntries(cqn.UPSERT) } }
|
|
33
|
+
if (cqn.DELETE && cqn.DELETE.from) return { method: 'DELETE', data: { DELETE: _cqnWithPublicEntries(cqn.DELETE) } }
|
|
34
|
+
|
|
35
|
+
cds.error(400, 'Invalid CQN object can not be processed.', JSON.stringify(cqn), _cqnToHcqlRequestConfig)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const _cqnToRequestConfig = (query, service, req) => {
|
|
39
|
+
const { kind, model } = service
|
|
40
|
+
|
|
41
|
+
const queryObject = cds.odata.urlify(query, { kind, model, method: req.method })
|
|
42
|
+
|
|
43
|
+
const requestConfig = {
|
|
44
|
+
method: queryObject.method,
|
|
45
|
+
url: queryObject.path
|
|
46
|
+
// REVISIT: remove when we can assume that number of remote services running Okra is negligible
|
|
47
|
+
// ugly workaround for Okra not allowing spaces in ( x eq 1 )
|
|
48
|
+
.replace(/\( /g, '(')
|
|
49
|
+
.replace(/ \)/g, ')')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (queryObject.method !== 'GET' && queryObject.method !== 'HEAD') {
|
|
53
|
+
requestConfig.data = kind === 'odata-v2' ? convertV2PayloadData(queryObject.body, req.target) : queryObject.body
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return requestConfig
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const _stringToRequestConfig = (query, data, target) => {
|
|
60
|
+
const cleanQuery = query.trim()
|
|
61
|
+
const blankIndex = cleanQuery.substring(0, 8).indexOf(' ')
|
|
62
|
+
|
|
63
|
+
const requestConfig = {
|
|
64
|
+
method: cleanQuery.substring(0, blankIndex).toUpperCase() || 'GET',
|
|
65
|
+
url: encodeURI(_formatPath(cleanQuery.substring(blankIndex, cleanQuery.length).trim()))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (data && requestConfig.method !== 'GET' && requestConfig.method !== 'HEAD') {
|
|
69
|
+
requestConfig.data = this.kind === 'odata-v2' ? Object.assign({}, convertV2PayloadData(data, target)) : data
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return requestConfig
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const _pathToRequestConfig = (method, path, data, target, srvName) => {
|
|
76
|
+
let url = path
|
|
77
|
+
|
|
78
|
+
if (!url.startsWith('/')) {
|
|
79
|
+
// extract entity name and instance identifier (either in "()" or after "/") from fully qualified path
|
|
80
|
+
const parts = path.match(/([\w.]*)([\W.]*)(.*)/)
|
|
81
|
+
if (!parts) url = '/' + path.match(/\w*$/)[0]
|
|
82
|
+
else if (url.startsWith(srvName)) url = '/' + parts[1].replace(srvName + '.', '') + parts[2] + parts[3]
|
|
83
|
+
else url = '/' + parts[1].match(/\w*$/)[0] + parts[2] + parts[3]
|
|
84
|
+
|
|
85
|
+
// normalize in case parts[2] already starts with /
|
|
86
|
+
url = url.replace(/^\/\//, '/')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const requestConfig = { method, url }
|
|
90
|
+
if (data && requestConfig.method !== 'GET' && requestConfig.method !== 'HEAD') {
|
|
91
|
+
requestConfig.data = this.kind === 'odata-v2' ? Object.assign({}, convertV2PayloadData(data, target)) : data
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return requestConfig
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const _hasHeader = (headers, header) =>
|
|
98
|
+
Object.keys(headers || [])
|
|
99
|
+
.map(k => k.toLowerCase())
|
|
100
|
+
.includes(header)
|
|
101
|
+
|
|
102
|
+
const KINDS_SUPPORTING_BATCH = { odata: true, 'odata-v2': true, 'odata-v4': true }
|
|
103
|
+
|
|
104
|
+
const _extractRequestConfig = (req, query, service) => {
|
|
105
|
+
if (service.kind === 'hcql' && typeof query === 'object') return _cqnToHcqlRequestConfig(query)
|
|
106
|
+
if (service.kind === 'hcql') throw new Error('The request has no query and cannot be served by HCQL remote services!')
|
|
107
|
+
if (typeof query === 'object') return _cqnToRequestConfig(query, service, req)
|
|
108
|
+
if (typeof query === 'string') return _stringToRequestConfig(query, req.data, req.target)
|
|
109
|
+
//> no model, no service.definition
|
|
110
|
+
return _pathToRequestConfig(req.method, req.path, req.data, req.target, service.definition?.name || service.namespace)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const _batchEnvelope = (requestConfig, maxGetUrlLength) => {
|
|
114
|
+
if (LOG._debug) {
|
|
115
|
+
LOG.debug(
|
|
116
|
+
`URL of remote request exceeds the configured max length of ${maxGetUrlLength}. Converting it to a $batch request.`
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const batchedRequest = [
|
|
121
|
+
'--batch1',
|
|
122
|
+
'Content-Type: application/http',
|
|
123
|
+
'Content-Transfer-Encoding: binary',
|
|
124
|
+
'',
|
|
125
|
+
`${requestConfig.method} ${requestConfig.url.replace(/^\//, '')} HTTP/1.1`,
|
|
126
|
+
...Object.keys(requestConfig.headers).map(k => `${k}: ${requestConfig.headers[k]}`),
|
|
127
|
+
'',
|
|
128
|
+
'',
|
|
129
|
+
'--batch1--',
|
|
130
|
+
''
|
|
131
|
+
].join('\r\n')
|
|
132
|
+
|
|
133
|
+
requestConfig.method = 'POST'
|
|
134
|
+
requestConfig.url = '/$batch'
|
|
135
|
+
requestConfig.headers['accept'] = 'multipart/mixed'
|
|
136
|
+
requestConfig.headers['content-type'] = 'multipart/mixed; boundary=batch1'
|
|
137
|
+
requestConfig._autoBatchedGet = true
|
|
138
|
+
requestConfig.data = batchedRequest
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports.extractRequestConfig = (req, query, service) => {
|
|
142
|
+
const requestConfig = _extractRequestConfig(req, query, service)
|
|
143
|
+
|
|
144
|
+
if (service.kind === 'odata-v2' && req.event === 'READ' && requestConfig.url?.match(/(\/any\()|(\/all\()/)) {
|
|
145
|
+
req.reject(501, 'Lambda expressions are not supported in OData v2')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
requestConfig.url = _formatPath(requestConfig.url)
|
|
149
|
+
requestConfig.headers = { accept: 'application/json,text/plain' }
|
|
150
|
+
|
|
151
|
+
// Forward the locale properties from the original request (including region variants or weight factors)
|
|
152
|
+
if (!_hasHeader(req.headers, 'accept-language') && req._locale) {
|
|
153
|
+
// If not given, it's taken from the user's locale (normalized and simplified)
|
|
154
|
+
requestConfig.headers['accept-language'] = req._locale
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Forward all dwc-* headers
|
|
158
|
+
if (service.options.forward_dwc_headers) {
|
|
159
|
+
const originalHeaders = req.context?.http.req.headers || {}
|
|
160
|
+
for (const k in originalHeaders) if (k.match(/^dwc-/)) requestConfig.headers[k] = originalHeaders[k]
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Ensure stream request body & appropriate content headers
|
|
164
|
+
if (
|
|
165
|
+
requestConfig.data &&
|
|
166
|
+
requestConfig.method !== 'GET' &&
|
|
167
|
+
requestConfig.method !== 'HEAD' &&
|
|
168
|
+
!(requestConfig.data instanceof Readable)
|
|
169
|
+
) {
|
|
170
|
+
if (typeof requestConfig.data === 'object' && !Buffer.isBuffer(requestConfig.data)) {
|
|
171
|
+
requestConfig.headers['content-type'] = 'application/json'
|
|
172
|
+
requestConfig.headers['content-length'] = Buffer.byteLength(JSON.stringify(requestConfig.data))
|
|
173
|
+
} else if (typeof requestConfig.data === 'string') {
|
|
174
|
+
requestConfig.headers['content-length'] = Buffer.byteLength(requestConfig.data)
|
|
175
|
+
} else if (Buffer.isBuffer(requestConfig.data)) {
|
|
176
|
+
requestConfig.headers['content-length'] = Buffer.byteLength(requestConfig.data)
|
|
177
|
+
if (!_hasHeader(req.headers, 'content-type')) requestConfig.headers['content-type'] = 'application/octet-stream'
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Add batch envelope if needed
|
|
182
|
+
const maxGetUrlLength = service.options.max_get_url_length ?? cds.env.remote?.max_get_url_length ?? 1028
|
|
183
|
+
if (
|
|
184
|
+
requestConfig.method === 'GET' &&
|
|
185
|
+
KINDS_SUPPORTING_BATCH[service.kind] &&
|
|
186
|
+
requestConfig.url.length > maxGetUrlLength
|
|
187
|
+
) {
|
|
188
|
+
_batchEnvelope(requestConfig, maxGetUrlLength)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (service.path) requestConfig.url = `${encodeURI(service.path)}${requestConfig.url}`
|
|
192
|
+
|
|
193
|
+
// Set axios responseType to 'arraybuffer' if returning binary in rest
|
|
194
|
+
if (req._binary) requestConfig.responseType = 'arraybuffer'
|
|
195
|
+
|
|
196
|
+
return requestConfig
|
|
197
|
+
}
|