@sap/cds 7.7.3 → 7.8.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.
Files changed (75) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/lib/auth/ias-auth.js +5 -3
  3. package/lib/auth/jwt-auth.js +4 -2
  4. package/lib/compile/cdsc.js +0 -10
  5. package/lib/compile/for/java.js +9 -5
  6. package/lib/compile/for/lean_drafts.js +1 -1
  7. package/lib/compile/to/edm.js +2 -1
  8. package/lib/compile/to/sql.js +0 -21
  9. package/lib/compile/to/srvinfo.js +13 -4
  10. package/lib/dbs/cds-deploy.js +7 -7
  11. package/lib/env/cds-requires.js +6 -0
  12. package/lib/index.js +4 -3
  13. package/lib/linked/classes.js +151 -88
  14. package/lib/linked/entities.js +27 -23
  15. package/lib/linked/models.js +57 -36
  16. package/lib/linked/types.js +42 -104
  17. package/lib/ql/Whereable.js +3 -3
  18. package/lib/req/context.js +8 -4
  19. package/lib/srv/protocols/hcql.js +2 -1
  20. package/lib/srv/protocols/http.js +7 -7
  21. package/lib/srv/protocols/index.js +31 -13
  22. package/lib/srv/protocols/odata-v4.js +79 -58
  23. package/lib/srv/srv-api.js +7 -6
  24. package/lib/srv/srv-dispatch.js +1 -12
  25. package/lib/srv/srv-tx.js +9 -13
  26. package/lib/utils/cds-utils.js +6 -5
  27. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +11 -8
  28. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +21 -12
  29. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +5 -3
  30. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +3 -7
  31. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +5 -0
  32. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +2 -1
  33. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +3 -7
  34. package/libx/_runtime/cds-services/services/utils/columns.js +6 -3
  35. package/libx/_runtime/cds.js +0 -13
  36. package/libx/_runtime/common/generic/input.js +3 -0
  37. package/libx/_runtime/common/generic/sorting.js +8 -6
  38. package/libx/_runtime/common/i18n/messages.properties +1 -0
  39. package/libx/_runtime/common/utils/cqn.js +5 -0
  40. package/libx/_runtime/common/utils/foreignKeyPropagations.js +7 -1
  41. package/libx/_runtime/common/utils/keys.js +2 -2
  42. package/libx/_runtime/common/utils/resolveView.js +2 -1
  43. package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -1
  44. package/libx/_runtime/common/utils/stream.js +0 -10
  45. package/libx/_runtime/common/utils/template.js +20 -35
  46. package/libx/_runtime/db/Service.js +5 -1
  47. package/libx/_runtime/db/utils/columns.js +1 -1
  48. package/libx/_runtime/fiori/lean-draft.js +14 -2
  49. package/libx/_runtime/messaging/Outbox.js +7 -5
  50. package/libx/_runtime/messaging/kafka.js +266 -0
  51. package/libx/_runtime/messaging/service.js +7 -5
  52. package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +1 -0
  53. package/libx/common/assert/validation.js +1 -1
  54. package/libx/odata/index.js +8 -2
  55. package/libx/odata/middleware/batch.js +340 -0
  56. package/libx/odata/middleware/create.js +43 -46
  57. package/libx/odata/middleware/delete.js +27 -15
  58. package/libx/odata/middleware/error.js +6 -5
  59. package/libx/odata/middleware/metadata.js +16 -15
  60. package/libx/odata/middleware/operation.js +107 -59
  61. package/libx/odata/middleware/parse.js +15 -7
  62. package/libx/odata/middleware/read.js +150 -24
  63. package/libx/odata/middleware/service-document.js +17 -6
  64. package/libx/odata/middleware/stream.js +34 -17
  65. package/libx/odata/middleware/update.js +123 -87
  66. package/libx/odata/parse/afterburner.js +131 -28
  67. package/libx/odata/parse/cqn2odata.js +1 -1
  68. package/libx/odata/parse/grammar.peggy +4 -5
  69. package/libx/odata/parse/multipartToJson.js +163 -0
  70. package/libx/odata/parse/parser.js +1 -1
  71. package/libx/odata/utils/index.js +29 -47
  72. package/libx/odata/utils/path.js +72 -0
  73. package/libx/odata/utils/result.js +123 -20
  74. package/package.json +1 -1
  75. package/server.js +4 -0
