@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.
- package/CHANGELOG.md +65 -3
- package/_i18n/i18n_es_MX.properties +110 -0
- package/apis/cds.d.ts +13 -12
- package/apis/core.d.ts +27 -108
- package/apis/cqn.d.ts +15 -18
- package/apis/csn.d.ts +95 -60
- package/apis/env.d.ts +25 -0
- package/apis/events.d.ts +124 -0
- package/apis/{reflect.d.ts → linked.d.ts} +27 -38
- package/apis/models.d.ts +60 -45
- package/apis/ql.d.ts +11 -5
- package/apis/{serve.d.ts → server.d.ts} +57 -31
- package/apis/services.d.ts +74 -145
- package/apis/test.d.ts +1 -1
- package/bin/serve.js +3 -0
- package/lib/compile/cds-compile.js +2 -2
- package/lib/compile/for/lean_drafts.js +1 -1
- package/lib/compile/to/edm.js +8 -3
- package/lib/compile/to/gql.js +4 -0
- package/lib/dbs/cds-deploy.js +52 -4
- package/lib/env/cds-requires.js +27 -15
- package/lib/env/defaults.js +1 -0
- package/lib/env/schemas/index.js +10 -0
- package/lib/index.js +8 -5
- package/lib/linked/models.js +8 -5
- package/lib/ql/CREATE.js +2 -0
- package/lib/ql/DELETE.js +1 -0
- package/lib/ql/DROP.js +2 -0
- package/lib/ql/INSERT.js +2 -22
- package/lib/ql/Query.js +59 -22
- package/lib/ql/SELECT.js +5 -0
- package/lib/ql/STREAM.js +2 -0
- package/lib/ql/UPDATE.js +2 -0
- package/lib/ql/UPSERT.js +3 -1
- package/lib/ql/cds-ql.js +21 -5
- package/lib/ql/infer.js +129 -0
- package/lib/req/cds-context.js +8 -5
- package/lib/srv/cds-connect.js +3 -1
- package/lib/utils/axios.js +4 -2
- package/lib/utils/cds-utils.js +9 -2
- package/lib/utils/data.js +3 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +12 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +26 -8
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/batch/BatchProcessor.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +11 -8
- package/libx/_runtime/common/code-ext/worker.js +5 -16
- package/libx/_runtime/common/generic/auth/capabilities.js +11 -2
- package/libx/_runtime/common/i18n/messages.properties +1 -0
- package/libx/_runtime/common/utils/postProcessing.js +1 -1
- package/libx/_runtime/common/utils/resolveView.js +20 -1
- package/libx/{common → _runtime/common}/utils/ucsn.js +19 -11
- package/libx/_runtime/db/expand/expandCQNToJoin.js +2 -2
- package/libx/_runtime/db/sql-builder/InsertBuilder.js +6 -1
- package/libx/_runtime/db/sql-builder/UpdateBuilder.js +6 -1
- package/libx/_runtime/db/sql-builder/dollar.js +7 -7
- package/libx/_runtime/fiori/generic/activate.js +2 -2
- package/libx/_runtime/fiori/generic/edit.js +25 -45
- package/libx/_runtime/fiori/generic/read.js +3 -5
- package/libx/_runtime/fiori/lean-draft.js +142 -64
- package/libx/_runtime/fiori/utils/delete.js +7 -1
- package/libx/_runtime/fiori/utils/handler.js +4 -6
- package/libx/_runtime/fiori/utils/lockInfo.js +27 -0
- package/libx/_runtime/fiori/utils/where.js +20 -1
- package/libx/_runtime/messaging/AMQPWebhookMessaging.js +3 -2
- package/libx/_runtime/messaging/Outbox.js +12 -47
- package/libx/_runtime/messaging/common-utils/AMQPClient.js +1 -3
- package/libx/_runtime/messaging/common-utils/authorizedRequest.js +3 -0
- package/libx/_runtime/messaging/common-utils/connections.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging.js +12 -13
- package/libx/_runtime/messaging/file-based.js +7 -5
- package/libx/_runtime/messaging/redis-messaging.js +10 -11
- package/libx/_runtime/messaging/service.js +12 -26
- package/libx/_runtime/remote/Service.js +52 -36
- package/libx/_runtime/remote/utils/client.js +22 -123
- package/libx/odata/afterburner.js +14 -5
- package/libx/odata/grammar.peggy +26 -7
- package/libx/odata/metadata.js +18 -1
- package/libx/odata/parser.js +1 -1
- package/libx/odata/service-document.js +0 -1
- package/libx/odata/utils.js +19 -3
- package/libx/{_runtime/messaging/outbox/utils.js → outbox/index.js} +94 -24
- package/libx/rest/middleware/parse.js +1 -1
- package/package.json +2 -2
- package/apis/connect.d.ts +0 -39
- package/bin/utils/modules.js +0 -7
- package/bin/utils/term.js +0 -56
- package/lib/env/schema.js +0 -9
- package/lib/linked/queries.js +0 -41
- package/lib/srv/protocols/odata-v2-proxy.js +0 -3699
- package/libx/common/asserts.js +0 -0
- package/libx/common/crud.js +0 -0
- package/libx/common/etag.js +0 -0
- package/libx/common/localized.js +0 -0
- package/libx/common/managed.js +0 -0
- package/libx/common/paging.js +0 -0
- package/libx/common/readme.md +0 -4
- package/libx/common/sorting.js +0 -0
- package/libx/common/temporal.js +0 -0
- package/libx/connect/auth.js +0 -0
- package/libx/connect/perf.js +0 -0
- package/libx/connect/readme.md +0 -3
- package/libx/fiori/draft/readme.md +0 -1
- package/libx/fiori/readme.md +0 -1
- package/libx/hana/readme.md +0 -1
- package/libx/msg/readme.md +0 -3
- package/libx/readme.md +0 -1
- package/libx/sqlite/readme.md +0 -1
- /package/libx/_runtime/{messaging/common-utils → common/utils}/waitingTime.js +0 -0
- /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 => {
|
|
@@ -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 {
|
|
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
|
|
162
|
-
|
|
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 =
|
|
167
|
-
const message = { ...(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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('
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9
|
+
|
|
7
10
|
const { formatVal } = require('../../odata/utils')
|
|
8
|
-
const { hasAliasedColumns } = require('./utils/data')
|
|
9
11
|
|
|
10
|
-
let _cloudSdkConnectivity
|
|
11
|
-
const
|
|
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
|
-
|
|
18
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
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 } =
|
|
27
|
-
const destinationName = typeof destination === 'string' && destination
|
|
20
|
+
const { executeHttpRequestWithOrigin } = _getCloudSdk()
|
|
28
21
|
|
|
29
|
-
if (
|
|
30
|
-
destination = {
|
|
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
|
-
|
|
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
|
|
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
|
|
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))
|
|
175
|
-
|
|
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':
|