@sap/cds 9.4.4 → 9.5.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 +81 -1
- package/_i18n/messages_en_US_saptrc.properties +1 -1
- package/common.cds +5 -2
- package/lib/compile/cds-compile.js +1 -0
- package/lib/compile/for/assert.js +64 -0
- package/lib/compile/for/flows.js +194 -58
- package/lib/compile/for/lean_drafts.js +75 -7
- package/lib/compile/parse.js +1 -1
- package/lib/compile/to/csn.js +6 -2
- package/lib/compile/to/edm.js +1 -1
- package/lib/compile/to/yaml.js +8 -1
- package/lib/dbs/cds-deploy.js +2 -2
- package/lib/env/cds-env.js +14 -4
- package/lib/env/defaults.js +6 -1
- package/lib/i18n/localize.js +1 -1
- package/lib/index.js +7 -7
- package/lib/req/event.js +4 -0
- package/lib/req/validate.js +4 -1
- package/lib/srv/cds.Service.js +2 -1
- package/lib/srv/middlewares/auth/ias-auth.js +5 -7
- package/lib/srv/middlewares/auth/index.js +1 -1
- package/lib/srv/protocols/index.js +7 -6
- package/lib/srv/srv-handlers.js +7 -0
- package/libx/_runtime/common/Service.js +5 -1
- package/libx/_runtime/common/constants/events.js +1 -0
- package/libx/_runtime/common/generic/assert.js +220 -0
- package/libx/_runtime/common/generic/flows.js +168 -108
- package/libx/_runtime/common/generic/input.js +6 -4
- package/libx/_runtime/common/utils/cqn.js +0 -24
- package/libx/_runtime/common/utils/normalizeTimestamp.js +2 -2
- package/libx/_runtime/common/utils/resolveView.js +8 -2
- package/libx/_runtime/common/utils/templateProcessor.js +10 -1
- package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +21 -9
- package/libx/_runtime/fiori/lean-draft.js +511 -379
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +39 -35
- package/libx/_runtime/messaging/enterprise-messaging.js +2 -2
- package/libx/_runtime/remote/Service.js +4 -5
- package/libx/_runtime/ucl/Service.js +111 -15
- package/libx/common/utils/streaming.js +1 -1
- package/libx/odata/middleware/batch.js +8 -6
- package/libx/odata/middleware/create.js +2 -2
- package/libx/odata/middleware/delete.js +2 -2
- package/libx/odata/middleware/metadata.js +18 -11
- package/libx/odata/middleware/read.js +2 -2
- package/libx/odata/middleware/service-document.js +1 -1
- package/libx/odata/middleware/update.js +1 -1
- package/libx/odata/parse/afterburner.js +46 -36
- package/libx/odata/parse/cqn2odata.js +2 -6
- package/libx/odata/parse/grammar.peggy +91 -13
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/index.js +2 -2
- package/libx/odata/utils/readAfterWrite.js +2 -0
- package/libx/queue/TaskRunner.js +26 -1
- package/libx/queue/index.js +11 -1
- package/package.json +1 -1
- package/srv/ucl-service.cds +2 -0
|
@@ -1,65 +1,64 @@
|
|
|
1
1
|
const cds = require('../../cds.js')
|
|
2
2
|
const express = require('express')
|
|
3
3
|
const getTenantInfo = require('./getTenantInfo.js')
|
|
4
|
-
const isSecured = () => cds.requires.auth && (cds.requires.auth.impl || cds.requires.auth.credentials)
|
|
5
4
|
|
|
6
5
|
const _isAll = a => a && a.includes('all')
|
|
7
|
-
|
|
6
|
+
|
|
7
|
+
const _xsuaa_fallback = () => {
|
|
8
|
+
let xsuaa = cds.env.requires.messaging.xsuaa
|
|
9
|
+
xsuaa = typeof xsuaa === 'string' ? xsuaa : 'xsuaa'
|
|
10
|
+
const xsuaa_config = cds.env.requires[xsuaa]
|
|
11
|
+
if (!xsuaa_config) throw new Error(`Fallback XSUAA instance '${xsuaa}' not found!`)
|
|
12
|
+
if (!xsuaa_config.credentials) throw new Error(`Fallback XSUAA instance '${xsuaa}' has no credentials!`)
|
|
13
|
+
const jwt_auth = require('../../../../lib/srv/middlewares/auth/jwt-auth.js')
|
|
14
|
+
return jwt_auth(xsuaa_config)
|
|
15
|
+
}
|
|
8
16
|
|
|
9
17
|
class EndpointRegistry {
|
|
10
18
|
constructor(basePath, LOG) {
|
|
11
19
|
const deployPath = basePath + '/deploy'
|
|
12
20
|
this.webhookCallbacks = new Map()
|
|
13
21
|
this.deployCallbacks = new Map()
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
|
|
23
|
+
const _IS_SECURED = !!(
|
|
24
|
+
cds.env.requires.auth &&
|
|
25
|
+
(cds.env.requires.auth.impl || !(cds.env.requires.auth.kind in { mocked: 1, 'mocked-auth': 1 }))
|
|
26
|
+
)
|
|
27
|
+
const _IS_PRODUCTION = process.env.NODE_ENV === 'production'
|
|
28
|
+
|
|
29
|
+
cds.app.use(basePath, cds.middlewares.context())
|
|
30
|
+
if (_IS_SECURED) {
|
|
31
|
+
// unofficial XSUAA fallback
|
|
32
|
+
if (cds.env.requires.messaging.xsuaa) {
|
|
33
|
+
cds.app.use(basePath, _xsuaa_fallback())
|
|
17
34
|
} else {
|
|
18
|
-
|
|
19
|
-
cds.app.use(basePath, cds.middlewares.context())
|
|
20
|
-
cds.app.use(basePath, jwt_auth(cds.requires.auth))
|
|
21
|
-
cds.app.use(basePath, (err, req, res, next) => {
|
|
22
|
-
if (err === 401) res.sendStatus(401)
|
|
23
|
-
else next(err)
|
|
24
|
-
})
|
|
35
|
+
cds.app.use(basePath, cds.middlewares.auth())
|
|
25
36
|
}
|
|
26
|
-
//
|
|
27
|
-
cds.app.use(basePath, (
|
|
28
|
-
|
|
29
|
-
if (cds.context.user._is_anonymous) return res.status(401).end()
|
|
30
|
-
next()
|
|
37
|
+
// ensure that anonymous users get 401 on messaging endpoints
|
|
38
|
+
cds.app.use(basePath, (_req, _res, next) => {
|
|
39
|
+
next(cds.context.user._is_anonymous ? 401 : undefined)
|
|
31
40
|
})
|
|
32
|
-
} else
|
|
33
|
-
|
|
34
|
-
LOG.warn('Messaging endpoints not secured')
|
|
35
|
-
}
|
|
36
|
-
// auth middlewares set cds.context.user
|
|
37
|
-
cds.app.use(basePath, cds.middlewares.context())
|
|
38
|
-
}
|
|
41
|
+
} else if (_IS_PRODUCTION) LOG.warn('Messaging endpoints not secured')
|
|
42
|
+
|
|
39
43
|
cds.app.use(basePath, express.json({ type: 'application/*+json' }))
|
|
40
44
|
cds.app.use(basePath, express.json())
|
|
41
45
|
cds.app.use(basePath, express.urlencoded({ extended: true }))
|
|
42
|
-
LOG._debug && LOG.debug('Register inbound endpoint', { basePath, method: 'OPTIONS' })
|
|
43
|
-
|
|
44
|
-
// Clear cds.context as it would interfere with subsequent transactions
|
|
45
|
-
// cds.app.use(basePath, (_req, _res, next) => {
|
|
46
|
-
// cds.context = undefined // REVISIT: Why is that necessary?
|
|
47
|
-
// next()
|
|
48
|
-
// })
|
|
49
46
|
|
|
47
|
+
LOG._debug && LOG.debug('Register inbound endpoint', { basePath, method: 'OPTIONS' })
|
|
50
48
|
cds.app.options(basePath, (req, res) => {
|
|
51
49
|
try {
|
|
52
|
-
if (
|
|
50
|
+
if (_IS_SECURED && !cds.context.user.is('emcallback')) return res.sendStatus(403)
|
|
53
51
|
res.set('webhook-allowed-origin', req.headers['webhook-request-origin'])
|
|
54
52
|
res.sendStatus(200)
|
|
55
53
|
} catch {
|
|
56
54
|
res.sendStatus(500)
|
|
57
55
|
}
|
|
58
56
|
})
|
|
57
|
+
|
|
59
58
|
LOG._debug && LOG.debug('Register inbound endpoint', { basePath, method: 'POST' })
|
|
60
59
|
cds.app.post(basePath, (req, res) => {
|
|
61
60
|
try {
|
|
62
|
-
if (
|
|
61
|
+
if (_IS_SECURED && !cds.context.user.is('emcallback')) return res.sendStatus(403)
|
|
63
62
|
const queueName = req.query.q
|
|
64
63
|
if (!queueName) {
|
|
65
64
|
LOG.error('Query parameter `q` not found.')
|
|
@@ -95,9 +94,11 @@ class EndpointRegistry {
|
|
|
95
94
|
return res.sendStatus(500)
|
|
96
95
|
}
|
|
97
96
|
})
|
|
97
|
+
|
|
98
98
|
cds.app.post(deployPath, async (req, res) => {
|
|
99
|
+
LOG.debug('Handling deploy request')
|
|
99
100
|
try {
|
|
100
|
-
if (
|
|
101
|
+
if (_IS_SECURED && !cds.context.user.is('emmanagement')) return res.sendStatus(403)
|
|
101
102
|
const tenants = req.body && !_isAll(req.body.tenants) && req.body.tenants
|
|
102
103
|
const queues = req.body && !_isAll(req.body.queues) && req.body.queues
|
|
103
104
|
const options = { wipeData: req.body && req.body.wipeData }
|
|
@@ -114,12 +115,15 @@ class EndpointRegistry {
|
|
|
114
115
|
const hasError = results.some(r => r.failed.length)
|
|
115
116
|
if (hasError) return res.status(500).send(results)
|
|
116
117
|
return res.status(201).send(results)
|
|
117
|
-
} catch {
|
|
118
|
+
} catch (e) {
|
|
119
|
+
LOG.error('Error while handling deploy request: ', e)
|
|
118
120
|
// REVISIT: Still needed with cds-mtxs?
|
|
119
121
|
// If an unknown tenant id is provided, cds-mtx will crash ("Cannot read property 'hanaClient' of undefined")
|
|
120
122
|
return res.sendStatus(500)
|
|
121
123
|
}
|
|
122
124
|
})
|
|
125
|
+
|
|
126
|
+
cds.app.use(basePath, cds.middlewares.errors())
|
|
123
127
|
}
|
|
124
128
|
|
|
125
129
|
registerWebhookCallback(queueName, cb) {
|
|
@@ -17,12 +17,12 @@ const BASE_PATH = '/messaging/enterprise-messaging'
|
|
|
17
17
|
|
|
18
18
|
const _checkAppURL = appURL => {
|
|
19
19
|
if (!appURL)
|
|
20
|
-
|
|
20
|
+
cds.error(
|
|
21
21
|
'Enterprise Messaging: You need to provide an HTTPS endpoint to your application.\n\nHint: You can set the application URI in environment variable `VCAP_APPLICATION.application_uris[0]`. This is needed because incoming messages are delivered through HTTP via webhooks.\nExample: `{ ..., "VCAP_APPLICATION": { "application_uris": ["my-app.com"] } }`\nIn case you want to use Enterprise Messaging in shared (that means single-tenant) mode, you can use kind `enterprise-messaging-shared`.'
|
|
22
22
|
)
|
|
23
23
|
|
|
24
24
|
if (appURL.startsWith('https://localhost'))
|
|
25
|
-
|
|
25
|
+
cds.error(
|
|
26
26
|
'The endpoint of your application is local and cannot be reached from Enterprise Messaging.\n\nHint: For local development you can set up a tunnel to your local endpoint and enter its public https endpoint in `VCAP_APPLICATION.application_uris[0]`.\nIn case you want to use Enterprise Messaging in shared (that means single-tenant) mode, you can use kind `enterprise-messaging-shared`.'
|
|
27
27
|
)
|
|
28
28
|
}
|
|
@@ -160,8 +160,7 @@ const resolvedTargetOfQuery = q => q?._transitions?.at(-1)?.target
|
|
|
160
160
|
const _resolveSelectionStrategy = selectionStrategy => {
|
|
161
161
|
const { DestinationSelectionStrategies } = getCloudSdkConnectivity()
|
|
162
162
|
const strategy = DestinationSelectionStrategies[selectionStrategy]
|
|
163
|
-
if (typeof strategy !== 'function')
|
|
164
|
-
throw new Error(`Unsupported destination selection strategy "${selectionStrategy}".`)
|
|
163
|
+
if (typeof strategy !== 'function') cds.error`Unsupported destination selection strategy "${selectionStrategy}".`
|
|
165
164
|
return strategy
|
|
166
165
|
}
|
|
167
166
|
|
|
@@ -185,7 +184,7 @@ const _getDestination = (name, credentials) => {
|
|
|
185
184
|
class RemoteService extends cds.Service {
|
|
186
185
|
init() {
|
|
187
186
|
if (([...this.entities].length || [...this.operations].length) && !this.options.credentials)
|
|
188
|
-
|
|
187
|
+
cds.error`No credentials configured for "${this.name}".`
|
|
189
188
|
|
|
190
189
|
this.kind = _getKind(this.options) // TODO: Simplify
|
|
191
190
|
|
|
@@ -236,7 +235,7 @@ class RemoteService extends cds.Service {
|
|
|
236
235
|
// Early validation on first request for use case without remote API
|
|
237
236
|
// Ideally, that's done on bootstrap of the remote service
|
|
238
237
|
if (typeof this.destination === 'object' && !this.destination.url)
|
|
239
|
-
|
|
238
|
+
cds.error`"url" or "destination" property must be configured in "credentials" of "${this.name}".`
|
|
240
239
|
|
|
241
240
|
const requestConfig = extractRequestConfig(req, query, this)
|
|
242
241
|
requestConfig.headers = _getHeaders(requestConfig.headers, req)
|
|
@@ -294,7 +293,7 @@ class RemoteService extends cds.Service {
|
|
|
294
293
|
)
|
|
295
294
|
// rewrite the query to a target entity served by this service...
|
|
296
295
|
const query = this.resolve(req.query)
|
|
297
|
-
if (!query)
|
|
296
|
+
if (!query) cds.error`Target ${req.target.name} cannot be resolved for service ${this.name}`
|
|
298
297
|
const target = query._target || req.target
|
|
299
298
|
// we need to provide target explicitly because it's cached within ensure_target
|
|
300
299
|
const _req = new cds.Request({ query, target, _resolved: true, headers: req.headers, method: req.method })
|
|
@@ -3,8 +3,10 @@ const LOG = cds.log('ucl')
|
|
|
3
3
|
|
|
4
4
|
const fs = require('fs').promises
|
|
5
5
|
const https = require('https')
|
|
6
|
+
const { getCloudSdk } = require('../remote/utils/cloudSdkProvider')
|
|
6
7
|
|
|
7
8
|
const { READ_QUERY, CREATE_MUTATION, UPDATE_MUTATION, DELETE_MUTATION } = require('./queries')
|
|
9
|
+
|
|
8
10
|
const TRUSTED_CERT = {
|
|
9
11
|
CANARY: {
|
|
10
12
|
ISSUER: 'CN=SAP PKI Certificate Service Client CA,OU=SAP BTP Clients,O=SAP SE,L=cf-eu10-canary,C=DE',
|
|
@@ -33,6 +35,21 @@ module.exports = class UCLService extends cds.Service {
|
|
|
33
35
|
return await this._dispatchNotification(req)
|
|
34
36
|
})
|
|
35
37
|
|
|
38
|
+
if (cds.env.requires.ucl?.destination) {
|
|
39
|
+
LOG.debug('Destination for asynchronous UCL callbacks configured.')
|
|
40
|
+
|
|
41
|
+
this.after(
|
|
42
|
+
[`assign/#succeeded`, `unassign/#succeeded`],
|
|
43
|
+
async (res, req) => await this.schedule('successfulMapping', { res, req })
|
|
44
|
+
)
|
|
45
|
+
this.after(
|
|
46
|
+
[`assign/#failed`, `unassign/#failed`],
|
|
47
|
+
async (res, req) => await this.schedule('failedMapping', { res, req })
|
|
48
|
+
)
|
|
49
|
+
this.on('successfulMapping', req => this.handleSuccessfulMapping(req.data.res, req.data.req))
|
|
50
|
+
this.on('failedMapping', req => this.handleFailedMapping(req.data.res, req.data.req))
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
await super.init()
|
|
37
54
|
|
|
38
55
|
if (cds.env.requires.ucl?.applicationTemplate) await this._upsertApplicationTemplate(cds.env.requires.ucl)
|
|
@@ -100,11 +117,7 @@ module.exports = class UCLService extends cds.Service {
|
|
|
100
117
|
async _dispatchNotification(req) {
|
|
101
118
|
// Call User defined handlers for tenant mapping notifications
|
|
102
119
|
|
|
103
|
-
|
|
104
|
-
if (req.headers.location)
|
|
105
|
-
throw new cds.error(400, 'Location header found in tenant mapping notification: Async flow not supported!')
|
|
106
|
-
|
|
107
|
-
LOG.debug('Tenant mapping notification:', req.data)
|
|
120
|
+
LOG.debug('Tenant mapping notification: ', req.data)
|
|
108
121
|
|
|
109
122
|
const { operation } = req.data?.context ?? {}
|
|
110
123
|
if (operation !== 'assign' && operation !== 'unassign')
|
|
@@ -113,14 +126,98 @@ module.exports = class UCLService extends cds.Service {
|
|
|
113
126
|
`Invalid operation "${operation}" in tenant mapping notification. Expected "assign" or "unassign".`
|
|
114
127
|
)
|
|
115
128
|
|
|
116
|
-
const
|
|
129
|
+
const locationUrl = req.headers.location
|
|
130
|
+
if (locationUrl?.length) return this._dispatchAsyncMappingNotification(req, operation, locationUrl)
|
|
131
|
+
return this._dispatchSyncMappingNotification(req, operation)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async _dispatchAsyncMappingNotification(req, operation, locationUrl) {
|
|
135
|
+
// Get destination based on configured destination name
|
|
136
|
+
const destinationName = cds.env.requires.ucl.destination
|
|
137
|
+
if (typeof destinationName != 'string' || !destinationName?.length)
|
|
138
|
+
throw new cds.error(500, 'UCL Notification includes location but no callback destination was configured')
|
|
139
|
+
const destination = await getCloudSdk().getDestination(destinationName)
|
|
140
|
+
|
|
141
|
+
// Make sure the received callback location matches the configured destination
|
|
142
|
+
let destinationHost, locationHost
|
|
143
|
+
try {
|
|
144
|
+
locationHost = new URL(locationUrl).host
|
|
145
|
+
} catch (e) {
|
|
146
|
+
throw new cds.error(400, `Failed parsing URL in location header: ${e.message}`)
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
destinationHost = new URL(destination.url).host
|
|
150
|
+
} catch (e) {
|
|
151
|
+
throw new cds.error(500, `Failed parsing URL in destination "${destinationName}": ${e.message}`)
|
|
152
|
+
}
|
|
153
|
+
if (locationHost !== destinationHost) {
|
|
154
|
+
throw new cds.error(
|
|
155
|
+
400,
|
|
156
|
+
`Host mismatch: locationUrl host (${locationHost}) does not match destination.url host (${destinationHost})`
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Schedule the tenant mapping task
|
|
161
|
+
await this.schedule(operation, req.data, req.headers)
|
|
117
162
|
|
|
163
|
+
// Respond 202 Accepted to UCL
|
|
164
|
+
req.http.res.status(202)
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async _dispatchSyncMappingNotification(req, operation) {
|
|
169
|
+
const response = (await this.send(operation, req.data)) ?? {}
|
|
118
170
|
if (response.error) req.http.res.status(400)
|
|
119
171
|
else response.state ??= operation === 'assign' ? 'CONFIG_PENDING' : 'READY'
|
|
120
|
-
|
|
121
172
|
return response
|
|
122
173
|
}
|
|
123
174
|
|
|
175
|
+
async handleSuccessfulMapping(callbackResult, tenantMapping) {
|
|
176
|
+
const { operation, operationId } = tenantMapping.data.context
|
|
177
|
+
|
|
178
|
+
LOG.debug(`UCL ${operation} operation succeeded:`, callbackResult, tenantMapping)
|
|
179
|
+
|
|
180
|
+
// SPII v3 (operationId exists) vs v2
|
|
181
|
+
const method = operationId ? 'PUT' : 'PATCH'
|
|
182
|
+
|
|
183
|
+
const data = {
|
|
184
|
+
...callbackResult,
|
|
185
|
+
state: callbackResult.state ?? (operation === 'assign' ? 'CONFIG_PENDING' : 'READY')
|
|
186
|
+
}
|
|
187
|
+
const response = await getCloudSdk().executeHttpRequest(
|
|
188
|
+
{ destinationName: cds.env.requires.ucl.destination },
|
|
189
|
+
{ method, url: tenantMapping.headers.location, data }
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if (response.status >= 500)
|
|
193
|
+
cds.error(`Callback to UCL for successful ${operation} operation failed: ${response.data.error}`)
|
|
194
|
+
if (response.status >= 400)
|
|
195
|
+
cds.error(`UCL rejected successful ${operation} operation callback: ${response.data.error}`, {
|
|
196
|
+
unrecoverable: true
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async handleFailedMapping(callbackError, tenantMapping) {
|
|
201
|
+
const { operation, operationId } = tenantMapping.data.context
|
|
202
|
+
|
|
203
|
+
LOG.error(`UCL ${operation} operation failed:`, callbackError, tenantMapping)
|
|
204
|
+
|
|
205
|
+
// SPII v3 (operationId exists) vs v2
|
|
206
|
+
const method = operationId ? 'PUT' : 'PATCH'
|
|
207
|
+
|
|
208
|
+
const response = await getCloudSdk().executeHttpRequest(
|
|
209
|
+
{ destinationName: cds.env.requires.ucl.destination },
|
|
210
|
+
{ method, url: tenantMapping.headers.location, data: { state: 'ERROR' } }
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
if (response.status >= 500)
|
|
214
|
+
cds.error(`Callback to UCL for failed ${operation} operation failed: ${response.data.error}`)
|
|
215
|
+
if (response.status >= 400)
|
|
216
|
+
cds.error(`UCL rejected failed ${operation} operation callback: ${response.data.error}`, {
|
|
217
|
+
unrecoverable: true
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
124
221
|
/*
|
|
125
222
|
* the rest is for upserting the application template
|
|
126
223
|
*/
|
|
@@ -162,22 +259,22 @@ module.exports = class UCLService extends cds.Service {
|
|
|
162
259
|
|
|
163
260
|
this._applicationTemplate = _getApplicationTemplate(cds.env.requires.ucl)
|
|
164
261
|
if (!this._applicationTemplate.applicationNamespace) {
|
|
165
|
-
|
|
262
|
+
cds.error(
|
|
166
263
|
'The UCL service requires a valid `applicationTemplate`, please provide it as described in the documentation.'
|
|
167
264
|
)
|
|
168
265
|
}
|
|
169
266
|
|
|
170
267
|
if (!cds.requires.multitenancy && cds.env.profile !== 'mtx-sidecar')
|
|
171
|
-
|
|
268
|
+
cds.error(
|
|
172
269
|
'The UCL service requires multitenancy, please enable it in your cds configuration with `cds.requires.multitenancy` or by using the mtx sidecar.'
|
|
173
270
|
)
|
|
174
271
|
if (!cds.env.requires.ucl.credentials)
|
|
175
|
-
|
|
272
|
+
cds.error('No credentials found for the UCL service, please bind the service to your app.')
|
|
176
273
|
|
|
177
274
|
if (!cds.env.requires.ucl.x509?.cert && !cds.env.requires.ucl.x509?.certPath)
|
|
178
|
-
|
|
275
|
+
cds.error('UCL requires `x509.cert` or `x509.certPath`.')
|
|
179
276
|
if (!cds.env.requires.ucl.x509?.pkey && !cds.env.requires.ucl.x509?.pkeyPath)
|
|
180
|
-
|
|
277
|
+
cds.error('UCL requires `x509.pkey` or `x509.pkeyPath`.')
|
|
181
278
|
|
|
182
279
|
const [cert, key] = await Promise.all([
|
|
183
280
|
cds.env.requires.ucl.x509?.cert ??
|
|
@@ -191,7 +288,7 @@ module.exports = class UCLService extends cds.Service {
|
|
|
191
288
|
const existingTemplate = await this._readTemplate()
|
|
192
289
|
const template = existingTemplate ? await this._updateTemplate(existingTemplate) : await this._createTemplate() // TODO: Make sure return value is correct
|
|
193
290
|
|
|
194
|
-
if (!template)
|
|
291
|
+
if (!template) cds.error('The UCL service could not create an application template.')
|
|
195
292
|
|
|
196
293
|
cds.once('listening', async () => {
|
|
197
294
|
const provisioning = await cds.connect.to('cds.xt.SaasProvisioningService')
|
|
@@ -234,8 +331,7 @@ module.exports = class UCLService extends cds.Service {
|
|
|
234
331
|
|
|
235
332
|
const body = JSON.parse(response.body)
|
|
236
333
|
|
|
237
|
-
if (body.errors)
|
|
238
|
-
throw new Error('Request to UCL service failed with:\n' + JSON.stringify(body.errors, null, 2))
|
|
334
|
+
if (body.errors) cds.error('Request to UCL service failed with:\n' + JSON.stringify(body.errors, null, 2))
|
|
239
335
|
|
|
240
336
|
resolve(body.data)
|
|
241
337
|
})
|
|
@@ -7,7 +7,7 @@ exports.validateMimetypeIsAcceptedOrThrow = (headers, contentType) => {
|
|
|
7
7
|
if (headers.accept.includes(contentType)) return
|
|
8
8
|
if (headers.accept.includes(contentType.slice(0,contentType.indexOf('/')) + '/*')) return
|
|
9
9
|
const msg = `Content type "${contentType}" is not listed in accept header "${headers.accept}"`
|
|
10
|
-
|
|
10
|
+
cds.error({ status: 406, message: msg })
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
// REVISIT: We should use express' res.type(...) instead of res.set('Content-Type', ...)
|
|
@@ -19,7 +19,7 @@ const CRLF = '\r\n'
|
|
|
19
19
|
* common
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
const _deserializationError = message => cds.error(`Deserialization Error: ${message}
|
|
22
|
+
const _deserializationError = message => cds.error({ status: 400, message: `Deserialization Error: ${message}` })
|
|
23
23
|
|
|
24
24
|
// Function must be called with an object containing exactly one key-value pair representing the property name and its value
|
|
25
25
|
const _validateProperty = (name, value, type) => {
|
|
@@ -39,8 +39,7 @@ const _validateBatch = body => {
|
|
|
39
39
|
|
|
40
40
|
_validateProperty('requests', requests, 'Array')
|
|
41
41
|
|
|
42
|
-
if (requests.length > cds.env.odata.batch_limit)
|
|
43
|
-
cds.error('BATCH_TOO_MANY_REQ', { code: 'BATCH_TOO_MANY_REQ', statusCode: 429 })
|
|
42
|
+
if (requests.length > cds.env.odata.batch_limit) cds.error({ status: 429, message: 'BATCH_TOO_MANY_REQ' })
|
|
44
43
|
|
|
45
44
|
const ids = {}
|
|
46
45
|
|
|
@@ -469,7 +468,7 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
|
|
|
469
468
|
|
|
470
469
|
const _multipartBatch = async (srv, router, req, res, next) => {
|
|
471
470
|
const boundary = getBoundary(req)
|
|
472
|
-
if (!boundary) return next(new cds.error('No boundary found in Content-Type header'
|
|
471
|
+
if (!boundary) return next(new cds.error({ status: 400, message: 'No boundary found in Content-Type header' }))
|
|
473
472
|
|
|
474
473
|
try {
|
|
475
474
|
const { requests } = await multipartToJson(req.body, boundary)
|
|
@@ -574,7 +573,7 @@ module.exports = adapter => {
|
|
|
574
573
|
|
|
575
574
|
return function odata_batch(req, res, next) {
|
|
576
575
|
if (req.method !== 'POST') {
|
|
577
|
-
|
|
576
|
+
cds.error({ status: 405, message: `Method ${req.method} is not allowed for calls to $batch endpoint` })
|
|
578
577
|
}
|
|
579
578
|
|
|
580
579
|
if (req.headers['content-type'].includes('application/json')) {
|
|
@@ -588,6 +587,9 @@ module.exports = adapter => {
|
|
|
588
587
|
})
|
|
589
588
|
}
|
|
590
589
|
|
|
591
|
-
|
|
590
|
+
cds.error({
|
|
591
|
+
status: 400,
|
|
592
|
+
message: 'Batch requests must have content type multipart/mixed or application/json'
|
|
593
|
+
})
|
|
592
594
|
}
|
|
593
595
|
}
|
|
@@ -25,7 +25,7 @@ module.exports = (adapter, isUpsert) => {
|
|
|
25
25
|
|
|
26
26
|
if (one && !isUpsert) {
|
|
27
27
|
const msg = 'Method POST is not allowed for singletons and individual entities'
|
|
28
|
-
|
|
28
|
+
cds.error({ status: 405, message: msg })
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const model = cds.context.model ?? service.model
|
|
@@ -35,7 +35,7 @@ module.exports = (adapter, isUpsert) => {
|
|
|
35
35
|
const data = req.body
|
|
36
36
|
if (Array.isArray(data)) {
|
|
37
37
|
const msg = 'Only single entity representations are allowed'
|
|
38
|
-
|
|
38
|
+
cds.error({ status: 400, message: msg })
|
|
39
39
|
}
|
|
40
40
|
odataBind(data, target)
|
|
41
41
|
normalizeTimeData(data, model, target)
|
|
@@ -11,7 +11,7 @@ module.exports = adapter => {
|
|
|
11
11
|
return function deleet(req, res, next) {
|
|
12
12
|
if (getPreferReturnHeader(req)) {
|
|
13
13
|
const msg = "The 'return' preference is not allowed in DELETE requests"
|
|
14
|
-
|
|
14
|
+
cds.error({ status: 400, message: msg })
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
// REVISIT: better solution for query._propertyAccess
|
|
@@ -21,7 +21,7 @@ module.exports = adapter => {
|
|
|
21
21
|
} = req._query
|
|
22
22
|
|
|
23
23
|
if (!one) {
|
|
24
|
-
|
|
24
|
+
cds.error({ status: 405, message: 'Method DELETE is not allowed for entity collections' })
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const model = cds.context.model ?? service.model
|
|
@@ -90,17 +90,24 @@ module.exports = adapter => {
|
|
|
90
90
|
const { tenant, features } = cds.context
|
|
91
91
|
|
|
92
92
|
try {
|
|
93
|
-
let edmx
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
(
|
|
99
|
-
|
|
100
|
-
model
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
93
|
+
let edmx = metadataCache.edm
|
|
94
|
+
|
|
95
|
+
if (!edmx) {
|
|
96
|
+
let model
|
|
97
|
+
// REVISIT: remove compat with cds^10
|
|
98
|
+
if (cds.env.features.odata_metadata_compat) {
|
|
99
|
+
const modelNeeded = cds.env.requires.extensibility || features?.given
|
|
100
|
+
model = modelNeeded ? await mps.getCsn(tenant, features) : undefined
|
|
101
|
+
} else {
|
|
102
|
+
model = cds.context.model ?? service.model
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// REVISIT: Why do we need the model provider at all to get the edmx?
|
|
106
|
+
// The locale is intentionally not provided to the model provider
|
|
107
|
+
// because we want to cache the unlocalized edmx and do localization on the fly
|
|
108
|
+
edmx = metadataCache.edm || (await mps.getEdmx({ tenant, model, service: service.definition.name }))
|
|
109
|
+
metadataCache.edm = edmx
|
|
110
|
+
}
|
|
104
111
|
const extBundle = cds.env.requires.extensibility && (await mps.getI18n({ tenant, locale }))
|
|
105
112
|
edmx = cds.localize(service.model, locale, edmx, extBundle)
|
|
106
113
|
metadataCache.xmlEtag[locale] = generateEtag(edmx)
|
|
@@ -183,7 +183,7 @@ module.exports = adapter => {
|
|
|
183
183
|
return function read(req, res, next) {
|
|
184
184
|
if (getPreferReturnHeader(req)) {
|
|
185
185
|
const msg = `The 'return' preference is not allowed in ${req.method} requests`
|
|
186
|
-
|
|
186
|
+
cds.error({ status: 400, message: msg })
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
// $apply with concat -> multiple queries with special handling
|
|
@@ -267,7 +267,7 @@ module.exports = adapter => {
|
|
|
267
267
|
const metadata = getODataMetadata(query, { result, isCollection: !one })
|
|
268
268
|
result = getODataResult(result, metadata, { isCollection: !one, property: _propertyAccess })
|
|
269
269
|
|
|
270
|
-
if (!result)
|
|
270
|
+
if (!result) cds.error(404)
|
|
271
271
|
|
|
272
272
|
res.send(result)
|
|
273
273
|
})
|
|
@@ -19,7 +19,7 @@ module.exports = adapter => {
|
|
|
19
19
|
return function service_document(req, res) {
|
|
20
20
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
21
21
|
const msg = `Method ${req.method} is not allowed for calls to the service endpoint`
|
|
22
|
-
|
|
22
|
+
cds.error({ status: 405, message: msg })
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const model = cds.context.model ?? service.model
|
|
@@ -54,7 +54,7 @@ module.exports = adapter => {
|
|
|
54
54
|
// REVISIT: patch on collection is allowed in odata 4.01
|
|
55
55
|
if (!one) {
|
|
56
56
|
if (req.method === 'PATCH')
|
|
57
|
-
|
|
57
|
+
cds.error(`Method ${req.method} is not allowed for entity collections`, { status: 405 })
|
|
58
58
|
const entity = service.model?.definitions[req._query._subject.ref[0]]
|
|
59
59
|
const keys = {},
|
|
60
60
|
data = req.body || {}
|