@sap/cds 7.5.2 → 7.6.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 (95) hide show
  1. package/CHANGELOG.md +79 -22
  2. package/app/index.js +1 -1
  3. package/lib/auth/index.js +3 -0
  4. package/lib/compile/extend.js +9 -4
  5. package/lib/compile/for/lean_drafts.js +3 -4
  6. package/lib/compile/load.js +11 -15
  7. package/lib/compile/minify.js +2 -4
  8. package/lib/compile/to/sql.js +6 -4
  9. package/lib/compile/to/srvinfo.js +25 -3
  10. package/lib/compile/to/yaml.js +1 -1
  11. package/lib/dbs/cds-deploy.js +7 -13
  12. package/lib/env/defaults.js +1 -10
  13. package/lib/env/schemas/cds-package.js +27 -0
  14. package/lib/env/schemas/cds-rc.js +693 -0
  15. package/lib/env/schemas/index.js +6 -4
  16. package/lib/i18n/localize.js +15 -1
  17. package/lib/index.js +40 -47
  18. package/lib/log/cds-error.js +6 -0
  19. package/lib/ql/Query.js +2 -1
  20. package/lib/ql/cds-ql.js +1 -2
  21. package/lib/ql/infer.js +0 -2
  22. package/lib/req/request.js +3 -6
  23. package/lib/srv/middlewares/trace.js +2 -2
  24. package/lib/srv/protocols/hcql.js +44 -30
  25. package/lib/srv/protocols/http.js +60 -0
  26. package/lib/srv/protocols/index.js +0 -7
  27. package/lib/srv/protocols/odata-v4.js +8 -2
  28. package/lib/srv/srv-api.js +129 -62
  29. package/lib/srv/srv-handlers.js +0 -1
  30. package/lib/srv/srv-models.js +1 -0
  31. package/lib/utils/cds-test.js +1 -1
  32. package/lib/utils/cds-utils.js +26 -0
  33. package/lib/utils/check-version.js +10 -13
  34. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +22 -6
  35. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +3 -4
  36. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +89 -21
  37. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/boundToCQN.js +4 -2
  38. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +1 -24
  39. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/updateToCQN.js +1 -7
  40. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/ApplyParser.js +3 -3
  41. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/TrustedResourceJsonSerializer.js +7 -0
  42. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +0 -5
  43. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +2 -0
  44. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +17 -1
  45. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +22 -2
  46. package/libx/_runtime/cds-services/services/utils/columns.js +1 -2
  47. package/libx/_runtime/common/aspects/Association.js +17 -9
  48. package/libx/_runtime/common/generic/crud.js +13 -22
  49. package/libx/_runtime/common/generic/etag.js +1 -1
  50. package/libx/_runtime/common/generic/input.js +9 -1
  51. package/libx/_runtime/common/generic/paging.js +3 -3
  52. package/libx/_runtime/common/generic/sorting.js +25 -15
  53. package/libx/_runtime/common/generic/stream.js +2 -16
  54. package/libx/_runtime/common/utils/copy.js +5 -0
  55. package/libx/_runtime/common/utils/cqn.js +1 -1
  56. package/libx/_runtime/common/utils/cqn2cqn4sql.js +4 -3
  57. package/libx/_runtime/common/utils/csn.js +0 -49
  58. package/libx/_runtime/common/utils/foreignKeyPropagations.js +5 -5
  59. package/libx/_runtime/common/utils/generateOnCond.js +50 -25
  60. package/libx/_runtime/common/utils/resolveView.js +5 -44
  61. package/libx/_runtime/common/utils/rewriteAsterisks.js +17 -4
  62. package/libx/_runtime/common/utils/stream.js +16 -15
  63. package/libx/_runtime/common/utils/streamProp.js +25 -22
  64. package/libx/_runtime/db/Service.js +27 -8
  65. package/libx/_runtime/db/generic/input.js +6 -1
  66. package/libx/_runtime/db/generic/rewrite.js +3 -2
  67. package/libx/_runtime/db/query/read.js +15 -5
  68. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +0 -11
  69. package/libx/_runtime/db/utils/columns.js +1 -0
  70. package/libx/_runtime/db/utils/stream.js +41 -0
  71. package/libx/_runtime/fiori/generic/read.js +2 -1
  72. package/libx/_runtime/fiori/generic/readOverDraft.js +1 -1
  73. package/libx/_runtime/fiori/lean-draft.js +216 -59
  74. package/libx/_runtime/hana/Service.js +1 -1
  75. package/libx/_runtime/hana/execute.js +53 -14
  76. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +34 -15
  77. package/libx/_runtime/remote/Service.js +2 -1
  78. package/libx/_runtime/remote/utils/client.js +1 -1
  79. package/libx/_runtime/sqlite/Service.js +1 -1
  80. package/libx/_runtime/sqlite/execute.js +17 -5
  81. package/libx/odata/afterburner.js +58 -19
  82. package/libx/odata/cqn2odata.js +6 -8
  83. package/libx/odata/create.js +44 -0
  84. package/libx/odata/delete.js +25 -0
  85. package/libx/odata/error.js +8 -3
  86. package/libx/odata/metadata.js +6 -8
  87. package/libx/odata/service-document.js +1 -1
  88. package/libx/odata/update.js +110 -0
  89. package/libx/odata/utils.js +9 -6
  90. package/libx/outbox/index.js +48 -78
  91. package/libx/rest/RestAdapter.js +0 -3
  92. package/package.json +1 -1
  93. package/lib/env/schemas/cds-package.json +0 -17
  94. package/lib/env/schemas/cds-rc.json +0 -740
  95. package/lib/ql/STREAM.js +0 -90
