@sap/cds 7.8.2 → 7.9.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 +37 -0
- package/_i18n/i18n_ar.properties +3 -0
- package/_i18n/i18n_cs.properties +3 -0
- package/_i18n/i18n_da.properties +3 -0
- package/_i18n/i18n_es_MX.properties +3 -0
- package/_i18n/i18n_fi.properties +3 -0
- package/_i18n/i18n_hu.properties +6 -0
- package/_i18n/i18n_ko.properties +3 -0
- package/_i18n/i18n_ms.properties +3 -0
- package/_i18n/i18n_nl.properties +3 -0
- package/_i18n/i18n_no.properties +3 -0
- package/_i18n/i18n_ro.properties +3 -0
- package/_i18n/i18n_sv.properties +3 -0
- package/_i18n/i18n_th.properties +3 -0
- package/_i18n/i18n_tr.properties +6 -0
- package/_i18n/i18n_zh_TW.properties +3 -0
- package/bin/serve.js +5 -5
- package/lib/auth/basic-auth.js +1 -1
- package/lib/compile/cdsc.js +33 -6
- package/lib/compile/etc/_localized.js +14 -7
- package/lib/compile/for/lean_drafts.js +9 -0
- package/lib/compile/to/edm-files.js +116 -0
- package/lib/compile/to/edm.js +8 -1
- package/lib/compile/to/hdbtabledata.js +3 -3
- package/lib/compile/to/sql.js +4 -2
- package/lib/compile/to/yaml.js +22 -21
- package/lib/dbs/cds-deploy.js +5 -6
- package/lib/env/cds-env.js +7 -0
- package/lib/env/cds-requires.js +20 -1
- package/lib/env/defaults.js +21 -5
- package/lib/env/schemas/cds-package.js +1 -1
- package/lib/env/schemas/cds-rc.js +85 -4
- package/lib/index.js +1 -1
- package/lib/linked/entities.js +10 -0
- package/lib/linked/models.js +1 -1
- package/lib/plugins.js +1 -1
- package/lib/ql/INSERT.js +17 -3
- package/lib/ql/Query.js +4 -0
- package/lib/ql/infer.js +1 -1
- package/lib/req/request.js +1 -1
- package/lib/srv/cds-serve.js +1 -0
- package/lib/srv/middlewares/cds-context.js +1 -1
- package/lib/srv/protocols/odata-v4.js +5 -6
- package/lib/srv/srv-models.js +9 -2
- package/lib/utils/cds-test.js +2 -0
- package/lib/utils/cds-utils.js +9 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.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/delete.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +3 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +22 -10
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +4 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +4 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +4 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +38 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +32 -21
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +0 -10
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +3 -1
- package/libx/_runtime/cds-services/services/utils/compareJson.js +2 -274
- package/libx/_runtime/{cds-services/services → common}/Service.js +39 -29
- package/libx/_runtime/common/generic/auth/autoexpose.js +41 -0
- package/libx/_runtime/common/generic/auth/index.js +2 -0
- package/libx/_runtime/common/generic/auth/readOnly.js +0 -11
- package/libx/_runtime/common/generic/auth/restrict.js +6 -5
- package/libx/_runtime/common/generic/auth/utils.js +1 -1
- package/libx/_runtime/common/generic/crud.js +5 -8
- package/libx/_runtime/common/generic/etag.js +8 -6
- package/libx/_runtime/common/generic/sorting.js +2 -2
- package/libx/_runtime/common/i18n/messages.properties +1 -0
- package/libx/_runtime/{cds-services/services → common}/utils/columns.js +4 -4
- package/libx/_runtime/common/utils/compareJson.js +274 -0
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
- package/libx/_runtime/{cds-services/services → common}/utils/differ.js +8 -8
- package/libx/_runtime/common/utils/ensureIEEE754.js +29 -0
- package/libx/_runtime/common/utils/{postProcessing.js → postProcess.js} +1 -3
- package/libx/_runtime/common/utils/resolveView.js +0 -16
- package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -1
- package/libx/_runtime/common/utils/search2cqn4sql.js +1 -1
- package/libx/_runtime/common/utils/streamProp.js +9 -2
- package/libx/_runtime/common/utils/ucsn.js +1 -1
- package/libx/_runtime/db/expand/expandCQNToJoin.js +5 -3
- package/libx/_runtime/db/generic/rewrite.js +7 -13
- package/libx/_runtime/fiori/generic/activate.js +1 -1
- package/libx/_runtime/fiori/generic/edit.js +1 -1
- package/libx/_runtime/fiori/generic/prepare.js +1 -1
- package/libx/_runtime/fiori/lean-draft.js +151 -46
- package/libx/_runtime/fiori/utils/handler.js +1 -1
- package/libx/_runtime/hana/execute.js +6 -2
- package/libx/_runtime/hana/search2cqn4sql.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -2
- package/libx/_runtime/messaging/event-broker.js +212 -0
- package/libx/_runtime/remote/Service.js +9 -32
- package/libx/_runtime/remote/utils/client.js +13 -21
- package/libx/_runtime/sqlite/convertAssocToOneManaged.js +7 -1
- package/libx/_runtime/sqlite/execute.js +8 -3
- package/libx/_runtime/ucl/Service.js +259 -0
- package/libx/common/assert/index.js +5 -11
- package/libx/common/assert/validation.js +6 -1
- package/libx/odata/index.js +47 -25
- package/libx/odata/middleware/batch.js +8 -7
- package/libx/odata/middleware/create.js +42 -16
- package/libx/odata/middleware/delete.js +18 -11
- package/libx/odata/middleware/metadata.js +15 -14
- package/libx/odata/middleware/operation.js +30 -40
- package/libx/odata/middleware/parse.js +2 -3
- package/libx/odata/middleware/read.js +59 -52
- package/libx/odata/middleware/service-document.js +7 -7
- package/libx/odata/middleware/stream.js +26 -24
- package/libx/odata/middleware/update.js +53 -92
- package/libx/odata/parse/afterburner.js +45 -47
- package/libx/odata/parse/grammar.peggy +3 -3
- package/libx/odata/parse/multipartToJson.js +10 -22
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/etag.js +13 -0
- package/libx/odata/utils/handler.js +120 -0
- package/libx/odata/utils/index.js +15 -2
- package/libx/odata/utils/metaInfo.js +410 -0
- package/libx/odata/utils/path.js +5 -2
- package/libx/odata/utils/readAfterWrite.js +23 -0
- package/libx/odata/utils/result.js +4 -5
- package/libx/rest/RestAdapter.js +4 -13
- package/libx/rest/middleware/parse.js +40 -7
- package/package.json +1 -1
- package/server.js +1 -0
- package/libx/_runtime/cds-services/util/dataProcessUtils.js +0 -93
- package/libx/_runtime/common/utils/thenable.js +0 -51
- package/libx/_runtime/rest/service.js +0 -2
- package/libx/odata/parse/parseToCqn.js +0 -39
- package/libx/rest/middleware/input.js +0 -54
- package/libx/rest/middleware/payload.js +0 -13
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
2
|
const { Object_keys } = cds.utils
|
|
3
3
|
const { UPDATE, SELECT } = cds.ql
|
|
4
|
-
const { getColumns } = require('../../
|
|
4
|
+
const { getColumns } = require('../../common/utils/columns')
|
|
5
5
|
const { ensureNoDraftsSuffix, ensureDraftsSuffix, ensureUnlocalized } = require('../../common/utils/draft')
|
|
6
6
|
const getTemplate = require('../../common/utils/template')
|
|
7
7
|
|
|
@@ -404,10 +404,14 @@ async function executeSelectStreamCQN({ model, query, dbc, user, locale, txTimes
|
|
|
404
404
|
if (result.length === 0) return
|
|
405
405
|
|
|
406
406
|
if (cds.env.features.stream_compat) {
|
|
407
|
-
const
|
|
407
|
+
const res = _convertNames(result[0], query.SELECT?.columns)
|
|
408
|
+
let [key, val] = Object.entries(res)[0]
|
|
408
409
|
if (val === null) return null
|
|
409
410
|
|
|
410
|
-
|
|
411
|
+
res.value = val
|
|
412
|
+
delete res[key]
|
|
413
|
+
|
|
414
|
+
return res
|
|
411
415
|
} else {
|
|
412
416
|
return dbc.name === 'hdb' ? result[0] : _convertNames(result[0], query.SELECT?.columns)
|
|
413
417
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const cds = require('../cds')
|
|
2
|
-
const { computeColumnsToBeSearched } = require('../
|
|
2
|
+
const { computeColumnsToBeSearched } = require('../common/utils/columns')
|
|
3
3
|
const searchToLike = require('../common/utils/searchToLike')
|
|
4
4
|
const { isContainsPredicateSupported, search2Contains } = require('./search2Contains')
|
|
5
5
|
const { addAliasToExpression } = require('../db/utils/generateAliases')
|
|
@@ -11,7 +11,6 @@ const _isAll = a => a && a.includes('all')
|
|
|
11
11
|
class EndpointRegistry {
|
|
12
12
|
constructor(basePath, LOG) {
|
|
13
13
|
const deployPath = basePath + '/deploy'
|
|
14
|
-
const paths = [basePath, deployPath]
|
|
15
14
|
this.webhookCallbacks = new Map()
|
|
16
15
|
this.deployCallbacks = new Map()
|
|
17
16
|
if (isSecured()) {
|
|
@@ -60,7 +59,7 @@ class EndpointRegistry {
|
|
|
60
59
|
cds.app.options(basePath, (req, res) => {
|
|
61
60
|
try {
|
|
62
61
|
if (isSecured() && !req.user.is('emcallback')) return res.sendStatus(403)
|
|
63
|
-
res.set('
|
|
62
|
+
res.set('webhook-allowed-origin', req.headers['webhook-request-origin'])
|
|
64
63
|
res.sendStatus(200)
|
|
65
64
|
} catch (error) {
|
|
66
65
|
res.sendStatus(500)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
const cds = require('../cds')
|
|
2
|
+
|
|
3
|
+
// eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
|
|
4
|
+
const express = require('express')
|
|
5
|
+
const https = require('https')
|
|
6
|
+
const crypto = require('crypto')
|
|
7
|
+
|
|
8
|
+
async function request(options, data) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const req = https.request(options, res => {
|
|
11
|
+
const chunks = []
|
|
12
|
+
res.on('data', chunk => {
|
|
13
|
+
chunks.push(chunk)
|
|
14
|
+
})
|
|
15
|
+
res.on('end', () => {
|
|
16
|
+
const response = {
|
|
17
|
+
statusCode: res.statusCode,
|
|
18
|
+
headers: res.headers,
|
|
19
|
+
body: Buffer.concat(chunks).toString()
|
|
20
|
+
}
|
|
21
|
+
if (res.statusCode > 299) {
|
|
22
|
+
reject({ message: response.body })
|
|
23
|
+
} else {
|
|
24
|
+
resolve(response)
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
req.on('error', error => {
|
|
29
|
+
reject(error)
|
|
30
|
+
})
|
|
31
|
+
if (data) {
|
|
32
|
+
req.write(JSON.stringify(data))
|
|
33
|
+
}
|
|
34
|
+
req.end()
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function _validateCertificate(req, res, next) {
|
|
39
|
+
this.LOG.debug('event broker trying to authenticate via mTLS')
|
|
40
|
+
|
|
41
|
+
if (req.headers['x-ssl-client-verify'] !== '0') {
|
|
42
|
+
this.LOG.info('cf did not validate client certificate.')
|
|
43
|
+
return res.status(401).json({ message: 'Authentication Failed' })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!req.headers['x-forwarded-client-cert']) {
|
|
47
|
+
this.LOG.info('no certificate in xfcc header.')
|
|
48
|
+
return res.status(401).json({ message: 'Authentication Failed' })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const bindingCert = new crypto.X509Certificate(this.options.credentials.certificate).toLegacyObject()
|
|
52
|
+
const clientCert = new crypto.X509Certificate(
|
|
53
|
+
`-----BEGIN CERTIFICATE-----\n${req.headers['x-forwarded-client-cert']}\n-----END CERTIFICATE-----`
|
|
54
|
+
).toLegacyObject()
|
|
55
|
+
|
|
56
|
+
const cfSubject = Buffer.from(req.headers['x-ssl-client-subject-cn'], 'base64').toString()
|
|
57
|
+
if (bindingCert.subject.CN !== clientCert.subject.CN || bindingCert.subject.CN !== cfSubject) {
|
|
58
|
+
this.LOG.info('certificate subject does not match')
|
|
59
|
+
return res.status(401).json({ message: 'Authentication Failed' })
|
|
60
|
+
}
|
|
61
|
+
this.LOG.debug('incoming Subject CN is valid.')
|
|
62
|
+
|
|
63
|
+
if (bindingCert.issuer.CN !== clientCert.issuer.CN) {
|
|
64
|
+
this.LOG.info('Certificate issuer subject does not match')
|
|
65
|
+
return res.status(401).json({ message: 'Authentication Failed' })
|
|
66
|
+
}
|
|
67
|
+
this.LOG.debug('incoming issuer subject CN is valid.')
|
|
68
|
+
|
|
69
|
+
if (bindingCert.issuer.O !== clientCert.issuer.O) {
|
|
70
|
+
this.LOG.info('Certificate issuer org does not match')
|
|
71
|
+
return res.status(401).json({ message: 'Authentication Failed' })
|
|
72
|
+
}
|
|
73
|
+
this.LOG.debug('incoming Issuer Org is valid.')
|
|
74
|
+
|
|
75
|
+
if (bindingCert.issuer.OU !== clientCert.issuer.OU) {
|
|
76
|
+
this.LOG.info('certificate issuer OU does not match')
|
|
77
|
+
return res.status(401).json({ message: 'Authentication Failed' })
|
|
78
|
+
}
|
|
79
|
+
this.LOG.debug('certificate issuer OU is valid.')
|
|
80
|
+
|
|
81
|
+
const valid_from = new Date(clientCert.valid_from)
|
|
82
|
+
const valid_to = new Date(clientCert.valid_to)
|
|
83
|
+
const now = new Date(Date.now())
|
|
84
|
+
if (valid_from <= now && valid_to >= now) {
|
|
85
|
+
this.LOG.debug('certificate validation completed')
|
|
86
|
+
next()
|
|
87
|
+
} else {
|
|
88
|
+
this.LOG.error('Certificate expired')
|
|
89
|
+
return res.status(401).json({ message: 'Authentication Failed' })
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// TODO: seems unused
|
|
94
|
+
function _checkAppDomains() {
|
|
95
|
+
const pattern = /.*\.cert\.cfapps\..*\.hana\.ondemand\.com/
|
|
96
|
+
const uris = JSON.parse(process.env.VCAP_APPLICATION).application_uris
|
|
97
|
+
const matchFound = uris.some(uri => pattern.test(uri))
|
|
98
|
+
if (matchFound)
|
|
99
|
+
this.LOG.warn(
|
|
100
|
+
`*.cert.cfapps.*.hana.ondemand.com domain is in use, this is not recommended in production! Please use 'mesh.cf.<region>.hana.ondemand.com' instead!`
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let instantiated = false
|
|
105
|
+
|
|
106
|
+
class EventBroker extends cds.MessagingService {
|
|
107
|
+
async init() {
|
|
108
|
+
// TODO: Only needed if there are subscriptions
|
|
109
|
+
if (instantiated)
|
|
110
|
+
throw new Error('Event Broker service must be a singleton service, you cannot have more than one instance.')
|
|
111
|
+
instantiated = true
|
|
112
|
+
await super.init()
|
|
113
|
+
cds.once('listening', () => {
|
|
114
|
+
this.startListening()
|
|
115
|
+
})
|
|
116
|
+
this.agent = this.getAgent()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
getAgent() {
|
|
120
|
+
try {
|
|
121
|
+
if (this.options.x509.certPath && this.options.x509.pkeyPath) {
|
|
122
|
+
return new https.Agent({
|
|
123
|
+
cert: cds.utils.fs.readFileSync(cds.utils.path.resolve(cds.root, this.options.x509.certPath)),
|
|
124
|
+
key: cds.utils.fs.readFileSync(cds.utils.path.resolve(cds.root, this.options.x509.pkeyPath))
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (this.LOG) this.LOG.error('GetCredentials', { error: error.message })
|
|
129
|
+
throw error
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async handle(msg) {
|
|
134
|
+
if (msg.inbound) return super.handle(msg)
|
|
135
|
+
const _msg = this.message4(msg)
|
|
136
|
+
await this.emitToEventBroker(_msg)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async startListening() {
|
|
140
|
+
if (!this._listenToAll.value && !this.subscribedTopics.size) return
|
|
141
|
+
await this.registerWebhookEndpoints()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async emitToEventBroker(msg) {
|
|
145
|
+
// TODO: CSN definition probably not needed, just in case...
|
|
146
|
+
// See if there's a CSN entry for that event
|
|
147
|
+
// const found = cds?.model.definitions[topicOrEvent]
|
|
148
|
+
// if (found) return found // case for fully-qualified event name
|
|
149
|
+
// for (const def in cds.model?.definitions) {
|
|
150
|
+
// const definition = cds.model.definitions[def]
|
|
151
|
+
// if (definition['@topic'] === topicOrEvent) return definition
|
|
152
|
+
// }
|
|
153
|
+
|
|
154
|
+
// TODO: What if we're in single tenant variant?
|
|
155
|
+
try {
|
|
156
|
+
const ceSource = `${this.options.credentials.ceSource[0]}/${cds.context.tenant}`
|
|
157
|
+
const hostname = this.options.credentials.eventing.http.x509.url.replace(/^https?:\/\//, '')
|
|
158
|
+
// TODO Cloud Events Handler CAP
|
|
159
|
+
const options = {
|
|
160
|
+
hostname: hostname,
|
|
161
|
+
method: 'POST',
|
|
162
|
+
headers: {
|
|
163
|
+
'ce-id': cds.utils.uuid(),
|
|
164
|
+
'ce-source': ceSource,
|
|
165
|
+
'ce-type': msg.event,
|
|
166
|
+
'ce-specversion': '1.0',
|
|
167
|
+
'Content-Type': 'application/json'
|
|
168
|
+
},
|
|
169
|
+
agent: this.agent
|
|
170
|
+
}
|
|
171
|
+
this.LOG.debug('HTTP headers:', JSON.stringify(options.headers))
|
|
172
|
+
this.LOG.debug('HTTP body:', JSON.stringify(msg.data))
|
|
173
|
+
await request(options, msg.data) // TODO: fetch does not work with mTLS as of today, requires another module. see https://github.com/nodejs/node/issues/48977
|
|
174
|
+
if (this.LOG._info) this.LOG.info('Emit', { topic: msg.event })
|
|
175
|
+
} catch (e) {
|
|
176
|
+
this.LOG.error('Emit failed:', e.message)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async registerWebhookEndpoints() {
|
|
181
|
+
const webhookBasePath = this.options.webhookPath || '/-/cds/event-broker/webhook'
|
|
182
|
+
cds.app.post(webhookBasePath, _validateCertificate.bind(this))
|
|
183
|
+
cds.app.post(webhookBasePath, express.json())
|
|
184
|
+
cds.app.post(webhookBasePath, this.onEventReceived.bind(this))
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async onEventReceived(req, res) {
|
|
188
|
+
try {
|
|
189
|
+
const event = req.headers['ce-type'] // TG27: type contains namespace, so there's no collision
|
|
190
|
+
const tenant = req.headers['ce-sapconsumertenant']
|
|
191
|
+
const msg = {
|
|
192
|
+
inbound: true,
|
|
193
|
+
event,
|
|
194
|
+
tenant,
|
|
195
|
+
data: req.body ? req.body : undefined,
|
|
196
|
+
headers: req.headers
|
|
197
|
+
}
|
|
198
|
+
cds.context = { user: cds.User.privileged }
|
|
199
|
+
if (tenant) cds.context.tenant = tenant // TODO: In single tenant case, we don't need a tenant
|
|
200
|
+
const tx = await this.tx()
|
|
201
|
+
await tx.emit(msg)
|
|
202
|
+
this.LOG.debug('Event processed successfully.')
|
|
203
|
+
return res.status(200).json({ message: 'OK' })
|
|
204
|
+
} catch (e) {
|
|
205
|
+
this.LOG.error('ERROR during inbound event processing:', e) // TODO: How does Event Broker do error handling?
|
|
206
|
+
res.status(500).json({ message: 'Internal Server Error!' })
|
|
207
|
+
throw e
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = EventBroker
|
|
@@ -3,8 +3,8 @@ const cds = require('../cds')
|
|
|
3
3
|
const { run, getReqOptions } = require('./utils/client')
|
|
4
4
|
const { getCloudSdk, getCloudSdkConnectivity, getCloudSdkResilience } = require('./utils/cloudSdkProvider')
|
|
5
5
|
const { hasAliasedColumns } = require('./utils/data')
|
|
6
|
-
const { resolveView, getTransition,
|
|
7
|
-
const
|
|
6
|
+
const { resolveView, getTransition, findQueryTarget } = require('../common/utils/resolveView')
|
|
7
|
+
const postProcess = require('../common/utils/postProcess')
|
|
8
8
|
const { formatVal } = require('../../odata/utils')
|
|
9
9
|
|
|
10
10
|
const _isSimpleCqnQuery = q => typeof q === 'object' && q !== null && !Array.isArray(q) && Object.keys(q).length > 0
|
|
@@ -268,51 +268,28 @@ class RemoteService extends cds.Service {
|
|
|
268
268
|
})
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
-
// FIXME: This is a dirty hack for this situation:
|
|
272
|
-
// - This PR has cds.Service.model setter to always consistently apply cds.compile.for.odata, also for RemoteServices, which wasn't the case before
|
|
273
|
-
// - because of that tests/_runtime/remote/__tests__/integration/odata.test.js fails, which relies on the former behavior of RemoteServices
|
|
274
|
-
// NOTE: that test would never have worked for RemoteServices bootstrapped from single cds.model, which is always cds.compiled.for.odata
|
|
275
|
-
// REVISIT: should become obsolete with Universal CSN
|
|
276
|
-
// set model(m) {
|
|
277
|
-
// const fn = cds.compile.for.odata
|
|
278
|
-
// try {
|
|
279
|
-
// cds.compile.for.odata = m => m
|
|
280
|
-
// super.model = m
|
|
281
|
-
// } finally {
|
|
282
|
-
// cds.compile.for.odata = fn
|
|
283
|
-
// }
|
|
284
|
-
// }
|
|
285
|
-
|
|
286
271
|
// Overload .handle in order to resolve projections up to a definition that is known by the remote service instance.
|
|
287
272
|
// Result is post processed according to the inverse projection in order to reflect the correct result of the original query.
|
|
288
273
|
async handle(req) {
|
|
289
|
-
|
|
290
|
-
if (req._resolved || cds.env.features.resolve_views === false) return super.handle(req)
|
|
291
|
-
|
|
292
|
-
if (req.target && req.target.name && this.definition && req.target.name.startsWith(this.definition.name + '.')) {
|
|
293
|
-
const result = await super.handle(req)
|
|
274
|
+
if (req._resolved) return super.handle(req)
|
|
294
275
|
|
|
276
|
+
if (req.target?.name?.startsWith(this.definition?.name + '.')) {
|
|
277
|
+
let result = await super.handle(req)
|
|
295
278
|
// only post process if alias was explicitly set in query
|
|
296
|
-
if (_selectOnlyWithAlias(req.query))
|
|
297
|
-
return postProcess(req.query, result, this, true)
|
|
298
|
-
}
|
|
299
|
-
|
|
279
|
+
if (_selectOnlyWithAlias(req.query)) result = postProcess(req.query, result, this, true)
|
|
300
280
|
return result
|
|
301
281
|
}
|
|
302
282
|
|
|
303
283
|
// req.query can be:
|
|
304
284
|
// - empty object in case of unbound action/function
|
|
305
285
|
// - undefined/null in case of plain string queries
|
|
306
|
-
if (_isSimpleCqnQuery(req.query)
|
|
286
|
+
if (this.model && _isSimpleCqnQuery(req.query)) {
|
|
307
287
|
const q = resolveView(req.query, this.model, this)
|
|
308
288
|
const t = findQueryTarget(q) || req.target
|
|
309
289
|
|
|
310
|
-
// compat
|
|
311
|
-
restoreLink(req)
|
|
312
|
-
|
|
313
290
|
// REVISIT: We need to provide target explicitly because it's cached already within ensure_target
|
|
314
|
-
const
|
|
315
|
-
const result = await super.dispatch(
|
|
291
|
+
const _req = new cds.Request({ query: q, target: t, _resolved: true, headers: req.headers, method: req.method })
|
|
292
|
+
const result = await super.dispatch(_req)
|
|
316
293
|
return postProcess(q, result, this, true)
|
|
317
294
|
}
|
|
318
295
|
|
|
@@ -166,27 +166,17 @@ const TYPES_TO_REMOVE = { function: 1, object: 1 }
|
|
|
166
166
|
const PROPS_TO_IGNORE = { cause: 1, name: 1 }
|
|
167
167
|
|
|
168
168
|
const _getSanitizedError = (e, reqOptions, options = { suppressRemoteResponseBody: false, batchRequest: false }) => {
|
|
169
|
-
|
|
169
|
+
const request = {
|
|
170
170
|
method: reqOptions.method,
|
|
171
171
|
url: e.config ? e.config.baseURL + e.config.url : reqOptions.url,
|
|
172
172
|
headers: e.config ? e.config.headers : reqOptions.headers
|
|
173
173
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
e.request.body = reqOptions.data
|
|
177
|
-
}
|
|
174
|
+
if (options.batchRequest) request.body = reqOptions.data
|
|
175
|
+
e.request = request
|
|
178
176
|
|
|
179
177
|
if (e.response) {
|
|
180
|
-
const response = {
|
|
181
|
-
|
|
182
|
-
statusText: e.response.statusText,
|
|
183
|
-
headers: e.response.headers
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (e.response.data && !options.suppressRemoteResponseBody) {
|
|
187
|
-
response.body = e.response.data
|
|
188
|
-
}
|
|
189
|
-
|
|
178
|
+
const response = { status: e.response.status, statusText: e.response.statusText, headers: e.response.headers }
|
|
179
|
+
if (e.response.data && !options.suppressRemoteResponseBody) response.body = e.response.data
|
|
190
180
|
e.response = response
|
|
191
181
|
}
|
|
192
182
|
|
|
@@ -214,6 +204,11 @@ const _getSanitizedError = (e, reqOptions, options = { suppressRemoteResponseBod
|
|
|
214
204
|
e = _e
|
|
215
205
|
}
|
|
216
206
|
|
|
207
|
+
// AxiosError's toJSON() method doesn't include the request and response objects
|
|
208
|
+
e.toJSON = function () {
|
|
209
|
+
return { ...this.__proto__.toJSON(), request: this.request, response: this.response }
|
|
210
|
+
}
|
|
211
|
+
|
|
217
212
|
return e
|
|
218
213
|
}
|
|
219
214
|
|
|
@@ -232,7 +227,7 @@ const run = async (requestConfig, options) => {
|
|
|
232
227
|
} catch (e) {
|
|
233
228
|
// > axios received status >= 400 -> gateway error
|
|
234
229
|
const msg = e?.response?.data?.error?.message?.value ?? e?.response?.data?.error?.message ?? e.message
|
|
235
|
-
e.message = msg ? 'Error during request to remote service:
|
|
230
|
+
e.message = msg ? 'Error during request to remote service: ' + msg : 'Request to remote service failed.'
|
|
236
231
|
const sanitizedError = _getSanitizedError(e, requestConfig, { suppressRemoteResponseBody })
|
|
237
232
|
const err = Object.assign(new Error(e.message), { statusCode: 502, reason: sanitizedError })
|
|
238
233
|
LOG._warn && LOG.warn(err)
|
|
@@ -251,11 +246,8 @@ const run = async (requestConfig, options) => {
|
|
|
251
246
|
const e = new Error("Received content-type 'text/html' which is not part of accepted content types")
|
|
252
247
|
e.response = response
|
|
253
248
|
const sanitizedError = _getSanitizedError(e, requestConfig, { suppressRemoteResponseBody })
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
reason: sanitizedError
|
|
257
|
-
})
|
|
258
|
-
|
|
249
|
+
const message = 'Error during request to remote service: ' + e.message
|
|
250
|
+
const err = Object.assign(new Error(message), { statusCode: 502, reason: sanitizedError })
|
|
259
251
|
LOG._warn && LOG.warn(err)
|
|
260
252
|
throw err
|
|
261
253
|
}
|
|
@@ -28,8 +28,14 @@ const _convert = (refEntries, req) => {
|
|
|
28
28
|
// only check refs in format {ref: ['assoc', 'id']}
|
|
29
29
|
continue
|
|
30
30
|
}
|
|
31
|
+
let element
|
|
32
|
+
if (req.target.elements[refEntry.ref[0]]) {
|
|
33
|
+
element = req.target.elements[refEntry.ref[0]]
|
|
34
|
+
} else if (req.target.elements[refEntry.ref[1]]?.isAssociation) {
|
|
35
|
+
// fallback: if first ref is not an element and second is an association, we assume first is an alias
|
|
36
|
+
element = req.target.elements[refEntry.ref[1]]
|
|
37
|
+
}
|
|
31
38
|
|
|
32
|
-
const element = req.target.elements[refEntry.ref[0]]
|
|
33
39
|
if (!element || !element.is2one) return
|
|
34
40
|
|
|
35
41
|
_convertRefForAssocToOneManaged(element, refEntry)
|
|
@@ -302,7 +302,7 @@ function executeGenericCQN(model, dbc, cqn, user, locale, txTimestamp) {
|
|
|
302
302
|
|
|
303
303
|
// REVISIT: consider deleting this function after removing stream_compat
|
|
304
304
|
async function executeSelectStreamCQN({ model, dbc, query, user, locale, txTimestamp }) {
|
|
305
|
-
|
|
305
|
+
let result = await executeSelectCQN(model, dbc, query, user, locale, txTimestamp)
|
|
306
306
|
|
|
307
307
|
if (result == null || result.length === 0) {
|
|
308
308
|
return
|
|
@@ -313,7 +313,9 @@ async function executeSelectStreamCQN({ model, dbc, query, user, locale, txTimes
|
|
|
313
313
|
return result
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
-
|
|
316
|
+
// REVISIT: following code to be deleted after cds.env.features.stream_compat is removed
|
|
317
|
+
if (Array.isArray(result)) result = result[0]
|
|
318
|
+
let [key, val] = Object.entries(result)[0]
|
|
317
319
|
if (val === null) {
|
|
318
320
|
return null
|
|
319
321
|
}
|
|
@@ -325,7 +327,10 @@ async function executeSelectStreamCQN({ model, dbc, query, user, locale, txTimes
|
|
|
325
327
|
stream_.push(val)
|
|
326
328
|
stream_.push(null)
|
|
327
329
|
|
|
328
|
-
|
|
330
|
+
result.value = stream_
|
|
331
|
+
delete result[key]
|
|
332
|
+
|
|
333
|
+
return result
|
|
329
334
|
}
|
|
330
335
|
|
|
331
336
|
module.exports = {
|