@sap/cds 7.6.4 → 7.7.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.
Files changed (97) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/_i18n/i18n.properties +3 -0
  3. package/app/index.js +14 -8
  4. package/bin/serve.js +51 -19
  5. package/common.cds +16 -0
  6. package/lib/auth/ias-auth.js +2 -2
  7. package/lib/auth/index.js +1 -1
  8. package/lib/auth/jwt-auth.js +1 -1
  9. package/lib/compile/cdsc.js +23 -11
  10. package/lib/compile/for/nodejs.js +2 -2
  11. package/lib/compile/for/odata.js +4 -0
  12. package/lib/compile/load.js +7 -2
  13. package/lib/compile/to/sql.js +3 -0
  14. package/lib/dbs/cds-deploy.js +197 -220
  15. package/lib/env/defaults.js +2 -1
  16. package/lib/index.js +8 -2
  17. package/lib/linked/types.js +1 -0
  18. package/lib/log/format/json.js +4 -1
  19. package/lib/plugins.js +2 -2
  20. package/lib/ql/SELECT.js +8 -8
  21. package/lib/req/context.js +22 -13
  22. package/lib/req/request.js +10 -4
  23. package/lib/srv/cds-connect.js +9 -3
  24. package/lib/srv/cds-serve.js +5 -3
  25. package/lib/srv/middlewares/ctx-model.js +1 -1
  26. package/lib/srv/protocols/odata-v4.js +38 -9
  27. package/lib/srv/srv-api.js +98 -140
  28. package/lib/srv/srv-models.js +2 -2
  29. package/lib/srv/srv-tx.js +1 -0
  30. package/lib/utils/cds-utils.js +32 -23
  31. package/lib/utils/data.js +1 -1
  32. package/lib/utils/tar.js +1 -1
  33. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +1 -3
  34. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +0 -2
  35. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +18 -1
  36. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +1 -1
  37. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +7 -3
  38. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +2 -1
  39. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/index.js +5 -0
  40. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +71 -25
  41. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +10 -2
  42. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +6 -1
  43. package/libx/_runtime/cds-services/util/assert.js +50 -240
  44. package/libx/_runtime/cds.js +5 -0
  45. package/libx/_runtime/common/aspects/any.js +53 -45
  46. package/libx/_runtime/common/generic/input.js +14 -10
  47. package/libx/_runtime/common/generic/paging.js +1 -1
  48. package/libx/_runtime/common/utils/cqn.js +1 -1
  49. package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
  50. package/libx/_runtime/common/utils/keys.js +1 -1
  51. package/libx/_runtime/common/utils/quotingStyles.js +1 -1
  52. package/libx/_runtime/common/utils/resolveStructured.js +4 -1
  53. package/libx/_runtime/common/utils/rewriteAsterisks.js +5 -12
  54. package/libx/_runtime/common/utils/stream.js +2 -16
  55. package/libx/_runtime/common/utils/streamProp.js +16 -6
  56. package/libx/_runtime/common/utils/ucsn.js +1 -0
  57. package/libx/_runtime/db/expand/expandCQNToJoin.js +1 -1
  58. package/libx/_runtime/db/sql-builder/InsertBuilder.js +1 -1
  59. package/libx/_runtime/db/utils/columns.js +6 -1
  60. package/libx/_runtime/fiori/generic/activate.js +11 -3
  61. package/libx/_runtime/fiori/generic/edit.js +8 -2
  62. package/libx/_runtime/fiori/lean-draft.js +94 -30
  63. package/libx/_runtime/hana/execute.js +2 -5
  64. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +12 -22
  65. package/libx/_runtime/messaging/service.js +6 -2
  66. package/libx/common/assert/index.js +232 -0
  67. package/libx/common/assert/type.js +109 -0
  68. package/libx/common/assert/utils.js +125 -0
  69. package/libx/common/assert/validation.js +109 -0
  70. package/libx/odata/index.js +5 -5
  71. package/libx/odata/middleware/create.js +83 -0
  72. package/libx/odata/middleware/delete.js +38 -0
  73. package/libx/odata/middleware/error.js +8 -0
  74. package/libx/odata/{metadata.js → middleware/metadata.js} +8 -6
  75. package/libx/odata/middleware/operation.js +78 -0
  76. package/libx/odata/middleware/parse.js +11 -0
  77. package/libx/odata/{read.js → middleware/read.js} +42 -20
  78. package/libx/odata/{service-document.js → middleware/service-document.js} +2 -1
  79. package/libx/odata/middleware/stream.js +237 -0
  80. package/libx/odata/middleware/update.js +165 -0
  81. package/libx/odata/{afterburner.js → parse/afterburner.js} +79 -29
  82. package/libx/odata/{cqn2odata.js → parse/cqn2odata.js} +5 -3
  83. package/libx/odata/{parseToCqn.js → parse/parseToCqn.js} +3 -6
  84. package/libx/odata/{utils.js → utils/index.js} +95 -9
  85. package/libx/outbox/index.js +2 -1
  86. package/libx/rest/RestAdapter.js +0 -1
  87. package/libx/rest/middleware/operation.js +6 -4
  88. package/libx/rest/middleware/parse.js +20 -2
  89. package/package.json +1 -1
  90. package/server.js +43 -71
  91. package/libx/odata/create.js +0 -44
  92. package/libx/odata/delete.js +0 -25
  93. package/libx/odata/error.js +0 -12
  94. package/libx/odata/update.js +0 -110
  95. /package/libx/odata/{grammar.peggy → parse/grammar.peggy} +0 -0
  96. /package/libx/odata/{parser.js → parse/parser.js} +0 -0
  97. /package/libx/odata/{result.js → utils/result.js} +0 -0
