@sap/cds 6.7.2 → 6.8.2
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 +53 -1
- package/README.md +1 -0
- 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-extension/index.js +13 -1
- package/bin/build/provider/mtx-sidecar/index.js +3 -3
- package/bin/build/provider/nodejs/index.js +23 -0
- package/bin/plugins.js +2 -1
- 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/compile/for/lean_drafts.js +0 -1
- 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 +6 -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/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/metadata.js +2 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +2 -3
- 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/cds-services/util/assert.js +28 -5
- package/libx/_runtime/common/aspects/any.js +4 -1
- package/libx/_runtime/common/generic/auth/utils.js +30 -41
- package/libx/_runtime/common/generic/crud.js +1 -1
- package/libx/_runtime/common/i18n/messages.properties +1 -1
- package/libx/_runtime/common/utils/generateOnCond.js +18 -22
- package/libx/_runtime/db/expand/expandCQNToJoin.js +49 -41
- package/libx/_runtime/db/expand/rawToExpanded.js +3 -5
- package/libx/_runtime/db/generic/rewrite.js +3 -0
- 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 +87 -53
- package/libx/_runtime/fiori/utils/handler.js +0 -6
- package/libx/_runtime/hana/customBuilder/CustomFunctionBuilder.js +1 -1
- package/libx/_runtime/hana/customBuilder/CustomReferenceBuilder.js +2 -1
- package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +0 -5
- package/libx/_runtime/hana/execute.js +18 -11
- 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 +83 -48
- package/libx/_runtime/remote/utils/client.js +17 -19
- package/libx/_runtime/sqlite/execute.js +2 -0
- package/libx/rest/middleware/read.js +2 -1
- package/libx/rest/middleware/update.js +1 -1
- package/package.json +1 -1
|
@@ -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])
|
|
@@ -29,6 +38,7 @@ const _setCorrectValue = (el, data, params, kind) => {
|
|
|
29
38
|
const _buildPartialUrlFunctions = (url, data, params, kind = 'odata-v4') => {
|
|
30
39
|
const funcParams = []
|
|
31
40
|
const queryOptions = []
|
|
41
|
+
|
|
32
42
|
// REVISIT: take params from params after importer fix (the keys should not be part of params)
|
|
33
43
|
for (const param in _extractParamsFromData(data, params)) {
|
|
34
44
|
if (kind === 'odata-v2') {
|
|
@@ -38,6 +48,7 @@ const _buildPartialUrlFunctions = (url, data, params, kind = 'odata-v4') => {
|
|
|
38
48
|
queryOptions.push(`@${param}=${_setCorrectValue(param, data, params, kind)}`)
|
|
39
49
|
}
|
|
40
50
|
}
|
|
51
|
+
|
|
41
52
|
return kind === 'odata-v2'
|
|
42
53
|
? `${url}?${funcParams.join('&')}`
|
|
43
54
|
: `${url}(${funcParams.join(',')})?${queryOptions.join('&')}`
|
|
@@ -52,9 +63,11 @@ const _extractParamsFromData = (data, params = {}) => {
|
|
|
52
63
|
|
|
53
64
|
const _buildKeys = (req, kind) => {
|
|
54
65
|
const keys = []
|
|
66
|
+
|
|
55
67
|
if (req.params && req.params.length > 0) {
|
|
56
68
|
const p1 = req.params[0]
|
|
57
69
|
if (typeof p1 !== 'object') return [p1]
|
|
70
|
+
|
|
58
71
|
for (const key in req.target.keys) {
|
|
59
72
|
keys.push(`${key}=${formatVal(p1[key], key, req.target, kind)}`)
|
|
60
73
|
}
|
|
@@ -64,6 +77,7 @@ const _buildKeys = (req, kind) => {
|
|
|
64
77
|
keys.push(`${key}=${formatVal(req.data[key], key, req.target, kind)}`)
|
|
65
78
|
}
|
|
66
79
|
}
|
|
80
|
+
|
|
67
81
|
return keys
|
|
68
82
|
}
|
|
69
83
|
|
|
@@ -89,6 +103,7 @@ const _handleUnboundActionFunction = (srv, def, req, event) => {
|
|
|
89
103
|
def &&
|
|
90
104
|
def.returns &&
|
|
91
105
|
(def.returns.type === 'cds.LargeBinary' || def.returns.type === 'cds.Binary')
|
|
106
|
+
|
|
92
107
|
return srv.send({ method: 'POST', path: `/${event}`, data: req.data, _binary: isBinary })
|
|
93
108
|
}
|
|
94
109
|
|
|
@@ -112,22 +127,26 @@ const _handleV2ActionFunction = (srv, def, req, event, kind) => {
|
|
|
112
127
|
const _handleV2BoundActionFunction = (srv, def, req, event, kind) => {
|
|
113
128
|
const params = []
|
|
114
129
|
const data = req.data
|
|
130
|
+
|
|
115
131
|
// REVISIT: take params from def.params, after importer fix (the keys should not be part of params)
|
|
116
132
|
for (const param in _extractParamsFromData(req.data, def.params)) {
|
|
117
133
|
params.push(`${param}=${formatVal(data[param], param, { elements: def.params }, kind)}`)
|
|
118
134
|
}
|
|
135
|
+
|
|
119
136
|
const keys = _buildKeys(req, this.kind)
|
|
120
137
|
if (keys.length === 1 && typeof req.params[0] !== 'object') {
|
|
121
138
|
params.push(`${Object.keys(req.target.keys)[0]}=${keys[0]}`)
|
|
122
139
|
} else {
|
|
123
140
|
params.push(...keys)
|
|
124
141
|
}
|
|
142
|
+
|
|
125
143
|
const url = `${`/${event}`}?${params.join('&')}`
|
|
126
144
|
return _sendV2RequestActionFunction(srv, def, url)
|
|
127
145
|
}
|
|
128
146
|
|
|
129
147
|
const _addHandlerActionFunction = (srv, def, target) => {
|
|
130
148
|
const event = def.name.match(/\w*$/)[0]
|
|
149
|
+
|
|
131
150
|
if (target) {
|
|
132
151
|
srv.on(event, target, async function (req) {
|
|
133
152
|
const shortEntityName = req.target.name.replace(`${this.namespace}.`, '')
|
|
@@ -135,65 +154,85 @@ const _addHandlerActionFunction = (srv, def, target) => {
|
|
|
135
154
|
const url = `/${shortEntityName}(${_buildKeys(req, this.kind).join(',')})/${this.namespace}.${event}`
|
|
136
155
|
return _handleBoundActionFunction(srv, def, req, url)
|
|
137
156
|
})
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (this.kind === 'odata-v2') return _handleV2ActionFunction(srv, def, req, event, this.kind)
|
|
141
|
-
return _handleUnboundActionFunction(srv, def, req, event)
|
|
142
|
-
})
|
|
157
|
+
|
|
158
|
+
return
|
|
143
159
|
}
|
|
144
|
-
}
|
|
145
160
|
|
|
146
|
-
|
|
147
|
-
|
|
161
|
+
srv.on(event, async function (req) {
|
|
162
|
+
if (this.kind === 'odata-v2') return _handleV2ActionFunction(srv, def, req, event, this.kind)
|
|
163
|
+
return _handleUnboundActionFunction(srv, def, req, event)
|
|
164
|
+
})
|
|
148
165
|
}
|
|
149
166
|
|
|
167
|
+
const _selectOnlyWithAlias = q => q?.SELECT && !q.SELECT._transitions && q.SELECT?.columns?.some(hasAliasedColumns)
|
|
168
|
+
|
|
150
169
|
const resolvedTargetOfQuery = q => {
|
|
151
170
|
const transitions = (typeof q === 'object' && (q.SELECT || q.INSERT || q.UPDATE || q.DELETE)._transitions) || []
|
|
152
171
|
return transitions.length && [transitions.length - 1].target
|
|
153
172
|
}
|
|
173
|
+
|
|
154
174
|
let logged
|
|
155
175
|
let sdkLoggerDisabled
|
|
176
|
+
|
|
177
|
+
const _resolveSelectionStrategy = options => {
|
|
178
|
+
if (typeof options?.selectionStrategy !== 'string') return
|
|
179
|
+
options.selectionStrategy = cloudSdkConnectivity().DestinationSelectionStrategies[options.selectionStrategy]
|
|
180
|
+
|
|
181
|
+
if (typeof options?.selectionStrategy !== 'function') {
|
|
182
|
+
throw new Error(`Unsupported destination selection strategy "${options.selectionStrategy}".`)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
156
186
|
class RemoteService extends cds.Service {
|
|
157
187
|
init() {
|
|
158
|
-
|
|
159
|
-
throw new Error(`No credentials configured for "${this.name}".`)
|
|
160
|
-
}
|
|
188
|
+
this.kind = getKind(this.options) // TODO: Simplify
|
|
161
189
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
190
|
+
/*
|
|
191
|
+
* set up connectivity stuff if credentials are provided
|
|
192
|
+
* throw error if no credentials are provided and the service has at least one entity or one action/function
|
|
193
|
+
*/
|
|
194
|
+
if (this.options.credentials) {
|
|
195
|
+
this.datasource = this.options.datasource
|
|
196
|
+
this.destinationOptions = this.options.destinationOptions
|
|
197
|
+
_resolveSelectionStrategy(this.destinationOptions)
|
|
198
|
+
this.destination =
|
|
199
|
+
this.options.credentials.destination ??
|
|
200
|
+
getDestination(this.definition?.name ?? this.datasource, this.options.credentials)
|
|
201
|
+
this.path = this.options.credentials.path
|
|
202
|
+
|
|
203
|
+
this.requestTimeout = this.options.credentials.requestTimeout
|
|
204
|
+
if (this.requestTimeout == null) this.requestTimeout = 60000
|
|
205
|
+
|
|
206
|
+
// REVISIT: remove cds.env.features.fetch_csrf in next major ^7
|
|
207
|
+
this.csrf = cds.env.features.fetch_csrf ?? this.options.csrf
|
|
208
|
+
this.csrfInBatch = this.options.csrfInBatch
|
|
209
|
+
if (cds.env.features.fetch_csrf && !logged) {
|
|
210
|
+
// for logging once for all remote services
|
|
211
|
+
logged = true
|
|
212
|
+
LOG._warn &&
|
|
213
|
+
LOG.warn(
|
|
214
|
+
'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'
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// REVISIT: use cds.log's logger in cloud sdk
|
|
177
219
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
220
|
+
// disable sdk logger if not in debug mode
|
|
221
|
+
if (!LOG._debug && !sdkLoggerDisabled) {
|
|
222
|
+
try {
|
|
223
|
+
// eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
|
|
224
|
+
const sdkUtils = require('@sap-cloud-sdk/util')
|
|
225
|
+
sdkUtils.setGlobalLogLevel('error')
|
|
226
|
+
|
|
227
|
+
// disable sdk logger once
|
|
228
|
+
sdkLoggerDisabled = true
|
|
229
|
+
} catch (err) {
|
|
230
|
+
/* might fail in cds repl due to winston's exception handler, see cap/issues#10134 */
|
|
231
|
+
}
|
|
190
232
|
}
|
|
233
|
+
} else if ([...this.entities].length || [...this.operations].length) {
|
|
234
|
+
throw new Error(`No credentials configured for "${this.name}".`)
|
|
191
235
|
}
|
|
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
236
|
|
|
198
237
|
const clearKeysFromData = function (req) {
|
|
199
238
|
if (req.target && req.target.keys) for (const k of Object.keys(req.target.keys)) delete req.data[k]
|
|
@@ -235,10 +274,7 @@ class RemoteService extends cds.Service {
|
|
|
235
274
|
}
|
|
236
275
|
|
|
237
276
|
let result = await run(reqOptions, additionalOptions)
|
|
238
|
-
|
|
239
|
-
result =
|
|
240
|
-
typeof query === 'object' && query.SELECT && query.SELECT.one && Array.isArray(result) ? result[0] : result
|
|
241
|
-
|
|
277
|
+
result = typeof query === 'object' && query.SELECT?.one && Array.isArray(result) ? result[0] : result
|
|
242
278
|
return result
|
|
243
279
|
})
|
|
244
280
|
}
|
|
@@ -286,7 +322,6 @@ class RemoteService extends cds.Service {
|
|
|
286
322
|
// REVISIT: We need to provide target explicitly because it's cached already within ensure_target
|
|
287
323
|
const newReq = new cds.Request({ query: q, target: t, headers: req.headers, _resolved: true, method: req.method })
|
|
288
324
|
const result = await super.dispatch(newReq)
|
|
289
|
-
|
|
290
325
|
return postProcess(q, result, this, true)
|
|
291
326
|
}
|
|
292
327
|
|
|
@@ -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
|
|
|
@@ -304,12 +306,9 @@ const run = async (
|
|
|
304
306
|
// > axios received status >= 400 -> gateway error
|
|
305
307
|
const msg = e?.response?.data?.error?.message?.value ?? e?.response?.data?.error?.message ?? e.message
|
|
306
308
|
e.message = msg ? 'Error during request to remote service: \n' + msg : 'Request to remote service failed.'
|
|
307
|
-
|
|
308
|
-
const sanitizedError = _getSanitizedError(e, requestConfig, {
|
|
309
|
-
suppressRemoteResponseBody: suppressRemoteResponseBody
|
|
310
|
-
})
|
|
311
|
-
|
|
309
|
+
const sanitizedError = _getSanitizedError(e, requestConfig, { suppressRemoteResponseBody })
|
|
312
310
|
const err = Object.assign(new Error(e.message), { statusCode: 502, reason: sanitizedError })
|
|
311
|
+
|
|
313
312
|
LOG._warn && LOG.warn(err)
|
|
314
313
|
throw err
|
|
315
314
|
}
|
|
@@ -327,11 +326,7 @@ const run = async (
|
|
|
327
326
|
) {
|
|
328
327
|
const e = new Error("Received content-type 'text/html' which is not part of accepted content types")
|
|
329
328
|
e.response = response
|
|
330
|
-
|
|
331
|
-
const sanitizedError = _getSanitizedError(e, requestConfig, {
|
|
332
|
-
suppressRemoteResponseBody: suppressRemoteResponseBody
|
|
333
|
-
})
|
|
334
|
-
|
|
329
|
+
const sanitizedError = _getSanitizedError(e, requestConfig, { suppressRemoteResponseBody })
|
|
335
330
|
const err = Object.assign(new Error(`Error during request to remote service: ${e.message}`), {
|
|
336
331
|
statusCode: 502,
|
|
337
332
|
reason: sanitizedError
|
|
@@ -381,6 +376,7 @@ const run = async (
|
|
|
381
376
|
if (response.data.d) {
|
|
382
377
|
return _purgeODataV2(response.data, resolvedTarget, returnType, requestConfig.headers)
|
|
383
378
|
}
|
|
379
|
+
|
|
384
380
|
return _purgeODataV4(response.data)
|
|
385
381
|
}
|
|
386
382
|
|
|
@@ -388,12 +384,10 @@ const run = async (
|
|
|
388
384
|
}
|
|
389
385
|
|
|
390
386
|
const getJwt = req => {
|
|
391
|
-
const headers = req
|
|
392
|
-
if (headers
|
|
387
|
+
const headers = req?.context?.headers
|
|
388
|
+
if (headers?.authorization) {
|
|
393
389
|
const token = headers.authorization.match(/^bearer (.+)/i)
|
|
394
|
-
if (token)
|
|
395
|
-
return token[1]
|
|
396
|
-
}
|
|
390
|
+
if (token) return token[1]
|
|
397
391
|
}
|
|
398
392
|
|
|
399
393
|
return null
|
|
@@ -467,6 +461,10 @@ const getReqOptions = (req, query, service) => {
|
|
|
467
461
|
? _stringToReqOptions(query, req.data, req.target)
|
|
468
462
|
: _pathToReqOptions(req.method, req.path, req.data, req.target)
|
|
469
463
|
|
|
464
|
+
if (service.kind === 'odata-v2' && req.event === 'READ' && reqOptions.url?.match(/(\/any\()|(\/all\()/)) {
|
|
465
|
+
req.reject(501, 'Lambda expressions are not supported in OData v2')
|
|
466
|
+
}
|
|
467
|
+
|
|
470
468
|
reqOptions.headers = { accept: 'application/json,text/plain' }
|
|
471
469
|
reqOptions.timeout = service.requestTimeout
|
|
472
470
|
|
|
@@ -114,8 +114,10 @@ function executeSelectCQN(model, dbc, query, user, locale, txTimestamp) {
|
|
|
114
114
|
) {
|
|
115
115
|
return expandV2(model, dbc, query, user, locale, txTimestamp, executeSelectCQN)
|
|
116
116
|
}
|
|
117
|
+
|
|
117
118
|
return _processExpand(model, dbc, query, user, locale, txTimestamp)
|
|
118
119
|
}
|
|
120
|
+
|
|
119
121
|
const { sql, values = [] } = sqlFactory(
|
|
120
122
|
query,
|
|
121
123
|
{
|
|
@@ -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 = {
|
|
@@ -9,7 +9,6 @@ const { deepCopyObject } = require('../../_runtime/common/utils/copy')
|
|
|
9
9
|
|
|
10
10
|
module.exports = async (_req, _res, next) => {
|
|
11
11
|
let { _srv: srv, _query: query, _target, _data, _params } = _req
|
|
12
|
-
|
|
13
12
|
let result,
|
|
14
13
|
status = 200
|
|
15
14
|
|
|
@@ -19,6 +18,7 @@ module.exports = async (_req, _res, next) => {
|
|
|
19
18
|
try {
|
|
20
19
|
// add the data (as copy, if upsert allowed)
|
|
21
20
|
query.data(UPSERT_ALLOWED ? deepCopyObject(_data) : _data)
|
|
21
|
+
|
|
22
22
|
// REVISIT: if PUT, req.method should be PUT -> Crud2Http maps UPSERT to PUT
|
|
23
23
|
result = await srv.dispatch(new RestRequest({ query, _target, method: _req.method, params: _params }))
|
|
24
24
|
if (_params && result) Object.assign(result, _params[_params.length - 1])
|