@sap/cds 7.0.3 → 7.1.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 (70) hide show
  1. package/CHANGELOG.md +46 -3
  2. package/_i18n/i18n_ar.properties +3 -0
  3. package/_i18n/i18n_cs.properties +4 -1
  4. package/_i18n/i18n_da.properties +3 -0
  5. package/_i18n/i18n_de.properties +3 -0
  6. package/_i18n/i18n_en.properties +3 -0
  7. package/_i18n/i18n_es.properties +3 -0
  8. package/_i18n/i18n_fi.properties +3 -0
  9. package/_i18n/i18n_fr.properties +3 -0
  10. package/_i18n/i18n_it.properties +3 -0
  11. package/_i18n/i18n_ja.properties +3 -0
  12. package/_i18n/i18n_ko.properties +3 -0
  13. package/_i18n/i18n_ms.properties +3 -0
  14. package/_i18n/i18n_nl.properties +3 -0
  15. package/_i18n/i18n_no.properties +3 -0
  16. package/_i18n/i18n_pl.properties +3 -0
  17. package/_i18n/i18n_pt.properties +3 -0
  18. package/_i18n/i18n_ro.properties +3 -0
  19. package/_i18n/i18n_ru.properties +3 -0
  20. package/_i18n/i18n_sv.properties +3 -0
  21. package/_i18n/i18n_th.properties +3 -0
  22. package/_i18n/i18n_zh_CN.properties +3 -0
  23. package/_i18n/i18n_zh_TW.properties +3 -0
  24. package/apis/core.d.ts +26 -30
  25. package/apis/cqn.d.ts +1 -0
  26. package/apis/ql.d.ts +2 -0
  27. package/apis/serve.d.ts +9 -0
  28. package/apis/services.d.ts +3 -2
  29. package/lib/compile/for/lean_drafts.js +21 -18
  30. package/lib/compile/to/srvinfo.js +1 -17
  31. package/lib/dbs/cds-deploy.js +11 -6
  32. package/lib/env/cds-env.js +3 -4
  33. package/lib/env/presets.js +14 -9
  34. package/lib/env/schemas/cds-package.json +3 -1
  35. package/lib/env/schemas/cds-rc.json +0 -4
  36. package/lib/linked/classes.js +112 -12
  37. package/lib/linked/entities.js +3 -0
  38. package/lib/ql/Whereable.js +1 -0
  39. package/lib/srv/cds-serve.js +2 -1
  40. package/lib/srv/protocols/_legacy.js +7 -6
  41. package/lib/srv/protocols/index.js +30 -72
  42. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +13 -4
  43. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/format/ResponseContentNegotiator.js +12 -0
  44. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/SerializerFactory.js +3 -1
  45. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/MetadataCache.js +1 -1
  46. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -4
  47. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +10 -4
  48. package/libx/_runtime/cds-services/services/utils/columns.js +8 -2
  49. package/libx/_runtime/cds-services/services/utils/differ.js +1 -1
  50. package/libx/_runtime/common/composition/data.js +49 -29
  51. package/libx/_runtime/common/composition/update.js +0 -1
  52. package/libx/_runtime/common/composition/utils.js +1 -1
  53. package/libx/_runtime/common/generic/crud.js +1 -1
  54. package/libx/_runtime/common/generic/input.js +18 -13
  55. package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
  56. package/libx/_runtime/common/utils/resolveView.js +115 -35
  57. package/libx/_runtime/common/utils/rewriteAsterisks.js +21 -0
  58. package/libx/_runtime/common/utils/search2cqn4sql.js +1 -1
  59. package/libx/_runtime/db/generic/rewrite.js +5 -4
  60. package/libx/_runtime/db/query/read.js +10 -9
  61. package/libx/_runtime/db/query/update.js +9 -18
  62. package/libx/_runtime/db/utils/deep.js +6 -5
  63. package/libx/_runtime/db/utils/normalizeTimeData.js +1 -1
  64. package/libx/_runtime/fiori/generic/activate.js +14 -19
  65. package/libx/_runtime/fiori/generic/edit.js +2 -5
  66. package/libx/_runtime/remote/utils/client.js +9 -5
  67. package/libx/odata/afterburner.js +5 -2
  68. package/libx/rest/middleware/operation.js +1 -1
  69. package/package.json +3 -3
  70. package/lib/srv/protocols/graphql.js +0 -30
