@sap/cds 7.4.2 → 7.5.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 (115) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/apis/cds.d.ts +1 -38
  3. package/apis/core.d.ts +21 -101
  4. package/apis/cqn.d.ts +18 -76
  5. package/apis/csn.d.ts +18 -114
  6. package/apis/events.d.ts +16 -123
  7. package/apis/internal/inference.d.ts +18 -32
  8. package/apis/linked.d.ts +18 -97
  9. package/apis/log.d.ts +19 -164
  10. package/apis/models.d.ts +18 -180
  11. package/apis/ql.d.ts +16 -323
  12. package/apis/reflect.d.ts +32 -0
  13. package/apis/server.d.ts +18 -135
  14. package/apis/services.d.ts +18 -380
  15. package/bin/cds-serve.js +5 -2
  16. package/bin/serve.js +7 -16
  17. package/lib/auth/basic-auth.js +3 -1
  18. package/lib/auth/ias-auth.js +62 -48
  19. package/lib/auth/ias-claims.js +34 -0
  20. package/lib/auth/index.js +55 -33
  21. package/lib/auth/jwt-auth.js +55 -52
  22. package/lib/compile/cdsc.js +2 -2
  23. package/lib/compile/to/edm.js +4 -4
  24. package/lib/compile/to/hdbtabledata.js +5 -8
  25. package/lib/compile/to/srvinfo.js +2 -2
  26. package/lib/env/cds-env.js +3 -9
  27. package/lib/env/cds-requires.js +16 -17
  28. package/lib/env/compat.js +0 -9
  29. package/lib/env/defaults.js +17 -6
  30. package/lib/i18n/localize.js +46 -42
  31. package/lib/index.js +6 -8
  32. package/lib/linked/classes.js +7 -118
  33. package/lib/linked/entities.js +1 -1
  34. package/lib/log/cds-log.js +15 -10
  35. package/lib/log/format/aspects/als.js +41 -0
  36. package/lib/log/format/aspects/cf.js +36 -0
  37. package/lib/log/format/json.js +96 -0
  38. package/lib/plugins.js +7 -3
  39. package/lib/req/context.js +4 -2
  40. package/lib/srv/cds-connect.js +3 -5
  41. package/lib/srv/cds-serve.js +13 -26
  42. package/lib/srv/factory.js +3 -3
  43. package/lib/srv/middlewares/index.js +0 -2
  44. package/lib/srv/middlewares/trace.js +2 -3
  45. package/lib/srv/protocols/_legacy.js +27 -30
  46. package/lib/srv/protocols/index.js +173 -58
  47. package/lib/srv/protocols/odata-v4.js +29 -16
  48. package/lib/srv/srv-api.js +8 -13
  49. package/lib/srv/srv-handlers.js +14 -14
  50. package/lib/utils/cds-utils.js +15 -0
  51. package/libx/_runtime/auth/index.js +4 -5
  52. package/libx/_runtime/auth/strategies/basic.js +2 -2
  53. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +23 -13
  54. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +6 -15
  55. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +10 -3
  56. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +5 -2
  57. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +2 -1
  58. package/libx/_runtime/cds-services/services/utils/columns.js +3 -9
  59. package/libx/_runtime/cds.js +13 -0
  60. package/libx/_runtime/common/composition/data.js +3 -0
  61. package/libx/_runtime/common/composition/delete.js +1 -1
  62. package/libx/_runtime/common/error/frontend.js +2 -2
  63. package/libx/_runtime/common/generic/auth/readOnly.js +1 -1
  64. package/libx/_runtime/common/generic/auth/restrictions.js +1 -1
  65. package/libx/_runtime/common/generic/sorting.js +4 -5
  66. package/libx/_runtime/common/utils/csn.js +23 -18
  67. package/libx/_runtime/common/utils/restrictions.js +6 -15
  68. package/libx/_runtime/db/generic/input.js +3 -2
  69. package/libx/_runtime/fiori/generic/readOverDraft.js +2 -5
  70. package/libx/_runtime/fiori/lean-draft.js +69 -5
  71. package/libx/_runtime/hana/Service.js +1 -1
  72. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +1 -1
  73. package/libx/_runtime/messaging/Outbox.js +3 -8
  74. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -0
  75. package/libx/_runtime/messaging/enterprise-messaging.js +1 -1
  76. package/libx/_runtime/messaging/file-based.js +1 -1
  77. package/libx/_runtime/messaging/service.js +7 -10
  78. package/libx/_runtime/remote/Service.js +15 -45
  79. package/libx/_runtime/remote/utils/client.js +20 -33
  80. package/libx/_runtime/remote/utils/cloudSdkProvider.js +30 -0
  81. package/libx/_runtime/sqlite/Service.js +2 -2
  82. package/libx/odata/afterburner.js +29 -21
  83. package/libx/odata/cqn2odata.js +1 -1
  84. package/libx/odata/error.js +7 -0
  85. package/libx/odata/grammar.peggy +16 -20
  86. package/libx/odata/metadata.js +73 -78
  87. package/libx/odata/parser.js +1 -1
  88. package/libx/odata/read.js +94 -0
  89. package/libx/odata/result.js +91 -0
  90. package/libx/odata/service-document.js +31 -37
  91. package/libx/odata/utils.js +2 -1
  92. package/libx/outbox/index.js +9 -4
  93. package/libx/rest/RestAdapter.js +68 -67
  94. package/libx/rest/middleware/create.js +20 -26
  95. package/libx/rest/middleware/delete.js +5 -3
  96. package/libx/rest/middleware/error.js +2 -3
  97. package/libx/rest/middleware/input.js +5 -5
  98. package/libx/rest/middleware/operation.js +96 -41
  99. package/libx/rest/middleware/parse.js +4 -6
  100. package/libx/rest/middleware/payload.js +5 -5
  101. package/libx/rest/middleware/read.js +11 -17
  102. package/libx/rest/middleware/update.js +20 -25
  103. package/package.json +2 -1
  104. package/server.js +7 -4
  105. package/srv/outbox.cds +9 -10
  106. package/apis/env.d.ts +0 -25
  107. package/apis/test.d.ts +0 -81
  108. package/apis/utils.d.ts +0 -15
  109. package/lib/auth/passport-basic.js +0 -14
  110. package/lib/auth/passport-digest.js +0 -16
  111. package/lib/env/presets.js +0 -35
  112. package/lib/log/format/cf.js +0 -16
  113. package/lib/log/format/kibana.js +0 -92
  114. package/lib/srv/middlewares/ctx-auth.js +0 -11
  115. package/libx/_runtime/cds-services/adapter/rest/utils/validation-checks.js +0 -119
