@sap/cds 7.4.2 → 7.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +94 -0
- package/apis/cds.d.ts +1 -38
- package/apis/core.d.ts +21 -101
- package/apis/cqn.d.ts +18 -76
- package/apis/csn.d.ts +18 -114
- package/apis/events.d.ts +16 -123
- package/apis/internal/inference.d.ts +18 -32
- package/apis/linked.d.ts +18 -97
- package/apis/log.d.ts +19 -164
- package/apis/models.d.ts +18 -180
- package/apis/ql.d.ts +16 -323
- package/apis/reflect.d.ts +32 -0
- package/apis/server.d.ts +18 -135
- package/apis/services.d.ts +18 -380
- package/bin/cds-serve.js +5 -2
- package/bin/serve.js +7 -16
- package/lib/auth/basic-auth.js +3 -1
- package/lib/auth/ias-auth.js +62 -48
- package/lib/auth/ias-claims.js +34 -0
- package/lib/auth/index.js +54 -33
- package/lib/auth/jwt-auth.js +55 -52
- package/lib/compile/cdsc.js +2 -2
- package/lib/compile/to/edm.js +4 -4
- package/lib/compile/to/hdbtabledata.js +5 -8
- package/lib/compile/to/srvinfo.js +2 -2
- package/lib/env/cds-env.js +3 -9
- package/lib/env/cds-requires.js +16 -17
- package/lib/env/compat.js +0 -9
- package/lib/env/defaults.js +17 -6
- package/lib/i18n/localize.js +46 -42
- package/lib/index.js +6 -8
- package/lib/linked/classes.js +7 -118
- package/lib/linked/entities.js +1 -1
- package/lib/log/cds-log.js +15 -10
- package/lib/log/format/aspects/als.js +41 -0
- package/lib/log/format/aspects/cf.js +36 -0
- package/lib/log/format/json.js +96 -0
- package/lib/plugins.js +7 -3
- package/lib/req/context.js +4 -2
- package/lib/srv/cds-connect.js +3 -5
- package/lib/srv/cds-serve.js +13 -26
- package/lib/srv/factory.js +3 -3
- package/lib/srv/middlewares/index.js +0 -2
- package/lib/srv/middlewares/trace.js +2 -3
- package/lib/srv/protocols/_legacy.js +27 -30
- package/lib/srv/protocols/index.js +173 -58
- package/lib/srv/protocols/odata-v4.js +29 -16
- package/lib/srv/srv-api.js +8 -13
- package/lib/srv/srv-handlers.js +14 -14
- package/lib/utils/cds-utils.js +15 -0
- package/libx/_runtime/auth/index.js +4 -5
- package/libx/_runtime/auth/strategies/basic.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +23 -13
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +6 -15
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +10 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +5 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +2 -1
- package/libx/_runtime/cds-services/services/utils/columns.js +3 -9
- package/libx/_runtime/cds.js +13 -0
- package/libx/_runtime/common/composition/data.js +3 -0
- package/libx/_runtime/common/composition/delete.js +1 -1
- package/libx/_runtime/common/error/frontend.js +2 -2
- package/libx/_runtime/common/generic/auth/readOnly.js +1 -1
- package/libx/_runtime/common/generic/auth/restrictions.js +1 -1
- package/libx/_runtime/common/generic/sorting.js +4 -5
- package/libx/_runtime/common/utils/csn.js +23 -18
- package/libx/_runtime/common/utils/restrictions.js +6 -15
- package/libx/_runtime/db/generic/input.js +3 -2
- package/libx/_runtime/fiori/generic/readOverDraft.js +2 -5
- package/libx/_runtime/fiori/lean-draft.js +69 -5
- package/libx/_runtime/hana/Service.js +1 -1
- package/libx/_runtime/messaging/AMQPWebhookMessaging.js +1 -1
- package/libx/_runtime/messaging/Outbox.js +3 -8
- package/libx/_runtime/messaging/enterprise-messaging.js +1 -1
- package/libx/_runtime/messaging/file-based.js +1 -1
- package/libx/_runtime/messaging/service.js +7 -10
- package/libx/_runtime/remote/Service.js +15 -45
- package/libx/_runtime/remote/utils/client.js +20 -33
- package/libx/_runtime/remote/utils/cloudSdkProvider.js +30 -0
- package/libx/_runtime/sqlite/Service.js +2 -2
- package/libx/odata/afterburner.js +29 -21
- package/libx/odata/cqn2odata.js +1 -1
- package/libx/odata/error.js +7 -0
- package/libx/odata/grammar.peggy +16 -20
- package/libx/odata/metadata.js +73 -78
- package/libx/odata/parser.js +1 -1
- package/libx/odata/read.js +94 -0
- package/libx/odata/result.js +91 -0
- package/libx/odata/service-document.js +31 -37
- package/libx/odata/utils.js +2 -1
- package/libx/outbox/index.js +9 -4
- package/libx/rest/RestAdapter.js +68 -67
- package/libx/rest/middleware/create.js +20 -26
- package/libx/rest/middleware/delete.js +5 -3
- package/libx/rest/middleware/error.js +2 -3
- package/libx/rest/middleware/input.js +5 -5
- package/libx/rest/middleware/operation.js +96 -41
- package/libx/rest/middleware/parse.js +4 -6
- package/libx/rest/middleware/payload.js +5 -5
- package/libx/rest/middleware/read.js +11 -17
- package/libx/rest/middleware/update.js +20 -25
- package/package.json +2 -1
- package/server.js +7 -4
- package/srv/outbox.cds +9 -10
- package/apis/env.d.ts +0 -25
- package/apis/test.d.ts +0 -81
- package/apis/utils.d.ts +0 -15
- package/lib/auth/passport-basic.js +0 -14
- package/lib/auth/passport-digest.js +0 -16
- package/lib/env/presets.js +0 -35
- package/lib/log/format/cf.js +0 -16
- package/lib/log/format/kibana.js +0 -92
- package/lib/srv/middlewares/ctx-auth.js +0 -11
- package/libx/_runtime/cds-services/adapter/rest/utils/validation-checks.js +0 -119
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
237
|
-
|
|
238
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
20
|
-
const { executeHttpRequestWithOrigin } =
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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)
|
package/libx/odata/cqn2odata.js
CHANGED
|
@@ -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')) {
|
package/libx/odata/grammar.peggy
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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} /
|
|
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
|
|