@sap/cds 6.7.1 → 6.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -1
- package/_i18n/i18n.properties +9 -6
- package/_i18n/i18n_ar.properties +6 -6
- package/_i18n/i18n_cs.properties +6 -6
- package/_i18n/i18n_da.properties +6 -6
- package/_i18n/i18n_de.properties +6 -6
- package/_i18n/i18n_en.properties +6 -6
- package/_i18n/i18n_es.properties +6 -6
- package/_i18n/i18n_fi.properties +6 -6
- package/_i18n/i18n_fr.properties +6 -6
- package/_i18n/i18n_hu.properties +6 -6
- package/_i18n/i18n_it.properties +6 -6
- package/_i18n/i18n_ja.properties +6 -6
- package/_i18n/i18n_ko.properties +6 -6
- package/_i18n/i18n_ms.properties +6 -6
- package/_i18n/i18n_nl.properties +6 -6
- package/_i18n/i18n_no.properties +6 -6
- package/_i18n/i18n_pl.properties +6 -6
- package/_i18n/i18n_pt.properties +6 -6
- package/_i18n/i18n_ro.properties +6 -6
- package/_i18n/i18n_ru.properties +6 -6
- package/_i18n/i18n_sv.properties +6 -6
- package/_i18n/i18n_th.properties +6 -6
- package/_i18n/i18n_tr.properties +8 -8
- package/_i18n/i18n_zh_CN.properties +3 -3
- package/_i18n/i18n_zh_TW.properties +6 -6
- package/apis/core.d.ts +30 -31
- package/apis/csn.d.ts +1 -1
- package/apis/ql.d.ts +69 -39
- package/apis/serve.d.ts +4 -3
- package/apis/services.d.ts +20 -7
- package/bin/build/buildTaskEngine.js +1 -1
- package/bin/build/index.js +1 -1
- package/bin/build/provider/buildTaskProviderInternal.js +9 -6
- package/bin/build/provider/hana/index.js +11 -4
- package/bin/build/provider/mtx-sidecar/index.js +3 -3
- package/bin/build/provider/nodejs/index.js +23 -0
- package/bin/version.js +3 -2
- package/common.cds +3 -2
- package/lib/auth/index.js +3 -0
- package/lib/auth/mocked-users.js +13 -0
- package/lib/compile/etc/_localized.js +3 -0
- package/lib/core/entities.js +7 -3
- package/lib/dbs/cds-deploy.js +36 -12
- package/lib/env/cds-env.js +47 -14
- package/lib/env/cds-requires.js +16 -7
- package/lib/env/defaults.js +2 -2
- package/lib/env/schemas/cds-rc.json +1 -8
- package/lib/index.js +1 -1
- package/lib/ql/STREAM.js +89 -0
- package/lib/ql/cds-ql.js +2 -1
- package/lib/req/request.js +5 -2
- package/lib/req/user.js +1 -1
- package/lib/srv/middlewares/index.js +9 -7
- package/lib/srv/middlewares/trace.js +6 -5
- package/lib/srv/srv-api.js +1 -0
- package/lib/utils/cds-utils.js +1 -1
- package/lib/utils/tar.js +30 -31
- package/libx/_runtime/audit/Service.js +96 -37
- package/libx/_runtime/audit/generic/personal/utils.js +26 -13
- package/libx/_runtime/audit/utils/v2.js +21 -22
- package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +7 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/batch/BatchProcessor.js +2 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +2 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +10 -3
- package/libx/_runtime/cds-services/services/Service.js +2 -7
- package/libx/_runtime/cds-services/services/utils/differ.js +1 -1
- package/libx/_runtime/common/aspects/any.js +1 -1
- package/libx/_runtime/common/generic/auth/utils.js +30 -41
- package/libx/_runtime/common/generic/input.js +14 -8
- package/libx/_runtime/common/i18n/messages.properties +1 -1
- package/libx/_runtime/db/expand/expandCQNToJoin.js +19 -17
- package/libx/_runtime/db/expand/rawToExpanded.js +3 -5
- package/libx/_runtime/db/utils/generateAliases.js +1 -1
- package/libx/_runtime/fiori/generic/activate.js +1 -1
- package/libx/_runtime/fiori/generic/before.js +18 -19
- package/libx/_runtime/fiori/generic/prepare.js +1 -1
- package/libx/_runtime/fiori/generic/read.js +1 -1
- package/libx/_runtime/fiori/lean-draft.js +27 -21
- package/libx/_runtime/fiori/utils/handler.js +0 -6
- package/libx/_runtime/hana/customBuilder/CustomFunctionBuilder.js +1 -1
- package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +0 -5
- package/libx/_runtime/hana/pool.js +26 -18
- package/libx/_runtime/hana/search2Contains.js +1 -1
- package/libx/_runtime/hana/search2cqn4sql.js +26 -18
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +23 -16
- package/libx/_runtime/messaging/outbox/utils.js +6 -1
- package/libx/_runtime/remote/Service.js +64 -38
- package/libx/_runtime/remote/utils/client.js +13 -9
- package/libx/rest/middleware/read.js +2 -1
- package/package.json +1 -1
|
@@ -17,6 +17,7 @@ function multiTenantServiceManager() {
|
|
|
17
17
|
if (e.code === 'MODULE_NOT_FOUND') return null
|
|
18
18
|
else throw e
|
|
19
19
|
}
|
|
20
|
+
|
|
20
21
|
const oldIm =
|
|
21
22
|
cds.requires.multitenancy?.['old-instance-manager'] ??
|
|
22
23
|
cds.env.requires?.['cds.xt.DeploymentService']?.['old-instance-manager']
|
|
@@ -93,8 +94,10 @@ async function credentials4(tenant, db) {
|
|
|
93
94
|
return new Promise((resolve, reject) => {
|
|
94
95
|
db._instance_manager.get(tenant, (err, res) => {
|
|
95
96
|
if (err) return reject(err)
|
|
96
|
-
if (!res)
|
|
97
|
+
if (!res) {
|
|
97
98
|
return reject(Object.assign(new Error(`There is no instance for tenant "${tenant}"`), { statusCode: 404 }))
|
|
99
|
+
}
|
|
100
|
+
|
|
98
101
|
resolve(res.credentials)
|
|
99
102
|
})
|
|
100
103
|
})
|
|
@@ -102,15 +105,9 @@ async function credentials4(tenant, db) {
|
|
|
102
105
|
|
|
103
106
|
function factory4(creds, tenant) {
|
|
104
107
|
return {
|
|
105
|
-
create:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
destroy: function (client) {
|
|
109
|
-
return hana.__disconnect(client)
|
|
110
|
-
},
|
|
111
|
-
validate: function (client) {
|
|
112
|
-
return hana.__isConnected(client)
|
|
113
|
-
}
|
|
108
|
+
create: () => hana.__connect(creds, tenant),
|
|
109
|
+
destroy: client => hana.__disconnect(client),
|
|
110
|
+
validate: client => hana.__isConnected(client)
|
|
114
111
|
}
|
|
115
112
|
}
|
|
116
113
|
|
|
@@ -121,7 +118,6 @@ const defaultConfig = { min: 0, max: 100, testOnBorrow: true, fifo: false }
|
|
|
121
118
|
|
|
122
119
|
const _getPoolConfig = function () {
|
|
123
120
|
const { pool: poolConfig } = cds.env.requires.db
|
|
124
|
-
|
|
125
121
|
const mergedConfig = Object.assign({}, defaultConfig, poolConfig)
|
|
126
122
|
|
|
127
123
|
// defaults
|
|
@@ -154,15 +150,19 @@ const _getMassagedCreds = function (creds) {
|
|
|
154
150
|
if (!('ca' in creds) && creds.certificate) {
|
|
155
151
|
creds.ca = creds.certificate
|
|
156
152
|
}
|
|
153
|
+
|
|
157
154
|
if ('encrypt' in creds && !('useTLS' in creds)) {
|
|
158
155
|
creds.useTLS = creds.encrypt
|
|
159
156
|
}
|
|
157
|
+
|
|
160
158
|
if ('hostname_in_certificate' in creds && !('sslHostNameInCertificate' in creds)) {
|
|
161
159
|
creds.sslHostNameInCertificate = creds.hostname_in_certificate
|
|
162
160
|
}
|
|
161
|
+
|
|
163
162
|
if ('validate_certificate' in creds && !('sslValidateCertificate' in creds)) {
|
|
164
163
|
creds.sslValidateCertificate = creds.validate_certificate
|
|
165
164
|
}
|
|
165
|
+
|
|
166
166
|
return creds
|
|
167
167
|
}
|
|
168
168
|
|
|
@@ -185,9 +185,11 @@ async function pool4(tenant, db) {
|
|
|
185
185
|
)
|
|
186
186
|
|
|
187
187
|
/*
|
|
188
|
-
* The error listener for "factoryCreateError" is registered
|
|
189
|
-
* If it fails due to invalid credentials, we delete the current pool from the pools map and overwrite the
|
|
190
|
-
*
|
|
188
|
+
* The error listener for "factoryCreateError" is registered to find out failed connection attempts.
|
|
189
|
+
* If it fails due to invalid credentials, we delete the current pool from the pools map and overwrite the
|
|
190
|
+
* pool factory create function.
|
|
191
|
+
* Background is that generic-pool will continue to try to open a connection by calling the factory create
|
|
192
|
+
* function until the "acquireTimeoutMillis" is reached.
|
|
191
193
|
* This ends up in many connection attempts for one request even though the credentials are invalid.
|
|
192
194
|
* Because of the deletion in the map, subsequent requests will fetch the credentials again.
|
|
193
195
|
*/
|
|
@@ -218,6 +220,7 @@ async function pool4(tenant, db) {
|
|
|
218
220
|
})
|
|
219
221
|
)
|
|
220
222
|
}
|
|
223
|
+
|
|
221
224
|
if ('then' in pools.get(tenant)) {
|
|
222
225
|
pools.set(tenant, await pools.get(tenant))
|
|
223
226
|
}
|
|
@@ -240,12 +243,17 @@ async function resilientAcquire(pool, attempts = 1) {
|
|
|
240
243
|
attempt++
|
|
241
244
|
}
|
|
242
245
|
}
|
|
246
|
+
|
|
243
247
|
if (client) return client
|
|
248
|
+
|
|
244
249
|
const { borrowed, pending, size, available, max } = pool
|
|
250
|
+
const message =
|
|
251
|
+
'Acquiring client from pool timed out. Please review your system setup, transaction handling, and pool configuration. ' +
|
|
252
|
+
`Pool State: borrowed: ${borrowed}, pending: ${pending}, size: ${size}, available: ${available}, max: ${max}`
|
|
245
253
|
err = getError(
|
|
246
254
|
Object.assign(err, {
|
|
247
255
|
statusCode: 503,
|
|
248
|
-
message
|
|
256
|
+
message
|
|
249
257
|
})
|
|
250
258
|
)
|
|
251
259
|
err._attempts = attempt
|
|
@@ -262,9 +270,9 @@ module.exports = {
|
|
|
262
270
|
client._pool = pool
|
|
263
271
|
return client
|
|
264
272
|
},
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
273
|
+
|
|
274
|
+
release: client => client._pool.release(client),
|
|
275
|
+
|
|
268
276
|
drain: async tenant => {
|
|
269
277
|
const pool = pools.get(tenant)
|
|
270
278
|
if (!pool) return
|
|
@@ -62,7 +62,7 @@ const search2Contains = (cqnSearchPhrase, columns) => {
|
|
|
62
62
|
const isContainsPredicateSupported = (query, entity, columns2Search) => {
|
|
63
63
|
const cqnSearchPhrase = query.SELECT.search
|
|
64
64
|
|
|
65
|
-
if (cqnSearchPhrase
|
|
65
|
+
if (cqnSearchPhrase?.[0]?.val === ' ') return false
|
|
66
66
|
|
|
67
67
|
// REVISIT: In the future, to further optimize search queries, you might
|
|
68
68
|
// want to remove the following condition(s).
|
|
@@ -5,21 +5,7 @@ const { isContainsPredicateSupported, search2Contains } = require('./search2Cont
|
|
|
5
5
|
const { addAliasToExpression } = require('../db/utils/generateAliases')
|
|
6
6
|
const targetAlias = 'Target'
|
|
7
7
|
const textsAlias = 'Texts'
|
|
8
|
-
|
|
9
|
-
const keys = Object.keys(entity.keys).filter(k => !entity.keys[k].isAssociation && !entity.keys[k].virtual)
|
|
10
|
-
const where = []
|
|
11
|
-
keys.forEach(key => {
|
|
12
|
-
if (where.length > 0) where.push('and')
|
|
13
|
-
where.push({ ref: [alias1, key] }, '=', { ref: [alias2, key] })
|
|
14
|
-
})
|
|
15
|
-
return where
|
|
16
|
-
}
|
|
17
|
-
const _generateContainsColumns = (columns, entity) => {
|
|
18
|
-
const columnsTarget = addAliasToExpression(columns, targetAlias)
|
|
19
|
-
const columns2SearchText = columns.filter(col => col.ref && entity.elements[col.ref[col.ref.length - 1]].localized)
|
|
20
|
-
const columnsText = addAliasToExpression(columns2SearchText, textsAlias)
|
|
21
|
-
return [...columnsTarget, ...columnsText]
|
|
22
|
-
}
|
|
8
|
+
|
|
23
9
|
/**
|
|
24
10
|
* Computes a CQN expression for a search query.
|
|
25
11
|
*
|
|
@@ -40,11 +26,12 @@ const _generateContainsColumns = (columns, entity) => {
|
|
|
40
26
|
const search2cqn4sql = (query, entity, options) => {
|
|
41
27
|
const cqnSearchPhrase = query.SELECT.search
|
|
42
28
|
if (!cqnSearchPhrase) return query
|
|
43
|
-
|
|
44
29
|
const localizedAssociation = entity.associations?.localized
|
|
30
|
+
|
|
45
31
|
if (localizedAssociation) {
|
|
46
32
|
let { columns: columns2Search = computeColumnsToBeSearched(query, entity), locale } = options
|
|
47
33
|
const viewAlias = query.SELECT.from.as ? query.SELECT.from.as : 'LocalizedView'
|
|
34
|
+
|
|
48
35
|
if (!query.SELECT.from.as) {
|
|
49
36
|
_addAliasToQuery(query, viewAlias)
|
|
50
37
|
}
|
|
@@ -56,14 +43,17 @@ const search2cqn4sql = (query, entity, options) => {
|
|
|
56
43
|
|
|
57
44
|
// left outer join the target table with the _texts table (the _texts table contains the translated texts)
|
|
58
45
|
subQuery.leftJoin(localizedAssociation.target, textsAlias).on(onCondition)
|
|
46
|
+
|
|
59
47
|
// add condition for equal keys of target table and localized view
|
|
60
48
|
subQuery.where(_generateKeysWhereCondition(entity, targetAlias, viewAlias))
|
|
61
49
|
const containsColumns = _generateContainsColumns(columns2Search, entity)
|
|
50
|
+
|
|
62
51
|
let expression
|
|
52
|
+
|
|
63
53
|
if (isContainsPredicateSupported(query, entity, columns2Search)) {
|
|
64
54
|
// generate CQN expression with `CONTAINS` predicate for the columns from the target and text table
|
|
65
55
|
expression = search2Contains(cqnSearchPhrase, containsColumns)
|
|
66
|
-
Object.defineProperty(
|
|
56
|
+
Object.defineProperty(expression, 'searchUsingContains', { value: true, enumerable: true })
|
|
67
57
|
} else {
|
|
68
58
|
expression = searchToLike(cqnSearchPhrase, containsColumns)
|
|
69
59
|
}
|
|
@@ -73,11 +63,29 @@ const search2cqn4sql = (query, entity, options) => {
|
|
|
73
63
|
// suppress the localize handler from redirecting the subQuery's target to the localized view
|
|
74
64
|
Object.defineProperty(subQuery, '_suppressLocalization', { value: true })
|
|
75
65
|
query.where('exists', subQuery)
|
|
76
|
-
|
|
77
66
|
return query
|
|
78
67
|
}
|
|
79
68
|
}
|
|
80
69
|
|
|
70
|
+
const _generateKeysWhereCondition = (entity, alias1, alias2) => {
|
|
71
|
+
const keys = Object.keys(entity.keys).filter(key => !entity.keys[key].isAssociation && !entity.keys[key].virtual)
|
|
72
|
+
const where = []
|
|
73
|
+
|
|
74
|
+
keys.forEach(key => {
|
|
75
|
+
if (where.length > 0) where.push('and')
|
|
76
|
+
where.push({ ref: [alias1, key] }, '=', { ref: [alias2, key] })
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
return where
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const _generateContainsColumns = (columns, entity) => {
|
|
83
|
+
const columnsTarget = addAliasToExpression(columns, targetAlias)
|
|
84
|
+
const columns2SearchText = columns.filter(col => col.ref && entity.elements[col.ref[col.ref.length - 1]].localized)
|
|
85
|
+
const columnsText = addAliasToExpression(columns2SearchText, textsAlias)
|
|
86
|
+
return [...columnsTarget, ...columnsText]
|
|
87
|
+
}
|
|
88
|
+
|
|
81
89
|
const _addAliasToQuery = (query, alias) => {
|
|
82
90
|
const SELECT = query.SELECT
|
|
83
91
|
SELECT.from.as = alias
|
|
@@ -2,8 +2,9 @@ const cds = require('../../cds.js')
|
|
|
2
2
|
// eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
|
|
3
3
|
const express = require('express')
|
|
4
4
|
const getTenantInfo = require('./getTenantInfo.js')
|
|
5
|
-
const isSecured = () => cds.requires.auth && cds.requires.auth.credentials
|
|
6
|
-
const
|
|
5
|
+
const isSecured = () => cds.requires.auth && (cds.requires.auth.impl || cds.requires.auth.credentials)
|
|
6
|
+
const _require = require('../../common/utils/require')
|
|
7
|
+
const { UNAUTHORIZED } = require('../../auth/utils')
|
|
7
8
|
|
|
8
9
|
const _isAll = a => a && a.includes('all')
|
|
9
10
|
|
|
@@ -14,18 +15,25 @@ class EndpointRegistry {
|
|
|
14
15
|
this.webhookCallbacks = new Map()
|
|
15
16
|
this.deployCallbacks = new Map()
|
|
16
17
|
if (isSecured()) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
if (cds.requires.auth.impl) {
|
|
19
|
+
const impl = _require(cds.resolve(cds.requires.auth.impl))
|
|
20
|
+
paths.forEach(path => cds.app.use(path, impl))
|
|
21
|
+
} else {
|
|
22
|
+
const JWTStrategy = require('../../auth/strategies/JWT.js')
|
|
23
|
+
// eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
|
|
24
|
+
const passport = _require('passport')
|
|
25
|
+
// REVISIT: It's unclear if the credentials from cds.requires.auth need to be used here.
|
|
26
|
+
// In principle, user-facing endpoints might differ from messaging ones.
|
|
27
|
+
passport.use(new JWTStrategy(cds.requires.auth.credentials))
|
|
28
|
+
paths.forEach(path => {
|
|
29
|
+
cds.app.use(path, passport.initialize())
|
|
30
|
+
cds.app.use(path, passport.authenticate('JWT', { session: false }))
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
// unsuccessful auth doesn't automatically reject!
|
|
23
34
|
paths.forEach(path => {
|
|
24
|
-
cds.app.use(path, passport.initialize())
|
|
25
|
-
cds.app.use(path, passport.authenticate('JWT', { session: false }))
|
|
26
35
|
cds.app.use(path, (req, res, next) => {
|
|
27
|
-
|
|
28
|
-
if (!req.user) return res.status(401).json({ message: 'Unauthorized' })
|
|
36
|
+
if (!req.user) res.status(401).json(UNAUTHORIZED)
|
|
29
37
|
next()
|
|
30
38
|
})
|
|
31
39
|
})
|
|
@@ -63,17 +71,16 @@ class EndpointRegistry {
|
|
|
63
71
|
try {
|
|
64
72
|
if (isSecured() && !req.user.is('emcallback')) return res.sendStatus(403)
|
|
65
73
|
const queueName = req.query.q
|
|
66
|
-
const authInfo = req.authInfo
|
|
67
74
|
const xAddress = req.headers['x-address']
|
|
68
75
|
const topic = xAddress && xAddress.match(/^topic:(.*)/)[1]
|
|
69
76
|
const payload = req.body
|
|
70
77
|
const cb = this.webhookCallbacks.get(queueName)
|
|
71
78
|
if (!topic || !payload || !queueName || !cb) return res.sendStatus(200)
|
|
72
|
-
const
|
|
73
|
-
const other =
|
|
79
|
+
const tenant = req.tenant || req.user.tenant
|
|
80
|
+
const other = tenant
|
|
74
81
|
? {
|
|
75
82
|
_: { req, res }, // For `cds.context.http`
|
|
76
|
-
tenant
|
|
83
|
+
tenant
|
|
77
84
|
}
|
|
78
85
|
: {}
|
|
79
86
|
if (!cb) return res.sendStatus(200)
|
|
@@ -15,6 +15,11 @@ const outboxRunner = new OutboxRunner()
|
|
|
15
15
|
const cdsUser = 'cds.internal.user'
|
|
16
16
|
const messageProcessorRegistered = Symbol('message processor registered')
|
|
17
17
|
|
|
18
|
+
const _get100NanosecondTimestampISOString = () => {
|
|
19
|
+
const [now, nanoseconds] = [new Date(), process.hrtime()[1]]
|
|
20
|
+
return now.toISOString().replace('Z', `${nanoseconds}`.padStart(9, '0').substring(3, 7) + 'Z')
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
const _getMessagesEntity = () => {
|
|
19
24
|
const messagesDbName = 'cds.outbox.Messages'
|
|
20
25
|
const messagesEntity = cds.model.definitions[messagesDbName]
|
|
@@ -221,7 +226,7 @@ const _createMessage = (name, msg, context) => {
|
|
|
221
226
|
const outboxMsg = {
|
|
222
227
|
ID: cds.utils.uuid(),
|
|
223
228
|
target: name,
|
|
224
|
-
timestamp:
|
|
229
|
+
timestamp: _get100NanosecondTimestampISOString(), // needs to be different for each emit
|
|
225
230
|
msg: JSON.stringify(_msg)
|
|
226
231
|
}
|
|
227
232
|
return outboxMsg
|
|
@@ -7,6 +7,14 @@ const { getKind, run, getDestination, getAdditionalOptions, getReqOptions } = re
|
|
|
7
7
|
const { formatVal } = require('../../odata/utils')
|
|
8
8
|
const { hasAliasedColumns } = require('./utils/data')
|
|
9
9
|
|
|
10
|
+
let _cloudSdkConnectivity
|
|
11
|
+
const cloudSdkConnectivity = () => {
|
|
12
|
+
if (_cloudSdkConnectivity) return _cloudSdkConnectivity
|
|
13
|
+
// eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
|
|
14
|
+
_cloudSdkConnectivity = require('@sap-cloud-sdk/connectivity')
|
|
15
|
+
return _cloudSdkConnectivity
|
|
16
|
+
}
|
|
17
|
+
|
|
10
18
|
const _isSimpleCqnQuery = q => typeof q === 'object' && q !== null && !Array.isArray(q) && Object.keys(q).length > 0
|
|
11
19
|
|
|
12
20
|
const _setHeaders = (defaultHeaders, req) => {
|
|
@@ -18,6 +26,7 @@ const _setHeaders = (defaultHeaders, req) => {
|
|
|
18
26
|
}, {})
|
|
19
27
|
)
|
|
20
28
|
}
|
|
29
|
+
|
|
21
30
|
const _setCorrectValue = (el, data, params, kind) => {
|
|
22
31
|
return typeof data[el] === 'object' && kind !== 'odata-v2'
|
|
23
32
|
? JSON.stringify(data[el])
|
|
@@ -151,49 +160,68 @@ const resolvedTargetOfQuery = q => {
|
|
|
151
160
|
const transitions = (typeof q === 'object' && (q.SELECT || q.INSERT || q.UPDATE || q.DELETE)._transitions) || []
|
|
152
161
|
return transitions.length && [transitions.length - 1].target
|
|
153
162
|
}
|
|
163
|
+
|
|
154
164
|
let logged
|
|
155
165
|
let sdkLoggerDisabled
|
|
166
|
+
|
|
167
|
+
const _resolveSelectionStrategy = options => {
|
|
168
|
+
if (typeof options?.selectionStrategy !== 'string') return
|
|
169
|
+
options.selectionStrategy = cloudSdkConnectivity().DestinationSelectionStrategies[options.selectionStrategy]
|
|
170
|
+
|
|
171
|
+
if (typeof options?.selectionStrategy !== 'function') {
|
|
172
|
+
throw new Error(`Unsupported destination selection strategy "${options.selectionStrategy}".`)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
156
176
|
class RemoteService extends cds.Service {
|
|
157
177
|
init() {
|
|
158
|
-
|
|
159
|
-
throw new Error(`No credentials configured for "${this.name}".`)
|
|
160
|
-
}
|
|
178
|
+
this.kind = getKind(this.options) // TODO: Simplify
|
|
161
179
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
180
|
+
/*
|
|
181
|
+
* set up connectivity stuff if credentials are provided
|
|
182
|
+
* throw error if no credentials are provided and the service has at least one entity or one action/function
|
|
183
|
+
*/
|
|
184
|
+
if (this.options.credentials) {
|
|
185
|
+
this.datasource = this.options.datasource
|
|
186
|
+
this.destinationOptions = this.options.destinationOptions
|
|
187
|
+
_resolveSelectionStrategy(this.destinationOptions)
|
|
188
|
+
this.destination =
|
|
189
|
+
this.options.credentials.destination ??
|
|
190
|
+
getDestination(this.definition?.name ?? this.datasource, this.options.credentials)
|
|
191
|
+
this.path = this.options.credentials.path
|
|
192
|
+
|
|
193
|
+
this.requestTimeout = this.options.credentials.requestTimeout
|
|
194
|
+
if (this.requestTimeout == null) this.requestTimeout = 60000
|
|
195
|
+
|
|
196
|
+
// REVISIT: remove cds.env.features.fetch_csrf in next major ^7
|
|
197
|
+
this.csrf = cds.env.features.fetch_csrf ?? this.options.csrf
|
|
198
|
+
this.csrfInBatch = this.options.csrfInBatch
|
|
199
|
+
if (cds.env.features.fetch_csrf && !logged) {
|
|
200
|
+
// for logging once for all remote services
|
|
201
|
+
logged = true
|
|
202
|
+
LOG._warn &&
|
|
203
|
+
LOG.warn(
|
|
204
|
+
'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'
|
|
205
|
+
)
|
|
206
|
+
}
|
|
177
207
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
208
|
+
// REVISIT: use cds.log's logger in cloud sdk
|
|
209
|
+
|
|
210
|
+
// disable sdk logger if not in debug mode
|
|
211
|
+
if (!LOG._debug && !sdkLoggerDisabled) {
|
|
212
|
+
try {
|
|
213
|
+
// eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
|
|
214
|
+
const sdkUtils = require('@sap-cloud-sdk/util')
|
|
215
|
+
sdkUtils.setGlobalLogLevel('error')
|
|
216
|
+
// disable sdk logger once
|
|
217
|
+
sdkLoggerDisabled = true
|
|
218
|
+
} catch (err) {
|
|
219
|
+
/* might fail in cds repl due to winston's exception handler, see cap/issues#10134 */
|
|
220
|
+
}
|
|
190
221
|
}
|
|
222
|
+
} else if ([...this.entities].length || [...this.operations].length) {
|
|
223
|
+
throw new Error(`No credentials configured for "${this.name}".`)
|
|
191
224
|
}
|
|
192
|
-
// REVISIT: remove cds.env.features.fetch_csrf in next major ^7
|
|
193
|
-
this.csrf = cds.env.features.fetch_csrf || this.options.csrf
|
|
194
|
-
this.csrfInBatch = this.options.csrfInBatch
|
|
195
|
-
this.path = this.options.credentials.path
|
|
196
|
-
this.kind = getKind(this.options) // TODO: Simplify
|
|
197
225
|
|
|
198
226
|
const clearKeysFromData = function (req) {
|
|
199
227
|
if (req.target && req.target.keys) for (const k of Object.keys(req.target.keys)) delete req.data[k]
|
|
@@ -235,9 +263,7 @@ class RemoteService extends cds.Service {
|
|
|
235
263
|
}
|
|
236
264
|
|
|
237
265
|
let result = await run(reqOptions, additionalOptions)
|
|
238
|
-
|
|
239
|
-
result =
|
|
240
|
-
typeof query === 'object' && query.SELECT && query.SELECT.one && Array.isArray(result) ? result[0] : result
|
|
266
|
+
result = typeof query === 'object' && query.SELECT?.one && Array.isArray(result) ? result[0] : result
|
|
241
267
|
|
|
242
268
|
return result
|
|
243
269
|
})
|
|
@@ -27,14 +27,16 @@ const _executeHttpRequest = async ({ requestConfig, destination, destinationOpti
|
|
|
27
27
|
const destinationName = typeof destination === 'string' && destination
|
|
28
28
|
|
|
29
29
|
if (destinationName) {
|
|
30
|
-
destination = { destinationName, ...(resolveDestinationOptions(destinationOptions, jwt)
|
|
30
|
+
destination = { destinationName, ...(resolveDestinationOptions(destinationOptions, jwt) ?? {}) }
|
|
31
31
|
} else if (destination.forwardAuthToken) {
|
|
32
32
|
destination = {
|
|
33
33
|
...destination,
|
|
34
34
|
headers: destination.headers ? { ...destination.headers } : {},
|
|
35
35
|
authentication: 'NoAuthentication'
|
|
36
36
|
}
|
|
37
|
+
|
|
37
38
|
delete destination.forwardAuthToken
|
|
39
|
+
|
|
38
40
|
if (jwt) {
|
|
39
41
|
destination.headers.authorization = `Bearer ${jwt}`
|
|
40
42
|
} else {
|
|
@@ -93,12 +95,12 @@ const getDestination = (name, credentials) => {
|
|
|
93
95
|
* @returns {import('@sap-cloud-sdk/connectivity').DestinationFetchOptions}
|
|
94
96
|
*/
|
|
95
97
|
const resolveDestinationOptions = function (options, jwt) {
|
|
96
|
-
if (!options && !jwt) return
|
|
98
|
+
if (!options && !jwt) return
|
|
97
99
|
|
|
98
|
-
const resolvedOptions = Object.assign({}, options
|
|
100
|
+
const resolvedOptions = Object.assign({}, options ?? {})
|
|
99
101
|
resolvedOptions.jwt = jwt
|
|
100
102
|
|
|
101
|
-
if (options
|
|
103
|
+
if (options?.selectionStrategy) {
|
|
102
104
|
resolvedOptions.selectionStrategy = options.selectionStrategy
|
|
103
105
|
}
|
|
104
106
|
|
|
@@ -388,12 +390,10 @@ const run = async (
|
|
|
388
390
|
}
|
|
389
391
|
|
|
390
392
|
const getJwt = req => {
|
|
391
|
-
const headers = req
|
|
392
|
-
if (headers
|
|
393
|
+
const headers = req?.context?.headers
|
|
394
|
+
if (headers?.authorization) {
|
|
393
395
|
const token = headers.authorization.match(/^bearer (.+)/i)
|
|
394
|
-
if (token)
|
|
395
|
-
return token[1]
|
|
396
|
-
}
|
|
396
|
+
if (token) return token[1]
|
|
397
397
|
}
|
|
398
398
|
|
|
399
399
|
return null
|
|
@@ -467,6 +467,10 @@ const getReqOptions = (req, query, service) => {
|
|
|
467
467
|
? _stringToReqOptions(query, req.data, req.target)
|
|
468
468
|
: _pathToReqOptions(req.method, req.path, req.data, req.target)
|
|
469
469
|
|
|
470
|
+
if (service.kind === 'odata-v2' && req.event === 'READ' && reqOptions.url?.match(/(\/any\()|(\/all\()/)) {
|
|
471
|
+
req.reject(501, 'Lambda expressions are not supported in OData v2')
|
|
472
|
+
}
|
|
473
|
+
|
|
470
474
|
reqOptions.headers = { accept: 'application/json,text/plain' }
|
|
471
475
|
reqOptions.timeout = service.requestTimeout
|
|
472
476
|
|
|
@@ -16,13 +16,14 @@ module.exports = async (_req, _res, next) => {
|
|
|
16
16
|
result = await srv.dispatch(req)
|
|
17
17
|
|
|
18
18
|
// 204 or 404?
|
|
19
|
-
if (result
|
|
19
|
+
if (result == null && query.SELECT.one) {
|
|
20
20
|
if (_target.ref.length > 1) status = 204
|
|
21
21
|
else throw { code: 404 }
|
|
22
22
|
}
|
|
23
23
|
} catch (e) {
|
|
24
24
|
return next(e)
|
|
25
25
|
}
|
|
26
|
+
|
|
26
27
|
// compat for mtx returning strings instead of objects
|
|
27
28
|
if (typeof result === 'object' && result !== null && '$count' in result) {
|
|
28
29
|
result = {
|