@sap/cds 7.3.0 → 7.4.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.
Files changed (109) hide show
  1. package/CHANGELOG.md +65 -3
  2. package/_i18n/i18n_es_MX.properties +110 -0
  3. package/apis/cds.d.ts +13 -12
  4. package/apis/core.d.ts +27 -108
  5. package/apis/cqn.d.ts +15 -18
  6. package/apis/csn.d.ts +95 -60
  7. package/apis/env.d.ts +25 -0
  8. package/apis/events.d.ts +124 -0
  9. package/apis/{reflect.d.ts → linked.d.ts} +27 -38
  10. package/apis/models.d.ts +60 -45
  11. package/apis/ql.d.ts +11 -5
  12. package/apis/{serve.d.ts → server.d.ts} +57 -31
  13. package/apis/services.d.ts +74 -145
  14. package/apis/test.d.ts +1 -1
  15. package/bin/serve.js +3 -0
  16. package/lib/compile/cds-compile.js +2 -2
  17. package/lib/compile/for/lean_drafts.js +1 -1
  18. package/lib/compile/to/edm.js +8 -3
  19. package/lib/compile/to/gql.js +4 -0
  20. package/lib/dbs/cds-deploy.js +52 -4
  21. package/lib/env/cds-requires.js +27 -15
  22. package/lib/env/defaults.js +1 -0
  23. package/lib/env/schemas/index.js +10 -0
  24. package/lib/index.js +8 -5
  25. package/lib/linked/models.js +8 -5
  26. package/lib/ql/CREATE.js +2 -0
  27. package/lib/ql/DELETE.js +1 -0
  28. package/lib/ql/DROP.js +2 -0
  29. package/lib/ql/INSERT.js +2 -22
  30. package/lib/ql/Query.js +59 -22
  31. package/lib/ql/SELECT.js +5 -0
  32. package/lib/ql/STREAM.js +2 -0
  33. package/lib/ql/UPDATE.js +2 -0
  34. package/lib/ql/UPSERT.js +3 -1
  35. package/lib/ql/cds-ql.js +21 -5
  36. package/lib/ql/infer.js +129 -0
  37. package/lib/req/cds-context.js +8 -5
  38. package/lib/srv/cds-connect.js +3 -1
  39. package/lib/utils/axios.js +4 -2
  40. package/lib/utils/cds-utils.js +9 -2
  41. package/lib/utils/data.js +3 -0
  42. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +12 -0
  43. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +26 -8
  44. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/batch/BatchProcessor.js +1 -1
  45. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +11 -8
  46. package/libx/_runtime/common/code-ext/worker.js +5 -16
  47. package/libx/_runtime/common/generic/auth/capabilities.js +11 -2
  48. package/libx/_runtime/common/i18n/messages.properties +1 -0
  49. package/libx/_runtime/common/utils/postProcessing.js +1 -1
  50. package/libx/_runtime/common/utils/resolveView.js +20 -1
  51. package/libx/{common → _runtime/common}/utils/ucsn.js +19 -11
  52. package/libx/_runtime/db/expand/expandCQNToJoin.js +2 -2
  53. package/libx/_runtime/db/sql-builder/InsertBuilder.js +6 -1
  54. package/libx/_runtime/db/sql-builder/UpdateBuilder.js +6 -1
  55. package/libx/_runtime/db/sql-builder/dollar.js +7 -7
  56. package/libx/_runtime/fiori/generic/activate.js +2 -2
  57. package/libx/_runtime/fiori/generic/edit.js +25 -45
  58. package/libx/_runtime/fiori/generic/read.js +3 -5
  59. package/libx/_runtime/fiori/lean-draft.js +142 -64
  60. package/libx/_runtime/fiori/utils/delete.js +7 -1
  61. package/libx/_runtime/fiori/utils/handler.js +4 -6
  62. package/libx/_runtime/fiori/utils/lockInfo.js +27 -0
  63. package/libx/_runtime/fiori/utils/where.js +20 -1
  64. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +3 -2
  65. package/libx/_runtime/messaging/Outbox.js +12 -47
  66. package/libx/_runtime/messaging/common-utils/AMQPClient.js +1 -3
  67. package/libx/_runtime/messaging/common-utils/authorizedRequest.js +3 -0
  68. package/libx/_runtime/messaging/common-utils/connections.js +1 -1
  69. package/libx/_runtime/messaging/enterprise-messaging.js +12 -13
  70. package/libx/_runtime/messaging/file-based.js +7 -5
  71. package/libx/_runtime/messaging/redis-messaging.js +10 -11
  72. package/libx/_runtime/messaging/service.js +12 -26
  73. package/libx/_runtime/remote/Service.js +52 -36
  74. package/libx/_runtime/remote/utils/client.js +22 -123
  75. package/libx/odata/afterburner.js +14 -5
  76. package/libx/odata/grammar.peggy +26 -7
  77. package/libx/odata/metadata.js +18 -1
  78. package/libx/odata/parser.js +1 -1
  79. package/libx/odata/service-document.js +0 -1
  80. package/libx/odata/utils.js +19 -3
  81. package/libx/{_runtime/messaging/outbox/utils.js → outbox/index.js} +94 -24
  82. package/libx/rest/middleware/parse.js +1 -1
  83. package/package.json +2 -2
  84. package/apis/connect.d.ts +0 -39
  85. package/bin/utils/modules.js +0 -7
  86. package/bin/utils/term.js +0 -56
  87. package/lib/env/schema.js +0 -9
  88. package/lib/linked/queries.js +0 -41
  89. package/lib/srv/protocols/odata-v2-proxy.js +0 -3699
  90. package/libx/common/asserts.js +0 -0
  91. package/libx/common/crud.js +0 -0
  92. package/libx/common/etag.js +0 -0
  93. package/libx/common/localized.js +0 -0
  94. package/libx/common/managed.js +0 -0
  95. package/libx/common/paging.js +0 -0
  96. package/libx/common/readme.md +0 -4
  97. package/libx/common/sorting.js +0 -0
  98. package/libx/common/temporal.js +0 -0
  99. package/libx/connect/auth.js +0 -0
  100. package/libx/connect/perf.js +0 -0
  101. package/libx/connect/readme.md +0 -3
  102. package/libx/fiori/draft/readme.md +0 -1
  103. package/libx/fiori/readme.md +0 -1
  104. package/libx/hana/readme.md +0 -1
  105. package/libx/msg/readme.md +0 -3
  106. package/libx/readme.md +0 -1
  107. package/libx/sqlite/readme.md +0 -1
  108. /package/libx/_runtime/{messaging/common-utils → common/utils}/waitingTime.js +0 -0
  109. /package/libx/{_runtime/messaging/outbox → outbox}/OutboxRunner.js +0 -0
