@sap/cds 9.4.5 → 9.5.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +79 -1
  2. package/_i18n/messages_en_US_saptrc.properties +1 -1
  3. package/common.cds +5 -2
  4. package/lib/compile/cds-compile.js +1 -0
  5. package/lib/compile/for/assert.js +64 -0
  6. package/lib/compile/for/flows.js +194 -58
  7. package/lib/compile/for/lean_drafts.js +75 -7
  8. package/lib/compile/parse.js +1 -1
  9. package/lib/compile/to/csn.js +6 -2
  10. package/lib/compile/to/edm.js +1 -1
  11. package/lib/compile/to/yaml.js +8 -1
  12. package/lib/dbs/cds-deploy.js +2 -2
  13. package/lib/env/cds-env.js +14 -4
  14. package/lib/env/defaults.js +6 -1
  15. package/lib/i18n/localize.js +1 -1
  16. package/lib/index.js +7 -7
  17. package/lib/req/event.js +4 -0
  18. package/lib/req/validate.js +3 -0
  19. package/lib/srv/cds.Service.js +2 -1
  20. package/lib/srv/middlewares/auth/ias-auth.js +5 -7
  21. package/lib/srv/middlewares/auth/index.js +1 -1
  22. package/lib/srv/protocols/index.js +7 -6
  23. package/lib/srv/srv-handlers.js +7 -0
  24. package/libx/_runtime/common/Service.js +5 -1
  25. package/libx/_runtime/common/constants/events.js +1 -0
  26. package/libx/_runtime/common/generic/assert.js +236 -0
  27. package/libx/_runtime/common/generic/flows.js +168 -108
  28. package/libx/_runtime/common/utils/cqn.js +0 -24
  29. package/libx/_runtime/common/utils/normalizeTimestamp.js +2 -2
  30. package/libx/_runtime/common/utils/resolveView.js +8 -2
  31. package/libx/_runtime/common/utils/templateProcessor.js +10 -1
  32. package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +21 -9
  33. package/libx/_runtime/fiori/lean-draft.js +511 -379
  34. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +39 -35
  35. package/libx/_runtime/messaging/enterprise-messaging.js +2 -2
  36. package/libx/_runtime/remote/Service.js +4 -5
  37. package/libx/_runtime/ucl/Service.js +111 -15
  38. package/libx/common/utils/streaming.js +1 -1
  39. package/libx/odata/middleware/batch.js +8 -6
  40. package/libx/odata/middleware/create.js +2 -2
  41. package/libx/odata/middleware/delete.js +2 -2
  42. package/libx/odata/middleware/metadata.js +18 -11
  43. package/libx/odata/middleware/read.js +2 -2
  44. package/libx/odata/middleware/service-document.js +1 -1
  45. package/libx/odata/middleware/update.js +1 -1
  46. package/libx/odata/parse/afterburner.js +24 -25
  47. package/libx/odata/parse/cqn2odata.js +2 -6
  48. package/libx/odata/parse/grammar.peggy +90 -12
  49. package/libx/odata/parse/parser.js +1 -1
  50. package/libx/odata/utils/index.js +2 -2
  51. package/libx/odata/utils/readAfterWrite.js +2 -0
  52. package/libx/queue/TaskRunner.js +26 -1
  53. package/libx/queue/index.js +11 -1
  54. package/package.json +1 -1
  55. 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
- let jwt_auth
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
- if (isSecured()) {
15
- if (cds.requires.auth.impl) {
16
- cds.app.use(basePath, cds.middlewares.before) // contains auth, trace, context
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
- jwt_auth ??= require('../../../../lib/srv/middlewares/auth/jwt-auth.js')
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
- // unsuccessful auth doesn't automatically reject!
27
- cds.app.use(basePath, (req, res, next) => {
28
- // REVISIT: we should probably pass an error into next so that a (custom) error middleware can handle it
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
- if (process.env.NODE_ENV === 'production') {
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 (isSecured() && !cds.context.user.is('emcallback')) return res.sendStatus(403)
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 (isSecured() && !cds.context.user.is('emcallback')) return res.sendStatus(403)
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 (isSecured() && !cds.context.user.is('emmanagement')) return res.sendStatus(403)
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
- throw new Error(
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
- throw new Error(
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
- throw new Error(`No credentials configured for "${this.name}".`)
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
- throw new Error(`"url" or "destination" property must be configured in "credentials" of "${this.name}".`)
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) throw new Error(`Target ${req.target.name} cannot be resolved for service ${this.name}`)
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
- // REVISIT: Java unpacks the location header and enables the async flow aswell
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 response = (await this.send(operation, req.data)) ?? {}
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
- throw new Error(
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
- throw new Error(
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
- throw new Error('No credentials found for the UCL service, please bind the service to your app.')
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
- throw new Error('UCL requires `x509.cert` or `x509.certPath`.')
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
- throw new Error('UCL requires `x509.pkey` or `x509.pkeyPath`.')
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) throw new Error('The UCL service could not create an application 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
- throw Object.assign(new Error(msg), { statusCode: 406 })
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}`, { code: 400 })
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', { code: 400 }))
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
- throw cds.error(`Method ${req.method} is not allowed for calls to $batch endpoint`, { code: 405 })
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
- throw cds.error('Batch requests must have content type multipart/mixed or application/json', { statusCode: 400 })
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
- throw Object.assign(new Error(msg), { statusCode: 405 })
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
- throw Object.assign(new Error(msg), { statusCode: 400 })
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
- throw Object.assign(new Error(msg), { statusCode: 400 })
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
- throw Object.assign(new Error('Method DELETE is not allowed for entity collections'), { statusCode: 405 })
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
- // If no extensibility nor fts, do not provide model to mtxs
95
- const modelNeeded = cds.env.requires.extensibility || features?.given
96
- edmx =
97
- metadataCache.edm ||
98
- (await mps.getEdmx({
99
- tenant,
100
- model: modelNeeded ? await mps.getCsn(tenant, features) : undefined,
101
- service: service.definition.name
102
- }))
103
- metadataCache.edm = edmx
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
- throw Object.assign(new Error(msg), { statusCode: 400 })
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) throw new cds.error({ code: 404 })
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
- throw Object.assign(new Error(msg), { statusCode: 405 })
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
- throw cds.error(`Method ${req.method} is not allowed for entity collections`, { status: 405 })
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 || {}