@sap/cds 8.7.2 → 8.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 (51) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/_i18n/i18n.properties +3 -0
  3. package/_i18n/i18n_cs.properties +6 -6
  4. package/_i18n/i18n_de.properties +3 -0
  5. package/_i18n/i18n_en.properties +3 -0
  6. package/_i18n/i18n_es.properties +3 -0
  7. package/_i18n/i18n_fr.properties +3 -0
  8. package/_i18n/i18n_it.properties +3 -0
  9. package/_i18n/i18n_ja.properties +3 -0
  10. package/_i18n/i18n_pl.properties +7 -4
  11. package/_i18n/i18n_pt.properties +3 -0
  12. package/_i18n/i18n_ru.properties +3 -0
  13. package/app/index.js +2 -30
  14. package/lib/env/cds-env.js +1 -1
  15. package/lib/env/cds-requires.js +16 -9
  16. package/lib/env/schemas/cds-package.js +1 -1
  17. package/lib/env/schemas/cds-rc.js +17 -4
  18. package/lib/index.js +1 -1
  19. package/lib/ql/SELECT.js +6 -1
  20. package/lib/req/request.js +5 -2
  21. package/lib/req/validate.js +3 -1
  22. package/lib/srv/bindings.js +31 -20
  23. package/lib/srv/cds-connect.js +1 -1
  24. package/lib/srv/middlewares/auth/mocked-users.js +1 -0
  25. package/lib/srv/protocols/okra.js +5 -7
  26. package/lib/srv/srv-dispatch.js +0 -5
  27. package/lib/test/cds-test.js +34 -6
  28. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -0
  29. package/libx/_runtime/common/generic/auth/service.js +2 -2
  30. package/libx/_runtime/common/generic/input.js +1 -1
  31. package/libx/_runtime/common/utils/binary.js +1 -35
  32. package/libx/_runtime/common/utils/rewriteAsterisks.js +5 -8
  33. package/libx/_runtime/fiori/lean-draft.js +2 -4
  34. package/libx/common/utils/path.js +1 -5
  35. package/libx/common/utils/streaming.js +76 -0
  36. package/libx/odata/middleware/create.js +1 -1
  37. package/libx/odata/middleware/delete.js +1 -1
  38. package/libx/odata/middleware/operation.js +48 -4
  39. package/libx/odata/middleware/read.js +1 -1
  40. package/libx/odata/middleware/stream.js +29 -101
  41. package/libx/odata/middleware/update.js +1 -1
  42. package/libx/odata/parse/afterburner.js +20 -1
  43. package/libx/odata/parse/grammar.peggy +108 -26
  44. package/libx/odata/parse/parser.js +1 -1
  45. package/libx/rest/RestAdapter.js +2 -16
  46. package/libx/rest/middleware/operation.js +38 -18
  47. package/libx/rest/middleware/parse.js +5 -25
  48. package/libx/rest/post-processing.js +33 -0
  49. package/libx/rest/pre-processing.js +38 -0
  50. package/package.json +1 -1
  51. package/libx/common/utils/index.js +0 -5