@@ -0,0 +1,38 @@
1
+ const cds = require('../../../')
2
+ const { UPDATE, DELETE } = cds.ql
3
+
4
+ const { odataError, getKeysFromPath } = require('../utils')
5
+
6
+ module.exports = srv =>
7
+ function deleete(req, res, next) {
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 } })
11
+ }
12
+
13
+ const { _query: query } = req
14
+
15
+ const {
16
+ SELECT: { one, from }
17
+ } = query
18
+
19
+ if (!one) {
20
+ // 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`))
22
+ }
23
+
24
+ // for read and delete, we provide keys in req.data
25
+ const data = getKeysFromPath(query.SELECT.from, srv)
26
+
27
+ // REVISIT: better
28
+ if (query._propertyAccess) data[query._propertyAccess] = null
29
+
30
+ // REVISIT: maybe also just dispatch a cds request here?
31
+ return srv
32
+ .run(query._propertyAccess ? UPDATE(from).set({ [query._propertyAccess]: null }) : DELETE.from(from), data)
33
+ .then(result => {
34
+ if (result === 0) return res.status(404).json({ error: { code: '404', message: 'Not Found' } })
35
+ res.sendStatus(204)
36
+ })
37
+ .catch(next)
38
+ }
@@ -0,0 +1,8 @@
1
+ const { normalizeError } = require('../../_runtime/common/error/frontend')
2
+
3
+ module.exports = _srv => (err, req, res, _next) => {
4
+ const { error, statusCode } = normalizeError(err, req)
5
+
6
+ // NOTE: normalizeError already does sanatization -> we can use as is
7
+ res.status(statusCode).json({ error })
8
+ }
@@ -1,7 +1,9 @@
1
- const cds = require('../../lib')
1
+ const cds = require('../../../')
2
2
  const LOG = cds.log('odata')
3
+
3
4
  const crypto = require('crypto')
4
- const { odataError } = require('./utils')
5
+
6
+ const { odataError } = require('../utils')
5
7
 
