@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.
- package/CHANGELOG.md +46 -3
- package/_i18n/i18n_ar.properties +3 -0
- package/_i18n/i18n_cs.properties +4 -1
- package/_i18n/i18n_da.properties +3 -0
- package/_i18n/i18n_de.properties +3 -0
- package/_i18n/i18n_en.properties +3 -0
- package/_i18n/i18n_es.properties +3 -0
- package/_i18n/i18n_fi.properties +3 -0
- package/_i18n/i18n_fr.properties +3 -0
- package/_i18n/i18n_it.properties +3 -0
- package/_i18n/i18n_ja.properties +3 -0
- package/_i18n/i18n_ko.properties +3 -0
- package/_i18n/i18n_ms.properties +3 -0
- package/_i18n/i18n_nl.properties +3 -0
- package/_i18n/i18n_no.properties +3 -0
- package/_i18n/i18n_pl.properties +3 -0
- package/_i18n/i18n_pt.properties +3 -0
- package/_i18n/i18n_ro.properties +3 -0
- package/_i18n/i18n_ru.properties +3 -0
- package/_i18n/i18n_sv.properties +3 -0
- package/_i18n/i18n_th.properties +3 -0
- package/_i18n/i18n_zh_CN.properties +3 -0
- package/_i18n/i18n_zh_TW.properties +3 -0
- package/apis/core.d.ts +26 -30
- package/apis/cqn.d.ts +1 -0
- package/apis/ql.d.ts +2 -0
- package/apis/serve.d.ts +9 -0
- package/apis/services.d.ts +3 -2
- package/lib/compile/for/lean_drafts.js +21 -18
- package/lib/compile/to/srvinfo.js +1 -17
- package/lib/dbs/cds-deploy.js +11 -6
- package/lib/env/cds-env.js +3 -4
- package/lib/env/presets.js +14 -9
- package/lib/env/schemas/cds-package.json +3 -1
- package/lib/env/schemas/cds-rc.json +0 -4
- package/lib/linked/classes.js +112 -12
- package/lib/linked/entities.js +3 -0
- package/lib/ql/Whereable.js +1 -0
- package/lib/srv/cds-serve.js +2 -1
- package/lib/srv/protocols/_legacy.js +7 -6
- package/lib/srv/protocols/index.js +30 -72
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +13 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/format/ResponseContentNegotiator.js +12 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/SerializerFactory.js +3 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/MetadataCache.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +10 -4
- package/libx/_runtime/cds-services/services/utils/columns.js +8 -2
- package/libx/_runtime/cds-services/services/utils/differ.js +1 -1
- package/libx/_runtime/common/composition/data.js +49 -29
- package/libx/_runtime/common/composition/update.js +0 -1
- package/libx/_runtime/common/composition/utils.js +1 -1
- package/libx/_runtime/common/generic/crud.js +1 -1
- package/libx/_runtime/common/generic/input.js +18 -13
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
- package/libx/_runtime/common/utils/resolveView.js +115 -35
- package/libx/_runtime/common/utils/rewriteAsterisks.js +21 -0
- package/libx/_runtime/common/utils/search2cqn4sql.js +1 -1
- package/libx/_runtime/db/generic/rewrite.js +5 -4
- package/libx/_runtime/db/query/read.js +10 -9
- package/libx/_runtime/db/query/update.js +9 -18
- package/libx/_runtime/db/utils/deep.js +6 -5
- package/libx/_runtime/db/utils/normalizeTimeData.js +1 -1
- package/libx/_runtime/fiori/generic/activate.js +14 -19
- package/libx/_runtime/fiori/generic/edit.js +2 -5
- package/libx/_runtime/remote/utils/client.js +9 -5
- package/libx/odata/afterburner.js +5 -2
- package/libx/rest/middleware/operation.js +1 -1
- package/package.json +3 -3
- package/lib/srv/protocols/graphql.js +0 -30
package/lib/linked/classes.js
CHANGED
|
@@ -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
|
-
*
|
|
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,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
package/lib/linked/entities.js
CHANGED
|
@@ -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,'_') }
|
package/lib/ql/Whereable.js
CHANGED
|
@@ -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]
|
package/lib/srv/cds-serve.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
|
63
|
-
const protocols = this.protocols4(srv.options?.to || srv.definition)
|
|
39
|
+
const logger = cds.log('adapters')
|
|
64
40
|
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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.
|
|
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
|
-
|
|
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 (
|
|
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
|
package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/MetadataCache.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const crypto = require('crypto')
|
|
4
4
|
|
|
5
5
|
const cds = require('../../../../../../cds')
|
|
6
|
-
const DEBUG = cds.debug('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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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({
|