@@ -0,0 +1,340 @@
1
+ const cds = require('../../../')
2
+ const { AsyncResource } = require('async_hooks')
3
+
4
+ // eslint-disable-next-line cds/no-missing-dependencies
5
+ const express = require('express')
6
+ const { STATUS_CODES } = require('http')
7
+
8
+ const multipartToJson = require('../parse/multipartToJson')
9
+
10
+ const HTTP_METHODS = { GET: 1, POST: 1, PUT: 1, PATCH: 1, DELETE: 1 }
11
+ const CT = { JSON: 'application/json', MULTIPART: 'multipart/mixed' }
12
+ const CRLF = '\r\n'
13
+
14
+ /*
15
+ * common
16
+ */
17
+
18
+ const _deserializationError = message => cds.error(`Deserialization Error: ${message}`, { code: 400 })
19
+
20
+ // Function must be called with an object containing exactly one key-value pair representing the property name and its value
21
+ const _validateProperty = (name, value, type) => {
22
+ if (value === undefined) throw _deserializationError(`Parameter '${name}' must not be undefined.`)
23
+
24
+ switch (type) {
25
+ case 'Array':
26
+ if (!Array.isArray(value)) throw _deserializationError(`Parameter '${name}' must be type of '${type}'.`)
27
+ break
28
+ default:
29
+ if (typeof value !== type) throw _deserializationError(`Parameter '${name}' must be type of '${type}'.`)
30
+ }
31
+ }
32
+
33
+ const _validateBatch = body => {
34
+ const { requests } = body
35
+
36
+ _validateProperty('requests', requests, 'Array')
37
+
38
+ const ids = {}
39
+
40
+ let previousAtomicityGroup
41
+ requests.forEach((request, i) => {
42
+ if (typeof request !== 'object')
43
+ throw _deserializationError(`Element of 'requests' array at index ${i} must be type of 'object'.`)
44
+
45
+ const { id, method, url, body, atomicityGroup, dependsOn } = request
46
+
47
+ _validateProperty('id', id, 'string')
48
+
49
+ if (ids[id]) throw _deserializationError(`Request ID '${id}' is not unique.`)
50
+ else ids[id] = request
51
+
52
+ // TODO: validate allowed methods or let express throw the error?
53
+ _validateProperty('method', method, 'string')
54
+ if (!(method.toUpperCase() in HTTP_METHODS))
55
+ throw _deserializationError(`Method '${method}' is not allowed. Only DELETE, GET, PATCH, POST or PUT are.`)
56
+
57
+ _validateProperty('url', url, 'string')
58
+ // TODO: need similar validation in multipart/mixed batch
59
+ if (url.startsWith('/$batch')) throw _deserializationError('Nested batch requests are not allowed.')
60
+
61
+ // TODO: support for non JSON bodies?
62
+ if (body !== undefined && typeof body !== 'object')
63
+ throw _deserializationError('A Content-Type header has to be specified for a non JSON body.')
64
+
65
+ // TODO
66
+ // if (!(method.toUpperCase() in { GET: 1, DELETE: 1 }) && !body)
67
+ // throw _deserializationError(`Body is required for ${method} requests.`)
68
+
69
+ if (atomicityGroup) {
70
+ _validateProperty('atomicityGroup', atomicityGroup, 'string')
71
+
72
+ // All request objects with the same value for atomicityGroup MUST be adjacent in the requests array
73
+ if (atomicityGroup !== previousAtomicityGroup) {
74
+ if (ids[atomicityGroup]) throw _deserializationError(`Atomicity group ID '${atomicityGroup}' is not unique.`)
75
+ else ids[atomicityGroup] = [request]
76
+ } else {
77
+ ids[atomicityGroup].push(request)
78
+ }
79
+ }
80
+
81
+ if (dependsOn) {
82
+ _validateProperty('dependsOn', dependsOn, 'Array')
83
+ dependsOn.forEach(dependsOnId => {
84
+ _validateProperty('dependent request ID', dependsOnId, 'string')
85
+
86
+ const dependency = ids[dependsOnId]
87
+ if (!dependency)
88
+ throw _deserializationError(`Request ID '${dependsOnId}' used in dependsOn has not been defined before.`)
89
+
90
+ const dependencyAtomicityGroup = dependency.atomicityGroup
91
+ if (dependencyAtomicityGroup && !dependsOn.includes(dependencyAtomicityGroup))
92
+ throw _deserializationError(
93
+ `The group '${dependencyAtomicityGroup}' of the referenced request '${dependsOnId}' must be listed in dependsOn of request '${id}'.`
94
+ )
95
+ })
96
+ }
97
+
98
+ // TODO: validate if, and headers
99
+
100
+ previousAtomicityGroup = atomicityGroup
101
+ })
102
+
103
+ return ids
104
+ }
105
+
106
+ const _createExpressReqResLookalike = (request, _req, _res) => {
107
+ const { id, method, url } = request
108
+ const ret = { id }
109
+
110
+ const req = (ret.req = new express.request.constructor())
111
+ req.__proto__ = express.request
112
+
113
+ // express internals
114
+ req.app = _req.app
115
+
116
+ req.method = method.toUpperCase()
117
+ req.url = url
118
+ req.query = {}
119
+ req.headers = request.headers || {}
120
+ req.body = request.body
121
+
122
+ // propagate user, tenant and locale
123
+ req.user = _req.user
124
+ req.tenant = _req.tenant
125
+ req.locale = _req.locale
126
+
127
+ const res = (ret.res = new express.response.constructor(req))
128
+ res.__proto__ = express.response
129
+
130
+ // express internals
131
+ res.app = _res.app
132
+
133
+ // back link
134
+ req.res = res
135
+
136
+ // resolve promise for subrequest via res.end()
137
+ ret.promise = new Promise((resolve, _reject) => {
138
+ res.end = (chunk, encoding) => {
139
+ res._chunk = chunk
140
+ res._encoding = encoding
141
+ resolve(ret)
142
+ }
143
+ })
144
+
145
+ return ret
146
+ }
147
+
148
+ const _transaction = async (srv, router) => {
149
+ return new Promise(res => {
150
+ const ret = {}
151
+ srv.tx(
152
+ async () =>
153
+ (ret.promise = new Promise((resolve, reject) => {
154
+ const proms = []
155
+ ret.add = AsyncResource.bind(function (request, req, res) {
156
+ const lookalike = _createExpressReqResLookalike(request, req, res)
157
+ router.handle(lookalike.req, lookalike.res)
158
+ request.promise = lookalike.promise
159
+ proms.push(request.promise)
160
+ return request.promise
161
+ })
162
+ ret.done = function () {
163
+ return Promise.allSettled(proms).then(resolve, reject)
164
+ }
165
+ res(ret)
166
+ }))
167
+ )
168
+ })
169
+ }
170
+
171
+ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) => {
172
+ body ??= req.body
173
+ ct ??= 'JSON'
174
+ const isJson = ct === 'JSON'
175
+ const _formatResponse = isJson ? _formatResponseJson : _formatResponseMultipart
176
+
177
+ try {
178
+ const ids = _validateBatch(body) // REVISIT: we will not be able to validate the whole once we stream
179
+
180
+ // TODO: if (!requests || !requests.length) throw new Error('At least one request, buddy!')
181
+
182
+ let previousAtomicityGroup
183
+ let separator
184
+ let tx
185
+
186
+ res.setHeader('content-type', CT[ct] + (!isJson ? ';boundary=' + boundary : ''))
187
+ res.setHeader('OData-Version', '4.0') //> REVISIT: Fiori/ UI5 wants this
188
+ res.status(200)
189
+ res.write(isJson ? '{"responses":[' : '')
190
+
191
+ const { requests } = body
192
+ for await (const request of requests) {
193
+ const { atomicityGroup } = request
194
+
195
+ if (!atomicityGroup || atomicityGroup !== previousAtomicityGroup) {
196
+ if (tx) await tx.done()
197
+ tx = await _transaction(srv, router)
198
+ if (atomicityGroup) ids[atomicityGroup].promise = tx.promise
199
+ }
200
+
201
+ const dependencies = request.dependsOn?.map(id => ids[id].promise)
202
+ if (dependencies) {
203
+ // TODO: fail the dependent request if dependency fails
204
+ await Promise.allSettled(dependencies)
205
+ }
206
+
207
+ tx.add(request, req, res).then(request => {
208
+ if (separator) res.write(separator)
209
+ else separator = isJson ? Buffer.from(',') : Buffer.from(CRLF)
210
+ _formatResponse(request, res, boundary)
211
+ })
212
+
213
+ if (!atomicityGroup) tx.done()
214
+
215
+ previousAtomicityGroup = atomicityGroup
216
+ }
217
+
218
+ if (tx) await tx.done()
219
+
220
+ res.write(isJson ? ']}' : `${CRLF}--${boundary}--${CRLF}`)
221
+ res.end()
222
+
223
+ return
224
+ } catch (e) {
225
+ next(e)
226
+ }
227
+ }
228
+
229
+ /*
230
+ * multipart/mixed
231
+ */
232
+
233
+ const _multipartBatch = (srv, router) => async (req, res, next) => {
234
+ const boundary = req.headers['content-type']?.match(/boundary=([\w_-]+)/i)?.[1]
235
+ if (!boundary) return next(cds.error('No boundary found in Content-Type header', { code: 400 }))
236
+
237
+ try {
238
+ const { requests } = await multipartToJson(req.body, boundary)
239
+ _processBatch(srv, router, req, res, next, { requests }, 'MULTIPART', boundary)
240
+ } catch (e) {
241
+ next(e)
242
+ }
243
+ }
244
+
245
+ const _formatResponseMultipart = (request, res, boundary) => {
246
+ const { /* id, */ res: response } = request
247
+
248
+ let txt = `--${boundary}${CRLF}content-type: application/http${CRLF}content-transfer-encoding: binary${CRLF}${CRLF}`
249
+ txt += `HTTP/1.1 ${response.statusCode} ${STATUS_CODES[response.statusCode]}${CRLF}`
250
+
251
+ // REVISIT: tests require specific sequence
252
+ const headers = {
253
+ 'odata-version': '4.0',
254
+ 'content-type': 'DUMMY',
255
+ ...response.getHeaders()
256
+ }
257
+ headers['content-type'] = 'application/json;odata.metadata=minimal' //> REVISIT: expected by tests
258
+ delete headers['content-length'] //> REVISIT: expected by tests
259
+ for (const key in headers) {
260
+ txt += key + ': ' + headers[key] + CRLF
261
+ }
262
+ txt += CRLF
263
+
264
+ if (response._chunk) {
265
+ let _json
266
+ try {
267
+ // REVISIT: tests require specific sequence -> fix and simply use res._chunk
268
+ _json = JSON.parse(response._chunk)
269
+ if (typeof _json !== 'object') throw new Error('not an object')
270
+ let meta = [],
271
+ data = []
272
+ for (const [k, v] of Object.entries(_json)) {
273
+ if (k.startsWith('@')) meta.push(`"${k}":"${v}"`)
274
+ else data.push(JSON.stringify({ [k]: v }).slice(1, -1))
275
+ }
276
+ const _json_as_txt = '{' + meta.join(',') + (meta.length && data.length ? ',' : '') + data.join(',') + '}'
277
+ txt += _json_as_txt
278
+ } catch {
279
+ // ignore error and take chunk as is (a text)
280
+ txt += response._chunk
281
+ txt = txt.replace('content-type: application/json;odata.metadata=minimal', 'content-type: text/plain')
282
+ }
283
+ }
284
+
285
+ res.write(txt)
286
+ }
287
+
288
+ /*
289
+ * application/json
290
+ */
291
+
292
+ const _formatStatics = {
293
+ comma: ','.charCodeAt(0),
294
+ body: Buffer.from('"body":'),
295
+ close: Buffer.from('}')
296
+ }
297
+
298
+ const _formatResponseJson = (request, res) => {
299
+ const { id, res: response } = request
300
+ const raw = Buffer.from(
301
+ JSON.stringify({
302
+ id,
303
+ status: response.statusCode,
304
+ headers: {
305
+ 'odata-version': '4.0',
306
+ 'content-type': 'application/json',
307
+ ...response.getHeaders()
308
+ }
309
+ })
310
+ )
311
+ // change last "}" into ","
312
+ raw[raw.byteLength - 1] = _formatStatics.comma
313
+ res.write(raw)
314
+ res.write(_formatStatics.body)
315
+ res.write(response._chunk)
316
+ res.write(_formatStatics.close)
317
+ }
318
+
319
+ const _jsonBatch = (srv, router) => (req, res, next) => {
320
+ _processBatch(srv, router, req, res, next)
321
+ }
322
+
323
+ module.exports = (srv, router) => {
324
+ const handleJsonBatch = _jsonBatch(srv, router)
325
+ const handleMultipartBatch = _multipartBatch(srv, router)
326
+
327
+ return function batch(req, res, next) {
328
+ if (req.headers['content-type'].includes('application/json')) {
329
+ return handleJsonBatch(req, res, next)
330
+ }
331
+
332
+ if (req.headers['content-type'].includes('multipart/mixed')) {
333
+ return express.text({ type: '*/*' })(req, res, () => {
334
+ handleMultipartBatch(req, res, next)
335
+ })
336
+ }
337
+
338
+ throw cds.error('Batch requests must have content type multipart/mixed or application/json', { code: 400 })
339
+ }
340
+ }
@@ -1,8 +1,8 @@
1
1
  const cds = require('../../../')