@@ -9,7 +9,9 @@
9
9
  "type": "string"
10
10
  },
11
11
  "cds": {
12
- "$ref": "cdsJsonSchema://schemas/cds-rc.json"
12
+ "$ref": "cdsJsonSchema://schemas/cds-rc.json",
13
+ "description": "CDS configuration",
14
+ "default": {}
13
15
  }
14
16
  }
15
17
  }
@@ -608,10 +608,6 @@
608
608
  "const": "hana",
609
609
  "description": "SAP HANA"
610
610
  },
611
- {
612
- "const": "hana-cloud",
613
- "description": "SAP HANA Cloud"
614
- },
615
611
  {
616
612
  "const": "sql",
617
613
  "description": "In-memory SQLite (development), SAP HANA (production)"
@@ -1,3 +1,6 @@
1
+ const { serve_on_root } = global.cds?.env.features || {}
2
+ const { middlewares } = global.cds?.env.requires || {}
3
+ const { protocols: envProtocols } = global.cds?.env || {}
1
4
  const { extend } = require('../lazy')
2
5
 
3
6
  class any {
@@ -31,22 +34,119 @@ class context extends any {}
31
34
 
32
35
  class service extends context {
33
36
 
37
+ static _protocols = []
38
+ get endpoints() { return super.endpoints = service.endpoints4(this) }
34
39
  get path() { return super.path = service.path4(this) }
35
40
 
36
41
  /**
37
- * Resolve a service endpoint path to mount it to as follows...
38
- * Use _path or def[@path] if given with leading '/' prepended if necessary.
39
- * Otherwise, use the service definition name with stripped 'Service'
42
+ * Return the first service endpoint path to mount it to
40
43
  */
41
- static path4 (srv, _path = (srv.definition || srv)['@path']) {
42
- if (_path) return _path.startsWith('/') ? _path : '/'+_path
43
- else return '/' + ( // return a sluggified variant of the service's name
44
- /[^.]+$/.exec(srv.name)[0] //> my.very.CatalogService --> CatalogService
45
- .replace(/Service$/,'') //> CatalogService --> Catalog
46
- .replace(/_/g,'-') //> foo_bar_baz --> foo-bar-baz
47
- .replace(/([a-z0-9])([A-Z])/g, (_,c,C) => c+'-'+C) //> ODataFooBarX9 --> OData-Foo-Bar-X9
48
- .toLowerCase() //> FOO --> foo
49
- )
44
+ static path4 (srv, options = {}) {
45
+ return this.endpoints4(srv, options)[0]?.path
46
+ }
47
+
48
+ /**
49
+ * Return an array of protocol names to be served by the given service.
50
+ * Ensure that 'odata' shortcuts are expanded to 'odata-v4'.
51
+ */
52
+ static protocols4 (def, options = {}) {
53
+ const ov4 = p => p === 'odata' ? 'odata-v4' : p
54
+
55
+ if (options.to) return [ov4(options.to)]
56
+
57
+ const protocols = this._protocols
58
+ if (def) {
59
+ // Check @protocol annotation
60
+ let atProtocol = def['@protocol']
61
+ if (atProtocol === 'none') return []
62
+ if (atProtocol) {
63
+ const result = (Array.isArray(atProtocol) ? atProtocol : [atProtocol]).map(p => ov4(typeof p === 'string' ? p : p.kind))
64
+ return [...new Set(result)]
65
+ }
66
+
67
+ // Check @odata, @rest, ... shortcuts
68
+ const shortcut = Object.keys(protocols).find(p => def['@'+p])
69
+ if (shortcut) return [ov4(shortcut)]
70
+ }
71
+
72
+ // No protocol annotation found -> serve odata
73
+ return ['odata-v4']
74
+ }
75
+
76
+ static endpoints4 (srv, options = {}) {
77
+ const def = srv?.definition || srv
78
+ if (!def) return []
79
+
80
+ // Return a sluggified variant of the service's name
81
+ const pathFromServiceName4 = (srv) => {
82
+ return (
83
+ /[^.]+$/.exec(srv.name)[0] //> my.very.CatalogService --> CatalogService
84
+ .replace(/Service$/,'') //> CatalogService --> Catalog
85
+ .replace(/_/g,'-') //> foo_bar_baz --> foo-bar-baz
86
+ .replace(/([a-z0-9])([A-Z])/g, (_,c,C) => c+'-'+C) //> ODataFooBarX9 --> OData-Foo-Bar-X9
87
+ .toLowerCase() //> FOO --> foo
88
+ )
89
+ }
90
+
91
+ // Return an array of service paths for a given service and protocol.
92
+ // The results are not prefixed with the protocol's path.
93
+ // Can return multiple results to support serving the same protocol on multiple paths:
94
+ // `@protocol: [{kind: 'odata', path: '/a'}, {kind: 'odata', path: '/b'}]`
95
+ const unprefixedPaths4 = (protocol, srv, options) => {
96
+ const def = srv?.definition || srv
97
+ if (options.at) return [options.at]
98
+ if (options.path) return [options.path]
99
+
100
+ const atProtocol = def['@protocol']
101
+ if (atProtocol) {
102
+ let paths = []
103
+ let protocols = Array.isArray(atProtocol) ? atProtocol : [atProtocol]
104
+ for (let p of protocols) {
105
+ let pName = (p.kind || p) === 'odata' ? 'odata-v4' : p.kind || p
106
+ if (pName === protocol) {
107
+ if (p.path) paths.push(p.path)
108
+ else paths.push(def['@path'] || pathFromServiceName4(srv))
109
+ }
110
+ }
111
+
112
+ // in case protocol is set via options.at but not contained in @protocol
113
+ if (paths.length === 0) paths.push(def['@path'] || pathFromServiceName4(srv))
114
+
115
+ return paths
116
+ }
117
+
118
+ if (def['@path']) return [def['@path']]
119
+ return [pathFromServiceName4(srv)]
120
+ }
121
+
122
+ const protocols = this.protocols4(def, options)
123
+
124
+ if (protocols.length > 1 && def['@path']?.[0] === '/') {
125
+ throw new Error(`Absolute @path (starting with '/') cannot be set for more than one protocol`)
126
+ }
127
+
128
+ const endpoints = []
129
+ for (let pName of protocols) {
130
+ let p = this._protocols[pName] || envProtocols[pName]
131
+ if (!p) throw new Error(`Protocol "${pName}" is unknown`)
132
+
133
+ const kind = pName
134
+ let prefix = middlewares ? p.path || '/' : '/' // > no middlewares, no prefix
135
+ if (!prefix.endsWith('/')) prefix += '/'
136
+ const unprefixedPaths = unprefixedPaths4(pName, srv, options)
137
+ for (let path of unprefixedPaths) {
138
+ if (path.startsWith('/')) endpoints.push({ kind, path})
139
+ else {
140
+ // Extra serve in root if serve_on_root=true, but not(!) in case of ...
141
+ // - multiple protocols
142
+ // - empty prefix, which is the case for legacy protocol adapter
143
+ // - middlewares=false (check only needed for handling of custom protocols)
144
+ if (middlewares && serve_on_root && protocols.length === 1 && prefix !== '/') endpoints.push({ kind, path: '/' + path })
145
+ endpoints.push({ kind, path: prefix + path })
146
+ }
147
+ }
148
+ }
149
+ return endpoints
50
150
  }
51
151
  }
52
152
 
@@ -1,3 +1,4 @@
1
+ const cds = require('../')
1
2
  const { struct } = require ('./classes')
2
3
 
3
4
  class entity extends struct {
@@ -12,6 +13,8 @@ class entity extends struct {
12
13
  return this.own('_compositions') || this.set('_compositions', this._elements (e => e instanceof Composition))
13
14
  }
14
15
  get drafts() {
16
+ // Remove this getter when old draft is removed
17
+ if (cds.env.fiori.lean_draft) return null
15
18
  return this.own('_drafts') || this.set('_drafts', this.elements?.HasDraftEntity && {
16
19
  name: this.name + '_drafts', keys: this.keys,
17
20
  toString(){ return this.name.replace(/\./g,'_') }
@@ -8,6 +8,7 @@ class Query extends require('./Query') {
8
8
  and(...x) { return this._where (x,'and') }
9
9
  or(...x) { return this._where (x,'or') }
10
10
  _where (args, and_or, _where) {
11
+ if (!args[0]) return this
11
12
  let pred = predicate4(args, _where)
12
13
  if (pred && pred.length > 0) {
13
14
  let _ = this[this.cmd]
@@ -143,7 +143,8 @@ async function _new (Service, d,m,o) {
143
143
  if (required.name) srv.name = required.name
144
144
  if (required.external && o.mocked) srv.mocked = true
145
145
  }
146
- if (!srv.path) srv.path = cds.service.path4(srv,o.at)
146
+ if (!srv.path) srv.path = cds.service.path4(srv,o)
147
+ if (!srv.endpoints) srv.endpoints = cds.service.endpoints4(srv,o)
147
148
  _pending[srv.name] = new Promise (r => srv[_ready]=r).finally(()=>{
148
149
  delete _pending[srv.name]
149
150
  delete srv[_ready]
@@ -6,12 +6,13 @@ const { ProtocolAdapter } = require('.')
6
6
 
7
7
  class LegacyProtocolAdapter extends ProtocolAdapter {
8
8
 
9
- static init() {
10
- return this.protocols = {
11
- "odata-v4": { get impl() { return libx.to.odata_v4 } },
12
- "odata": { get impl() { return libx.to.odata_v4 } },
13
- "rest": { get impl() { return libx.to.rest } },
14
- }
9
+ static init(protocols = { ...cds.env.protocols }) {
10
+ // Legacy protocol behavior for odata and rest,
11
+ // but ensure that additional protocols like graphql can be configured
12
+ protocols['odata-v4'] = { get impl() { return libx.to.odata_v4 } }
13
+ protocols['odata'] = { get impl() { return libx.to.odata_v4 } }
14
+ protocols['rest'] = { get impl() { return libx.to.rest } }
15
+ return this.protocols = cds.service._protocols = protocols
15
16
  }
16
17
 
17
18
  static serve (srv, /* in: */ app) {
@@ -7,7 +7,8 @@ class ProtocolAdapter {
7
7
  * Provides canonicalized protocols configurations
8
8
  */
9
9
  static init (protocols = { ...cds.env.protocols }) {
10
- return this.protocols = protocols
10
+ for (let each in protocols) protocols[each].kind = each
11
+ return this.protocols = cds.service._protocols = protocols
11
12
  }
12
13
 
13
14
  /**
@@ -25,90 +26,47 @@ class ProtocolAdapter {
25
26
  /**
26
27
  * Constructs a new adapter for the given service, or returns a formerly constructed one
27
28
  */
28
- static for (srv, p = srv.options?.to || protocol4(srv.definition)) { // TODO default for param p?
29
+ static for (srv, p = srv.options?.to || protocol4(srv.definition)) { // TODO default for param p?
29
30
  const cache = srv._adapters || (srv._adapters={}); if (p in cache) return cache[p]
30
31
  const impl = this.middlewareFor(p), conf = this.protocols[p]
31
32
  return cache[p] = impl (srv, conf)
32
33
  }
33
34
 
34
- /**
35
- * Returns the defined protocols for the given service
36
- */
37
- static protocols4 (def) {
38
- // String, provided via srv.options.to
39
- if(typeof def === 'string') {
40
- return [{ kind: def }]
41
- }
42
-
43
- // @protocol
44
- let atProtocol = def?.['@protocol']
45
- if (atProtocol) {
46
- if (!Array.isArray(atProtocol)) atProtocol = [atProtocol]
47
- return atProtocol.map(p => typeof p === 'string' ? { kind: p } : p)
48
- }
49
-
50
- // @odata, @rest, ...
51
- const atProtocolDirect = Object.keys(this.protocols).find(p => def?.['@'+p])
52
- if (atProtocolDirect) return [{ kind: atProtocolDirect }]
53
-
54
- // No protocol annotation found -> serve odata
55
- return [{ kind: Object.keys(this.protocols)[0] || 'odata-v4' }]
56
- }
57
-
58
35
  /**
59
36
  * Constructs a new adapter for the given service, and mounts it to an express app.
60
37
  */
61
38
  static serve (srv, /* in: */ app, { before, after } = cds.middlewares) {
62
- const DEBUG = cds.debug('adapters')
63
- const protocols = this.protocols4(srv.options?.to || srv.definition)
39
+ const logger = cds.log('adapters')
64
40
 
65
- if (protocols.length > 1 && !cds.requires.middlewares) {
66
- cds.error `Cannot serve multiple protocols if cds.requires.middlewares is set to false`
41
+ const endpoints = cds.service.endpoints4(srv, srv.options)
42
+
43
+ if (endpoints.length > 1 && !cds.requires.middlewares) {
44
+ cds.error `Cannot serve multiple endpoints if cds.requires.middlewares is set to false`
67
45
  }
68
46
 
69
- const srvDefPath = srv.definition?.['@path']
70
- if (protocols.length > 1 && srvDefPath?.[0] === '/') {
71
- cds.error `Cannot serve entity with absolute @path (starting with '/') for more than one protocol`
72
- }
73
-
74
- let adapter, path, prefix
75
- for (let p of protocols) {
76
- adapter = this.for(srv, p.kind); if (!adapter) continue
77
- path = srv.options?.at || srv.options?.path || p.path || srvDefPath || cds.service.path4(srv).slice(1)
78
-
79
- if (path[0] !== '/') {
80
- if (protocols.length === 1 && cds.env.features.serve_on_root) {
81
- app.use (`/${path}/webapp/`, (_,res) => res.sendStatus(404))
82
- DEBUG?.('app.use(', path, ', ... )')
83
- app.use (`/${path}`, before, adapter, after)
84
- }
47
+ for (let endpoint of endpoints) {
48
+ let adapter = this.for(srv, endpoint.kind); if (!adapter) continue
49
+ // REVISIT: still needed with new handling in @sap/cds-fiori?
50
+ app.use (`${endpoint.path}/webapp/`, (_,res) => res.sendStatus(404))
51
+ app.use (adapter.path = endpoint.path, before, adapter, after)
52
+ logger._debug && logger.debug('app.use(', endpoint.path, ', ... )')
85
53
 
86
- if (!cds.env.features.serve_on_root) {
87
- // log warning for changed path if $metadata is accessed
88
- let logged = false
89
- app.use(`*/${path}/\\$metadata`, (req,res,next) => {
90
- if (!logged) {
91
- const logger = cds.log('adapters')
92
- logger._warn && logger.warn(`With @sap/cds version 7, the service path has changed to '${srv.path}'.
93
- If you use SAP Fiori Elements, make sure to adapt the 'dataSources.uri' paths
94
- in 'manifest.json' files accordingly. For more information, see the release notes at
95
- https://cap.cloud.sap/docs/releases/jun23.`)
96
- logged = true
97
- }
98
- next()
99
- })
100
- }
101
-
102
- prefix = this.protocols[p.kind].path
103
- prefix = prefix ? (prefix.endsWith('/') ? prefix : prefix + '/') : '/'
104
- path = prefix + path
54
+ // REVISIT: move to @sap/cds-fiori
55
+ // log warning for changed path if $metadata is accessed
56
+ if (!cds.env.features.serve_on_root && endpoint.path.startsWith(this.protocols[endpoint.kind].path)) {
57
+ const oldPath = endpoint.path.replace(this.protocols[endpoint.kind].path, '')
58
+ let logged = false
59
+ app.use(`*${oldPath}/\\$metadata`, (req,res,next) => {
60
+ if (!logged) {
61
+ logger._warn && logger.warn(`With @sap/cds version 7, the service path has changed to '${endpoint.path}'.
62
+ If you use SAP Fiori Elements, make sure to adapt the 'dataSources.uri' paths
63
+ in 'manifest.json' files accordingly. For more information, see the release notes at
64
+ https://cap.cloud.sap/docs/releases/jun23.`)
65
+ logged = true
66
+ }
67
+ next()
68
+ })
105
69
  }
106
-
107
- app.use (`${path}/webapp/`, (_,res) => res.sendStatus(404))
108
- DEBUG?.('app.use(', path, ', ... )')
109
- app.use (path, before, adapter, after)
110
- // REVISIT this doesn't handle multiple protocols correctly
111
- srv.path = path
112
70
  }
113
71
  }
114
72
  }
@@ -118,6 +76,6 @@ const protocols = Object.keys(ProtocolAdapter.init())
118
76
  const protocol4 = (def, _default = protocols[0] || 'odata-v4') => def?.['@protocol'] || protocols.find(p => def['@'+p]) || _default
119
77
 
120
78
  module.exports = { ProtocolAdapter, protocol4 }
121
- if (!cds.env.protocols && !cds.requires.middlewares) {
79
+ if (!cds.requires.middlewares) {
122
80
  module.exports.ProtocolAdapter = require('./_legacy')
123
81
  }
@@ -16,7 +16,7 @@ const FUNCTION = { [BOUND_FUNCTION]: 1, [FUNCTION_IMPORT]: 1 }
16
16
  const { isCustomOperation } = require('../utils/request')
17
17
  const { getActionOrFunctionReturnType } = require('../utils/handlerUtils')
18
18
  const { validateResourcePath } = require('../utils/request')
19
- const { toODataResult, postProcess } = require('../utils/result')
19
+ const { toODataResult, postProcess, setStreamingHeaders } = require('../utils/result')
20
20
  const { isStreaming } = require('../utils/stream')
21
21
  const { resolveStructuredName } = require('../utils/handlerUtils')
22
22
  const getError = require('../../../../common/error')
@@ -245,7 +245,15 @@ const _reliablePagingPossible = req => {
245
245
  const _readCollection = async (tx, req, odataReq) => {
246
246
  commonGenericPaging(req)
247
247
  commonGenericSorting(req)
248
+
248
249
  const result = (await tx.dispatch(req)) || []
250
+
251
+ if (req.http?.req?.headers?.accept?.match(/application\/pdf/)) {
252
+ if (!result.$mediaContentType) result.$mediaContentType = 'application/pdf'
253
+ setStreamingHeaders(result, req)
254
+ return result
255
+ }
256
+
249
257
  if (Array.isArray(req.query)) {
250
258
  const adjustedResult = []
251
259
  if (req.query[0].SELECT.count) adjustedResult.$count = 0
@@ -287,9 +295,7 @@ const _readCollection = async (tx, req, odataReq) => {
287
295
  }
288
296
  }
289
297
 
290
- const odataResult = toODataResult(result, req)
291
-
292
- return odataResult
298
+ return toODataResult(result, req)
293
299
  }
294
300
 
295
301
  /**
@@ -483,6 +489,9 @@ const read = service => {
483
489
  if (changeset) {
484
490
  // for passing into commit
485
491
  odataReq.getBatchApplicationData().results[changeset].push({ result, req })
492
+ } else if (result.value && isStreaming(odataReq.getUriInfo().getPathSegments())) {
493
+ result.value.on('end', () => tx.commit(result).catch(() => {}))
494
+ result.value.once('error', err => tx.rollback(err).catch(() => {}))
486
495
  } else {
487
496
  await tx.commit(result)
488
497
  }
@@ -17,6 +17,8 @@ const HttpHeader = require('../http/HttpHeader')
17
17
  const ResponseContract = require('../core/ResponseContract')
18
18
  const NotAcceptableError = require('../errors/NotAcceptableError')
19
19
 
20
+ const SerializerFactory = require('../serializer/SerializerFactory')
21
+
20
22
  class ResponseContentNegotiator {
21
23
  /**
22
24
  * Sets the logger.
@@ -194,6 +196,16 @@ class ResponseContentNegotiator {
194
196
  }
195
197
 
196
198
  if (!formatDescription) {
199
+ if (acceptHeader?.match(/application\/pdf/) && representationKind === 'ENTITY_COLLECTION') {
200
+ return new ResponseContract()
201
+ .setRepresentationKind(representationKind)
202
+ .setSerializerFunction((context, data, options = {}, next) => {
203
+ options.stream = true
204
+ SerializerFactory.value(context, data, options, next)
205
+ })
206
+ .setContentTypeInfo(new ContentTypeInfo().setMimeType('application/pdf'))
207
+ }
208
+
197
209
  this._logger.warning(
198
210
  `No matching content type found for representation kind '${representationKind}'` +
199
211
  ` and accept '${acceptHeader}'`
@@ -427,7 +427,9 @@ class SerializerFactory {
427
427
  if (type.getKind() === EdmTypeKind.DEFINITION) type = type.getUnderlyingType()
428
428
 
429
429
  let propertyOrReturnType
430
- if (uriInfo.getLastSegment()._isStreamByDollarValue) {
430
+ if (options?.stream) {
431
+ type = EdmPrimitiveTypeKind.Stream
432
+ } else if (uriInfo.getLastSegment()._isStreamByDollarValue) {
431
433
  for (const [propertyName, property] of uriInfo.getLastSegment(-1).getEdmType().getOwnProperties()) {
432
434
  if (property.getType() === EdmPrimitiveTypeKind.Stream) {
433
435
  propertyOrReturnType = property
@@ -3,7 +3,7 @@
3
3
  const crypto = require('crypto')
4
4
 
5
5
  const cds = require('../../../../../../cds')
6
- const DEBUG = cds.debug('odata', '[odata] -')
6
+ const DEBUG = cds.debug('odata')
7
7
 
8
8
  const CACHE_LIMIT_KB = (cds.env.odata && cds.env.odata.metadataCacheLimit) || 10240 // 10 Megabyte cache limit per mimeType
9
9
 
@@ -7,11 +7,8 @@ const { getNavigationIfStruct } = require('../../../../common/utils/structured')
7
7
  const getTemplate = require('../../../../common/utils/template')
8
8
  const templateProcessor = require('../../../../common/utils/templateProcessor')
9
9
 
10
- const _ignoreColumns = (columns, options) => {
10
+ const _ignoreColumns = columns => {
11
11
  if (!(Array.isArray(columns) && columns.some(c => c === '*' || c.as || c.ref))) return true
12
- // REVISIT expand=* => list all expanded properties (as by okra's implementation)
13
- // OData spec is clarified (https://github.tools.sap/cap/cds/pull/1183#pullrequestreview-1775872)
14
- if (options && !options.$select && options.$expand === '*') return true
15
12
  }
16
13
 
17
14
  const _getNestedQueryOptions = (ref, expand, expandString) => {
@@ -46,9 +46,9 @@ const _getPropertyName = req => {
46
46
  }
47
47
  }
48
48
 
49
- const _cleanupMetadata = (odataResult, result, req) => {
50
- if (typeof result !== 'object') return odataResult
49
+ const setStreamingHeaders = (result, req) => {
51
50
  // backwards compatibility for content-type in stream
51
+ if ('$mediaContentType' in result) result['*@odata.mediaContentType'] = result.$mediaContentType
52
52
  if ('*@odata.mediaContentType' in result) result.$mediaContentType = result['*@odata.mediaContentType']
53
53
  if ('$mediaContentDispositionFilename' in result && req) {
54
54
  const cdt = result.$mediaContentDispositionType || 'attachment'
@@ -57,6 +57,12 @@ const _cleanupMetadata = (odataResult, result, req) => {
57
57
  `${cdt}; filename="${encodeURIComponent(result.$mediaContentDispositionFilename)}"`
58
58
  )
59
59
  }
60
+ }
61
+
62
+ const _cleanupMetadata = (odataResult, result, req) => {
63
+ if (typeof result !== 'object') return odataResult
64
+
65
+ setStreamingHeaders(result, req)
60
66
 
61
67
  const keysToCleanup = {
62
68
  // do not set "@odata.context" as it may be inherited of remote service
@@ -370,7 +376,6 @@ const postProcess = (req, res, service, result, previousResult) => {
370
376
 
371
377
  const postProcessMinimal = (req, service, result) => {
372
378
  const { target, timestamp } = req
373
-
374
379
  if (!target || !result) return
375
380
 
376
381
  setLocationHeader(req, service)
@@ -386,5 +391,6 @@ const postProcessMinimal = (req, service, result) => {
386
391
  module.exports = {
387
392
  toODataResult,
388
393
  postProcess,
389
- postProcessMinimal
394
+ postProcessMinimal,
395
+ setStreamingHeaders
390
396
  }
@@ -40,6 +40,12 @@ const getColumns = (
40
40
  return columns
41
41
  }
42
42
 
43
+ const _isColumnCalculated = (query, columnName) => {
44
+ if (!query) return false
45
+ if (query.SELECT?.columns?.find(col => col.xpr && col.as === columnName)) return true
46
+ return _isColumnCalculated(query._target?.query, columnName)
47
+ }
48
+
43
49
  const _getSearchableColumns = entity => {
44
50
  const columnsOptions = { removeIgnore: true, filterVirtual: true }
45
51
  const columns = getColumns(entity, columnsOptions)
@@ -82,11 +88,11 @@ const _getSearchableColumns = entity => {
82
88
  if (atLeastOneColumnIsSearchable) return false
83
89
 
84
90
  // the element is considered searchable if it is explicitly annotated as such or
85
- // if it is not annotated and the column is typed as a string (excluding elements/elements expressions)
91
+ // if it is not annotated and the column is typed as a string (excluding calculated fields)
86
92
  return (
87
93
  annotatedColumnValue === undefined &&
88
94
  column._type === DEFAULT_SEARCHABLE_TYPE &&
89
- !entity?.query?.SELECT?.columns?.find(col => col.xpr && col.as === column.name)
95
+ !_isColumnCalculated(entity?.query, column.name)
90
96
  )
91
97
  })
92
98
 
@@ -22,7 +22,7 @@ module.exports = class Differ {
22
22
  // Don't take into account virtual or computed properties to make the diff result
23
23
  // consistent with the ones for UPDATE/CREATE (where we don't have access to that
24
24
  // information).
25
- if (!element.key && (element.virtual || element['@Core.Computed'])) continue
25
+ if (!element.key && (element.virtual || element._isReadOnly)) continue
26
26
  if (element.isComposition) {
27
27
  if (element._target._hasPersistenceSkip) continue
28
28
  columns.push({