6
8
  const _requestedFormat = (queryOption, header) => {
7
9
  if (queryOption) return queryOption.match(/json/i) ? 'json' : 'xml'
@@ -91,10 +93,10 @@ module.exports = srv =>
91
93
  // REVISIT: remove check later
92
94
  if (mpSupportsEmptyLocale()) {
93
95
  // If no extensibility nor fts, do not provide model to mtxs
94
- const modelNeeded =
95
- cds.env.requires.extensibility ||
96
- (cds.env.requires.toggles && Object.keys(cds.context.features || {}).length)
97
- edmx = metadataCache.edm || (await mps.getEdmx({ tenant, model: modelNeeded && srv.model, service: srv.definition.name }))
96
+ const modelNeeded = cds.env.requires.extensibility || cds.context.features?.given
97
+ edmx =
98
+ metadataCache.edm ||
99
+ (await mps.getEdmx({ tenant, model: modelNeeded && srv.model, service: srv.definition.name }))
98
100
  metadataCache.edm = edmx
99
101
  const extBundle = cds.env.requires.extensibility && (await mps.getI18n({ tenant, locale }))
100
102
  edmx = cds.localize(srv.model, locale, edmx, extBundle)
@@ -0,0 +1,78 @@
1
+ const cds = require('../../../')
2
+
3
+ const { toODataResult } = require('../utils/result')
4
+ const { cds2edm } = require('../utils')
5
+
6
+ const { deepCopy } = require('../../_runtime/common/utils/copy')
7
+
8
+ // REVISIT: move to or rewrite in libx/odata
9
+ const metaInfo = require('../../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
10
+
11
+ module.exports = srv => (req, res, next) => {
12
+ let { operation, args } = req._query.SELECT.from.ref.slice(-1)[0]
13
+ if (!operation) return next() //> create or read
14
+
15
+ // unbound vs. bound
16
+ let entity
17
+ if (srv.model.definitions[operation]) {
18
+ operation = srv.model.definitions[operation]
19
+ } else {
20
+ req._query.SELECT.from.ref.pop()
21
+ // TODO: this does not work when navigating to the entity
22
+ const lastRef = req._query.SELECT.from.ref.slice(-1)[0]
23
+ entity = lastRef.id || lastRef
24
+ entity = srv.model.definitions[entity]
25
+ operation = entity.actions[operation]
26
+ }
27
+
28
+ const data = args || deepCopy(req.body)
29
+
30
+ // assert payload
31
+ const assertOptions = { filter: true, http: { req }, mandatories: true }
32
+ const errs = cds.assert(data, operation, assertOptions)
33
+ if (errs) {
34
+ if (errs.length === 1) throw errs[0]
35
+ throw Object.assign(new Error('MULTIPLE_ERRORS'), { statusCode: 400, details: errs })
36
+ }
37
+
38
+ // REVISIT: when is operation.name actually prefixed with the service name?
39
+ const event = operation.name.replace(`${srv.name}.`, '')
40
+
41
+ // TODO: params
42
+ const cdsReq = new cds.Request({ query: entity ? req._query : undefined, event, data, params: [] })
43
+
44
+ srv
45
+ .dispatch(cdsReq)
46
+ .then(result => {
47
+ // REVISIT: result === undefined valid for modelled return type?
48
+ if (!operation.returns || result === undefined) return res.sendStatus(204)
49
+
50
+ if (operation.returns._type?.match?.(/^cds\./)) {
51
+ // TODO: check result type
52
+ return res.json({
53
+ '@odata.context': `${entity ? '../' : ''}$metadata#${cds2edm[operation.returns._type]}`,
54
+ value: result
55
+ })
56
+ }
57
+
58
+ const assertOptions = { mandatories: true } //> TODO: more needed?
59
+ // TODO: error targets are not correct if return type is "many X"
60
+ const assertDefinition = operation.returns.items || operation.returns
61
+ const errs = cds.assert(result, assertDefinition, assertOptions)
62
+ if (errs) {
63
+ // TODO: proper error handling
64
+ }
65
+
66
+ const info = metaInfo(req._query, event, srv, result, req)
67
+ // FIXME: info.metadata.isCollection is incorrect for draftActivate
68
+ if (event === 'draftActivate') info.metadata.isCollection = false
69
+ result = toODataResult(result, info)
70
+
71
+ // TODO: toODataResult() doesn't seem to handle this case
72
+ if (entity && !result['@odata.context'].match(/^\.\.\//))
73
+ result['@odata.context'] = '../' + result['@odata.context']
74
+
75
+ res.json(result)
76
+ })
77
+ .catch(next)
78
+ }
@@ -0,0 +1,11 @@
1
+ const cds = require('../../../')
2
+
3
+ module.exports = srv => (req, _, next) => {
4
+ // if not a GET, use req.path instead of req.url to ignore query parameters
5
+ req._query = cds.odata.parse(req.method === 'GET' ? req.url : req.path, { service: srv, baseUrl: req.baseUrl })
6
+
7
+ const preferReturn = req.headers.prefer?.match(/\W?return=(\w+)/i)
8
+ if (preferReturn) req._preferReturn = preferReturn[1]
9
+
10
+ next()
11
+ }
@@ -1,8 +1,13 @@
1
- const metaInfo = require('../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
2
- const cds = require('../../')
3
- const { toODataResult } = require('./result')
1
+ const cds = require('../../../')
2
+ const { toODataResult } = require('../utils/result')
4
3
  const querystring = require('node:querystring')
5
- const { getPageSize } = require('../_runtime/common/generic/paging')
4
+ const { getPageSize } = require('../../_runtime/common/generic/paging')
5
+ const { handleStreamProperties } = require('../../_runtime/common/utils/streamProp')
6
+
7
+ const { getKeysFromPath } = require('../utils')
8
+
9
+ // REVISIT: move to or rewrite in libx/odata
10
+ const metaInfo = require('../../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
6
11
 
7
12
  const _getCount = result =>
8
13
  Array.isArray(result)
@@ -16,17 +21,15 @@ const _calculateNextLink = (req, result) => {
16
21
  if ($skiptoken) {
17
22
  const queryParamsWithSkipToken = { ...req.http.req.query, $skiptoken }
18
23
  // REVISIT: slice replaces leading '/'. Always starts with '/'?
19
- result.$nextLink = (
20
- req.http.req.path.slice(1) + '?' + querystring.stringify(queryParamsWithSkipToken, '&', '=', { encodeURIComponent: e => e })
21
- )
24
+ result.$nextLink =
25
+ req.http.req.path.slice(1) +
26
+ '?' +
27
+ querystring.stringify(queryParamsWithSkipToken, '&', '=', { encodeURIComponent: e => e })
22
28
  }
23
-
24
29
  }
25
30
 
26
31
  const _calculateSkiptoken = (req, result) => {
27
- const limit = Array.isArray(req.query)
28
- ? getPageSize(req.query[0]._target).max
29
- : req.query.SELECT.limit?.rows?.val
32
+ const limit = Array.isArray(req.query) ? getPageSize(req.query[0]._target).max : req.query.SELECT.limit?.rows?.val
30
33
  const top = parseInt(req.http.req.query.$top)
31
34
  if (limit === result.length && limit !== top) {
32
35
  const token = req.http.req.query.$skiptoken
@@ -64,18 +67,34 @@ const _reliablePagingPossible = req => {
64
67
 
65
68
  module.exports = srv =>
66
69
  function read(req, res, next) {
67
- const query = cds.odata.parse(req.url, { service: srv, baseUrl: req.baseUrl })
70
+ if (req._preferReturn) {
71
+ const message = `The 'return' preference is not allowed in ${req.method} requests`
72
+ return res.status(400).json({ error: { code: '400', message } })
73
+ }
74
+
75
+ const { _query: query } = req
76
+
77
+ // mainly for @odata.context
78
+ const info = metaInfo(query, 'READ', srv, {}, req, false)
79
+
80
+ const lastPathElement = req.path.split('/').slice(-1)[0]
81
+
82
+ handleStreamProperties(query.target, query.SELECT.columns, srv.model)
68
83
 
69
84
  // we need the cds request, so we can access the modified query, which is cloned due to lean-draft, so we need to use dispatch here and pass a cds req
70
- const cdsReq = new cds.Request({query})
85
+ const cdsReq = new cds.Request({ query })
86
+ // for read and delete, we provide keys in req.data
87
+ cdsReq.data = getKeysFromPath(query.SELECT.from, srv)
88
+
89
+ // REVISIT: what is this for? some tests fail without it... we should find a better solution!
90
+ Object.defineProperty(query.SELECT, '_4odata', { value: true })
91
+
71
92
  return srv
72
93
  .dispatch(cdsReq)
73
94
  .then(result => {
74
- if (!result.$nextLink) {
75
- _calculateNextLink(cdsReq, result)
76
- }
95
+ if (result == null && query._target._isSingleton && query._target['@odata.singleton.nullable'])
96
+ return res.sendStatus(204)
77
97
 
78
- const lastPathElement = req.path.split('/').slice(-1)[0]
79
98
  if (lastPathElement === '$count') {
80
99
  result = _getCount(result)
81
100
  return res.send(result.toString())
@@ -83,12 +102,15 @@ module.exports = srv =>
83
102
  return res.send(result[query._propertyAccess].toString())
84
103
  }
85
104
 
86
- // mainly for @odata.context
87
- const info = metaInfo(query, 'READ', srv, {}, req, false)
105
+ if (query._propertyAccess && result[query._propertyAccess] === null) return res.sendStatus(204)
106
+
107
+ if (result == null) return res.status(404).json({ error: { code: '404', message: 'Not Found' } })
108
+
109
+ if (info.metadata.isCollection && !result.$nextLink) _calculateNextLink(cdsReq, result)
88
110
  result = toODataResult(result, info)
89
111
 
90
112
  // Express interprets numbers as HTTP status codes
91
- return res.send(typeof result === 'number' ? result.toString() : result)
113
+ res.send(typeof result === 'number' ? result.toString() : result)
92
114
  })
93
115
  .catch(next)
94
116
  }
@@ -1,5 +1,6 @@
1
+ const cds = require('../../../')
2
+
1
3
  const crypto = require('crypto')
2
- const cds = require('../../lib')
3
4
 
4
5
  const normalize_header = value => {
5
6
  return value.split(',').map(str => str.trim())
@@ -0,0 +1,237 @@
1
+ const cds = require('../../../')
2
+ const { Readable } = require('node:stream')
3
+ const getError = require('../../_runtime/common/error')
4
+ const { getTransition } = require('../../_runtime/common/utils/resolveView')
5
+ const LOG = cds.log('odata')
6
+ const { getKeysFromPath } = require('../utils')
7
+
8
+ const _resolveContentProperty = (target, annotName, resolvedProp) => {
9
+ if (target.elements[resolvedProp]) {
10
+ return resolvedProp
11
+ }
12
+ LOG._warn &&
13
+ LOG.warn(
14
+ `"${annotName}" in entity "${target.name}" points to property "${resolvedProp}" which was renamed or is not part of the projection. You must update the annotation value.`
15
+ )
16
+ const mapping = getTransition(target, cds.db).mapping
17
+ const key = [...mapping.entries()].find(({ 1: val }) => val.ref[0] === resolvedProp)
18
+ return key?.length && key[0]
19
+ }
20
+
21
+ const isStream = query => {
22
+ const { _propertyAccess, target } = query
23
+ const element = target.elements[_propertyAccess]
24
+ return Boolean(element?.['@Core.MediaType'])
25
+ }
26
+
27
+ const isStreamByDollarValue = (query, previous, last) => {
28
+ return query.SELECT.one && last === '$value' && !(previous in query.target.elements)
29
+ }
30
+
31
+ const _addMetadataProperty = (query, property, annotName, odataName) => {
32
+ if (typeof property[annotName] === 'object') {
33
+ const contentProperty = _resolveContentProperty(
34
+ query.target,
35
+ annotName,
36
+ property[annotName]['='].replaceAll(/\./g, '_')
37
+ )
38
+ query.target.elements[contentProperty]
39
+ ? query.SELECT.columns.push({ ref: [contentProperty], as: odataName })
40
+ : LOG._warn &&
41
+ LOG.warn(`"${annotName.split('.')[1]}" ${contentProperty} not found in entity "${query.target.name}".`)
42
+ } else {
43
+ query.SELECT.columns.push({ val: property[annotName], as: odataName })
44
+ }
45
+ }
46
+
47
+ const addStreamMetadata = query => {
48
+ // new odata parser sets streaming property in SELECT.from
49
+ const ref = query.SELECT.columns?.[0].ref || query.SELECT.from.ref
50
+ const propertyName = ref[ref.length - 1]
51
+ let mediaTypeProperty
52
+ for (let key in query.target.elements) {
53
+ const val = query.target.elements[key]
54
+ if (val['@Core.MediaType'] && val.name === propertyName) {
55
+ mediaTypeProperty = val
56
+ break
57
+ }
58
+ }
59
+
60
+ _addMetadataProperty(query, mediaTypeProperty, '@Core.MediaType', '$mediaContentType')
61
+
62
+ if (mediaTypeProperty['@Core.ContentDisposition.Filename']) {
63
+ _addMetadataProperty(
64
+ query,
65
+ mediaTypeProperty,
66
+ '@Core.ContentDisposition.Filename',
67
+ '$mediaContentDispositionFilename'
68
+ )
69
+ }
70
+
71
+ if (mediaTypeProperty['@Core.ContentDisposition.Type']) {
72
+ query.SELECT.columns.push({
73
+ val: mediaTypeProperty['@Core.ContentDisposition.Type'],
74
+ as: '$mediaContentDispositionType'
75
+ })
76
+ }
77
+ }
78
+
79
+ const validateStream = (req, res, result) => {
80
+ // REVISIT: compat, should actually be treated as object
81
+ if (!Array.isArray(result)) result = [result]
82
+
83
+ // Reading one entity or a property of it should yield only a result length of one.
84
+ if (result.length === 0 || result[0] === undefined) {
85
+ if (req.headers['if-none-match']) {
86
+ // TODO this should probably end the request?
87
+ res.status(304)
88
+ return
89
+ }
90
+ throw getError(404)
91
+ }
92
+
93
+ if (result.length > 1) throw getError(400)
94
+
95
+ if (result[0] === null) return null
96
+
97
+ result = result[0]
98
+
99
+ const headers = req.headers
100
+ const contentType = result.$mediaContentType
101
+
102
+ if (!headers?.accept || !contentType) return
103
+
104
+ if (
105
+ !headers.accept.includes('*/*') &&
106
+ !headers.accept.includes(contentType) &&
107
+ !headers.accept.includes(contentType.split('/')[0] + '/*')
108
+ ) {
109
+ throw Object.assign(new Error(`Content type "${contentType}" not listed in accept header "${headers.accept}".`), {
110
+ statusCode: 406
111
+ })
112
+ }
113
+ }
114
+
115
+ const _ensureStream = stream => {
116
+ if (stream === null) return null
117
+ // temp workaround for url streaming
118
+ const stream_ = new Readable()
119
+ stream_.push(stream)
120
+ stream_.push(null)
121
+ return stream_
122
+ }
123
+
124
+ const normalizeStream = (result, propertyName, lastPathElement, target) => {
125
+ let readable = result
126
+ if (typeof result === 'object') {
127
+ if (propertyName && result[propertyName] !== undefined) {
128
+ readable = result[propertyName]
129
+ }
130
+ // implicit streaming
131
+ else if (lastPathElement === '$value') {
132
+ const property = Object.values(target.elements).find(
133
+ el => el.type === 'cds.LargeBinary' && result[el.name] !== undefined
134
+ )
135
+ readable = property && result[property.name]
136
+ }
137
+ // result.value can be obtained from custom handlers
138
+ else if (result.value !== undefined) {
139
+ readable = result.value
140
+ }
141
+ }
142
+
143
+ if (!(readable instanceof Readable)) {
144
+ readable = _ensureStream(readable)
145
+ }
146
+
147
+ if (readable) {
148
+ readable.on('error', () => {
149
+ readable.removeAllListeners('error')
150
+ // readable.destroy() does not end stream in node 10 and 12
151
+ readable.push(null)
152
+ })
153
+ }
154
+
155
+ return readable
156
+ }
157
+
158
+ const setStreamingHeaders = (result, res) => {
159
+ // backwards compatibility for content-type in stream
160
+ if (result['$mediaContentType']) res.setHeader('Content-Type', result.$mediaContentType)
161
+ else if (result['*@odata.mediaContentType']) res.setHeader('Content-Type', result['*@odata.mediaContentType'])
162
+ else res.setHeader('Content-Type', 'application/octet-stream')
163
+
164
+ if ('$mediaContentDispositionFilename' in result) {
165
+ const cdt = result.$mediaContentDispositionType || 'attachment'
166
+ res.setHeader(
167
+ 'Content-Disposition',
168
+ `${cdt}; filename="${encodeURIComponent(result.$mediaContentDispositionFilename)}"`
169
+ )
170
+ }
171
+ }
172
+
173
+ const stream = srv =>
174
+ function streamHandler(req, res, next) {
175
+ const { _query: query } = req
176
+
177
+ const [previous, lastPathElement] = req.path.split('/').slice(-2)
178
+ const _isStreamByDollarValue = isStreamByDollarValue(query, previous, lastPathElement)
179
+
180
+ if (_isStreamByDollarValue) {
181
+ for (const k in query.target.elements) {
182
+ if (query.target.elements[k]['@Core.MediaType']) {
183
+ query.SELECT.columns = [{ ref: [k] }]
184
+ query._propertyAccess = k
185
+ break
186
+ }
187
+ }
188
+ }
189
+
190
+ query.SELECT.columns ??= ['*']
191
+
192
+ const _isStream = isStream(query) || _isStreamByDollarValue
193
+
194
+ if (!_isStream) {
195
+ return next(null, req, res)
196
+ }
197
+
198
+ if (!query.target['@cds.persistence.skip']) addStreamMetadata(query)
199
+
200
+ // we need the cds request, so we can access the modified query, which is cloned due to lean-draft, so we need to use dispatch here and pass a cds req
201
+ const cdsReq = new cds.Request({ query })
202
+ // for read and delete, we provide keys in req.data
203
+ cdsReq.data = getKeysFromPath(query.SELECT.from, srv)
204
+
205
+ // REVISIT: what is this for? some tests fail without it... we should find a better solution!
206
+ Object.defineProperty(query.SELECT, '_4odata', { value: true })
207
+
208
+ return srv.tx(() => {
209
+ return srv
210
+ .dispatch(cdsReq)
211
+ .then(async result => {
212
+ validateStream(req, res, result)
213
+
214
+ const stream = normalizeStream(result, query._propertyAccess, lastPathElement, query.target)
215
+ if (stream === null) {
216
+ if (req.headers['if-none-match']) {
217
+ return res.status(304).json({})
218
+ }
219
+ return res.status(204).json({})
220
+ }
221
+
222
+ setStreamingHeaders(result, res)
223
+
224
+ return new Promise((resolve, reject) => {
225
+ stream.pipe(res)
226
+ stream.on('end', () => resolve(result))
227
+ stream.once('error', reject)
228
+ })
229
+ })
230
+ .catch(next)
231
+ })
232
+ }
233
+
234
+ module.exports = {
235
+ stream,
236
+ isStream
237
+ }
@@ -0,0 +1,165 @@
1
+ const cds = require('../../../')
2
+ const { INSERT, UPDATE } = cds.ql
3
+
4
+ const { toODataResult } = require('../utils/result')
5
+ const { odataError, getKeysFromPath } = require('../utils')
6
+
7
+ const { deepCopy } = require('../../_runtime/common/utils/copy')
8
+
9
+ // REVISIT: move to or rewrite in libx/odata
10
+ const { readAfterWrite } = require('../../_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite')
11
+ const metaInfo = require('../../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
12
+
13
+ const _isUpsertAllowed = ({ target, data, event }) => {
14
+ return (
15
+ !(cds.env.runtime && cds.env.runtime.allow_upsert === false) &&
16
+ !(target && target._isDraftEnabled && (!cds.env.fiori.lean_draft || (!data.IsActiveEntity && event === 'PATCH')))
17
+ )
18
+ }
19
+
20
+ const _isNavigationWithKeyInParent = (keys, data, pathExpression, model) => {
21
+ // keys not in data
22
+ if (keys && Object.keys(keys).some(key => key in data)) {
23
+ return false
24
+ }
25
+
26
+ const nav = pathExpression.ref && pathExpression.ref.length !== 0 && pathExpression.ref[1]
27
+ const parent = pathExpression.ref && pathExpression.ref[0].id
28
+
29
+ // not a navigation
30
+ if (!parent || !nav) {
31
+ return false
32
+ }
33
+
34
+ const navID = typeof nav === 'string' ? nav : nav.id
35
+ const navElement = model.definitions[parent].elements[navID]
36
+
37
+ // not a containment
38
+ if (!navElement._isContained) {
39
+ return false
40
+ }
41
+
42
+ const where = pathExpression.ref[0].where
43
+ return parent && navElement && where
44
+ }
45
+
46
+ const _hasEtag = target => target._etag
47
+
48
+ module.exports = srv =>
49
+ function update(req, res, next) {
50
+ const { _query: query } = req
51
+
52
+ const {
53
+ SELECT: { one, from }
54
+ } = query
55
+
56
+ // REVISIT: patch on collection is allowed in odata 4.01
57
+ if (!one) {
58
+ return res.status(405).json(odataError('405', `Method ${req.method} not allowed for ENTITY.COLLECTION`))
59
+ }
60
+
61
+ // REVISIT: better
62
+ const isPropertyAccess = !!query._propertyAccess
63
+
64
+ const updateData = isPropertyAccess ? { [query._propertyAccess]: req.body.value } : deepCopy(req.body)
65
+
66
+ if (!isPropertyAccess) {
67
+ // add keys from url into payload (overwriting if already present)
68
+ Object.assign(updateData, getKeysFromPath(from, srv))
69
+
70
+ // assert complex
71
+ const assertOptions = { filter: true, http: { req }, mandatories: req.method === 'PUT' || undefined }
72
+ const errs = cds.assert(updateData, query.target, assertOptions)
73
+ if (errs) {
74
+ if (errs.length === 1) throw errs[0]
75
+ throw Object.assign(new Error('MULTIPLE_ERRORS'), { statusCode: 400, details: errs })
76
+ }
77
+ } else {
78
+ // TODO: assert primitive
79
+ }
80
+
81
+ const updateQuery = UPDATE.entity(from).with(updateData)
82
+
83
+ // we need the cds request, so we can access req._.readAfterWrite
84
+ const cdsReq = new cds.Request({ query: updateQuery })
85
+
86
+ // REVISIT: adjust in getter?
87
+ if (req.method === 'PUT') cdsReq.method = 'PUT'
88
+
89
+ // rewrite event for draft-enabled entities
90
+ if (query.target._isDraftEnabled) cdsReq.event = 'PATCH'
91
+
92
+ return srv
93
+ .dispatch(cdsReq)
94
+ .then(async result => {
95
+ if (!(isPropertyAccess && !_hasEtag(query.target)) && cdsReq._.readAfterWrite) {
96
+ // TODO see if in old odata impl for other checks that should happen
97
+ result = await readAfterWrite(cdsReq, srv, { operation: { result } })
98
+ }
99
+
100
+ // REVISIT: metaInfo needs original query in case of property access, but why?
101
+ const info = metaInfo(isPropertyAccess ? query : updateQuery, 'UPDATE', srv, result, req)
102
+
103
+ if (
104
+ result == null ||
105
+ req._preferReturn === 'minimal' ||
106
+ (isPropertyAccess && result[query._propertyAccess] == null) ||
107
+ info.metadata.isStream
108
+ )
109
+ return res.sendStatus(204)
110
+
111
+ result = toODataResult(result, info)
112
+ return res.send(result)
113
+ })
114
+ .catch(async e => {
115
+ // UPSERT
116
+ const is404 = e.code === 404 || e.status === 404 || e.statusCode === 404
117
+ if (
118
+ is404 &&
119
+ !isPropertyAccess &&
120
+ _isUpsertAllowed({ target: query.target, data: updateData, event: req.method })
121
+ ) {
122
+ // PUT / PATCH with if-match header means "only if already exists" -> no insert if it does not
123
+ if (req.headers['if-match']) throw Object.assign(new Error('412'), { statusCode: 412 })
124
+
125
+ // check only works with req.body and not with updateDate
126
+ if (_isNavigationWithKeyInParent(query.target.keys, req.body, from, srv.model)) {
127
+ // REVISIT: better error message
128
+ return res.status(422).json(odataError('422', `Unprocessable Entity`))
129
+ }
130
+
131
+ // REVISIT:
132
+ // can we somehow "replay" the request with POST?
133
+ // or should we call the create handler directly?
134
+
135
+ const insertData = deepCopy(req.body)
136
+
137
+ // add keys from url into payload (overwriting if already present)
138
+ Object.assign(insertData, getKeysFromPath(from, srv))
139
+
140
+ // assert payload
141
+ const assertOptions = { filter: true, http: { req }, mandatories: true }
142
+ const errs = cds.assert(insertData, query.target, assertOptions)
143
+ if (errs) {
144
+ if (errs.length === 1) throw errs[0]
145
+ throw Object.assign(new Error('MULTIPLE_ERRORS'), { statusCode: 400, details: errs })
146
+ }
147
+
148
+ // REVISIT: up_XX needs to be looked up -> composition of aspect
149
+ const insertQuery = INSERT.into(from).entries(insertData)
150
+ const cdsReq = new cds.Request({ query: insertQuery })
151
+ let result = await srv.dispatch(cdsReq)
152
+
153
+ if (cdsReq._.readAfterWrite) {
154
+ // TODO see if in old odata impl for other checks that should happen
155
+ result = await readAfterWrite(cdsReq, srv, { operation: { result } })
156
+ }
157
+
158
+ const info = metaInfo(insertQuery, 'CREATE', srv, result, req)
159
+ result = toODataResult(result, info)
160
+ return res.status(201).send(result)
161
+ }
162
+ throw e
163
+ })
164
+ .catch(next)
165
+ }