@@ -1,16 +1,18 @@
1
- const cds = require('..'), { Event, Request } = cds
2
- const add_methods_to = require ('./srv-methods')
3
-
1
+ const cds = require('..'),
2
+ { Event, Request } = cds
3
+ const add_methods_to = require('./srv-methods')
4
+ const LOG = cds.log()
4
5
 
5
6
  class Service extends require('./srv-handlers') {
6
-
7
- constructor (name, model, o) {
7
+ constructor(name, model, o) {
8
8
  if (is_object(name)) {
9
- [ model, o ] = [ name, model ]
10
- let srv = cds.linked(model).services[0] || cds.error.expected `${{model}} passed as first argument to be a CSN with a single service definition`
9
+ ;[model, o] = [name, model]
10
+ let srv =
11
+ cds.linked(model).services[0] ||
12
+ cds.error.expected`${{ model }} passed as first argument to be a CSN with a single service definition`
11
13
  name = srv.name
12
14
  }
13
- super (name || new.target.name) .options = o || (o={})
15
+ super(name || new.target.name).options = o || (o = {})
14
16
  if (o.kind) this.kind = o.kind // shortcut
15
17
  if (model) this.model = model
16
18
  }
@@ -18,11 +20,11 @@ class Service extends require('./srv-handlers') {
18
20
  /**
19
21
  * Subclasses may override this to prepare the given model appropriately
20
22
  */
21
- set model (csn) {
23
+ set model(csn) {
22
24
  if (csn) {
23
- let {definitions:defs={}} = super.model = cds.compile.for.nodejs(csn)
25
+ let { definitions: defs = {} } = (super.model = cds.compile.for.nodejs(csn))
24
26
  super.definition = defs[this.options?.service] || defs[this.name]
25
- add_methods_to (this)
27
+ add_methods_to(this)
26
28
  } else {
27
29
  super.model = undefined
28
30
  }
@@ -31,89 +33,146 @@ class Service extends require('./srv-handlers') {
31
33
  /**
32
34
  * Messaging API to emit asynchronous event messages, i.e. instances of `cds.Event`.
33
35
  */
34
- emit (event, data, headers) {
35
- const eve = event instanceof Event ? event : new Event (
36
- is_object(event) ? event : { event, data, headers }
37
- )
38
- return this.dispatch (eve)
36
+ emit(event, data, headers) {
37
+ const eve = event instanceof Event ? event : new Event(is_object(event) ? event : { event, data, headers })
38
+ return this.dispatch(eve)
39
39
  }
40
40
 
41
41
  /**
42
42
  * REST-style API to send synchronous requests...
43
43
  */
44
- send (method, path, data, headers) {
45
- const req = method instanceof Request ? method : new Request (
46
- is_object(method) ? method : is_object(path) ? { method, data:path, headers:data } : { method, path, data, headers }
47
- )
48
- return this.dispatch (req)
49
- }
50
- get (...args) { return is_rest(args[0]) ? this.send('GET', ...args) : this.read (...args) }
51
- put (...args) { return is_rest(args[0]) ? this.send('PUT', ...args) : this.update (...args) }
52
- post (...args) { return is_rest(args[0]) ? this.send('POST', ...args) : this.create (...args) }
53
- patch (...args) { return is_rest(args[0]) ? this.send('PATCH', ...args) : this.update (...args) }
54
- delete (...args) { return is_rest(args[0]) ? this.send('DELETE',...args) : DELETE.from (...args).bind(this) }
44
+ send(method, path, data, headers) {
45
+ const req =
46
+ method instanceof Request
47
+ ? method
48
+ : new Request(
49
+ is_object(method)
50
+ ? method
51
+ : is_object(path)
52
+ ? { method, data: path, headers: data }
53
+ : { method, path, data, headers }
54
+ )
55
+ return this.dispatch(req)
56
+ }
57
+ get(...args) {
58
+ return is_rest(args[0]) ? this.send('GET', ...args) : this.read(...args)
59
+ }
60
+ put(...args) {
61
+ return is_rest(args[0]) ? this.send('PUT', ...args) : this.update(...args)
62
+ }
63
+ post(...args) {
64
+ return is_rest(args[0]) ? this.send('POST', ...args) : this.create(...args)
65
+ }
66
+ patch(...args) {
67
+ return is_rest(args[0]) ? this.send('PATCH', ...args) : this.update(...args)
68
+ }
69
+ delete(...args) {
70
+ return is_rest(args[0]) ? this.send('DELETE', ...args) : DELETE.from(...args).bind(this)
71
+ }
55
72
 
56
73
  /**
57
74
  * Querying API to send synchronous requests...
58
75
  */
59
- run (query, data) {
76
+ run(query, data) {
60
77
  if (typeof query === 'function') {
61
- const ctx = cds.context, fn = query
78
+ const ctx = cds.context,
79
+ fn = query
62
80
 
63
- if (!ctx?.tx) return this.tx(fn) // run fn with root tx
81
+ if (!ctx?.tx) return this.tx(fn) // run fn with root tx
64
82
  else {
65
- if (!ctx.tx._done) return fn(this.tx(ctx)) // run fn with nested tx
66
- else if (ctx.tx._done === 'rolled back') // > reject
83
+ if (!ctx.tx._done) return fn(this.tx(ctx)) // run fn with nested tx
84
+ else if (ctx.tx._done === 'rolled back')
85
+ // > reject
67
86
  ctx.tx._throw_closed_error()
68
- else return this.tx(fn) // run fn with detached root tx
87
+ else return this.tx(fn) // run fn with detached root tx
69
88
  }
70
89
  }
71
90
 
72
- const req = new Request ({ query, data })
73
- return this.dispatch (req)
91
+ const req = new Request({ query, data })
92
+ return this.dispatch(req)
93
+ }
94
+ read(...args) {
95
+ return is_query(args[0]) ? this.run(...args) : SELECT(...args).bind(this)
96
+ }
97
+ // Deprecated: To be removed with the next major release
98
+ stream(...args) {
99
+ cds._logDeprecation('stream method is deprecated and will be removed in upcoming releases!')
100
+ if (is_query(args[0])) {
101
+ Object.defineProperty(args[0], '_stream', { value: true, enumerable: false })
102
+ if (cds.env.features.stream_compat)
103
+ Object.defineProperty(args[0], '_streaming', { value: true, enumerable: false })
104
+ return this.run(...args).then(result => (result ? Object.values(result)[0] : result))
105
+ }
106
+ const q = (args ? SELECT.one.columns(args) : SELECT.one).bind(this)
107
+ Object.defineProperty(q, '_stream', { value: true, enumerable: false })
108
+ if (cds.env.features.stream_compat) Object.defineProperty(q, '_streaming', { value: true, enumerable: false })
109
+ return q
110
+ }
111
+ insert(...args) {
112
+ return INSERT(...args).bind(this)
113
+ }
114
+ create(...args) {
115
+ return INSERT.into(...args).bind(this)
116
+ }
117
+ update(...args) {
118
+ return UPDATE.entity(...args).bind(this)
119
+ }
120
+ upsert(...args) {
121
+ return UPSERT(...args).bind(this)
122
+ }
123
+ exists(...args) {
124
+ return SELECT.one([1])
125
+ .from(...args)
126
+ .bind(this)
74
127
  }
75
- read (...args) { return is_query(args[0]) ? this.run(...args) : SELECT(...args).bind(this) }
76
- stream (...args) { return is_query(args[0]) ? this.run(...args) : STREAM(...args).bind(this) }
77
- insert (...args) { return INSERT(...args).bind(this) }
78
- create (...args) { return INSERT.into(...args).bind(this) }
79
- update (...args) { return UPDATE.entity(...args).bind(this) }
80
- upsert (...args) { return UPSERT(...args).bind(this) }
81
- exists (...args) { return SELECT.one([1]).from(...args).bind(this) }
82
128
 
83
129
  /**
84
130
  * Streaming API variant of .run(). Subclasses should override this to support real streaming.
85
131
  * The default implementation doesn't stream, but simply invokes the callback on each row.
86
132
  * The callback function is invoked with (row, index).
87
133
  */
88
- foreach (query, data, callback) {
89
- if (!callback) [ data, callback ] = [ undefined, data ]
90
- return this.run (query, data) .then (rows => rows.forEach(callback) || rows)
134
+ foreach(query, data, callback) {
135
+ if (!callback) [data, callback] = [undefined, data]
136
+ return this.run(query, data).then(rows => rows.forEach(callback) || rows)
91
137
  }
92
138
 
93
139
  /**
94
140
  * Model Reflection API...
95
141
  */
96
- get namespace() {
97
- return super.namespace = this.definition?.name || this.model?.namespace
98
- || !(this.isDatabaseService) && !/\W/.test(this.name) && this.name || undefined
142
+ get namespace() {
143
+ return (super.namespace =
144
+ this.definition?.name ||
145
+ this.model?.namespace ||
146
+ (!this.isDatabaseService && !/\W/.test(this.name) && this.name) ||
147
+ undefined)
99
148
  }
100
149
 
101
- get operations() { return super.operations = _reflect (this, d => d.kind === 'action' || d.kind === 'function') }
102
- get entities() { return super.entities = _reflect (this, d => d.kind === 'entity') }
103
- get events() { return super.events = _reflect (this, d => d.kind === 'event') }
104
- get types() { return super.types = _reflect (this, d => !d.kind || d.kind === 'type') }
150
+ get operations() {
151
+ return (super.operations = _reflect(this, d => d.kind === 'action' || d.kind === 'function'))
152
+ }
153
+ get entities() {
154
+ return (super.entities = _reflect(this, d => d.kind === 'entity'))
155
+ }
156
+ get events() {
157
+ return (super.events = _reflect(this, d => d.kind === 'event'))
158
+ }
159
+ get types() {
160
+ return (super.types = _reflect(this, d => !d.kind || d.kind === 'type'))
161
+ }
105
162
 
106
163
  /**
107
164
  * Flag to control whether this service is extensible.
108
165
  * Can be overridden by subclasses.
109
166
  * REVISIT cds.xt name check should move to respective services
110
167
  */
111
- get isExtensible() { return !this.name?.startsWith('cds.xt.') && this.model === cds.model}
168
+ get isExtensible() {
169
+ return !this.name?.startsWith('cds.xt.') && this.model === cds.model
170
+ }
112
171
 
113
172
  /**
114
173
  * Subclasses may override this to free private resources
115
174
  */
116
- disconnect (tenant) {
175
+ disconnect(tenant) {
117
176
  if (this === cds.db) {
118
177
  if (tenant && cds.db.dbcs) cds.db.dbcs.delete[tenant]
119
178
  else if (!tenant) cds.db = undefined //> REVISIT: should go into DatabaseService
@@ -121,11 +180,19 @@ class Service extends require('./srv-handlers') {
121
180
  delete cds.services[this.name]
122
181
  }
123
182
 
124
- get path() { return super.path = cds.service.protocols.path4(this) }
125
- set path(p) { super.path = p }
183
+ get path() {
184
+ return (super.path = cds.service.protocols.path4(this))
185
+ }
186
+ set path(p) {
187
+ super.path = p
188
+ }
126
189
 
127
- get endpoints() { return super.endpoints = cds.service.protocols.endpoints4(this) }
128
- set endpoints(p) { super.endpoints = p }
190
+ get endpoints() {
191
+ return (super.endpoints = cds.service.protocols.endpoints4(this))
192
+ }
193
+ set endpoints(p) {
194
+ super.endpoints = p
195
+ }
129
196
  }
130
197
 
131
198
  const { dispatch, handle } = require('./srv-dispatch')
@@ -138,8 +205,8 @@ Service.prototype._is_service_instance = Service._is_service_class = true //> fo
138
205
  module.exports = Service
139
206
 
140
207
  // Helpers...
141
- const _reflect = (srv,filter) => !srv.model ? [] : srv.model.childrenOf (srv.namespace,filter)
208
+ const _reflect = (srv, filter) => (!srv.model ? [] : srv.model.childrenOf(srv.namespace, filter))
142
209
  const is_rest = x => x && typeof x === 'string' && x[0] === '/'
143
- const is_query = x => x && x.bind || is_array(x) && !x.raw
144
- const is_array = (x) => Array.isArray(x) && !x.raw
145
- const is_object = (x) => typeof x === 'object'
210
+ const is_query = x => (x && x.bind) || (is_array(x) && !x.raw)
211
+ const is_array = x => Array.isArray(x) && !x.raw
212
+ const is_object = x => typeof x === 'object'
@@ -131,7 +131,6 @@ class EventHandler {
131
131
  }
132
132
 
133
133
 
134
- const is_impl = x => typeof x === 'function' && !(x.prototype && /^class\b/.test(x))
135
134
  const is_array = Array.isArray
136
135
  const AlternativeEvents = {
137
136
  SELECT: 'READ',
@@ -62,6 +62,7 @@ class ExtendedModels {
62
62
  _has_extensions = tenant && extensibility && await _is_extended(tenant)
63
63
  } catch (error) {
64
64
  // Better error message for client
65
+ if (error.status === 404) throw error
65
66
  cds.error('`extensibility: true` is configured but table "cds.xt.Extensions" does not exist. Please redeploy.', error)
66
67
  }
67
68
  if (!_has_extensions) {
@@ -168,7 +168,7 @@ class Test extends require('./axios') {
168
168
  function require (mod) { try { return module.require(mod) } catch(e) {
169
169
  if (e.code === 'MODULE_NOT_FOUND') throw new Error (`
170
170
  Failed to load required package '${mod}'. Please add it thru:
171
- npm add -D chai chai-as-promised chai-subset
171
+ npm add -D chai@4 chai-as-promised chai-subset
172
172
  `)}}
173
173
  }
174
174
  get assert() { return this.chai.assert }
@@ -28,6 +28,16 @@ exports.Object_keys = o => ({
28
28
  })
29
29
 
30
30
 
31
+ /**
32
+ * CSN-aware function to deep clone objectx
33
+ */
34
+ exports.clone = (x) => {
35
+ const y = structuredClone(x)
36
+ if (x.$sources) Object.defineProperty (y, '$sources', { value: x.$sources })
37
+ return y
38
+ }
39
+
40
+
31
41
  /**
32
42
  * Simple helper to always access results as arrays.
33
43
  */
@@ -180,6 +190,22 @@ exports.find = function find (base, patterns='*', filter=()=>true) {
180
190
  return files
181
191
  }
182
192
 
193
+ exports.deprecated = (fn, { kind = 'Method', old = fn.name+'()', use } = {}) => function() {
194
+ if (!fn.warned) {
195
+ let o={}; Error.captureStackTrace(o)
196
+ console.warn (
197
+ '\n------------------------------------------------------------------------------',
198
+ '\nDEPRECATED:', old, '\n',
199
+ '\n ', kind, old, 'is deprecated and will be removed in upcoming releases!',
200
+ use ? `\n => Please use ${use} instead.` : '', '\n',
201
+ o.stack.replace(/^Error\n\s*at.*\n/,'\n'), '\n',
202
+ '\n------------------------------------------------------------------------------\n',
203
+ )
204
+ if (cds.env.features.deprecated !== 'show all') fn.warned = true
205
+ }
206
+ return fn.apply (this, arguments)
207
+ }
208
+
183
209
  exports.csv = require('./csv-reader')
184
210
 
185
211
  /**
@@ -1,23 +1,20 @@
1
- const major_minor = version => {
2
- let [ major, minor ] = version.split('.').map(x => +x)
3
- return { version, major, minor }
4
- }
5
- const required = major_minor(
1
+ const required = _major_minor(
6
2
  require('../../package.json').engines.node.match(/>=(.*)/)[1]
7
3
  )
8
- const given = major_minor(
4
+ const given = _major_minor(
9
5
  process.version.match(/^v(\d+\.\d+)/)[1]
10
6
  )
11
7
 
12
- if (given.major < required.major || given.major === required.major && given.minor < required.minor) process.exit (
13
- process.stderr.write (`
8
+ if (given.major < required.major || given.major === required.major && given.minor < required.minor) process.exit (process.stderr.write (`
14
9
  Node.js v${required.version} or higher is required for @sap/cds.
15
10
  Current v${given.version} does not satisfy this.
16
- \n`
17
- ) || 1)
11
+ \n`) || 1)
18
12
 
19
- if (given.major < 18) {
20
- process.stderr.write (`WARNING: \n
13
+ if (given.major < 18) process.stderr.write (`WARNING: \n
21
14
  Node.js v${given.major} has reached end of life. Please upgrade to v18 or higher.
22
- `)
15
+ \n`)
16
+
17
+ function _major_minor (version) {
18
+ let [ major, minor ] = version.split('.').map(x => +x)
19
+ return { version, major, minor }
23
20
  }
@@ -1,7 +1,5 @@
1
1
  /* eslint-disable complexity */
2
2
  const cds = require('../../../cds')
3
- // requesting logger without module on purpose!
4
- const LOG = cds.log()
5
3
 
6
4
  const {
7
5
  uri: {
@@ -63,6 +61,26 @@ function _add4Odata(query) {
63
61
  else Object.defineProperty(query.SELECT, '_4odata', { value: true })
64
62
  }
65
63
 
64
+ function _addAllColumns(query) {
65
+ if (Array.isArray(query)) {
66
+ query.forEach(q => {
67
+ if (!q.SELECT.from.SELECT) _addAllColumns(q)
68
+ })
69
+ } else {
70
+ if (!query.SELECT.columns) query.SELECT.columns = ['*']
71
+ }
72
+ }
73
+
74
+ function _handleStreamProperties(target, query, model) {
75
+ if (Array.isArray(query)) {
76
+ query.forEach(q => {
77
+ _handleStreamProperties(target, q, model)
78
+ })
79
+ } else {
80
+ handleStreamProperties(target, query.SELECT.columns, model)
81
+ }
82
+ }
83
+
66
84
  /**
67
85
  * Class representing an OData request.
68
86
  * @extends cds.Request
@@ -95,8 +113,8 @@ class ODataRequest extends cds.Request {
95
113
  const { event, unbound } = info
96
114
  if (event === 'READ') {
97
115
  _add4Odata(query)
98
- if (query.SELECT && !query.SELECT.columns) query.SELECT.columns = ['*']
99
- if (!isStreaming(segments)) handleStreamProperties(target, query, service.model, true)
116
+ _addAllColumns(query)
117
+ if (!isStreaming(segments)) _handleStreamProperties(target, query, service.model)
100
118
  }
101
119
  const _queryOptions = odataReq.getQueryOptions()
102
120
  // prettier-ignore
@@ -155,8 +173,6 @@ class ODataRequest extends cds.Request {
155
173
  else if (type === 'DELETE' && data.IsActiveEntity !== true) event = 'CANCEL'
156
174
  }
157
175
 
158
- if (query.STREAM) event = 'STREAM'
159
-
160
176
  // mark query as for an OData READ
161
177
  if (event === 'READ') Object.defineProperty(query.SELECT, '_4odata', { value: true })
162
178
 
@@ -1,4 +1,3 @@
1
- const { join } = require('path')
2
1
  const cds = require('../../../../cds')
3
2
  const LOG = cds.log('odata')
4
3
 
@@ -7,9 +6,9 @@ const { normalizeError } = require('../../../../common/error/frontend')
7
6
  const getError = require('../../../../common/error')
8
7
 
9
8
  const mpSupportsEmptyLocale = () => {
10
- const pkg = require(join('@sap/cds-mtxs', 'package.json'))
11
- const version = pkg.version.match(/^(\d+\.)?(\d+\.)?(\*|\d+)$/).map(Number)
12
- return version[1] > 1 || (version[1] === 1 && version[2] >= 12)
9
+ const pkg = require(require.resolve('@sap/cds-mtxs/package.json'))
10
+ const [major, minor] = pkg.version.split('.').map(Number)
11
+ return major > 1 || (major === 1 && minor >= 12)
13
12
  }
14
13
 
15
14
  /**
@@ -1,5 +1,5 @@
1
1
  const cds = require('../../../../cds')
2
-
2
+ const LOG = cds.log('odata')
3
3
  const ODataRequest = require('../ODataRequest')
4
4
 
5
5
  const {
@@ -23,7 +23,9 @@ const getError = require('../../../../common/error')
23
23
  const { getSapMessages } = require('../../../../common/error/frontend')
24
24
  const { getPageSize, commonGenericPaging } = require('../../../../common/generic/paging')
25
25
  const { handler: commonGenericSorting } = require('../../../../common/generic/sorting')
26
- const { isNewStream, transformRedirectProperties } = require('../../../../common/utils/stream')
26
+ const { transformRedirectProperties } = require('../../../../common/utils/stream')
27
+ const { getTransition } = require('../../../../common/utils/resolveView')
28
+ const { Readable } = require('stream')
27
29
 
28
30
  /**
29
31
  * Checks whether a bound function or function import is invoked.
@@ -274,6 +276,7 @@ const _readCollection = async (tx, req, odataReq) => {
274
276
  ? getPageSize(req.query[0]._target).max
275
277
  : req.query.SELECT.limit && req.query.SELECT.limit.rows && req.query.SELECT.limit.rows.val
276
278
  const top = odataReq.getUriInfo().getQueryOption(QueryOptions.TOP) || parseInt(odataReq._queryOptions?.$top)
279
+
277
280
  if (limit && limit === result.length && limit !== top && !('$nextLink' in result)) {
278
281
  const token = odataReq.getUriInfo().getQueryOption(QueryOptions.SKIPTOKEN) || odataReq._queryOptions?.$skiptoken
279
282
  if (cds.env.query.limit.reliablePaging && _reliablePagingPossible(req)) {
@@ -298,6 +301,64 @@ const _readCollection = async (tx, req, odataReq) => {
298
301
  return toODataResult(result, req)
299
302
  }
300
303
 
304
+ const _resolveContentProperty = (req, annotName, resolvedProp) => {
305
+ if (req.target.elements[resolvedProp]) {
306
+ return resolvedProp
307
+ } else {
308
+ LOG._warn &&
309
+ LOG.warn(
310
+ `"${annotName}" in entity "${req.target.name}" points to property "${resolvedProp}" which was renamed or is not part of the projection. You must update the annotation value.`
311
+ )
312
+ const mapping = getTransition(req.target, cds.db).mapping
313
+ const key = [...mapping.entries()].find(({ 1: val }) => val.ref[0] === resolvedProp)
314
+ return key?.length && key[0]
315
+ }
316
+ }
317
+
318
+ const _addMetadataProperty = (req, property, annotName, odataName) => {
319
+ if (typeof property[annotName] === 'object') {
320
+ const contentProperty = _resolveContentProperty(req, annotName, property[annotName]['='].replaceAll(/\./g, '_'))
321
+ req.target.elements[contentProperty]
322
+ ? req.query.SELECT.columns.push({ ref: [contentProperty], as: odataName })
323
+ : LOG._warn &&
324
+ LOG.warn(`"${annotName.split('.')[1]}" ${contentProperty} not found in entity "${req.target.name}".`)
325
+ } else {
326
+ req.query.SELECT.columns.push({ val: property[annotName], as: odataName })
327
+ }
328
+ }
329
+
330
+ const _addStreamMetadata = req => {
331
+ // new odata parser sets streaming property in SELECT.from
332
+ const ref = req.query.SELECT.columns?.[0].ref || req.query.SELECT.from.ref
333
+ const propertyName = ref[ref.length - 1]
334
+ let mediaTypeProperty
335
+ for (let key in req.target.elements) {
336
+ const val = req.target.elements[key]
337
+ if (val['@Core.MediaType'] && val.name === propertyName) {
338
+ mediaTypeProperty = val
339
+ break
340
+ }
341
+ }
342
+
343
+ _addMetadataProperty(req, mediaTypeProperty, '@Core.MediaType', '$mediaContentType')
344
+
345
+ if (mediaTypeProperty['@Core.ContentDisposition.Filename']) {
346
+ _addMetadataProperty(
347
+ req,
348
+ mediaTypeProperty,
349
+ '@Core.ContentDisposition.Filename',
350
+ '$mediaContentDispositionFilename'
351
+ )
352
+ }
353
+
354
+ if (mediaTypeProperty['@Core.ContentDisposition.Type']) {
355
+ req.query.SELECT.columns.push({
356
+ val: mediaTypeProperty['@Core.ContentDisposition.Type'],
357
+ as: '$mediaContentDispositionType'
358
+ })
359
+ }
360
+ }
361
+
301
362
  /**
302
363
  * Reading the full entity or only a property of it is alike.
303
364
  * In case of an entity, odata-v4 wants the value an object structure,
@@ -309,7 +370,8 @@ const _readCollection = async (tx, req, odataReq) => {
309
370
  * @private
310
371
  */
311
372
  const _readStream = async (tx, req) => {
312
- req.query._streaming = true
373
+ if (cds.env.features.stream_compat) req.query._streaming = true
374
+ else if (!req.target['@cds.persistence.skip']) _addStreamMetadata(req)
313
375
 
314
376
  let result = await tx.dispatch(req)
315
377
 
@@ -330,8 +392,10 @@ const _readStream = async (tx, req) => {
330
392
  if (result[0] === null) return null
331
393
 
332
394
  result = result[0]
333
- const readable = result.value
334
395
 
396
+ const readable = cds.env.features.stream_compat
397
+ ? result.value
398
+ : Object.values(result).find(v => v instanceof Readable)
335
399
  if (readable) {
336
400
  readable.on('error', () => {
337
401
  readable.removeAllListeners('error')
@@ -393,26 +457,20 @@ const _readAndTransform = (tx, req, odataReq) => {
393
457
  return _readCollection(tx, req, odataReq)
394
458
  }
395
459
 
396
- if (isNewStream()) {
397
- if (req.query.STREAM) {
398
- return _readStream(tx, req)
399
- }
400
- } else {
401
- // REVISIT: move to afterburner
402
- if (segments[segments.length - 1]._isStreamByDollarValue) {
403
- for (const k in req.target.elements) {
404
- if (req.target.elements[k]['@Core.MediaType']) {
405
- req.query.SELECT.columns = [{ ref: [k] }]
406
- break
407
- }
460
+ // REVISIT: move to afterburner
461
+ if (segments[segments.length - 1]._isStreamByDollarValue) {
462
+ for (const k in req.target.elements) {
463
+ if (req.target.elements[k]['@Core.MediaType']) {
464
+ req.query.SELECT.columns = [{ ref: [k] }]
465
+ break
408
466
  }
409
-
410
- return _readStream(tx, req)
411
467
  }
412
468
 
413
- if (isStreaming(segments)) {
414
- return _readStream(tx, req)
415
- }
469
+ return _readStream(tx, req)
470
+ }
471
+
472
+ if (isStreaming(segments)) {
473
+ return _readStream(tx, req)
416
474
  }
417
475
 
418
476
  if (req.target._isSingleton) {
@@ -443,6 +501,14 @@ const _removeKeysForParams = result => {
443
501
  return options
444
502
  }
445
503
 
504
+ const _ensureStream = result => {
505
+ // temp workaround for url streaming
506
+ const stream_ = new Readable()
507
+ stream_.push(result.value)
508
+ stream_.push(null)
509
+ result.value = stream_
510
+ }
511
+
446
512
  /**
447
513
  * The handler that will be registered with odata-v4.
448
514
  *
@@ -490,6 +556,8 @@ const read = service => {
490
556
  // for passing into commit
491
557
  odataReq.getBatchApplicationData().results[changeset].push({ result, req })
492
558
  } else if (result.value && isStreaming(odataReq.getUriInfo().getPathSegments())) {
559
+ // REVISIT: temp workaround for url streaming
560
+ if (!(result.value instanceof Readable)) _ensureStream(result)
493
561
  result.value.on('end', () => tx.commit(result).catch(() => {}))
494
562
  result.value.once('error', err => tx.rollback(err).catch(() => {}))
495
563
  } else {
@@ -11,18 +11,20 @@ const {
11
11
  ENTITY_COLLECTION,
12
12
  NAVIGATION_TO_MANY,
13
13
  NAVIGATION_TO_ONE,
14
- PRIMITIVE_PROPERTY
14
+ PRIMITIVE_PROPERTY,
15
+ SINGLETON
15
16
  } = require('../okra/odata-server').uri.UriResource.ResourceKind
16
17
 
17
18
  const SUPPORTED_SEGMENT_KINDS = {
18
19
  [BOUND_ACTION]: 1,
19
20
  [BOUND_FUNCTION]: 1,
21
+ [COUNT]: 1,
20
22
  [ENTITY]: 1,
21
23
  [ENTITY_COLLECTION]: 1,
22
24
  [NAVIGATION_TO_ONE]: 1,
23
25
  [NAVIGATION_TO_MANY]: 1,
24
26
  [PRIMITIVE_PROPERTY]: 1,
25
- [COUNT]: 1
27
+ [SINGLETON]: 1
26
28
  }
27
29
 
28
30
  /**