@@ -1,8 +1,11 @@
1
1
  const https = require('https')
2
2
  const requestToken = require('../http-utils/token')
3
+ const cds = require('../../cds')
4
+ const LOG = cds.log('http-messaging') // not public
3
5
 
4
6
  const authorizedRequest = ({ method, uri, path, oa2, tenant, dataObj, headers, tokenStore }) => {
5
7
  return new Promise((resolve, reject) => {
8
+ if (LOG._debug) LOG.debug({ method, uri, path, oa2, tenant, dataObj, headers, tokenStore })
6
9
  ;((tokenStore.token && Promise.resolve(tokenStore.token)) || requestToken(oa2, tenant, tokenStore))
7
10
  .catch(err => reject(err))
8
11
  .then(token => {
@@ -1,4 +1,4 @@
1
- const waitingTime = require('./waitingTime')
1
+ const waitingTime = require('../../common/utils/waitingTime')
2
2
 
3
3
  const _connectUntilConnected = (client, LOG, x) => {
4
4
  const _waitingTime = waitingTime(x)
@@ -45,10 +45,7 @@ class EnterpriseMessaging extends AMQPWebhookMessaging {
45
45
  const provisioningSrv = await cds.connect.to('cds.xt.SaasProvisioningService')
46
46
  deploymentSrv.impl(() => {
47
47
  deploymentSrv.after('subscribe', async (_res, req) => {
48
- const { tenant } = req.data
49
- let subdomain
50
- const tenantInfo = await getTenantInfo(tenant)
51
- subdomain = tenantInfo.subdomain
48
+ const { subscribedSubdomain: subdomain } = req.data.metadata
52
49
  const management = await this.getManagement(subdomain).waitUntilReady()
53
50
  await management.deploy()
54
51
  })
@@ -158,35 +155,37 @@ class EnterpriseMessaging extends AMQPWebhookMessaging {
158
155
  })
159
156
  }
160
157
 
161
- async emit(event, ...etc) {
162
- const msg = this.message4(event, ...etc)
158
+ async handle(msg) {
159
+ if (msg.inbound) return super.handle(msg)
160
+ const _msg = this.message4(msg)
163
161
  const _optionsMessagingREST = optionsMessagingREST(this.options)
164
162
  const context = this.context || cds.context
165
- const tenant = context && context.tenant
166
- const topic = msg.event
167
- const message = { ...(msg.headers || {}), data: msg.data }
163
+ const tenant = cds.requires.multitenancy && context && context.tenant
164
+ const topic = _msg.event
165
+ const message = { ...(_msg.headers || {}), data: _msg.data }
168
166
 
169
167
  const contentType =
170
- msg.headers && ['id', 'source', 'specversion', 'type'].every(el => el in msg.headers)
168
+ _msg.headers && ['id', 'source', 'specversion', 'type'].every(el => el in _msg.headers)
171
169
  ? 'application/cloudevents+json'
172
170
  : 'application/json'
173
171
 
174
172
  await this.queued(() => {})()
175
173
 
176
174
  try {
177
- await authorizedRequest({
175
+ const params = {
178
176
  method: 'POST',
179
177
  uri: _optionsMessagingREST.uri,
180
178
  path: `/messagingrest/v1/topics/${encodeURIComponent(topic)}/messages`,
181
179
  oa2: _optionsMessagingREST.oa2,
182
- tenant,
183
180
  dataObj: message,
184
181
  headers: {
185
182
  'Content-Type': contentType,
186
183
  'x-qos': 1
187
184
  },
188
185
  tokenStore: {}
189
- })
186
+ }
187
+ if (tenant) params.tenant = tenant
188
+ await authorizedRequest(params)
190
189
  } catch (e) {
191
190
  // Note: If the topic rules don't allow the topic, we get a 403 (which is a strange choice by Event Mesh)
192
191
  if (e && (e.statusCode === 400 || e.statusCode === 403)) e.unrecoverable = true
@@ -19,7 +19,8 @@ class FileBasedMessaging extends MessagingService {
19
19
  return super.init()
20
20
  }
21
21
 
22
- async emit(msg) {
22
+ async handle(msg) {
23
+ if (msg.inbound) return super.handle(msg)
23
24
  const _msg = this.message4(msg)
24
25
  const e = _msg.event
25
26
  delete _msg.event
@@ -53,10 +54,11 @@ class FileBasedMessaging extends MessagingService {
53
54
  if (this.subscribedTopics.has(topic)) {
54
55
  const event = this.subscribedTopics.get(topic)
55
56
  if (!event) return
56
- // REVISIT: should we use this.dispatch instead of super.emit for inbound messages?
57
- super
58
- .emit({ event, ...json, inbound: true })
59
- .catch(e => this.LOG.error('ERROR occured in asynchronous event processing:', e))
57
+ this.tx(tx =>
58
+ tx
59
+ .emit({ event, ...json, inbound: true })
60
+ .catch(e => this.LOG.error('ERROR occured in asynchronous event processing:', e))
61
+ )
60
62
  } else other.push(each + '\n')
61
63
  }
62
64
  } catch (e) {
@@ -1,9 +1,8 @@
1
1
  // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
2
2
  const redis = require('redis')
3
3
  const cds = require('../../../lib')
4
- const waitingTime = require('./common-utils/waitingTime')
4
+ const waitingTime = require('../common/utils/waitingTime')
5
5
  const normalizeIncomingMessage = require('./common-utils/normalizeIncomingMessage')
6
- const { hasPersistentOutbox } = require('./outbox/utils')
7
6
 
8
7
  const _handleReconnects = (client, LOG) => {
9
8
  client.on('reconnecting', () => {
@@ -57,6 +56,14 @@ class RedisMessaging extends cds.MessagingService {
57
56
  })
58
57
  }
59
58
 
59
+ async handle(msg) {
60
+ if (msg.inbound) return super.handle(msg)
61
+ const _msg = this.message4(msg)
62
+ this.LOG._info && this.LOG.info('Emit', { topic: _msg.event })
63
+ if (!this._ready && msg._fromOutbox) throw new Error('Redis connection not ready')
64
+ await this.client.publish(_msg.event, JSON.stringify({ data: _msg.data, ...(_msg.headers || {}) }))
65
+ }
66
+
60
67
  async startListening() {
61
68
  let subscriber
62
69
  for (const topic of [...this.subscribedTopics].map(kv => kv[0])) {
@@ -71,21 +78,13 @@ class RedisMessaging extends cds.MessagingService {
71
78
  const msg = normalizeIncomingMessage(message)
72
79
  msg.event = topic
73
80
  try {
74
- await super.emit(msg)
81
+ await this.tx({ user: cds.User.privileged }, tx => tx.emit(msg))
75
82
  } catch (e) {
76
83
  this.LOG.error('ERROR occured in asynchronous event processing:', e)
77
84
  }
78
85
  })
79
86
  }
80
87
  }
81
-
82
- async emit(msg) {
83
- const _msg = this.message4(msg)
84
- this.LOG._info && this.LOG.info('Emit', { topic: _msg.event })
85
- if (!this._ready && hasPersistentOutbox(this, cds.context && cds.context.tenant))
86
- throw new Error('Redis connection not ready')
87
- await this.client.publish(_msg.event, JSON.stringify({ data: _msg.data, ...(_msg.headers || {}) }))
88
- }
89
88
  }
90
89
 
91
90
  module.exports = RedisMessaging
@@ -1,6 +1,5 @@
1
1
  const cds = require('../cds')
2
2
  const queued = require('./common-utils/queued')
3
- const OutboxService = require('./Outbox')
4
3
  const ExtendedModels = require('../../../lib/srv/srv-models')
5
4
 
6
5
  const appId = require('./common-utils/appId')
@@ -21,7 +20,7 @@ const _warnAndStripTopicPrefix = (event, LOG) => {
21
20
  }
22
21
 
23
22
  // There's currently no mechanism to detect mocked services, this is the best we can do.
24
- class MessagingService extends OutboxService {
23
+ class MessagingService extends cds.Service {
25
24
  init() {
26
25
  // enables queued async operations (without awaiting)
27
26
  this.queued = queued()
@@ -58,9 +57,10 @@ class MessagingService extends OutboxService {
58
57
  // calls to srv.emit are forwarded to this.emit, which is expected to
59
58
  // be overwritten by subclasses to write events to message channel
60
59
  const topic = _topic(declared)
61
- srv.on(event, msg => {
60
+ srv.on(event, async msg => {
62
61
  const { data, headers } = msg
63
- return this.tx(msg).emit({ event: topic, data, headers })
62
+ const messaging = await cds.connect.to('messaging') // needed for potential outbox
63
+ return messaging.tx(msg).emit({ event: topic, data, headers })
64
64
  })
65
65
  }
66
66
  })
@@ -77,29 +77,15 @@ class MessagingService extends OutboxService {
77
77
  return super.init()
78
78
  }
79
79
 
80
- async emit(event, data, headers) {
81
- const _msg = typeof event === 'object' ? event : { event, data, headers }
82
- if (_msg instanceof cds.Event) return super.emit(_msg)
83
- if (_msg.inbound && !cds.context) {
84
- return cds._context.run(
85
- {
86
- tenant: _msg.tenant,
87
- user: cds.User.privileged,
88
- ...(_msg._?.req && { req: _msg._.req }),
89
- ...(_msg._?.res && { res: _msg._.res })
90
- },
91
- async () => {
92
- if (cds.model) {
93
- const ctx = cds.context
94
- ctx.model = await ExtendedModels.model4(ctx.tenant, ctx.features)
95
- }
96
- const msg = new cds.Event(this.message4(_msg))
97
- return super.emit(msg)
98
- }
99
- )
80
+ async handle(msg) {
81
+ if (msg.inbound) {
82
+ if (cds.model) {
83
+ const ctx = cds.context
84
+ ctx.model = await ExtendedModels.model4(ctx.tenant, ctx.features)
85
+ }
86
+ return super.handle(this.message4(msg))
100
87
  }
101
- const msg = new cds.Event(this.message4(_msg))
102
- return super.emit(msg)
88
+ return super.handle(msg)
103
89
  }
104
90
 
105
91
  on(event, cb) {
@@ -1,21 +1,25 @@
1
1
  const cds = require('../cds')
2
2
  const LOG = cds.log('remote')
3
3
 
4
+ const { run, getReqOptions } = require('./utils/client')
5
+ const { hasAliasedColumns } = require('./utils/data')
6
+
4
7
  const { resolveView, getTransition, restoreLink, findQueryTarget } = require('../common/utils/resolveView')
5
8
  const { postProcess } = require('../common/utils/postProcessing')
6
- const { getKind, run, getDestination, getAdditionalOptions, getReqOptions } = require('./utils/client')
9
+
7
10
  const { formatVal } = require('../../odata/utils')
8
- const { hasAliasedColumns } = require('./utils/data')
9
11
 
10
- let _cloudSdkConnectivity, _cloudSdkResilience
11
- const cloudSdkConnectivity = () => {
12
+ let _cloudSdkConnectivity
13
+ const _getCloudSdkConnectivity = () => {
12
14
  if (_cloudSdkConnectivity) return _cloudSdkConnectivity
13
15
  // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
14
16
  _cloudSdkConnectivity = require('@sap-cloud-sdk/connectivity')
15
17
  return _cloudSdkConnectivity
16
18
  }
17
- const cloudSdkResilience = () => {
18
- if (_cloudSdkResilience !== undefined) return _cloudSdkResilience
19
+
20
+ let _cloudSdkResilience
21
+ const _getCloudSdkResilience = () => {
22
+ if (_cloudSdkResilience) return _cloudSdkResilience
19
23
  // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
20
24
  _cloudSdkResilience = require('@sap-cloud-sdk/resilience')
21
25
  return _cloudSdkResilience
@@ -96,7 +100,9 @@ const _handleBoundActionFunction = (srv, def, req, url) => {
96
100
  if (def.params) {
97
101
  const data = _extractParamsFromData(req.data, def.params)
98
102
  url = _buildPartialUrlFunctions(url, data, def.params)
99
- } else url = `${url}()`
103
+ } else {
104
+ url = `${url}()`
105
+ }
100
106
 
101
107
  return srv.get(url)
102
108
  }
@@ -105,11 +111,7 @@ const _handleUnboundActionFunction = (srv, def, req, event) => {
105
111
  if (def.kind === 'action') {
106
112
  // REVISIT: only for "rest" unbound actions/functions, we enforce axios to return a buffer
107
113
  // required by cds-mt
108
- const isBinary =
109
- srv.kind === 'rest' &&
110
- def &&
111
- def.returns &&
112
- (def.returns.type === 'cds.LargeBinary' || def.returns.type === 'cds.Binary')
114
+ const isBinary = srv.kind === 'rest' && def?.returns?.type.match(/binary/i)
113
115
  const { headers, data } = req
114
116
 
115
117
  return srv.send({ method: 'POST', path: `/${event}`, headers, data, _binary: isBinary })
@@ -163,14 +165,12 @@ const _addHandlerActionFunction = (srv, def, target) => {
163
165
  const url = `/${shortEntityName}(${_buildKeys(req, this.kind).join(',')})/${this.namespace}.${event}`
164
166
  return _handleBoundActionFunction(srv, def, req, url)
165
167
  })
166
-
167
- return
168
+ } else {
169
+ srv.on(event, async function (req) {
170
+ if (this.kind === 'odata-v2') return _handleV2ActionFunction(srv, def, req, event, this.kind)
171
+ return _handleUnboundActionFunction(srv, def, req, event)
172
+ })
168
173
  }
169
-
170
- srv.on(event, async function (req) {
171
- if (this.kind === 'odata-v2') return _handleV2ActionFunction(srv, def, req, event, this.kind)
172
- return _handleUnboundActionFunction(srv, def, req, event)
173
- })
174
174
  }
175
175
 
176
176
  const _selectOnlyWithAlias = q => q?.SELECT && !q.SELECT._transitions && q.SELECT?.columns?.some(hasAliasedColumns)
@@ -180,20 +180,38 @@ const resolvedTargetOfQuery = q => {
180
180
  return transitions.length && [transitions.length - 1].target
181
181
  }
182
182
 
183
- let logged
184
-
185
183
  const _resolveSelectionStrategy = options => {
186
184
  if (typeof options?.selectionStrategy !== 'string') return
187
- options.selectionStrategy = cloudSdkConnectivity().DestinationSelectionStrategies[options.selectionStrategy]
188
185
 
186
+ options.selectionStrategy = _getCloudSdkConnectivity().DestinationSelectionStrategies[options.selectionStrategy]
189
187
  if (typeof options?.selectionStrategy !== 'function') {
190
188
  throw new Error(`Unsupported destination selection strategy "${options.selectionStrategy}".`)
191
189
  }
192
190
  }
193
191
 
192
+ const _getKind = options => {
193
+ const kind = (options.credentials && options.credentials.kind) || options.kind
194
+ if (typeof kind === 'object') {
195
+ const k = Object.keys(kind).find(
196
+ key => key === 'odata' || key === 'odata-v4' || key === 'odata-v2' || key === 'rest'
197
+ )
198
+ // odata-v4 is equivalent of odata
199
+ return k === 'odata-v4' ? 'odata' : k
200
+ }
201
+ return kind
202
+ }
203
+
204
+ const _getDestination = (name, credentials) => {
205
+ // Cloud SDK wants property "queryParameters" but we have documented "queries"
206
+ if (credentials.queries && !credentials.queryParameters) credentials.queryParameters = credentials.queries
207
+ return { name, ...credentials }
208
+ }
209
+
210
+ let logged
211
+
194
212
  class RemoteService extends cds.Service {
195
213
  init() {
196
- this.kind = getKind(this.options) // TODO: Simplify
214
+ this.kind = _getKind(this.options) // TODO: Simplify
197
215
 
198
216
  /*
199
217
  * set up connectivity stuff if credentials are provided
@@ -205,7 +223,7 @@ class RemoteService extends cds.Service {
205
223
  _resolveSelectionStrategy(this.destinationOptions)
206
224
  this.destination =
207
225
  this.options.credentials.destination ??
208
- getDestination(this.definition?.name ?? this.datasource, this.options.credentials)
226
+ _getDestination(this.definition?.name ?? this.datasource, this.options.credentials)
209
227
  this.path = this.options.credentials.path
210
228
 
211
229
  // `requestTimeout` API is kept as it was public
@@ -250,27 +268,24 @@ class RemoteService extends cds.Service {
250
268
  this.on('*', async (req, next) => {
251
269
  const { query } = req
252
270
  if (!query && !(typeof req.path === 'string')) return next()
271
+
253
272
  // early validation on first request for use case without remote API
254
273
  // ideally, that's done on bootstrap of the remote service
255
274
  if (typeof this.destination === 'object' && !this.destination.url)
256
275
  throw new Error(`"url" or "destination" property must be configured in "credentials" of "${this.name}".`)
257
276
  if (this._resilienceMiddlewares && !this._resilienceMiddlewares.timeout)
258
- this._resilienceMiddlewares.timeout = cloudSdkResilience().timeout(this.requestTimeout)
277
+ this._resilienceMiddlewares.timeout = _getCloudSdkResilience().timeout(this.requestTimeout)
259
278
 
260
- const resolvedTarget = resolvedTargetOfQuery(query) || getTransition(req.target, this).target
261
279
  const reqOptions = getReqOptions(req, query, this)
262
280
  reqOptions.headers = _setHeaders(reqOptions.headers, req)
281
+
282
+ const { kind, destination, destinationOptions, csrf, csrfInBatch } = this
283
+ const resolvedTarget = resolvedTargetOfQuery(query) || getTransition(req.target, this).target
263
284
  const returnType = req._returnType
264
- const additionalOptions = getAdditionalOptions(
265
- req,
266
- this.destination,
267
- this.kind,
268
- resolvedTarget,
269
- returnType,
270
- this.destinationOptions,
271
- this.csrf,
272
- this.csrfInBatch
273
- )
285
+ const additionalOptions = { destination, kind, resolvedTarget, returnType, destinationOptions, csrf, csrfInBatch }
286
+
287
+ const jwt = req?.context?.headers?.authorization?.split(/^bearer /i)[1]
288
+ if (jwt) additionalOptions.jwt = jwt
274
289
 
275
290
  // hidden compat flag in order to suppress logging response body of failed request
276
291
  if (req._suppressRemoteResponseBody) {
@@ -334,4 +349,5 @@ class RemoteService extends cds.Service {
334
349
  }
335
350
 
336
351
  RemoteService.prototype.isExternal = true
352
+
337
353
  module.exports = RemoteService
@@ -7,41 +7,34 @@ const { convertV2ResponseData, deepSanitize, convertV2PayloadData } = require('.
7
7
 
8
8
  let _cloudSdk
9
9
 
10
- const PPPD = {
11
- POST: 1,
12
- PUT: 1,
13
- PATCH: 1,
14
- DELETE: 1
15
- }
16
-
10
+ const PPPD = { POST: 1, PUT: 1, PATCH: 1, DELETE: 1 }
17
11
  const KINDS_SUPPORTING_BATCH = { odata: 1, 'odata-v2': 1, 'odata-v4': 1 }
18
12
 
19
13
  const _sanitizeHeaders = headers => {
20
- if (headers && headers.authorization) headers.authorization = headers.authorization.split(' ')[0] + ' ...'
21
-
14
+ // REVISIT: is this in-place modification intended?
15
+ if (headers?.authorization) headers.authorization = headers.authorization.split(' ')[0] + ' ***'
22
16
  return headers
23
17
  }
24
18
 
25
19
  const _executeHttpRequest = async ({ requestConfig, destination, destinationOptions, jwt, csrf, csrfInBatch }) => {
26
- const { executeHttpRequestWithOrigin } = cloudSdk()
27
- const destinationName = typeof destination === 'string' && destination
20
+ const { executeHttpRequestWithOrigin } = _getCloudSdk()
28
21
 
29
- if (destinationName) {
30
- destination = { destinationName, ...(resolveDestinationOptions(destinationOptions, jwt) ?? {}) }
22
+ if (typeof destination === 'string') {
23
+ destination = {
24
+ destinationName: destination,
25
+ ...destinationOptions,
26
+ ...{ jwt: destinationOptions?.jwt !== undefined ? destinationOptions.jwt : jwt }
27
+ }
28
+ if (destination.jwt !== undefined && !destination.jwt) delete destination.jwt // don't pass any value
31
29
  } else if (destination.forwardAuthToken) {
32
30
  destination = {
33
31
  ...destination,
34
32
  headers: destination.headers ? { ...destination.headers } : {},
35
33
  authentication: 'NoAuthentication'
36
34
  }
37
-
38
35
  delete destination.forwardAuthToken
39
-
40
- if (jwt) {
41
- destination.headers.authorization = `Bearer ${jwt}`
42
- } else {
43
- LOG._warn && LOG.warn('Missing JWT token for forwardAuthToken')
44
- }
36
+ if (jwt) destination.headers.authorization = `Bearer ${jwt}`
37
+ else LOG._warn && LOG.warn('Missing JWT token for forwardAuthToken')
45
38
  }
46
39
 
47
40
  let requestOptions
@@ -64,62 +57,20 @@ const _executeHttpRequest = async ({ requestConfig, destination, destinationOpti
64
57
  const maxBodyLength = cds.env?.remote?.max_body_length
65
58
  requestConfig = {
66
59
  ...requestConfig,
67
- headers: {
68
- custom: { ...requestConfig.headers }
69
- },
60
+ headers: { custom: { ...requestConfig.headers } },
70
61
  ...(maxBodyLength && { maxBodyLength })
71
62
  }
72
63
 
73
64
  return executeHttpRequestWithOrigin(destination, requestConfig, requestOptions)
74
65
  }
75
66
 
76
- const cloudSdk = () => {
67
+ const _getCloudSdk = () => {
77
68
  if (_cloudSdk) return _cloudSdk
78
69
  // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
79
70
  _cloudSdk = require('@sap-cloud-sdk/http-client')
80
71
  return _cloudSdk
81
72
  }
82
73
 
83
- const getDestination = (name, credentials) => {
84
- // Cloud SDK wants property "queryParameters" but we have documented "queries"
85
- if (credentials.queries && !credentials.queryParameters) {
86
- credentials.queryParameters = credentials.queries
87
- }
88
-
89
- return { name, ...credentials }
90
- }
91
-
92
- /**
93
- * @param {import('@sap-cloud-sdk/connectivity').DestinationFetchOptions} [options]
94
- * @param {string} [jwt]
95
- * @returns {import('@sap-cloud-sdk/connectivity').DestinationFetchOptions}
96
- */
97
- const resolveDestinationOptions = function (options, jwt) {
98
- if (!options && !jwt) return
99
-
100
- const resolvedOptions = Object.assign({}, options ?? {})
101
- resolvedOptions.jwt = jwt
102
-
103
- if (options?.selectionStrategy) {
104
- resolvedOptions.selectionStrategy = options.selectionStrategy
105
- }
106
-
107
- return resolvedOptions
108
- }
109
-
110
- const getKind = options => {
111
- const kind = (options.credentials && options.credentials.kind) || options.kind
112
- if (typeof kind === 'object') {
113
- const k = Object.keys(kind).find(
114
- key => key === 'odata' || key === 'odata-v4' || key === 'odata-v2' || key === 'rest'
115
- )
116
- // odata-v4 is equivalent of odata
117
- return k === 'odata-v4' ? 'odata' : k
118
- }
119
-
120
- return kind
121
- }
122
-
123
74
  /**
124
75
  * Rest Client
125
76
  */
@@ -278,21 +229,10 @@ const _getSanitizedError = (e, reqOptions, options = { suppressRemoteResponseBod
278
229
  }
279
230
 
280
231
  // eslint-disable-next-line complexity
281
- const run = async (
282
- requestConfig,
283
- {
284
- destination,
285
- jwt,
286
- kind,
287
- resolvedTarget,
288
- returnType,
289
- suppressRemoteResponseBody,
290
- destinationOptions,
291
- csrf,
292
- csrfInBatch
293
- }
294
- ) => {
232
+ const run = async (requestConfig, options) => {
295
233
  let response
234
+
235
+ const { destination, destinationOptions, jwt, csrf, csrfInBatch, suppressRemoteResponseBody } = options
296
236
  try {
297
237
  response = await _executeHttpRequest({
298
238
  requestConfig,
@@ -368,31 +308,19 @@ const run = async (
368
308
  }
369
309
  }
370
310
 
311
+ const { kind, resolvedTarget, returnType } = options
371
312
  if (kind === 'odata-v4') return _purgeODataV4(response.data)
372
313
  if (kind === 'odata-v2') return _purgeODataV2(response.data, resolvedTarget, returnType, requestConfig.headers)
373
314
  if (kind === 'odata') {
374
315
  if (typeof response.data !== 'object') return response.data
375
316
  // try to guess if we need to purge v2 or v4
376
- if (response.data.d) {
377
- return _purgeODataV2(response.data, resolvedTarget, returnType, requestConfig.headers)
378
- }
379
-
317
+ if (response.data.d) return _purgeODataV2(response.data, resolvedTarget, returnType, requestConfig.headers)
380
318
  return _purgeODataV4(response.data)
381
319
  }
382
320
 
383
321
  return response.data
384
322
  }
385
323
 
386
- const getJwt = req => {
387
- const headers = req?.context?.headers
388
- if (headers?.authorization) {
389
- const token = headers.authorization.match(/^bearer (.+)/i)
390
- if (token) return token[1]
391
- }
392
-
393
- return null
394
- }
395
-
396
324
  const _cqnToReqOptions = (query, service, req) => {
397
325
  const { kind, model } = service
398
326
  const method = req.method
@@ -528,41 +456,12 @@ const getReqOptions = (req, query, service) => {
528
456
  if (service.path) reqOptions.url = `${encodeURI(service.path)}${reqOptions.url}`
529
457
 
530
458
  // set axios responseType to 'arraybuffer' if returning binary in rest
531
- if (req._binary) {
532
- reqOptions.responseType = 'arraybuffer'
533
- }
459
+ if (req._binary) reqOptions.responseType = 'arraybuffer'
534
460
 
535
461
  return reqOptions
536
462
  }
537
463
 
538
- const getAdditionalOptions = (
539
- req,
540
- destination,
541
- kind,
542
- resolvedTarget,
543
- returnType,
544
- destinationOptions,
545
- csrf,
546
- csrfInBatch
547
- ) => {
548
- const jwt = getJwt(req)
549
- const additionalOptions = {
550
- destination,
551
- kind,
552
- resolvedTarget,
553
- returnType,
554
- destinationOptions,
555
- csrf,
556
- csrfInBatch
557
- }
558
- if (jwt) additionalOptions.jwt = jwt
559
- return additionalOptions
560
- }
561
-
562
464
  module.exports = {
563
- getKind,
564
465
  run,
565
- getReqOptions,
566
- getDestination,
567
- getAdditionalOptions
466
+ getReqOptions
568
467
  }
@@ -125,12 +125,15 @@ function getResolvedElement(entity, { ref }) {
125
125
  return element
126
126
  }
127
127
 
128
+ const forbidden = { '(': 1, and: 1, or: 1, not: 1, ')': 1 }
129
+
128
130
  function _processWhere(where, entity) {
129
131
  for (let i = 0; i < where.length; i++) {
130
132
  const ref = where[i]
133
+ const operator = where[i + 1]
131
134
  const val = where[i + 2]
132
135
 
133
- if (ref === '(' || ref === ')' || ref === 'and' || ref === 'or' || ref === 'not' || val === 'not' || ref.func) {
136
+ if (ref in forbidden || val in forbidden || ref.func) {
134
137
  continue
135
138
  }
136
139
  if (ref.xpr) {
@@ -138,6 +141,11 @@ function _processWhere(where, entity) {
138
141
  continue
139
142
  }
140
143
 
144
+ if (operator in forbidden) {
145
+ // xpr check needs to be done first, else it could happen, that we ignore xpr OR xpr
146
+ continue;
147
+ }
148
+
141
149
  let valIndex = -1
142
150
  let refIndex = -1
143
151
  if (typeof val === 'object') {
@@ -169,15 +177,16 @@ function _convertVal(element, value) {
169
177
  case 'cds.UInt8':
170
178
  case 'cds.Int16':
171
179
  case 'cds.Int32':
180
+ if (!/^\d+$/.test(value)) throw new Error('Not a valid integer')
172
181
  // eslint-disable-next-line no-case-declarations
173
182
  const n = Number(value)
174
- if (Number.isSafeInteger(n)) return n
175
- throw new Error('Not a valid integer') // TODO
183
+ if (!Number.isSafeInteger(n)) throw new Error('Not a valid integer')
184
+ return n
176
185
 
177
186
  case 'cds.String':
178
- case 'cds.LargeString':
187
+ case 'cds.LargeString':
179
188
  return String(value)
180
- case 'cds.Double':
189
+ case 'cds.Double':
181
190
  return parseFloat(value)
182
191
  case 'cds.Decimal':
183
192
  case 'cds.DecimalFloat':