2
2
  const { INSERT } = cds.ql
3
3
 
4
- const { toODataResult } = require('../utils/result')
5
- const { odataError, getKeysFromPath } = require('../utils')
4
+ const { toODataResult, postProcess } = require('../utils/result')
5
+ const { calculateLocationHeader, getKeysAndParamsFromPath, handleSapMessages } = require('../utils')
6
6
 
7
7
  const { deepCopy } = require('../../_runtime/common/utils/copy')
8
8
 
@@ -10,74 +10,71 @@ const { deepCopy } = require('../../_runtime/common/utils/copy')
10
10
  const { readAfterWrite } = require('../../_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite')
11
11
  const metaInfo = require('../../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
12
12
 
13
- const _calculateLocationHeader = (target, srv, result) => {
14
- const targetName = target.name.replace(`${srv.name}.`, '')
15
-
16
- const keyValuePairs = Object.keys(target.keys).reduce((acc, key) => {
17
- acc[key] = result[key]
18
- return acc
19
- }, {})
20
-
21
- let keys
22
- const entries = Object.entries(keyValuePairs)
23
- if (entries.length === 1) {
24
- keys = entries[0][1]
25
- } else {
26
- keys = entries.map(([key, value]) => `${key}=${value}`).join(',')
27
- }
28
-
29
- return `${targetName}(${keys})`
30
- }
31
-
32
13
  module.exports = srv =>
33
14
  function create(req, res, next) {
34
- const { _query: query } = req
35
-
36
15
  const {
37
- SELECT: { one, from }
38
- } = query
16
+ SELECT: { one, from },
17
+ target
18
+ } = req._query
39
19
 
40
20
  if (one) {
41
- const singleton = query.target._isSingleton
42
- const error = odataError('405', `Method ${req.method} not allowed for ${singleton ? 'SINGLETON' : 'ENTITY'}`)
43
- return res.status(405).json(error)
21
+ // REVISIT: don't use "SINGLETON" or "ENTITY" as that are okra terms
22
+ throw Object.assign(
23
+ new Error(`Method ${req.method} not allowed for ${target._isSingleton ? 'SINGLETON' : 'ENTITY'}`),
24
+ { statusCode: 405 }
25
+ )
44
26
  }
45
27
 
28
+ // payload & params
46
29
  const data = deepCopy(req.body)
47
-
30
+ const { keys, params } = getKeysAndParamsFromPath(from, srv)
48
31
  // add keys from url into payload (overwriting if already present)
49
- Object.assign(data, getKeysFromPath(from, srv))
32
+ Object.assign(data, keys)
50
33
 
51
34
  // assert payload
52
35
  const assertOptions = { filter: true, http: { req }, mandatories: true }
53
- const errs = cds.assert(data, query.target, assertOptions)
36
+ const errs = cds.assert(data, target, assertOptions)
54
37
  if (errs) {
55
38
  if (errs.length === 1) throw errs[0]
56
39
  throw Object.assign(new Error('MULTIPLE_ERRORS'), { statusCode: 400, details: errs })
57
40
  }
58
41
 
59
- const insertQuery = INSERT.into(from).entries(data)
42
+ // query
43
+ const query = INSERT.into(from).entries(data)
60
44
 
61
- // we need the cds request, so we can access req._.readAfterWrite
62
- const cdsReq = new cds.Request({ query: insertQuery })
45
+ // we need a cds.Request for multiple reasons, incl. params, headers, sap-messages, read after write, ...
46
+ const cdsReq = new cds.Request({ query, params, req, res })
47
+ Object.defineProperty(cdsReq, 'protocol', { value: 'odata-v4' })
63
48
 
64
49
  // rewrite event for draft-enabled entities
65
- if (query.target._isDraftEnabled) cdsReq.event = 'NEW'
50
+ if (target._isDraftEnabled) cdsReq.event = 'NEW'
66
51
 
52
+ // REVISIT: only via srv.run in combination with srv.dispatch inside
53
+ // we automatically either use a single auto-managed tx for the req (i.e., insert and read after write in same tx)
54
+ // or the auto-managed tx opened for the respective atomicity group, if exists
67
55
  return srv
68
- .dispatch(cdsReq)
69
- .then(async result => {
70
- if (cdsReq._.readAfterWrite) {
71
- // TODO see if in old odata impl for other checks that should happen
72
- result = await readAfterWrite(cdsReq, srv, { operation: { result } })
73
- }
56
+ .run(() => {
57
+ return srv.dispatch(cdsReq).then(result => {
58
+ handleSapMessages(cdsReq, req, res)
74
59
 
75
- if (result == null || req._preferReturn === 'minimal') return res.sendStatus(204)
60
+ if (cdsReq._.readAfterWrite) {
61
+ return readAfterWrite(cdsReq, srv, { operation: { result } })
62
+ }
76
63
 
77
- const location = _calculateLocationHeader(cdsReq.target, srv, result)
78
- const info = metaInfo(insertQuery, 'CREATE', srv, result, req)
64
+ return result
65
+ })
66
+ })
67
+ .then(result => {
68
+ // we use an extra then block, after getting the result, so the transaction is commited, before sending the response
69
+ if (result == null) return res.sendStatus(204)
70
+ const isMinimal = req._preferReturn === 'minimal'
71
+ postProcess(cdsReq.target, srv, result, isMinimal)
72
+ if (result['$etag']) res.set('etag', result['$etag'])
73
+ res.set('location', calculateLocationHeader(cdsReq.target, srv, result))
74
+ if (isMinimal) return res.sendStatus(204)
75
+ const info = metaInfo(query, 'CREATE', srv, result, req)
79
76
  result = toODataResult(result, info)
80
- res.set('location', location).status(201).send(result)
77
+ res.status(201).set('Content-Type', 'application/json;IEEE754Compatible=true').send(result)
81
78
  })
82
- .catch(next)
79
+ .catch(next) // should be outside, so tx can be rolled back in case of errors
83
80
  }
@@ -1,37 +1,49 @@
1
1
  const cds = require('../../../')
2
2
  const { UPDATE, DELETE } = cds.ql
3
3
 
4
- const { odataError, getKeysFromPath } = require('../utils')
4
+ const { getKeysAndParamsFromPath, handleSapMessages } = require('../utils')
5
5
 
6
6
  module.exports = srv =>
7
7
  function deleete(req, res, next) {
8
8
  if (req._preferReturn) {
9
- const message = `The 'return' preference is not allowed in ${req.method} requests`
10
- return res.status(400).json({ error: { code: '400', message } })
9
+ throw Object.assign(new Error(`The 'return' preference is not allowed in ${req.method} requests`), {
10
+ statusCode: 400
11
+ })
11
12
  }
12
13
 
13
- const { _query: query } = req
14
-
14
+ // REVISIT: better solution for query._propertyAccess
15
15
  const {
16
- SELECT: { one, from }
17
- } = query
16
+ SELECT: { one, from },
17
+ target,
18
+ _propertyAccess
19
+ } = req._query
18
20
 
19
21
  if (!one) {
20
22
  // REVISIT: don't use "ENTITY.COLLECTION" as that's an okra term
21
- return res.status(405).json(odataError('405', `Method DELETE not allowed for ENTITY.COLLECTION`))
23
+ throw Object.assign(new Error('Method DELETE not allowed for ENTITY.COLLECTION'), { statusCode: 405 })
22
24
  }
23
25
 
24
- // for read and delete, we provide keys in req.data
25
- const data = getKeysFromPath(query.SELECT.from, srv)
26
+ // payload & params
27
+ const { keys, params } = getKeysAndParamsFromPath(from, srv)
28
+ const data = keys //> for read and delete, we provide keys in req.data
29
+ if (_propertyAccess) data[_propertyAccess] = null //> delete of property -> set to null
30
+
31
+ // query
32
+ const query = _propertyAccess ? UPDATE(from).set({ [_propertyAccess]: null }) : DELETE.from(from)
26
33
 
27
- // REVISIT: better
28
- if (query._propertyAccess) data[query._propertyAccess] = null
34
+ // we need a cds.Request for multiple reasons, incl. params, headers, sap-messages, read after write, ...
35
+ const cdsReq = new cds.Request({ query, data, params, req, res })
36
+ Object.defineProperty(cdsReq, 'protocol', { value: 'odata-v4' })
37
+
38
+ // rewrite event for draft-enabled entities
39
+ if (target._isDraftEnabled && cdsReq.data.IsActiveEntity === false) cdsReq.event = 'CANCEL'
29
40
 
30
- // REVISIT: maybe also just dispatch a cds request here?
31
41
  return srv
32
- .run(query._propertyAccess ? UPDATE(from).set({ [query._propertyAccess]: null }) : DELETE.from(from), data)
42
+ .dispatch(cdsReq)
33
43
  .then(result => {
34
- if (result === 0) return res.status(404).json({ error: { code: '404', message: 'Not Found' } })
44
+ handleSapMessages(cdsReq, req, res)
45
+
46
+ if (result === 0) throw Object.assign(new Error('Not found'), { statusCode: 404 })
35
47
  res.sendStatus(204)
36
48
  })
37
49
  .catch(next)
@@ -1,8 +1,9 @@
1
1
  const { normalizeError } = require('../../_runtime/common/error/frontend')
2
2
 
3
- module.exports = _srv => (err, req, res, _next) => {
4
- const { error, statusCode } = normalizeError(err, req)
3
+ module.exports = _srv =>
4
+ function error(err, req, res, _next) {
5
+ const { error, statusCode } = normalizeError(err, req)
5
6
 
6
- // NOTE: normalizeError already does sanatization -> we can use as is
7
- res.status(statusCode).json({ error })
8
- }
7
+ // NOTE: normalizeError already does sanatization -> we can use as is
8
+ res.status(statusCode).json({ error })
9
+ }
@@ -3,8 +3,6 @@ const LOG = cds.log('odata')
3
3
 
4
4
  const crypto = require('crypto')
5
5
 
6
- const { odataError } = require('../utils')
7
-
8
6
  const _requestedFormat = (queryOption, header) => {
9
7
  if (queryOption) return queryOption.match(/json/i) ? 'json' : 'xml'
10
8
  if (header) {
@@ -36,9 +34,9 @@ const normalize_header = value => {
36
34
  return value.split(',').map(str => str.trim())
37
35
  }
38
36
 
39
- const validate_etag = (ifNoneMatch, etag) => {
40
- const ifNoneMatchEtags = normalize_header(ifNoneMatch)
41
- return ifNoneMatchEtags.includes(etag) || ifNoneMatchEtags.includes('*')
37
+ const validate_etag = (header, etag) => {
38
+ const normalized = normalize_header(header)
39
+ return normalized.includes(etag) || normalized.includes('*') || normalized.includes('"*"')
42
40
  }
43
41
 
44
42
  const generateEtag = s => {
@@ -54,7 +52,7 @@ const mpSupportsEmptyLocale = () => {
54
52
  module.exports = srv =>
55
53
  async function metadata(req, res, _next) {
56
54
  if (req.method !== 'GET')
57
- return res.status(405).json(odataError('METHOD_NOT_ALLOWED', `Method ${req.method} not allowed for $metadata.`))
55
+ throw Object.assign(new Error(`Method ${req.method} not allowed for $metadata.`), { statusCode: 405 })
58
56
 
59
57
  const tenant = cds.context.tenant
60
58
  const locale = cds.context.locale
@@ -66,6 +64,13 @@ module.exports = srv =>
66
64
 
67
65
  const etag = format === 'json' ? metadataCache.jsonEtag?.[locale] : metadataCache.xmlEtag?.[locale]
68
66
 
67
+ if (req.headers['if-match']) {
68
+ if (etag) {
69
+ const valid = validate_etag(req.headers['if-match'], etag)
70
+ if (!valid) return res.status(412).end()
71
+ }
72
+ }
73
+
69
74
  if (req.headers['if-none-match']) {
70
75
  if (etag) {
71
76
  const unchanged = validate_etag(req.headers['if-none-match'], etag)
@@ -79,14 +84,10 @@ module.exports = srv =>
79
84
  const { 'cds.xt.ModelProviderService': mps } = cds.services
80
85
  if (mps) {
81
86
  if (format === 'json')
82
- res
83
- .status(400)
84
- .json(
85
- odataError(
86
- 'UNSUPPORTED_METADATA_TYPE',
87
- 'JSON metadata is not supported if cds.requires.extensibilty: true.'
88
- )
89
- )
87
+ throw Object.assign(new Error('JSON metadata is not supported if cds.requires.extensibilty: true.'), {
88
+ statusCode: 400,
89
+ code: 'UNSUPPORTED_METADATA_TYPE'
90
+ })
90
91
 
91
92
  try {
92
93
  let edmx
@@ -113,7 +114,7 @@ module.exports = srv =>
113
114
  LOG.error(e)
114
115
  }
115
116
 
116
- return res.status(503).json(odataError('SERVICE_UNAVAILABLE', 'Service unavailable'))
117
+ throw Object.assign(new Error('Service Unavailable'), { statusCode: 503 })
117
118
  }
118
119
  }
119
120