@@ -1,3 +1,12 @@
1
+ module.exports = require_local('@cap-js/cds-test')
2
+
3
+ // TODO remove the code below with cds 9
4
+ if (!module.exports) {
5
+
6
+ const { RED, RESET } = require('../utils/colors')
7
+ // eslint-disable-next-line no-console
8
+ console.error(RED+'Test support will move to package @cap-js/cds-test in @sap/cds v9.\nGet ready and add a dependency to it:\n npm add -D @cap-js/cds-test\n'+RESET)
9
+
1
10
  /**
2
11
  * Instances of this class are constructed and returned by cds.test().
3
12
  */
@@ -149,15 +158,23 @@ class Test extends require('./axios') {
149
158
  * Lazily loads and returns an instance of chai
150
159
  */
151
160
  get chai() {
152
- let chai = require('chai')
161
+ const chai = require('chai')
153
162
  chai.use (require('chai-subset'))
154
- chai.use (require('chai-as-promised'))
163
+ const chaip = require('chai-as-promised')
164
+ chai.use (chaip.default/*v8 on ESM*/ ?? chaip/*v7*/)
155
165
  return chai
156
166
  function require (mod) { try { return module.require(mod) } catch(e) {
157
- if (e.code === 'MODULE_NOT_FOUND') throw new Error (`
158
- Failed to load required package '${mod}'. Please add it thru:
159
- npm add -D chai@4 chai-as-promised@7 chai-subset
160
- `)}}
167
+ if (e.code === 'MODULE_NOT_FOUND')
168
+ throw new Error (`Failed to load required package '${mod}'. Please add it thru:`
169
+ + `\n npm add -D chai chai-as-promised chai-subset`, {cause: e})
170
+ else if (e.name === 'SyntaxError') // Jest stumbling over ESM
171
+ throw new Error (`Jest failed to load ESM package '${mod}'.`
172
+ + `\nDowngrade '${mod}' to the major version before, or use a different test runner like 'node --test'.\n`, {cause: e})
173
+ else if (e.code === 'ERR_REQUIRE_ESM') // node --test on older Node versions
174
+ throw new Error (`Failed to load ESM package '${mod}'. This only is supported on Node.js >= 23.`
175
+ + `\nUpgrade your Node.js installation or downgrade '${mod}' to the major version before.\n`, {cause: e})
176
+ else throw e
177
+ }}
161
178
  }
162
179
  set expect(x) { super.expect = x }
163
180
  get expect() { return _expect || this.chai.expect }
@@ -235,3 +252,14 @@ let _expect = undefined
235
252
  }))
236
253
  }
237
254
  })()
255
+
256
+ }
257
+
258
+ function require_local(id) {
259
+ const cds = require('..')
260
+ try {
261
+ return require(require.resolve(id, { paths: [cds.root, __dirname] }))
262
+ } catch (e) {
263
+ if (e.code !== 'MODULE_NOT_FOUND') throw e
264
+ }
265
+ }
@@ -210,6 +210,7 @@ class ODataRequest extends cds.Request {
210
210
  this._.odataRes = odataRes
211
211
 
212
212
  Object.defineProperty(this, 'protocol', { value: 'odata' })
213
+ req._okra_logger?.(this)
213
214
  }
214
215
  }
215
216
 
@@ -14,8 +14,8 @@ function handler(req) {
14
14
  }
15
15
 
16
16
  const requires = _getRequiresAsArray(this.definition)
17
-
18
- if (!requires || requires.some(role => req.user.is(role))) return
17
+ // internal-user is considered as a concept to protect the endpoints, local app service calls are always allowed
18
+ if (!requires || requires.some(role => req.user.is(role)) || requires.includes('internal-user')) return
19
19
  reject(req, getRejectReason(req, '@requires', this.definition))
20
20
  }
21
21
 