@@ -94,6 +94,7 @@ class EndpointRegistry {
94
94
  }
95
95
  })
96
96
  } catch (error) {
97
+ LOG.error(error)
97
98
  return res.sendStatus(500)
98
99
  }
99
100
  })
@@ -85,7 +85,7 @@ class EnterpriseMessaging extends AMQPWebhookMessaging {
85
85
  const doNotDeploy = cds.requires.multitenancy && !this.options.deployForProvider
86
86
  if (doNotDeploy) this.LOG._info && this.LOG.info('Skipping deployment of messaging artifacts for provider account')
87
87
  super.startListening({ doNotDeploy })
88
- if (!doNotDeploy && (this._listenToAll || this.subscribedTopics.size)) {
88
+ if (!doNotDeploy && (this._listenToAll.value || this.subscribedTopics.size)) {
89
89
  const management = this.getManagement()
90
90
  // Webhooks will perform an OPTIONS call on creation to check the availability of the app.
91
91
  // On systems like Cloud Foundry the app URL will only be advertised once
@@ -36,7 +36,7 @@ class FileBasedMessaging extends MessagingService {
36
36
  }
37
37
 
38
38
  startWatching() {
39
- if (!this._listenToAll && !this.subscribedTopics.size) return
39
+ if (!this._listenToAll.value && !this.subscribedTopics.size) return
40
40
  const watcher = async () => {
41
41
  if (!(await touched(this.file, this.recent))) return // > not touched since last check
42
42
  // REVISIT: Bad if lock file wasn't cleaned up (due to crashes...)
@@ -6,15 +6,11 @@ const appId = require('./common-utils/appId')
6
6
 
7
7
  const _topic = declared => declared['@topic'] || declared.name
8
8
 
9
- let usedTopicOnce = false
10
- const _warnAndStripTopicPrefix = (event, LOG) => {
9
+ const _warnAndStripTopicPrefix = event => {
11
10
  if (event.startsWith('topic:')) {
11
+ cds._logDeprecation('The topic prefix `topic:` is deprecated and has no effect. Please remove it.')
12
12
  // backwards compatibility
13
13
  event = event.replace(/topic:/, '')
14
- if (!usedTopicOnce) {
15
- LOG._warn && LOG.warn('The topic prefix `topic:` is deprecated and has no effect. Please remove it.')
16
- }
17
- usedTopicOnce = true
18
14
  }
19
15
  return event
20
16
  }
@@ -25,6 +21,7 @@ class MessagingService extends cds.Service {
25
21
  // enables queued async operations (without awaiting)
26
22
  this.queued = queued()
27
23
  this.subscribedTopics = new Map()
24
+ this._listenToAll = { value: false }
28
25
  this.LOG = cds.log(this.kind ? `${this.kind}|messaging` : 'messaging')
29
26
  // Only for one central `messaging` service, otherwise all technical services would register themselves
30
27
  if (this.name === 'messaging') {
@@ -89,10 +86,10 @@ class MessagingService extends cds.Service {
89
86
  }
90
87
 
91
88
  on(event, cb) {
92
- const _event = _warnAndStripTopicPrefix(event, this.LOG)
89
+ const _event = _warnAndStripTopicPrefix(event)
93
90
  // save all subscribed topics (not needed for local-messaging)
94
91
  if (event !== '*') this.subscribedTopics.set(this.prepareTopic(_event, true), _event)
95
- else this._listenToAll = true
92
+ else this._listenToAll.value = true
96
93
  return super.on(_event, cb)
97
94
  }
98
95
 
@@ -119,7 +116,7 @@ class MessagingService extends cds.Service {
119
116
 
120
117
  message4(msg) {
121
118
  const _msg = { ...msg }
122
- _msg.event = _warnAndStripTopicPrefix(_msg.event, this.LOG)
119
+ _msg.event = _warnAndStripTopicPrefix(_msg.event)
123
120
  if (!_msg.headers) _msg.headers = {}
124
121
  if (!_msg.inbound) {
125
122
  _msg.headers = { ..._msg.headers } // don't change the original object
@@ -129,7 +126,7 @@ class MessagingService extends cds.Service {
129
126
  const subscribedEvent =
130
127
  this.subscribedTopics.get(_msg.event) ||
131
128
  (this.wildcarded && this.subscribedTopics.get(this.wildcarded(_msg.event)))
132
- if (!subscribedEvent && !this._listenToAll)
129
+ if (!subscribedEvent && !this._listenToAll.value)
133
130
  throw new Error(`No handler for incoming message with topic '${_msg.event}' found.`)
134
131
  _msg.event = subscribedEvent || _msg.event
135
132
  }
@@ -1,30 +1,12 @@
1
1
  const cds = require('../cds')
2
- const LOG = cds.log('remote')
3
2
 
4
3
  const { run, getReqOptions } = require('./utils/client')
4
+ const { getCloudSdk, getCloudSdkConnectivity, getCloudSdkResilience } = require('./utils/cloudSdkProvider')
5
5
  const { hasAliasedColumns } = require('./utils/data')
6
-
7
6
  const { resolveView, getTransition, restoreLink, findQueryTarget } = require('../common/utils/resolveView')
8
7
  const { postProcess } = require('../common/utils/postProcessing')
9
-
10
8
  const { formatVal } = require('../../odata/utils')
11
9
 
12
- let _cloudSdkConnectivity
13
- const _getCloudSdkConnectivity = () => {
14
- if (_cloudSdkConnectivity) return _cloudSdkConnectivity
15
- // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
16
- _cloudSdkConnectivity = require('@sap-cloud-sdk/connectivity')
17
- return _cloudSdkConnectivity
18
- }
19
-
20
- let _cloudSdkResilience
21
- const _getCloudSdkResilience = () => {
22
- if (_cloudSdkResilience) return _cloudSdkResilience
23
- // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
24
- _cloudSdkResilience = require('@sap-cloud-sdk/resilience')
25
- return _cloudSdkResilience
26
- }
27
-
28
10
  const _isSimpleCqnQuery = q => typeof q === 'object' && q !== null && !Array.isArray(q) && Object.keys(q).length > 0
29
11
 
30
12
  const _setHeaders = (defaultHeaders, req) => {
@@ -183,7 +165,7 @@ const resolvedTargetOfQuery = q => {
183
165
  const _resolveSelectionStrategy = options => {
184
166
  if (typeof options?.selectionStrategy !== 'string') return
185
167
 
186
- options.selectionStrategy = _getCloudSdkConnectivity().DestinationSelectionStrategies[options.selectionStrategy]
168
+ options.selectionStrategy = getCloudSdkConnectivity().DestinationSelectionStrategies[options.selectionStrategy]
187
169
  if (typeof options?.selectionStrategy !== 'function') {
188
170
  throw new Error(`Unsupported destination selection strategy "${options.selectionStrategy}".`)
189
171
  }
@@ -207,8 +189,6 @@ const _getDestination = (name, credentials) => {
207
189
  return { name, ...credentials }
208
190
  }
209
191
 
210
- let logged
211
-
212
192
  class RemoteService extends cds.Service {
213
193
  init() {
214
194
  this.kind = _getKind(this.options) // TODO: Simplify
@@ -227,24 +207,17 @@ class RemoteService extends cds.Service {
227
207
  this.path = this.options.credentials.path
228
208
 
229
209
  // `requestTimeout` API is kept as it was public
230
- this.requestTimeout = this.options.credentials.requestTimeout
231
- if (this.requestTimeout == null) this.requestTimeout = 60000
210
+ this.requestTimeout = this.options.credentials.requestTimeout ?? 60000
211
+
212
+ this.csrf = this.options.csrf
213
+ this.csrfInBatch = this.options.csrfInBatch
232
214
 
233
215
  // we're using this as an object to allow remote services without the need for Cloud SDK
234
216
  // required for BAS creating remote services only for events
235
217
  // at first request the middlewares are created
236
- this._resilienceMiddlewares = {}
237
-
238
- // REVISIT: remove cds.env.features.fetch_csrf in next major ^7
239
- this.csrf = cds.env.features.fetch_csrf ?? this.options.csrf
240
- this.csrfInBatch = this.options.csrfInBatch
241
- if (cds.env.features.fetch_csrf && !logged) {
242
- // for logging once for all remote services
243
- logged = true
244
- LOG._warn &&
245
- LOG.warn(
246
- 'Configuration option "cds.env.features.fetch_csrf" is deprecated.\n Please use "csrf"/"csrfInBatch" as described in https://cap.cloud.sap/docs/node.js/remote-services'
247
- )
218
+ this.middlewares = {
219
+ timeout: getCloudSdkResilience().timeout(this.requestTimeout),
220
+ csrf: this.csrf && getCloudSdk().csrf(this.csrf)
248
221
  }
249
222
  } else if ([...this.entities].length || [...this.operations].length) {
250
223
  throw new Error(`No credentials configured for "${this.name}".`)
@@ -261,9 +234,7 @@ class RemoteService extends cds.Service {
261
234
  }
262
235
  }
263
236
 
264
- for (const each of this.operations) {
265
- _addHandlerActionFunction(this, each)
266
- }
237
+ for (const each of this.operations) _addHandlerActionFunction(this, each)
267
238
 
268
239
  this.on('*', async (req, next) => {
269
240
  const { query } = req
@@ -273,16 +244,14 @@ class RemoteService extends cds.Service {
273
244
  // ideally, that's done on bootstrap of the remote service
274
245
  if (typeof this.destination === 'object' && !this.destination.url)
275
246
  throw new Error(`"url" or "destination" property must be configured in "credentials" of "${this.name}".`)
276
- if (this._resilienceMiddlewares && !this._resilienceMiddlewares.timeout)
277
- this._resilienceMiddlewares.timeout = _getCloudSdkResilience().timeout(this.requestTimeout)
278
247
 
279
248
  const reqOptions = getReqOptions(req, query, this)
280
249
  reqOptions.headers = _setHeaders(reqOptions.headers, req)
281
250
 
282
- const { kind, destination, destinationOptions, csrf, csrfInBatch } = this
251
+ const { kind, destination, destinationOptions } = this
283
252
  const resolvedTarget = resolvedTargetOfQuery(query) || getTransition(req.target, this).target
284
253
  const returnType = req._returnType
285
- const additionalOptions = { destination, kind, resolvedTarget, returnType, destinationOptions, csrf, csrfInBatch }
254
+ const additionalOptions = { destination, kind, resolvedTarget, returnType, destinationOptions }
286
255
 
287
256
  const jwt = req?.context?.headers?.authorization?.split(/^bearer /i)[1]
288
257
  if (jwt) additionalOptions.jwt = jwt
@@ -300,7 +269,7 @@ class RemoteService extends cds.Service {
300
269
 
301
270
  // FIXME: This is a dirty hack for this situation:
302
271
  // - This PR has cds.Service.model setter to always consistently apply cds.compile.for.odata, also for RemoteServices, which wasn't the case before
303
- // - because of that tests/_runtime/remote/__tests__/integration/odata.test.js fails, which relies on the former behavoir of RemoteServices
272
+ // - because of that tests/_runtime/remote/__tests__/integration/odata.test.js fails, which relies on the former behavior of RemoteServices
304
273
  // NOTE: that test would never have worked for RemoteServices bootstrapped from single cds.model, which is always cds.compiled.for.odata
305
274
  // REVISIT: should become obsolete with Universal CSN
306
275
  // set model(m) {
@@ -321,10 +290,12 @@ class RemoteService extends cds.Service {
321
290
 
322
291
  if (req.target && req.target.name && this.definition && req.target.name.startsWith(this.definition.name + '.')) {
323
292
  const result = await super.handle(req)
293
+
324
294
  // only post process if alias was explicitly set in query
325
295
  if (_selectOnlyWithAlias(req.query)) {
326
296
  return postProcess(req.query, result, this, true)
327
297
  }
298
+
328
299
  return result
329
300
  }
330
301
 
@@ -349,5 +320,4 @@ class RemoteService extends cds.Service {
349
320
  }
350
321
 
351
322
  RemoteService.prototype.isExternal = true
352
-
353
323
  module.exports = RemoteService
@@ -1,14 +1,11 @@
1
1
  const cds = require('../../cds')
2
2
  const LOG = cds.log('remote')
3
+ const { getCloudSdk } = require('./cloudSdkProvider')
3
4
 
4
5
  const SANITIZE_VALUES = process.env.NODE_ENV === 'production' && cds.env.log.sanitize_values !== false
5
-
6
6
  const { convertV2ResponseData, deepSanitize, convertV2PayloadData } = require('./data')
7
7
 
8
- let _cloudSdk
9
-
10
- const PPPD = { POST: 1, PUT: 1, PATCH: 1, DELETE: 1 }
11
- const KINDS_SUPPORTING_BATCH = { odata: 1, 'odata-v2': 1, 'odata-v4': 1 }
8
+ const KINDS_SUPPORTING_BATCH = { odata: true, 'odata-v2': true, 'odata-v4': true }
12
9
 
13
10
  const _sanitizeHeaders = headers => {
14
11
  // REVISIT: is this in-place modification intended?
@@ -16,14 +13,14 @@ const _sanitizeHeaders = headers => {
16
13
  return headers
17
14
  }
18
15
 
19
- const _executeHttpRequest = async ({ requestConfig, destination, destinationOptions, jwt, csrf, csrfInBatch }) => {
20
- const { executeHttpRequestWithOrigin } = _getCloudSdk()
16
+ const _executeHttpRequest = async ({ requestConfig, destination, destinationOptions, jwt }) => {
17
+ const { executeHttpRequestWithOrigin } = getCloudSdk()
21
18
 
22
19
  if (typeof destination === 'string') {
23
20
  destination = {
24
21
  destinationName: destination,
25
22
  ...destinationOptions,
26
- ...{ jwt: destinationOptions?.jwt !== undefined ? destinationOptions.jwt : jwt }
23
+ ...{ jwt: destinationOptions?.jwt === undefined ? jwt : destinationOptions.jwt }
27
24
  }
28
25
  if (destination.jwt !== undefined && !destination.jwt) delete destination.jwt // don't pass any value
29
26
  } else if (destination.forwardAuthToken) {
@@ -37,11 +34,6 @@ const _executeHttpRequest = async ({ requestConfig, destination, destinationOpti
37
34
  else LOG._warn && LOG.warn('Missing JWT token for forwardAuthToken')
38
35
  }
39
36
 
40
- let requestOptions
41
- if (PPPD[requestConfig.method]) {
42
- requestOptions = { fetchCsrfToken: requestConfig._autoBatch ? csrfInBatch === true : csrf === true }
43
- }
44
-
45
37
  if (LOG._debug) {
46
38
  const req2log = { headers: _sanitizeHeaders({ ...requestConfig.headers }) }
47
39
  if (requestConfig.method !== 'GET' && requestConfig.method !== 'DELETE')
@@ -61,19 +53,15 @@ const _executeHttpRequest = async ({ requestConfig, destination, destinationOpti
61
53
  ...(maxBodyLength && { maxBodyLength })
62
54
  }
63
55
 
56
+ // set `fetchCsrfToken` to `false` because we mount a custom CSRF middleware
57
+ const requestOptions = { fetchCsrfToken: false }
64
58
  return executeHttpRequestWithOrigin(destination, requestConfig, requestOptions)
65
59
  }
66
60
 
67
- const _getCloudSdk = () => {
68
- if (_cloudSdk) return _cloudSdk
69
- // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
70
- _cloudSdk = require('@sap-cloud-sdk/http-client')
71
- return _cloudSdk
72
- }
73
-
74
61
  /**
75
62
  * Rest Client
76
63
  */
64
+
77
65
  /**
78
66
  * Normalizes server path.
79
67
  *
@@ -136,6 +124,7 @@ function _normalizeMetadata(prefix, data, results) {
136
124
 
137
125
  return target
138
126
  }
127
+
139
128
  const _getPurgedRespActionFunc = (data, returnType) => {
140
129
  // return type is primitive value or inline/complex type
141
130
  if (returnType.kind === 'type' && !returnType.items && Object.values(data).length === 1) {
@@ -238,9 +227,7 @@ const run = async (requestConfig, options) => {
238
227
  requestConfig,
239
228
  destination,
240
229
  destinationOptions,
241
- jwt,
242
- csrf,
243
- csrfInBatch
230
+ jwt
244
231
  })
245
232
  } catch (e) {
246
233
  // > axios received status >= 400 -> gateway error
@@ -248,16 +235,13 @@ const run = async (requestConfig, options) => {
248
235
  e.message = msg ? 'Error during request to remote service: \n' + msg : 'Request to remote service failed.'
249
236
  const sanitizedError = _getSanitizedError(e, requestConfig, { suppressRemoteResponseBody })
250
237
  const err = Object.assign(new Error(e.message), { statusCode: 502, reason: sanitizedError })
251
-
252
238
  LOG._warn && LOG.warn(err)
253
239
  throw err
254
240
  }
255
241
 
256
242
  // text/html indicates a redirect -> reject
257
243
  if (
258
- response.headers &&
259
- response.headers['content-type'] &&
260
- response.headers['content-type'].includes('text/html') &&
244
+ response.headers?.['content-type']?.includes('text/html') &&
261
245
  !(
262
246
  requestConfig.headers.accept.includes('text/html') ||
263
247
  requestConfig.headers.accept.includes('text/*') ||
@@ -284,6 +268,7 @@ const run = async (requestConfig, options) => {
284
268
  // 2. entry contains request status code and request headers
285
269
  // 3. entry contains data or error
286
270
  const responseDataSplitted = response.data.split('\r\n\r\n')
271
+
287
272
  // remove closing batch id
288
273
  const [content] = responseDataSplitted[2].split('\r\n')
289
274
  const contentJSON = JSON.parse(content)
@@ -303,7 +288,6 @@ const run = async (requestConfig, options) => {
303
288
  })
304
289
 
305
290
  LOG._warn && LOG.warn(err)
306
-
307
291
  throw err
308
292
  }
309
293
  }
@@ -313,6 +297,7 @@ const run = async (requestConfig, options) => {
313
297
  if (kind === 'odata-v2') return _purgeODataV2(response.data, resolvedTarget, returnType, requestConfig.headers)
314
298
  if (kind === 'odata') {
315
299
  if (typeof response.data !== 'object') return response.data
300
+
316
301
  // try to guess if we need to purge v2 or v4
317
302
  if (response.data.d) return _purgeODataV2(response.data, resolvedTarget, returnType, requestConfig.headers)
318
303
  return _purgeODataV4(response.data)
@@ -396,9 +381,6 @@ const getReqOptions = (req, query, service) => {
396
381
 
397
382
  reqOptions.headers = { accept: 'application/json,text/plain' }
398
383
 
399
- // add resilience middlewares for Cloud SDK
400
- reqOptions.middleware = [service._resilienceMiddlewares.timeout]
401
-
402
384
  if (!_hasHeader(req.headers, 'accept-language')) {
403
385
  // Forward the locale properties from the original request (including region variants or weight factors),
404
386
  // if not given, it's taken from the user's locale (normalized and simplified)
@@ -428,11 +410,11 @@ const getReqOptions = (req, query, service) => {
428
410
  if (!_hasHeader(req.headers, 'content-type')) reqOptions.headers['content-type'] = 'application/octet-stream'
429
411
  }
430
412
  }
413
+
431
414
  reqOptions.url = formatPath(reqOptions.url)
432
415
 
433
416
  // batch envelope if needed
434
- const maxGetUrlLength =
435
- service.options.max_get_url_length || (cds.env.remote && cds.env.remote.max_get_url_length) || 1028
417
+ const maxGetUrlLength = service.options.max_get_url_length ?? cds.env.remote?.max_get_url_length ?? 1028
436
418
  if (KINDS_SUPPORTING_BATCH[service.kind] && reqOptions.method === 'GET' && reqOptions.url.length > maxGetUrlLength) {
437
419
  reqOptions._autoBatch = true
438
420
  reqOptions.data = [
@@ -453,6 +435,11 @@ const getReqOptions = (req, query, service) => {
453
435
  reqOptions.url = '/$batch'
454
436
  }
455
437
 
438
+ // mount resilience and csrf middlewares for SAP Cloud SDK
439
+ reqOptions.middleware = [service.middlewares.timeout]
440
+ const fetchCsrfToken = !!(reqOptions._autoBatch ? service.csrfInBatch : service.csrf)
441
+ if (fetchCsrfToken) reqOptions.middleware.push(service.middlewares.csrf)
442
+
456
443
  if (service.path) reqOptions.url = `${encodeURI(service.path)}${reqOptions.url}`
457
444
 
458
445
  // set axios responseType to 'arraybuffer' if returning binary in rest
@@ -0,0 +1,30 @@
1
+ let _cloudSdkConnectivity
2
+ const getCloudSdkConnectivity = () => {
3
+ if (_cloudSdkConnectivity) return _cloudSdkConnectivity
4
+ // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
5
+ _cloudSdkConnectivity = require('@sap-cloud-sdk/connectivity')
6
+ return _cloudSdkConnectivity
7
+ }
8
+
9
+ let _cloudSdkResilience
10
+ const getCloudSdkResilience = () => {
11
+ if (_cloudSdkResilience) return _cloudSdkResilience
12
+ // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
13
+ _cloudSdkResilience = require('@sap-cloud-sdk/resilience')
14
+ return _cloudSdkResilience
15
+ }
16
+
17
+ let _cloudSdk
18
+ const getCloudSdk = () => {
19
+ if (_cloudSdk) return _cloudSdk
20
+
21
+ // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
22
+ _cloudSdk = require('@sap-cloud-sdk/http-client')
23
+ return _cloudSdk
24
+ }
25
+
26
+ module.exports = {
27
+ getCloudSdkConnectivity,
28
+ getCloudSdkResilience,
29
+ getCloudSdk
30
+ }
@@ -72,7 +72,7 @@ module.exports = class SQLiteDatabase extends DatabaseService {
72
72
  }
73
73
 
74
74
  _registerBeforeHandlers() {
75
- this.before(['CREATE', 'UPDATE'], '*', this._input) // > has to run before rewrite
75
+ this.before(['CREATE', 'UPDATE', 'UPSERT'], '*', this._input) // > has to run before rewrite
76
76
  this.before(['CREATE', 'READ', 'UPDATE', 'DELETE', 'UPSERT'], '*', this._rewrite)
77
77
 
78
78
  if (cds.env.fiori.lean_draft && !cds.db?.cqn2sql) this.before('READ', '*', convertDraftAdminPathExpression)
@@ -125,7 +125,7 @@ module.exports = class SQLiteDatabase extends DatabaseService {
125
125
 
126
126
  dbc._queued = []
127
127
 
128
- if (cds.env.features.assert_integrity && cds.env.features.assert_integrity_type == 'DB') {
128
+ if (cds.env.features.assert_integrity === 'db') {
129
129
  await new Promise((resolve, reject) => {
130
130
  dbc.exec('PRAGMA foreign_keys = ON', err => {
131
131
  if (err) reject(err)
@@ -177,7 +177,7 @@ function _convertVal(element, value) {
177
177
  case 'cds.Integer':
178
178
  case 'cds.Int16':
179
179
  case 'cds.Int32':
180
- if (!/^-?\d+$/.test(value)) throw new Error('Not a valid integer')
180
+ if (!/^-?\+?\d+$/.test(value)) throw new Error('Not a valid integer')
181
181
  // eslint-disable-next-line no-case-declarations
182
182
  const n = Number(value)
183
183
  if (!Number.isSafeInteger(n)) throw new Error('Not a valid integer')
@@ -216,6 +216,30 @@ function _processSegments(from, model, namespace, cqn) {
216
216
  let incompleteKeys = false
217
217
  let one
218
218
  let target
219
+
220
+ function _handleCollectionBoundActions(i) {
221
+ let action
222
+ if (current.actions) {
223
+ const nextRef = typeof ref[i + 1] === 'string' && ref[i + 1]
224
+ const shortName = nextRef && nextRef.replace(namespace + '.', '')
225
+ action = shortName && current.actions[shortName]
226
+ }
227
+
228
+ incompleteKeys = ref[i].where ? false : i === ref.length - 1 || one ? false : true
229
+
230
+ if (incompleteKeys && action) {
231
+ if (
232
+ action['@cds.odata.bindingparameter.collection'] ||
233
+ (action.params && Object.values(action.params).some(e => e?.items?.type === '$self'))
234
+ ) {
235
+ incompleteKeys = false
236
+ } else {
237
+ const msg = `"${action.name}" must be called on a single instance of "${current.name}".`
238
+ throw Object.assign(new Error(msg), { statusCode: 400 })
239
+ }
240
+ }
241
+ }
242
+
219
243
  for (let i = 0; i < ref.length; i++) {
220
244
  const seg = ref[i].id || ref[i]
221
245
  const whereRef = ref[i].where
@@ -291,26 +315,7 @@ function _processSegments(from, model, namespace, cqn) {
291
315
  target = current
292
316
  one = !!(ref[i].where || current._isSingleton)
293
317
 
294
- let action
295
- if (current.actions) {
296
- const nextRef = typeof ref[i + 1] === 'string' && ref[i + 1]
297
- const shortName = nextRef && nextRef.replace(namespace + '.', '')
298
- action = shortName && current.actions[shortName]
299
- }
300
-
301
- incompleteKeys = ref[i].where ? false : i === ref.length - 1 || one ? false : true
302
-
303
- if (incompleteKeys && action) {
304
- if (
305
- action['@cds.odata.bindingparameter.collection'] ||
306
- (action.params && Object.values(action.params).some(e => e?.items?.type === '$self'))
307
- ) {
308
- incompleteKeys = false
309
- } else {
310
- const msg = `"${action.name}" must be called on a single instance of "${current.name}".`
311
- throw Object.assign(new Error(msg), { statusCode: 400 })
312
- }
313
- }
318
+ _handleCollectionBoundActions(i)
314
319
 
315
320
  if (whereRef) {
316
321
  keyCount += addRefToWhereIfNecessary(whereRef, current)
@@ -341,6 +346,9 @@ function _processSegments(from, model, namespace, cqn) {
341
346
  incompleteKeys = one || i === ref.length - 1 ? false : true
342
347
  current = model.definitions[current.target]
343
348
  target = current
349
+
350
+ _handleCollectionBoundActions(i)
351
+
344
352
  if (ref[i].where) {
345
353
  keyCount += addRefToWhereIfNecessary(ref[i].where, current)
346
354
  _resolveAliasesInXpr(ref[i].where, current)
@@ -119,7 +119,7 @@ const _format = (cur, elementName, target, kind, isLambda, func) => {
119
119
  if (typeof cur !== 'object') return encodeURIComponent(formatVal(cur, elementName, target, kind))
120
120
  if (hasValidProps(cur, 'ref'))
121
121
  return encodeURIComponent(isLambda ? [LAMBDA_VARIABLE, ...cur.ref].join('/') : cur.ref[0].id || cur.ref.join('/'))
122
- if (hasValidProps(cur, 'val')) return encodeURIComponent(formatVal(cur.val, elementName, target, kind, func))
122
+ if (hasValidProps(cur, 'val')) return encodeURIComponent(formatVal(cur.val, elementName, target, kind, func, cur.literal))
123
123
  if (hasValidProps(cur, 'xpr')) return `(${_xpr(cur.xpr, target, kind, isLambda)})`
124
124
  // REVISIT: How to detect the types for all functions?
125
125
  if (hasValidProps(cur, 'func')) {
@@ -0,0 +1,7 @@
1
+ module.exports = srv => (err, req, res, next) => {
2
+ if (err.code >= 400 && err.code < 500) {
3
+ return res.status(err.code).send(err.message)
4
+ }
5
+
6
+ return res.status(500).send('Internal Server Error')
7
+ }
@@ -26,17 +26,21 @@
26
26
 
27
27
  //
28
28
  // ---------- JavaScript Helpers -------------
29
+
29
30
  {
30
31
 
32
+ const TECHNICAL_OPTS = ['$value'] // odata parts to be handled somewhere else
33
+ const OPERATORS = { eq: '=', ne: '!=', lt: '<', gt: '>', le: '<=', ge: '>=' }
34
+ const SUPPORTED_APPLY_TRANSFORMATIONS = { topcount: true, bottomcount: true, topsum: false, bottomsum: false, toppercent: false, bottompercent: false }
35
+
31
36
  const $ = Object.assign
32
37
  const { strict, minimal } = options
33
38
  const stack = []
34
39
  let SELECT, count
35
- const TECHNICAL_OPTS = ['$value'] // odata parts to be handled somewhere else
36
- // we keep that here to allow for usage in https://peggyjs.org/online
40
+
37
41
  const safeNumber =
38
42
  options.safeNumber ||
39
- function (inputString) {
43
+ function (inputString) { //> keep that here to allow for usage in https://peggyjs.org/online
40
44
  if (typeof inputString !== 'string') return inputString
41
45
  // Try to parse the input string as a floating-point number using parseFloat
42
46
  const parsedFloat = parseFloat(inputString)
@@ -104,7 +108,6 @@
104
108
  newCqn[i] = _addNormalQueryOptions(newCqn[i], cqn, onlyColumnsFromExpand)
105
109
  }
106
110
  } else newCqn = _addNormalQueryOptions(newCqn, cqn, onlyColumnsFromExpand)
107
-
108
111
  return newCqn
109
112
  }
110
113
  const _addNormalQueryOptions = (cqn, topCqn, onlyColumnsFromExpand) => {
@@ -239,7 +242,7 @@
239
242
  }
240
243
  ;(SELECT.limit || (SELECT.limit = {})).offset = { val }
241
244
  }
242
- //Second parameter needed, to assure that order is correct
245
+ // Second parameter needed, to assure that order is correct
243
246
  const _setOrderBy = (appendObj, first = false) => {
244
247
  SELECT.orderBy = SELECT.orderBy
245
248
  ? first
@@ -397,7 +400,7 @@
397
400
  "$search=" o s:search { if (s) SELECT.search = s } /
398
401
  "$count=" o count /
399
402
  "$apply=" o trafos:transformations { return trafos } /
400
- //Workaround to support empty expand even if not OData compliant old adapter supported it and did not crash
403
+ // Workaround to support empty expand even if not OData compliant old adapter supported it and did not crash
401
404
  "$expand=" {return null}
402
405
 
403
406
  temporal = ("$at" / "$from" / "$toInclusive" / "$to") "=" date
@@ -451,7 +454,7 @@
451
454
  })
452
455
 
453
456
 
454
- //REVISIT: per OData spec $apply should be also supported inside of $expand
457
+ // REVISIT: per OData spec $apply should be also supported inside of $expand
455
458
  expand
456
459
  = (
457
460
  c:('*' / ref) {
@@ -589,8 +592,8 @@
589
592
  })* {
590
593
  if(mainTransformation === undefined) return
591
594
  additionalTransformation = (Array.isArray(additionalTransformation)) ? additionalTransformation : [additionalTransformation]
592
- //Loop through additionalTransformation
593
- //Loop through each element, add it to current level, if element is already part of result, increase level
595
+ // Loop through additionalTransformation
596
+ // Loop through each element, add it to current level, if element is already part of result, increase level
594
597
  for(let trafos of additionalTransformation) {
595
598
  for(const trafo in trafos) {
596
599
  if (trafo === 'limit' && trafos.limit && mainTransformation.limit && mainTransformation.limit.offset && trafos.limit.rows)
@@ -635,10 +638,10 @@
635
638
 
636
639
  //
637
640
  // ---------- Expressions ------------
641
+
638
642
  comparison
639
643
  = a:operand _ o:$("eq" / "ne" / "lt" / "gt" / "le" / "ge") _ b:operand {
640
- const op = { eq:'=', ne:'!=', lt:'<', gt:'>', le:'<=', ge:'>=' }[o] || o
641
- return [ a, op, b ]
644
+ return [ a, OPERATORS[o] || o, b ]
642
645
  }
643
646
 
644
647
  listFilterParam = aliased:aliasedParam { return { list: aliased } } / listRoundBrackets
@@ -742,20 +745,12 @@
742
745
 
743
746
  // REVISIT: All transformations below need improvment
744
747
  "search" search:searchTrafo{return search} /
745
- "concat" con:concatTrafo{return con} / //Return con so that concat string is not returned
748
+ "concat" con:concatTrafo{return con} / //> Return con so that concat string is not returned
746
749
  "compute" compute:computeTrafo{return compute} /
747
750
  "top" top:topTrafo{return top} /
748
751
  "skip" skip:skipTrafo{return skip} /
749
752
  "orderby" order:orderbyTrafo{return order} /
750
753
  func:("topcount"i/"bottomcount"i/"topsum"i/"bottomsum"i/"toppercent"i/"bottompercent"i) args:commonFuncTrafo {
751
- const SUPPORTED_APPLY_TRANSFORMATIONS = {
752
- "topcount": true,
753
- "bottomcount": true,
754
- "topsum": false,
755
- "bottomsum": false,
756
- "toppercent": false,
757
- "bottompercent": false
758
- }
759
754
  func = func.toLowerCase()
760
755
  if (!SUPPORTED_APPLY_TRANSFORMATIONS[func]) {
761
756
  throw Object.assign(new Error(`Transformation "${func}" in $apply is not yet supported.`), { statusCode: 501 })
@@ -850,6 +845,7 @@
850
845
  = OPEN o o:orderby o2:( COMMA o2:orderby{return o2} )* o CLOSE {
851
846
  return {orderBy: [o,...o2]}
852
847
  }
848
+
853
849
  //
854
850
  // ---------- Literals -----------
855
851