@sap/cds 9.2.1 → 9.3.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 (72) hide show
  1. package/CHANGELOG.md +85 -1
  2. package/_i18n/i18n_es.properties +3 -3
  3. package/_i18n/i18n_es_MX.properties +3 -3
  4. package/_i18n/i18n_fr.properties +2 -2
  5. package/_i18n/messages.properties +6 -0
  6. package/app/index.js +0 -1
  7. package/bin/deploy.js +1 -1
  8. package/bin/serve.js +7 -20
  9. package/lib/compile/cdsc.js +3 -0
  10. package/lib/compile/for/flows.js +102 -0
  11. package/lib/compile/for/nodejs.js +28 -0
  12. package/lib/compile/to/edm.js +11 -4
  13. package/lib/core/classes.js +1 -1
  14. package/lib/core/linked-csn.js +8 -0
  15. package/lib/dbs/cds-deploy.js +12 -12
  16. package/lib/env/cds-env.js +1 -1
  17. package/lib/env/cds-requires.js +21 -20
  18. package/lib/env/defaults.js +2 -1
  19. package/lib/index.js +5 -6
  20. package/lib/log/cds-log.js +6 -5
  21. package/lib/log/format/aspects/cf.js +2 -2
  22. package/lib/plugins.js +1 -1
  23. package/lib/ql/cds-ql.js +0 -3
  24. package/lib/req/request.js +3 -3
  25. package/lib/req/response.js +12 -7
  26. package/lib/srv/bindings.js +17 -17
  27. package/lib/srv/cds-connect.js +6 -9
  28. package/lib/srv/cds-serve.js +74 -137
  29. package/lib/srv/cds.Service.js +49 -0
  30. package/lib/srv/factory.js +4 -4
  31. package/lib/srv/middlewares/auth/ias-auth.js +33 -9
  32. package/lib/srv/middlewares/auth/index.js +3 -2
  33. package/lib/srv/middlewares/auth/jwt-auth.js +20 -6
  34. package/lib/srv/protocols/hcql.js +16 -1
  35. package/lib/srv/srv-dispatch.js +1 -1
  36. package/lib/utils/cds-utils.js +4 -8
  37. package/lib/utils/csv-reader.js +27 -7
  38. package/libx/_runtime/cds.js +0 -6
  39. package/libx/_runtime/common/Service.js +5 -0
  40. package/libx/_runtime/common/generic/crud.js +1 -1
  41. package/libx/_runtime/common/generic/flows.js +106 -0
  42. package/libx/_runtime/common/generic/paging.js +3 -3
  43. package/libx/_runtime/common/utils/differ.js +5 -15
  44. package/libx/_runtime/common/utils/resolveView.js +2 -2
  45. package/libx/_runtime/common/utils/rewriteAsterisks.js +10 -4
  46. package/libx/_runtime/fiori/lean-draft.js +76 -40
  47. package/libx/_runtime/messaging/enterprise-messaging.js +1 -1
  48. package/libx/_runtime/messaging/service.js +7 -0
  49. package/libx/_runtime/remote/Service.js +68 -62
  50. package/libx/_runtime/remote/utils/client.js +29 -216
  51. package/libx/_runtime/remote/utils/query.js +197 -0
  52. package/libx/_runtime/ucl/Service.js +180 -112
  53. package/libx/_runtime/ucl/queries.js +61 -0
  54. package/libx/odata/ODataAdapter.js +1 -4
  55. package/libx/odata/index.js +2 -10
  56. package/libx/odata/middleware/error.js +8 -1
  57. package/libx/odata/middleware/stream.js +1 -1
  58. package/libx/odata/middleware/update.js +12 -2
  59. package/libx/odata/parse/afterburner.js +113 -20
  60. package/libx/odata/parse/cqn2odata.js +1 -3
  61. package/libx/rest/middleware/parse.js +9 -2
  62. package/package.json +2 -2
  63. package/server.js +2 -0
  64. package/srv/app-service.js +1 -0
  65. package/srv/db-service.js +1 -0
  66. package/srv/msg-service.js +1 -0
  67. package/srv/remote-service.js +1 -0
  68. package/srv/ucl-service.cds +32 -0
  69. package/srv/ucl-service.js +1 -0
  70. package/lib/ql/resolve.js +0 -45
  71. package/libx/common/assert/type-strict.js +0 -109
  72. 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, convertV2PayloadData } = require('./data')
8
-
9
- const KINDS_SUPPORTING_BATCH = { odata: true, 'odata-v2': true, 'odata-v4': true }
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
- destinationName: destination,
23
- ...destinationOptions,
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 && LOG.warn('Missing JWT token for forwardAuthToken')
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 formattedPath
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
- const run = async (requestConfig, options) => {
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
+ }