@@ -260,7 +260,7 @@ async function commonGenericInput(req) {
260
260
  const bound = req.target.actions?.[req.event] || req.target.actions?.[req._.event]
261
261
  if (bound) assertOptions.path = [bound['@cds.odata.bindingparameter.name'] || 'in']
262
262
 
263
- if (req.protocol) assertOptions.rejectIgnore = true
263
+ if (req.protocol && !_is_activate) assertOptions.rejectIgnore = true
264
264
 
265
265
  const errs = cds.validate(req.data, req.target, assertOptions)
266
266
  if (errs) {
@@ -1,5 +1,3 @@
1
- const getTemplate = require('./template')
2
-
3
1
  // convert the standard base64 encoding to the URL-safe variant
4
2
  const toBase64url = value => {
5
3
  const buffer = Buffer.isBuffer(value) ? value : Buffer.from(value, 'base64')
@@ -29,40 +27,8 @@ const isInvalidBase64string = value => {
29
27
  return base64value.length > normalized.length
30
28
  }
31
29
 
32
- const _picker = element => {
33
- const categories = {}
34
- if (Array.isArray(element)) return
35
- if (element.type !== 'cds.Binary' && element.type !== 'cds.LargeBinary') return
36
- categories['convert_binary'] = true
37
- return categories
38
- }
39
-
40
- const _processorFn =
41
- toBuffer =>
42
- ({ row, key, plain: categories }) => {
43
- if (categories['convert_binary'] && row[key] != null) {
44
- if (toBuffer && typeof row[key] === 'string') row[key] = Buffer.from(row[key], 'base64')
45
- if (!toBuffer && Buffer.isBuffer(row[key])) row[key] = row[key].toString('base64')
46
- }
47
- }
48
-
49
- const _processBinaryData = (data, srv, definition, toBuffer) => {
50
- const template = getTemplate('rest-payload', srv, definition, { pick: _picker })
51
- template.process(data, _processorFn(toBuffer))
52
- }
53
-
54
- const base64ToBuffer = (data, srv, definition) => {
55
- _processBinaryData(data, srv, definition, true)
56
- }
57
-
58
- const bufferToBase64 = (data, srv, definition) => {
59
- _processBinaryData(data, srv, definition, false)
60
- }
61
-
62
30
  module.exports = {
63
31
  normalizeBase64string,
64
32
  isInvalidBase64string,
65
- toBase64url,
66
- base64ToBuffer,
67
- bufferToBase64
33
+ toBase64url
68
34
  }
@@ -24,9 +24,11 @@ const _expandColumn = (column, target, _4db) => {
24
24
 
25
25
  const _resolveTarget = (ref, target) => {
26
26
  if (ref.length > 1) {
27
- if (target.elements[ref[0]]) {
27
+ const element = target.elements[ref[0]]
28
+ if (element) {
29
+ if (element.isAssociation) throw cds.error(`Navigation "${ref.join('/')}" in expand is not supported`)
28
30
  // structured
29
- return _resolveTarget(ref.slice(1), target.elements[ref[0]])
31
+ return _resolveTarget(ref.slice(1), element)
30
32
  } else {
31
33
  // in case there is an alias, try with the next entry
32
34
  return _resolveTarget(ref.slice(1), target)
@@ -107,12 +109,7 @@ const rewriteAsterisks = (query, model, options) => {
107
109
  // REVISIT these are two nasty hacks for UNION and JOIN,
108
110
  // which should be implemented generically.
109
111
  // Please, do not continue to develop here if possible.
110
- if (
111
- query.SELECT.from.SET &&
112
- query.SELECT.from.SET.args[0] &&
113
- query.SELECT.from.SET.args[0].SELECT &&
114
- query.SELECT.from.SET.args[0].SELECT.columns
115
- ) {
112
+ if (query.SELECT.from.SET?.args[0]?.SELECT?.columns) {
116
113
  // > best-effort derive column list from first join element if given
117
114
  query.SELECT.columns = query.SELECT.from.SET.args[0].SELECT.columns.map(c => ({
118
115
  ref: [c.as || c.ref[c.ref.length - 1]]
@@ -384,11 +384,9 @@ cds.ApplicationService.prototype.handle = async function (req) {
384
384
  (query.DELETE && 'DELETE') ||
385
385
  req.event
386
386
  _req.params = req.params
387
- _req._ = Object.assign({}, req._ || {})
388
- _req._.params = req.params
389
- _req._.query = query
390
387
  if (req.protocol) _req.protocol = req.protocol
391
388
  _req._ = req._
389
+ if (!_req._.event) _req._.event = req.event
392
390
  const cqnData = _req.query.UPDATE?.data || _req.query.INSERT?.entries?.[0]
393
391
  if (cqnData) _req.data = cqnData // must point to the same object
394
392
  Object.defineProperty(_req, '_messages', {
@@ -922,7 +920,7 @@ const Read = {
922
920
  const orderByExpr = query.SELECT.orderBy
923
921
  const getOrderByColumns = columns => {
924
922
  const selectAll = columns === undefined || columns.includes('*')
925
- const queryColumns = !selectAll && columns && columns.map(column => column?.ref?.[0]).filter(c => c)
923
+ const queryColumns = !selectAll && columns && columns.map(column => column.as || column?.ref?.[0]).filter(c => c)
926
924
  const newColumns = []
927
925
 
928
926
  for (const column of orderByExpr) {
@@ -3,7 +3,7 @@ const { getKeysForNavigationFromRefPath } = require('../../_runtime/common/utils
3
3
 
4
4
  // REVISIT: do we already have something like this _without using okra api_?
5
5
  // REVISIT: should we still support process.env.CDS_FEATURES_PARAMS? probably nobody uses it...
6
- const getKeysAndParamsFromPath = (from, { model }) => {
6
+ exports.getKeysAndParamsFromPath = (from, { model }) => {
7
7
  if (!from.ref || !from.ref.length) return {}
8
8
 
9
9
  const keys = {}
@@ -44,7 +44,3 @@ const getKeysAndParamsFromPath = (from, { model }) => {
44
44
 
45
45
  return { keys, params }
46
46
  }
47
-
48
- module.exports = {
49
- getKeysAndParamsFromPath
50
- }
@@ -0,0 +1,76 @@
1
+ const { Readable } = require('node:stream')
2
+
3
+ exports.validateMimetypeIsAcceptedOrThrow = (headers, contentType) => {
4
+ if (!contentType || !headers?.accept) return
5
+ if (headers.accept.includes('*/*')) return
6
+ if (headers.accept.includes(contentType)) return
7
+ if (headers.accept.includes(contentType.slice(0,contentType.indexOf('/')) + '/*')) return
8
+ const msg = `Content type "${contentType}" is not listed in accept header "${headers.accept}"`
9
+ throw Object.assign(new Error(msg), { statusCode: 406 })
10
+ }
11
+
12
+ // REVISIT: We should use express' res.type(...) instead of res.set('Content-Type', ...)
13
+ const _mimetypes = {
14
+ '.pdf': 'application/pdf',
15
+ '.csv': 'text/csv',
16
+ '.html': 'text/html',
17
+ '.css': 'text/css',
18
+ '.png': 'image/png',
19
+ '.jpeg': 'image/jpeg',
20
+ '.jpg': 'image/jpeg',
21
+ }
22
+
23
+ const { extname } = require('path')
24
+ const _mimetype4 = filename => {
25
+ if (!filename) return
26
+ const filetype = extname(filename).toLowerCase()
27
+ return _mimetypes[filetype]
28
+ }
29
+
30
+ const _annotation = (def,a) => {
31
+ if (!def) return
32
+ if (typeof def[a] === 'string') return def[a]
33
+ }
34
+
35
+ // REVISIT: Such helpers are a pain -> use classes with methods instead, e.g. RestAdapter extends HttpAdapter, ODataAdapter extends RestAdapter, etc.
36
+ exports.collectStreamMetadata = (result, operation, query) => {
37
+ const element = query?._propertyAccess ? query.target?.elements?.[query._propertyAccess] : undefined
38
+ const returns = operation?.returns
39
+
40
+ const filename =
41
+ result.$mediaContentDispositionFilename ?? // legacy -> support for odata only?
42
+ result.filename ??
43
+ _annotation (returns, '@Core.ContentDisposition.Filename')
44
+ _annotation (element, '@Core.ContentDisposition.Filename')
45
+
46
+ const disposition =
47
+ result.$mediaContentDispositionType ?? // legacy -> support for odata only?
48
+ _annotation (returns, '@Core.ContentDisposition.Type') ??
49
+ _annotation (element, '@Core.ContentDisposition.Type') ??
50
+ (filename ? 'attachment' : 'inline')
51
+
52
+ const mimetype =
53
+ result['*@odata.mediaContentType'] ?? // compat -> support for odata only?
54
+ result.$mediaContentType ?? // legacy -> support for odata only?
55
+ result.mimetype ??
56
+ _mimetype4 (filename) ??
57
+ _mimetype4 (result.path) ?? // e.g. for file downloads
58
+ _annotation (returns, '@Core.MediaType') ??
59
+ _annotation (element, '@Core.MediaType') ??
60
+ 'application/octet-stream' // REVISIT: or rather default to undefined?
61
+
62
+ return { mimetype, filename, disposition }
63
+ }
64
+
65
+ exports.getReadable = function readable4 (result) {
66
+ if (result == null) return
67
+ if (typeof result !== 'object') {
68
+ const stream = new Readable()
69
+ stream.push(result)
70
+ stream.push(null)
71
+ return stream
72
+ }
73
+ if (result instanceof Readable) return result
74
+ if (result.value) return readable4 (result.value) // REVISIT: for OData legacy only?
75
+ if (Array.isArray(result)) return readable4 (result[0]) // compat // REVISIT: remove ?
76
+ }
@@ -8,7 +8,7 @@ const readAfterWrite4 = require('../utils/readAfterWrite')
8
8
  const getODataResult = require('../utils/result')
9
9
  const normalizeTimeData = require('../utils/normalizeTimeData')
10
10
 
11
- const { getKeysAndParamsFromPath } = require('../../common/utils')
11
+ const { getKeysAndParamsFromPath } = require('../../common/utils/path')
12
12
 
13
13
  module.exports = (adapter, isUpsert) => {
14
14
  // REVISIT: adapter should be this
@@ -3,7 +3,7 @@ const { UPDATE, DELETE } = cds.ql
3
3
 
4
4
  const { handleSapMessages, getPreferReturnHeader } = require('../utils')
5
5
 
6
- const { getKeysAndParamsFromPath } = require('../../common/utils')
6
+ const { getKeysAndParamsFromPath } = require('../../common/utils/path')
7
7
 
8
8
  module.exports = adapter => {
9
9
  const { service } = adapter
@@ -1,11 +1,16 @@
1
1
  const cds = require('../../../')
2
2
 
3
- const { cds2edm, handleSapMessages, calculateLocationHeader } = require('../utils')
4
3
  const getODataMetadata = require('../utils/metadata')
5
4
  const postProcess = require('../utils/postProcess')
6
5
  const getODataResult = require('../utils/result')
7
6
 
8
- const { getKeysAndParamsFromPath } = require('../../common/utils')
7
+ const { cds2edm, handleSapMessages, calculateLocationHeader } = require('../utils')
8
+ const { getKeysAndParamsFromPath } = require('../../common/utils/path')
9
+ const {
10
+ collectStreamMetadata,
11
+ getReadable,
12
+ validateMimetypeIsAcceptedOrThrow
13
+ } = require('../../common/utils/streaming')
9
14
 
10
15
  const _findEdmNameFor = (definition, namespace, fullyQualified = false) => {
11
16
  let name
@@ -93,20 +98,59 @@ module.exports = adapter => {
93
98
  // we need a cds.Request for multiple reasons, incl. params, headers, sap-messages, read after write, ...
94
99
  const cdsReq = adapter.request4({ query, event, data, params, headers, target: query?.target, req, res })
95
100
 
101
+ const _isStream = operation.returns?._type === 'cds.LargeBinary' && !!operation.returns['@Core.MediaType']
102
+
96
103
  // NOTES:
97
104
  // - only via srv.run in combination with srv.dispatch inside,
98
105
  // we automatically either use a single auto-managed tx for the req (i.e., insert and read after write in same tx)
99
106
  // or the auto-managed tx opened for the respective atomicity group, if exists
100
107
  // - in the then block of .run(), the transaction is committed (i.e., before sending the response) if a single auto-managed tx is used
101
108
  return service
102
- .run(() => service.dispatch(cdsReq))
109
+ .run(() =>
110
+ service.dispatch(cdsReq).then(result => {
111
+ if (res.headersSent) return result
112
+ if (!_isStream) return result
113
+ handleSapMessages(cdsReq, req, res)
114
+
115
+ const stream = getReadable(result)
116
+ if (!stream) return res.sendStatus(204)
117
+
118
+ const { mimetype, filename, disposition } = collectStreamMetadata(result, operation, query)
119
+ validateMimetypeIsAcceptedOrThrow(req.headers, mimetype)
120
+ if (!res.get('content-type')) res.set('content-type', mimetype)
121
+ if (filename && !res.get('content-disposition'))
122
+ res.set('content-disposition', `${disposition}; filename="${encodeURIComponent(filename)}"`)
123
+
124
+ return new Promise((resolve, reject) => {
125
+ if (res.destroyed)
126
+ return reject(
127
+ new cds.error({ code: 'ERR_STREAM_PREMATURE_CLOSE', message: 'Response was closed while streaming' })
128
+ )
129
+ stream.pipe(res)
130
+ stream.on('end', () => resolve(result))
131
+ stream.once('error', reject)
132
+ let finished = false
133
+ res.on('finish', () => (finished = true))
134
+ res.on(
135
+ 'close',
136
+ () =>
137
+ !finished &&
138
+ reject(
139
+ new cds.error({
140
+ code: 'ERR_STREAM_PREMATURE_CLOSE',
141
+ message: 'Response was closed while streaming'
142
+ })
143
+ )
144
+ )
145
+ }).then(() => res.end())
146
+ })
147
+ )
103
148
  .then(result => {
104
149
  if (res.headersSent) return
105
150
 
106
151
  handleSapMessages(cdsReq, req, res)
107
152
 
108
153
  if (operation.returns?.items && result == null) result = []
109
-
110
154
  if (!operation.returns || result == null) return res.sendStatus(204)
111
155
 
112
156
  if (operation.returns._type?.match?.(/^cds\./)) {
@@ -7,7 +7,7 @@ const getODataMetadata = require('../utils/metadata')
7
7
  const postProcess = require('../utils/postProcess')
8
8
  const getODataResult = require('../utils/result')
9
9
 
10
- const { getKeysAndParamsFromPath } = require('../../common/utils')
10
+ const { getKeysAndParamsFromPath } = require('../../common/utils/path')
11
11
 
12
12
  const { getPageSize } = require('../../_runtime/common/generic/paging')
13
13
  const { handleStreamProperties } = require('../../_runtime/common/utils/streamProp')
@@ -1,13 +1,14 @@
1
1
  const cds = require('../../../')
2
2
  const LOG = cds.log('odata')
3
-
4
- const { Readable } = require('node:stream')
3
+ const getError = require('../../_runtime/common/error')
5
4
 
6
5
  const { handleSapMessages, validateIfNoneMatch, isStream, isRedirect } = require('../utils')
7
-
8
- const { getKeysAndParamsFromPath } = require('../../common/utils')
9
-
10
- const getError = require('../../_runtime/common/error')
6
+ const { getKeysAndParamsFromPath } = require('../../common/utils/path')
7
+ const {
8
+ collectStreamMetadata,
9
+ validateMimetypeIsAcceptedOrThrow,
10
+ getReadable
11
+ } = require('../../common/utils/streaming')
11
12
  const { getTransition } = require('../../_runtime/common/utils/resolveView')
12
13
 
13
14
  const _resolveContentProperty = (target, annotName, resolvedProp) => {
@@ -71,94 +72,6 @@ const _addStreamMetadata = query => {
71
72
  }
72
73
  }
73
74
 
74
- const _validateStream = (req, result) => {
75
- // REVISIT: compat, should actually be treated as object
76
- if (!Array.isArray(result)) result = [result]
77
-
78
- // Reading one entity or a property of it should yield only a result length of one.
79
- if (result.length === 0 || result[0] === undefined) throw getError(404)
80
-
81
- if (result.length > 1) throw getError(400)
82
-
83
- if (result[0] === null) return
84
-
85
- result = result[0]
86
-
87
- const headers = req.headers
88
- const contentType = result.$mediaContentType
89
-
90
- if (!headers?.accept || !contentType) return
91
-
92
- if (
93
- !headers.accept.includes('*/*') &&
94
- !headers.accept.includes(contentType) &&
95
- !headers.accept.includes(contentType.split('/')[0] + '/*')
96
- ) {
97
- const msg = `Content type "${contentType}" is not listed in accept header "${headers.accept}"`
98
- throw Object.assign(new Error(msg), { statusCode: 406 })
99
- }
100
- }
101
-
102
- const _ensureStream = stream => {
103
- if (stream === null) return null
104
- // temp workaround for url streaming
105
- const stream_ = new Readable()
106
- stream_.push(stream)
107
- stream_.push(null)
108
- return stream_
109
- }
110
-
111
- const _normalizeStream = (result, propertyName, lastPathElement, target) => {
112
- if (!result) return null
113
-
114
- let readable = result
115
- if (typeof result === 'object') {
116
- if (propertyName && result[propertyName] !== undefined) {
117
- readable = result[propertyName]
118
- }
119
- // implicit streaming
120
- else if (lastPathElement === '$value') {
121
- const property = Object.values(target.elements).find(
122
- el => el.type === 'cds.LargeBinary' && result[el.name] !== undefined
123
- )
124
- readable = property && result[property.name]
125
- }
126
- // result.value can be obtained from custom handlers
127
- else if (result.value !== undefined) {
128
- readable = result.value
129
- }
130
- }
131
-
132
- if (!(readable instanceof Readable)) {
133
- readable = _ensureStream(readable)
134
- }
135
-
136
- if (readable) {
137
- readable.on('error', () => {
138
- readable.removeAllListeners('error')
139
- // readable.destroy() does not end stream in node 10 and 12
140
- readable.push(null)
141
- })
142
- }
143
-
144
- return readable
145
- }
146
-
147
- const _setStreamingHeaders = (result, res) => {
148
- // backwards compatibility for Content-Type in stream
149
- if (result['$mediaContentType']) res.setHeader('Content-Type', result.$mediaContentType)
150
- else if (result['*@odata.mediaContentType']) res.setHeader('Content-Type', result['*@odata.mediaContentType'])
151
- else res.setHeader('Content-Type', 'application/octet-stream')
152
-
153
- if ('$mediaContentDispositionFilename' in result) {
154
- const cdt = result.$mediaContentDispositionType || 'attachment'
155
- res.setHeader(
156
- 'content-disposition',
157
- `${cdt}; filename="${encodeURIComponent(result.$mediaContentDispositionFilename)}"`
158
- )
159
- }
160
- }
161
-
162
75
  module.exports = adapter => {
163
76
  const { service } = adapter
164
77
 
@@ -171,7 +84,7 @@ module.exports = adapter => {
171
84
  if (isRedirect(query)) {
172
85
  const cdsReq = adapter.request4({ query, req, res })
173
86
 
174
- service.dispatch(cdsReq).then(result => {
87
+ return service.dispatch(cdsReq).then(result => {
175
88
  if (result[query._propertyAccess]) res.set('Location', result[query._propertyAccess])
176
89
  return res.sendStatus(307)
177
90
  })
@@ -219,17 +132,32 @@ module.exports = adapter => {
219
132
  .run(() => {
220
133
  return service.dispatch(cdsReq).then(async result => {
221
134
  if (res.headersSent) return
222
-
223
- _validateStream(req, result)
135
+ if (result === undefined) throw getError(404) // entity is not existing
136
+ if (result === null) return res.sendStatus(204) // custom handler returns null
224
137
 
225
138
  if (validateIfNoneMatch(cdsReq.target, req.headers?.['if-none-match'], result)) return res.sendStatus(304)
226
139
 
227
- const stream = _normalizeStream(result, query._propertyAccess, lastPathElement, query.target)
228
- if (stream === null) return res.sendStatus(204)
140
+ const { mimetype, filename, disposition } = collectStreamMetadata(result, undefined, query)
141
+ if (pdfMimeType && !mimetype) result.mimetype = 'application/pdf' // REVISIT: Is compat still needed?
142
+
143
+ // REVISIT: If accessed property is undefined - why prevent 404?
144
+ if (query._propertyAccess && result[query._propertyAccess] !== undefined) {
145
+ result = result[query._propertyAccess]
146
+ } else if (lastPathElement === '$value') {
147
+ // Implicit streaming
148
+ const property = Object.values(query.target.elements).find(
149
+ el => el.type === 'cds.LargeBinary' && result[el.name] !== undefined
150
+ )
151
+ result = property && result[property.name]
152
+ }
229
153
 
230
- if (pdfMimeType && !result.$mediaContentType) result.$mediaContentType = 'application/pdf'
154
+ const stream = getReadable(result)
155
+ if (!stream) return res.sendStatus(204)
231
156
 
232
- _setStreamingHeaders(result, res)
157
+ validateMimetypeIsAcceptedOrThrow(req.headers, mimetype)
158
+ if (!res.get('content-type')) res.set('content-type', mimetype)
159
+ if (filename && !res.get('content-disposition'))
160
+ res.set('content-disposition', `${disposition}; filename="${encodeURIComponent(filename)}"`)
233
161
 
234
162
  return new Promise((resolve, reject) => {
235
163
  if (res.destroyed)
@@ -8,7 +8,7 @@ const readAfterWrite4 = require('../utils/readAfterWrite')
8
8
  const getODataResult = require('../utils/result')
9
9
  const normalizeTimeData = require('../utils/normalizeTimeData')
10
10
 
11
- const { getKeysAndParamsFromPath } = require('../../common/utils')
11
+ const { getKeysAndParamsFromPath } = require('../../common/utils/path')
12
12
 
13
13
  const _isUpsertAllowed = ({ target, data, event }) => {
14
14
  return (
@@ -411,7 +411,8 @@ function _processSegments(from, model, namespace, cqn, protocol) {
411
411
  ref[i].args = current['@open']
412
412
  ? Object.assign({}, from._params)
413
413
  : Object.keys(from._params).reduce((acc, cur) => {
414
- if (current.params && cur in current.params) acc[cur] = from._params[cur]
414
+ const param = cur.startsWith('@') ? cur.slice(1) : cur
415
+ if (current.params && param in current.params) acc[param] = from._params[cur]
415
416
  return acc
416
417
  }, {})
417
418
  _resolveImplicitFunctionParameters(ref[i].args)
@@ -803,6 +804,24 @@ module.exports = (cqn, model, namespace, protocol) => {
803
804
  // one?
804
805
  if (one) cqn.SELECT.one = true
805
806
 
807
+ // hierarchy requests, quick check to avoid unnecessary traversing
808
+ // REVISIT: Should be done via annotation on backlink, would make lookup easier
809
+ if (target?.elements?.LimitedDescendantCount) {
810
+ let uplinkName
811
+ for (const key in target) {
812
+ if (key.match(/@Aggregation\.RecursiveHierarchy#.*\.ParentNavigationProperty/)) {
813
+ // Qualifiers are bad for lookups
814
+ uplinkName = target[key]['=']
815
+ break
816
+ }
817
+ }
818
+ const setRecurseRef = SELECT => {
819
+ if (SELECT.from.SELECT) setRecurseRef(SELECT.from.SELECT)
820
+ if (SELECT.recurse) SELECT.recurse.ref[0] = uplinkName
821
+ }
822
+ if (uplinkName) setRecurseRef(cqn.SELECT)
823
+ }
824
+
806
825
  // REVISIT: better
807
826
  // set target (csn definition) for later retrieval
808
827
  cqn.__target = current.parent?.kind === 'entity' ? `${current.parent.name}:$:${current.name}` : current.name