@sap/cds 6.8.4 → 7.0.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 (214) hide show
  1. package/CHANGELOG.md +58 -5
  2. package/README.md +0 -1
  3. package/bin/cds-serve.js +50 -3
  4. package/bin/deploy/to-hana.js +1 -0
  5. package/bin/serve.js +16 -20
  6. package/lib/auth/basic-auth.js +6 -4
  7. package/lib/auth/index.js +4 -3
  8. package/lib/auth/jwt-auth.js +2 -5
  9. package/lib/compile/cds-compile.js +34 -89
  10. package/lib/compile/cdsc.js +11 -0
  11. package/lib/compile/etc/properties.js +2 -2
  12. package/lib/compile/for/lean_drafts.js +36 -69
  13. package/lib/compile/for/nodejs.js +2 -1
  14. package/lib/compile/load.js +1 -1
  15. package/lib/compile/minify.js +2 -0
  16. package/lib/compile/to/csn.js +74 -0
  17. package/{bin/build/provider/hana/2tabledata.js → lib/compile/to/hdbtabledata.js} +4 -6
  18. package/lib/compile/to/json.js +1 -1
  19. package/lib/compile/to/sql.js +8 -6
  20. package/lib/dbs/cds-deploy.js +174 -114
  21. package/lib/env/cds-env.js +64 -79
  22. package/lib/env/cds-requires.js +11 -28
  23. package/lib/env/defaults.js +13 -3
  24. package/lib/env/plugins.js +1 -12
  25. package/lib/env/presets.js +25 -21
  26. package/lib/index.js +121 -147
  27. package/lib/{core/reflect.js → linked/models.js} +2 -2
  28. package/lib/{core/infer.js → linked/queries.js} +2 -0
  29. package/lib/{core/index.js → linked/types.js} +2 -1
  30. package/lib/log/cds-error.js +13 -7
  31. package/lib/log/format/cf.js +1 -1
  32. package/lib/plugins.js +49 -0
  33. package/lib/ql/Query.js +0 -9
  34. package/lib/ql/STREAM.js +0 -1
  35. package/lib/req/context.js +2 -7
  36. package/lib/req/request.js +6 -2
  37. package/lib/req/response.js +23 -10
  38. package/lib/srv/middlewares/ctx-model.js +1 -1
  39. package/lib/srv/middlewares/errors.js +1 -1
  40. package/lib/srv/protocols/_legacy.js +1 -0
  41. package/lib/srv/protocols/graphql.js +7 -16
  42. package/lib/srv/protocols/index.js +59 -45
  43. package/lib/srv/protocols/odata-v2-proxy.js +2 -70
  44. package/lib/srv/srv-api.js +9 -3
  45. package/lib/srv/srv-dispatch.js +12 -9
  46. package/lib/srv/srv-models.js +4 -21
  47. package/lib/srv/srv-tx.js +15 -12
  48. package/lib/utils/cds-test.js +14 -9
  49. package/lib/utils/cds-utils.js +2 -12
  50. package/lib/utils/check-version.js +17 -0
  51. package/{bin/build → lib/utils}/csv-reader.js +23 -24
  52. package/libx/_runtime/auth/index.js +27 -23
  53. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +15 -72
  54. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
  55. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +0 -2
  56. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +33 -63
  57. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +14 -18
  58. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +15 -5
  59. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +5 -4
  60. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +37 -40
  61. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/updateToCQN.js +7 -1
  62. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +101 -38
  63. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/errors/AbstractError.js +5 -1
  64. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +2 -1
  65. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/ResourceJsonDeserializer.js +9 -8
  66. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
  67. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +15 -11
  68. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +4 -0
  69. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +5 -2
  70. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +1 -123
  71. package/libx/_runtime/cds-services/services/Service.js +79 -107
  72. package/libx/_runtime/cds-services/services/utils/columns.js +23 -19
  73. package/libx/_runtime/cds-services/services/utils/compareJson.js +11 -1
  74. package/libx/_runtime/cds-services/services/utils/differ.js +7 -2
  75. package/libx/_runtime/cds-services/util/assert.js +65 -2
  76. package/libx/_runtime/common/composition/data.js +1 -0
  77. package/libx/_runtime/common/generic/auth/expand.js +1 -1
  78. package/libx/_runtime/common/generic/auth/restrict.js +5 -10
  79. package/libx/_runtime/common/generic/auth/restrictions.js +40 -0
  80. package/libx/_runtime/common/generic/auth/utils.js +1 -2
  81. package/libx/_runtime/common/generic/crud.js +32 -16
  82. package/libx/_runtime/common/generic/etag.js +133 -104
  83. package/libx/_runtime/common/generic/input.js +6 -21
  84. package/libx/_runtime/common/generic/put.js +1 -1
  85. package/libx/_runtime/common/generic/stream.js +52 -0
  86. package/libx/_runtime/common/generic/temporal.js +25 -8
  87. package/libx/_runtime/common/i18n/messages.properties +0 -2
  88. package/libx/_runtime/common/utils/cqn.js +1 -1
  89. package/libx/_runtime/common/utils/cqn2cqn4sql.js +5 -2
  90. package/libx/_runtime/common/utils/csn.js +0 -51
  91. package/libx/_runtime/common/utils/etag.js +30 -0
  92. package/libx/_runtime/common/utils/keys.js +1 -1
  93. package/libx/_runtime/common/utils/normalizeTimestamp.js +25 -0
  94. package/libx/_runtime/common/utils/path.js +1 -1
  95. package/libx/_runtime/common/utils/resolveView.js +2 -1
  96. package/libx/_runtime/common/utils/rewriteAsterisks.js +6 -4
  97. package/libx/_runtime/common/utils/search2cqn4sql.js +12 -16
  98. package/libx/_runtime/common/utils/stream.js +140 -0
  99. package/libx/_runtime/common/utils/streamProp.js +29 -12
  100. package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +0 -2
  101. package/libx/_runtime/db/generic/index.js +0 -2
  102. package/libx/_runtime/db/query/delete.js +2 -2
  103. package/libx/_runtime/db/query/insert.js +2 -2
  104. package/libx/_runtime/db/query/read.js +2 -2
  105. package/libx/_runtime/db/query/run.js +2 -2
  106. package/libx/_runtime/db/query/update.js +2 -2
  107. package/libx/_runtime/db/sql-builder/BaseBuilder.js +0 -6
  108. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +23 -12
  109. package/libx/_runtime/db/sql-builder/FunctionBuilder.js +18 -6
  110. package/libx/_runtime/db/sql-builder/InsertBuilder.js +1 -0
  111. package/libx/_runtime/db/sql-builder/SelectBuilder.js +3 -7
  112. package/libx/_runtime/db/sql-builder/UpsertBuilder.js +1 -0
  113. package/libx/_runtime/db/utils/normalizeTimeData.js +7 -3
  114. package/libx/_runtime/fiori/draft.js +2 -0
  115. package/libx/_runtime/fiori/generic/activate.js +8 -9
  116. package/libx/_runtime/fiori/generic/before.js +30 -20
  117. package/libx/_runtime/fiori/generic/cancel.js +5 -3
  118. package/libx/_runtime/fiori/generic/delete.js +5 -3
  119. package/libx/_runtime/fiori/generic/edit.js +7 -7
  120. package/libx/_runtime/fiori/generic/index.js +10 -16
  121. package/libx/_runtime/fiori/generic/new.js +5 -3
  122. package/libx/_runtime/fiori/generic/patch.js +11 -8
  123. package/libx/_runtime/fiori/generic/prepare.js +13 -6
  124. package/libx/_runtime/fiori/generic/read.js +12 -6
  125. package/libx/_runtime/fiori/lean-draft.js +207 -152
  126. package/libx/_runtime/fiori/utils/delete.js +10 -5
  127. package/libx/_runtime/fiori/utils/req.js +17 -5
  128. package/libx/_runtime/fiori/utils/stream.js +36 -0
  129. package/libx/_runtime/hana/Service.js +12 -9
  130. package/libx/_runtime/hana/conversion.js +10 -15
  131. package/libx/_runtime/hana/driver.js +2 -0
  132. package/libx/_runtime/hana/execute.js +28 -6
  133. package/libx/_runtime/hana/pool.js +36 -122
  134. package/libx/_runtime/hana/search2cqn4sql.js +34 -36
  135. package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +2 -6
  136. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +3 -1
  137. package/libx/_runtime/messaging/enterprise-messaging.js +10 -58
  138. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  139. package/libx/_runtime/remote/Service.js +20 -1
  140. package/libx/_runtime/remote/utils/client.js +3 -5
  141. package/libx/_runtime/sqlite/Service.js +4 -6
  142. package/libx/_runtime/sqlite/conversion.js +3 -13
  143. package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +9 -6
  144. package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +6 -1
  145. package/libx/_runtime/sqlite/execute.js +5 -16
  146. package/libx/odata/afterburner.js +22 -6
  147. package/libx/odata/grammar.pegjs +6 -1
  148. package/libx/odata/parser.js +1 -1
  149. package/libx/rest/RestAdapter.js +16 -9
  150. package/libx/rest/RestRequest.js +1 -1
  151. package/libx/rest/middleware/input.js +2 -1
  152. package/libx/rest/middleware/operation.js +1 -0
  153. package/libx/rest/middleware/parse.js +3 -2
  154. package/libx/rest/middleware/payload.js +9 -8
  155. package/libx/rest/middleware/read.js +1 -0
  156. package/package.json +9 -16
  157. package/app/fiori/preview.js +0 -270
  158. package/app/fiori/routes.js +0 -59
  159. package/bin/build/buildTaskEngine.js +0 -360
  160. package/bin/build/buildTaskFactory.js +0 -283
  161. package/bin/build/buildTaskHandler.js +0 -241
  162. package/bin/build/buildTaskProvider.js +0 -22
  163. package/bin/build/buildTaskProviderFactory.js +0 -175
  164. package/bin/build/cds.js +0 -5
  165. package/bin/build/constants.js +0 -66
  166. package/bin/build/index.js +0 -58
  167. package/bin/build/provider/buildTaskHandlerEdmx.js +0 -82
  168. package/bin/build/provider/buildTaskHandlerFeatureToggles.js +0 -131
  169. package/bin/build/provider/buildTaskHandlerInternal.js +0 -254
  170. package/bin/build/provider/buildTaskProviderInternal.js +0 -383
  171. package/bin/build/provider/fiori/index.js +0 -171
  172. package/bin/build/provider/hana/2migration.js +0 -179
  173. package/bin/build/provider/hana/index.js +0 -505
  174. package/bin/build/provider/hana/migrationtable.js +0 -472
  175. package/bin/build/provider/hana/template/.hdiconfig-haas +0 -163
  176. package/bin/build/provider/hana/template/.hdiconfig-hanacloud +0 -137
  177. package/bin/build/provider/hana/template/.hdinamespace +0 -4
  178. package/bin/build/provider/hana/template/package.json +0 -12
  179. package/bin/build/provider/hana/template/undeploy.json +0 -5
  180. package/bin/build/provider/java/index.js +0 -111
  181. package/bin/build/provider/java-cf/index.js +0 -1
  182. package/bin/build/provider/mtx/index.js +0 -268
  183. package/bin/build/provider/mtx/resourcesTarBuilder.js +0 -95
  184. package/bin/build/provider/mtx-extension/index.js +0 -131
  185. package/bin/build/provider/mtx-sidecar/index.js +0 -137
  186. package/bin/build/provider/node-cf/index.js +0 -1
  187. package/bin/build/provider/nodejs/index.js +0 -192
  188. package/bin/build/util.js +0 -299
  189. package/bin/cds.js +0 -125
  190. package/bin/deploy/to-hana/cfUtil.js +0 -355
  191. package/bin/deploy/to-hana/gitUtil.js +0 -57
  192. package/bin/deploy/to-hana/hana.js +0 -306
  193. package/bin/deploy/to-hana/hdiDeployUtil.js +0 -153
  194. package/bin/deploy/to-hana/index.js +0 -16
  195. package/bin/deploy/to-hana/mtaUtil.js +0 -170
  196. package/bin/mtx/in-cds.js +0 -17
  197. package/bin/plugins.js +0 -32
  198. package/bin/run.js +0 -24
  199. package/bin/utils/log.js +0 -24
  200. package/bin/version.js +0 -178
  201. package/libx/_runtime/audit/Service.js +0 -222
  202. package/libx/_runtime/audit/generic/personal/access.js +0 -61
  203. package/libx/_runtime/audit/generic/personal/index.js +0 -56
  204. package/libx/_runtime/audit/generic/personal/modification.js +0 -132
  205. package/libx/_runtime/audit/generic/personal/utils.js +0 -186
  206. package/libx/_runtime/audit/utils/log.js +0 -23
  207. package/libx/_runtime/audit/utils/v2.js +0 -176
  208. package/libx/_runtime/db/data-conversion/timestamp.js +0 -9
  209. package/libx/_runtime/db/generic/integrity.js +0 -455
  210. package/srv/audit-log.cds +0 -87
  211. package/srv/mtx.cds +0 -2
  212. package/srv/mtx.js +0 -8
  213. /package/lib/{core → linked}/classes.js +0 -0
  214. /package/lib/{core → linked}/entities.js +0 -0
@@ -57,27 +57,30 @@ const _log = (req, challenges) => {
57
57
  LOG.debug(`User "${req.user.id}" request URL`, req.url, '\n', ...challengesLog)
58
58
  }
59
59
 
60
- const cap_auth_callback = (req, res, next, internalError, user, challenges) => {
60
+ const cap_auth_callback = (req, res, next, internalError, user, arg) => {
61
61
  // An internal error occurs during the authentication process
62
62
  if (internalError) {
63
- // REVISIT: What to do? Security log?
64
- return res.status(401).json(UNAUTHORIZED) // no details to client
63
+ return res.status(401).json({ error: UNAUTHORIZED }) // no details to client
65
64
  }
66
65
 
67
- let infoChallenges
68
-
69
- if (challenges) {
70
- if (Array.isArray(challenges)) {
71
- infoChallenges = challenges.filter(ele => ele)
72
- infoChallenges = infoChallenges.length ? infoChallenges : undefined
66
+ let challenges
67
+ if (arg) {
68
+ if (!user) {
69
+ // > challenges
70
+ if (Array.isArray(arg)) {
71
+ challenges = arg.filter(ele => ele)
72
+ challenges = challenges.length ? challenges : undefined
73
+ } else {
74
+ challenges = [arg]
75
+ }
73
76
  } else {
74
- req.authInfo = challenges
77
+ // REVISIT: req._.req.authInfo compat
78
+ if (arg.verifyToken) req.authInfo = arg
75
79
  }
76
80
  }
81
+ req.user = user || Object.defineProperty(new cds.User(), '_challenges', { enumerable: false, value: challenges })
82
+ _log(req, challenges)
77
83
 
78
- req.user = user || Object.defineProperty(new cds.User(), '_challenges', { enumerable: false, value: infoChallenges })
79
- Object.defineProperty(req.user, '_req', { enumerable: false, value: req })
80
- _log(req, infoChallenges)
81
84
  next()
82
85
  }
83
86
 
@@ -102,14 +105,13 @@ const _mountMockAuth = (srv, app, strategy, config) => {
102
105
  }
103
106
 
104
107
  const _mountPassportAuth = (srv, app, strategy, config) => {
105
- if (!config.credentials)
106
- return (
107
- LOG._warn &&
108
- LOG.warn(`
109
- No XSUAA instance bound to application, but "${config.strategy}" configured.
110
- This is NOT recommended in production!
111
- `)
112
- )
108
+ if (strategy in { jwt: 1, xsuaa: 1 } && !config.credentials) {
109
+ LOG._warn &&
110
+ LOG.warn(`Authentication kind "${config.kind}" configured, but no XSUAA instance bound to application.
111
+ This is NOT recommended in production!`)
112
+ return
113
+ }
114
+
113
115
  if (!passport) passport = _require('passport')
114
116
 
115
117
  // initialize strategy
@@ -134,6 +136,7 @@ const _mountPassportAuth = (srv, app, strategy, config) => {
134
136
  module.exports = (srv, options = srv.options) => {
135
137
  const handlers = [],
136
138
  app = { use: h => handlers.push(h) }
139
+
137
140
  // NOTE: options.auth is not an official API
138
141
  let config = 'auth' in options ? options.auth : cds.env.requires.auth
139
142
  if (!config) {
@@ -192,6 +195,7 @@ module.exports = (srv, options = srv.options) => {
192
195
 
193
196
  // so we don't log the same stuff multiple times
194
197
  logged = true
198
+
195
199
  return handlers
196
200
  }
197
201
 
@@ -207,8 +211,8 @@ const cap_enforce_login = (req, res, next) => {
207
211
  if (req.user && req.user.is('authenticated-user')) return next() // pass if user is authenticated
208
212
  if (!req.user || req.user._is_anonymous) {
209
213
  if (req.user && req.user._challenges) res.set('WWW-Authenticate', req.user._challenges.join(';'))
210
- return res.status(401).json(UNAUTHORIZED) // no details to client // REVISIT: security log in else case?
214
+ return res.status(401).json({ error: UNAUTHORIZED }) // no details to client
211
215
  } else {
212
- return res.status(403).json(FORBIDDEN) // no details to client // REVISIT: security log?
216
+ return res.status(403).json({ error: FORBIDDEN }) // no details to client
213
217
  }
214
218
  }
@@ -1,3 +1,4 @@
1
+ /* eslint-disable complexity */
1
2
  const cds = require('../../../cds')
2
3
  // requesting logger without module on purpose!
3
4
  const LOG = cds.log()
@@ -15,6 +16,8 @@ const metaInfo = require('./utils/metaInfo')
15
16
  const odataToCQN = require('./odata-to-cqn')
16
17
  const { getData, getParams } = require('./utils/data')
17
18
  const { isCustomOperation } = require('./utils/request')
19
+ const { isStreaming } = require('./utils/stream')
20
+ const { handleStreamProperties } = require('../../../common/utils/streamProp')
18
21
 
19
22
  function _isCorrectCallToViewWithParams(csdlStructuredType) {
20
23
  return (
@@ -75,10 +78,11 @@ class ODataRequest extends cds.Request {
75
78
  ? odataReq.getBatchApplicationData().req
76
79
  : odataReq.getIncomingRequest()
77
80
  const res = req.res
81
+ const segments = odataReq.getUriInfo().getPathSegments()
78
82
 
79
83
  if (cds.env.features.odata_new_parser) {
80
84
  // REVISIT need to resolve target after replacing okra <= maybe just take one from afterburner?
81
- const target = _getTarget(service, [...odataReq.getUriInfo().getPathSegments()])
85
+ const target = _getTarget(service, [...segments])
82
86
  // REVISIT payload after replacing okra
83
87
  const data = getData(type, odataReq, service, target)
84
88
  const query = odataToCQN(type, service, target, data, odataReq, upsert)
@@ -88,7 +92,11 @@ class ODataRequest extends cds.Request {
88
92
  const { user } = req
89
93
  const info = metaInfo(query, type, service, data, req, upsert)
90
94
  const { event, unbound } = info
91
- if (event === 'READ') _add4Odata(query)
95
+ if (event === 'READ') {
96
+ _add4Odata(query)
97
+ if (query.SELECT && !query.SELECT.columns) query.SELECT.columns = ['*']
98
+ if (!isStreaming(segments)) handleStreamProperties(target, query, service.model, true)
99
+ }
92
100
  const _queryOptions = odataReq.getQueryOptions()
93
101
  super({ event, target, data, query: unbound ? {} : query, user, method, headers, req, res, _queryOptions })
94
102
  this._metaInfo = info.metadata
@@ -96,7 +104,7 @@ class ODataRequest extends cds.Request {
96
104
  /*
97
105
  * target
98
106
  */
99
- const target = _getTarget(service, [...odataReq.getUriInfo().getPathSegments()])
107
+ const target = _getTarget(service, [...segments])
100
108
 
101
109
  /*
102
110
  * data
@@ -106,9 +114,7 @@ class ODataRequest extends cds.Request {
106
114
  /*
107
115
  * query
108
116
  */
109
- const operation = isCustomOperation(odataReq.getUriInfo().getPathSegments())
110
- ? odataReq.getUriInfo().getLastSegment().getKind()
111
- : type
117
+ const operation = isCustomOperation(segments) ? odataReq.getUriInfo().getLastSegment().getKind() : type
112
118
  const query = odataToCQN(operation, service, target, data, odataReq, upsert)
113
119
 
114
120
  /*
@@ -147,6 +153,8 @@ class ODataRequest extends cds.Request {
147
153
  else if (type === 'DELETE' && data.IsActiveEntity !== true) event = 'CANCEL'
148
154
  }
149
155
 
156
+ if (query.STREAM) event = 'STREAM'
157
+
150
158
  // mark query as for an OData READ
151
159
  if (event === 'READ') Object.defineProperty(query.SELECT, '_4odata', { value: true })
152
160
 
@@ -168,23 +176,6 @@ class ODataRequest extends cds.Request {
168
176
  super({ event, target, data, query, user, method, headers, req, res, _queryOptions, tenant })
169
177
  }
170
178
 
171
- /*
172
- * req.run
173
- */
174
- Object.defineProperty(this, 'run', {
175
- configurable: true,
176
- get:
177
- () =>
178
- (...args) => {
179
- if (!cds._deprecationWarningForRun) {
180
- LOG._warn && LOG.warn('req.run is deprecated and will be removed.')
181
- cds._deprecationWarningForRun = true
182
- }
183
-
184
- return cds.tx(this).run(...args)
185
- }
186
- })
187
-
188
179
  /*
189
180
  * req.params
190
181
  */
@@ -233,55 +224,7 @@ class ODataRequest extends cds.Request {
233
224
  }
234
225
  })
235
226
 
236
- /*
237
- * req.isConcurrentResource
238
- */
239
- // REVISIT: re-implement in runtime w/o using okra
240
- Object.defineProperty(this, 'isConcurrentResource', {
241
- get() {
242
- this._isConcurrentResource = this._isConcurrentResource || odataReq.getConcurrentResource() !== null
243
- return this._isConcurrentResource
244
- }
245
- })
246
-
247
- /*
248
- * req.isConditional
249
- */
250
- // REVISIT: re-implement in runtime w/o using okra
251
- Object.defineProperty(this, 'isConditional', {
252
- get() {
253
- this._isConditional = this._isConditional || odataReq.isConditional()
254
- return this._isConditional
255
- }
256
- })
257
-
258
- Object.defineProperty(this, '_isOData', { value: true })
259
-
260
- /*
261
- * req.validateEtag()
262
- */
263
- // REVISIT: re-implement in runtime w/o using okra
264
- this.validateEtag = (...args) => {
265
- return odataReq.validateEtag(...args)
266
- }
267
-
268
- /*
269
- * req.getUriInfo()
270
- * req.getUrlObject()
271
- *
272
- * In draft context req object is cloned.
273
- * Defining a property here will not work.
274
- */
275
- // REVISIT: re-implement in runtime w/o using okra
276
- this.getUriInfo = () => {
277
- this._uriInfo = this._uriInfo || odataReq.getUriInfo()
278
- return this._uriInfo
279
- }
280
-
281
- this.getUrlObject = () => {
282
- this._urlObject = this._urlObject || odataReq.getUrlObject()
283
- return this._urlObject
284
- }
227
+ Object.defineProperty(this, 'protocol', { value: 'odata-v4' })
285
228
  }
286
229
  }
287
230
 
@@ -58,7 +58,7 @@ const create = service => {
58
58
  }
59
59
 
60
60
  if (result == null || isReturnMinimal(req)) {
61
- odataRes.setStatusCode(204)
61
+ odataRes.setStatusCode(204, { overwrite: true })
62
62
  }
63
63
  } catch (e) {
64
64
  err = e
@@ -22,8 +22,6 @@ const metadata = service => {
22
22
  const { 'cds.xt.ModelProviderService': mps } = cds.services
23
23
  let edmx = mps
24
24
  ? await mps.getEdmx({ tenant, model: service.model, service: service.definition.name, locale })
25
- : cds.mtx && (await cds.mtx.isExtended(tenant))
26
- ? await cds.mtx.getEdmx(tenant, service.definition.name, locale)
27
25
  : cds.localize(
28
26
  service.model,
29
27
  locale,
@@ -17,12 +17,13 @@ const { isCustomOperation } = require('../utils/request')
17
17
  const { getActionOrFunctionReturnType } = require('../utils/handlerUtils')
18
18
  const { validateResourcePath } = require('../utils/request')
19
19
  const { toODataResult, postProcess } = require('../utils/result')
20
- const { isStreaming, getStreamProperties } = require('../utils/stream')
20
+ const { isStreaming } = require('../utils/stream')
21
21
  const { resolveStructuredName } = require('../utils/handlerUtils')
22
22
  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
27
 
27
28
  /**
28
29
  * Checks whether a bound function or function import is invoked.
@@ -144,43 +145,6 @@ const _isNavigationToOne = segments =>
144
145
  segments[segments.length - 1].getKind() === NAVIGATION_TO_ONE &&
145
146
  segments[segments.length - 1].getKeyPredicates().length === 0
146
147
 
147
- const _hasRedirectProperty = elements => {
148
- return Object.values(elements).some(val => {
149
- return val['@Core.IsURL']
150
- })
151
- }
152
-
153
- const _addMediaType = (key, entry, mediaType) => {
154
- if (mediaType) {
155
- if (typeof mediaType === 'object') {
156
- entry[`${key}@odata.mediaContentType`] = entry[Object.values(mediaType)[0]]
157
- } else {
158
- entry[`${key}@odata.mediaContentType`] = mediaType
159
- }
160
- }
161
- }
162
-
163
- const _transformRedirectProperties = (req, result) => {
164
- if (!Array.isArray(result) || result.length === 0) {
165
- return
166
- }
167
-
168
- // optimization
169
- if (!_hasRedirectProperty(req.target.elements)) {
170
- return
171
- }
172
-
173
- for (const entry of result) {
174
- for (const key in entry) {
175
- if (entry[key] !== undefined && req.target.elements[key]['@Core.IsURL']) {
176
- entry[`${key}@odata.mediaReadLink`] = entry[key]
177
- _addMediaType(key, entry, req.target.elements[key]['@Core.MediaType'])
178
- delete entry[key]
179
- }
180
- }
181
- }
182
- }
183
-
184
148
  const _getResult = (nameArr, result) => {
185
149
  if (nameArr.length === 0) return result
186
150
  return _getResult(nameArr.slice(1), result[nameArr[0]])
@@ -211,6 +175,10 @@ const _readEntityOrProperty = async (tx, req, segments) => {
211
175
  * If no entity is related, the service returns 204 No Content.
212
176
  */
213
177
  if (result == null) {
178
+ if (req.headers['if-none-match']) {
179
+ req._.odataRes.setStatusCode(304)
180
+ return
181
+ }
214
182
  if (_isNavigationToOne(segments)) return toODataResult(null)
215
183
  throw getError(404)
216
184
  }
@@ -226,8 +194,6 @@ const _readEntityOrProperty = async (tx, req, segments) => {
226
194
  const propertyElement = segments[segments.length - index].getProperty()
227
195
 
228
196
  if (propertyElement === null) {
229
- _transformRedirectProperties(req, result)
230
-
231
197
  return toODataResult(result[0], req)
232
198
  }
233
199
 
@@ -323,8 +289,6 @@ const _readCollection = async (tx, req, odataReq) => {
323
289
 
324
290
  const odataResult = toODataResult(result, req)
325
291
 
326
- _transformRedirectProperties(req, result)
327
-
328
292
  return odataResult
329
293
  }
330
294
 
@@ -341,22 +305,26 @@ const _readCollection = async (tx, req, odataReq) => {
341
305
  const _readStream = async (tx, req) => {
342
306
  req.query._streaming = true
343
307
 
344
- const { contentType, contentDispositionFilename, contentDispositionType } = await getStreamProperties(req, tx.model)
345
-
346
308
  let result = await tx.dispatch(req)
347
309
 
348
310
  // REVISIT: compat, should actually be treated as object
349
311
  if (!Array.isArray(result)) result = [result]
350
312
 
351
313
  // Reading one entity or a property of it should yield only a result length of one.
352
- if (result.length === 0 || result[0] === undefined) throw getError(404)
314
+ if (result.length === 0 || result[0] === undefined) {
315
+ if (req.headers['if-none-match']) {
316
+ req._.odataRes.setStatusCode(304)
317
+ return
318
+ }
319
+ throw getError(404)
320
+ }
353
321
 
354
322
  if (result.length > 1) throw getError(400)
355
323
 
356
324
  if (result[0] === null) return null
357
325
 
358
326
  result = result[0]
359
- const { value: readable } = result
327
+ const readable = result.value
360
328
 
361
329
  if (readable) {
362
330
  readable.on('error', () => {
@@ -367,6 +335,7 @@ const _readStream = async (tx, req) => {
367
335
  }
368
336
 
369
337
  const headers = req._.odataReq.getHeaders()
338
+ const contentType = result.$mediaContentType
370
339
 
371
340
  if (
372
341
  contentType &&
@@ -379,12 +348,6 @@ const _readStream = async (tx, req) => {
379
348
  req.reject(406, `Content type "${contentType}" not listed in accept header "${headers.accept}".`)
380
349
  }
381
350
 
382
- if (contentType) result.$mediaContentType = contentType
383
- if (contentDispositionFilename) {
384
- result.$mediaContentDispositionFilename = contentDispositionFilename
385
- if (contentDispositionType) result.$mediaContentDispositionType = contentDispositionType
386
- }
387
-
388
351
  return toODataResult(result, req)
389
352
  }
390
353
 
@@ -424,20 +387,26 @@ const _readAndTransform = (tx, req, odataReq) => {
424
387
  return _readCollection(tx, req, odataReq)
425
388
  }
426
389
 
427
- // REVISIT: move to afterburner
428
- if (segments[segments.length - 1]._isStreamByDollarValue) {
429
- for (const k in req.target.elements) {
430
- if (req.target.elements[k]['@Core.MediaType']) {
431
- req.query.SELECT.columns = [{ ref: [k] }]
432
- break
433
- }
390
+ if (isNewStream()) {
391
+ if (req.query.STREAM) {
392
+ return _readStream(tx, req)
434
393
  }
394
+ } else {
395
+ // REVISIT: move to afterburner
396
+ if (segments[segments.length - 1]._isStreamByDollarValue) {
397
+ for (const k in req.target.elements) {
398
+ if (req.target.elements[k]['@Core.MediaType']) {
399
+ req.query.SELECT.columns = [{ ref: [k] }]
400
+ break
401
+ }
402
+ }
435
403
 
436
- return _readStream(tx, req)
437
- }
404
+ return _readStream(tx, req)
405
+ }
438
406
 
439
- if (isStreaming(segments)) {
440
- return _readStream(tx, req)
407
+ if (isStreaming(segments)) {
408
+ return _readStream(tx, req)
409
+ }
441
410
  }
442
411
 
443
412
  if (req.target._isSingleton) {
@@ -448,6 +417,7 @@ const _readAndTransform = (tx, req, odataReq) => {
448
417
  }
449
418
 
450
419
  const _postProcess = (odataReq, req, odataRes, service, result) => {
420
+ transformRedirectProperties(req, service, result.value)
451
421
  const functionReturnType = getActionOrFunctionReturnType(
452
422
  odataReq.getUriInfo().getPathSegments(),
453
423
  service.model.definitions
@@ -10,6 +10,11 @@ module.exports = srv => {
10
10
  const req = odataReq.getBatchApplicationData()
11
11
  ? odataReq.getBatchApplicationData().req
12
12
  : odataReq.getIncomingRequest()
13
+
14
+ // REVISIT: ensure there always is a user (should be the case with new middlewares -> remove with old middlewares)
15
+ // prettier-ignore
16
+ if (!req.user) req.user = new cds.User.default
17
+
13
18
  const { res, user, path, headers } = req
14
19
 
15
20
  const { protectMetadata } = cds.env.odata
@@ -19,31 +24,22 @@ module.exports = srv => {
19
24
  }
20
25
 
21
26
  // in case of $batch we need to challenge directly, as the header is not processed if in $batch response body
22
- if (restricted && path.endsWith('/$batch')) {
23
- if (user?._challenges) {
24
- res.set('WWW-Authenticate', user._challenges.join(';'))
25
- return next(UNAUTHORIZED)
26
- } else if (user._is_anonymous && req.login) {
27
- res.set('WWW-Authenticate', `Basic realm="Users"`) // REVISIT: should be req.login(), and fiori app works with that but cds/tests/_runtime/auth/__tests__/strategies.test.js fails
28
- return next(UNAUTHORIZED)
29
- // return req.login()
30
- }
27
+ if (restricted && path.endsWith('/$batch') && req.user._is_anonymous) {
28
+ // NOTE: "return req._login()" would not invoke custom error handlers
29
+ if (req._login) res.set('WWW-Authenticate', `Basic realm="Users"`)
30
+ else if (user._challenges) res.set('WWW-Authenticate', user._challenges.join(';'))
31
+ return next(UNAUTHORIZED)
31
32
  }
32
33
 
33
34
  // check @requires as soon as possible (DoS)
34
35
  if (requires && !requires.some(r => user.is(r))) {
35
36
  // > unauthorized or forbidden?
36
- if (user._is_anonymous) {
37
- if (user._challenges) {
38
- res.set('WWW-Authenticate', user._challenges.join(';'))
39
- } else if (req.login) {
40
- res.set('WWW-Authenticate', `Basic realm="Users"`) // REVISIT: should be req.login(), and fiori app works with that but cds/tests/_runtime/auth/__tests__/strategies.test.js fails
41
- // return req.login()
42
- }
43
- // REVISIT: security log in else case?
37
+ if (req.user._is_anonymous) {
38
+ // NOTE: "return req._login()" would not invoke custom error handlers
39
+ if (req._login) res.set('WWW-Authenticate', `Basic realm="Users"`)
40
+ else if (user._challenges) res.set('WWW-Authenticate', user._challenges.join(';'))
44
41
  return next(UNAUTHORIZED)
45
42
  }
46
- // REVISIT: security log?
47
43
  return next(FORBIDDEN)
48
44
  }
49
45
 
@@ -72,7 +72,7 @@ const _create = async (req, odataReq, odataRes, tx) => {
72
72
  }
73
73
  })
74
74
 
75
- odataRes.setStatusCode(201)
75
+ odataRes.setStatusCode(201, { overwrite: true })
76
76
  req = new ODataRequest(DATA_CREATE_HANDLER, tx, odataReq, odataRes, true)
77
77
  result = await tx.dispatch(req)
78
78
 
@@ -93,9 +93,14 @@ const _updateThenCreate = async (req, odataReq, odataRes, tx) => {
93
93
  try {
94
94
  result = await tx.dispatch(req)
95
95
  } catch (e) {
96
- if ((e.code === 404 || e.status === 404 || e.statusCode === 404) && _isUpsertAllowed(req.target)) {
97
- // REVISIT: remove error (and child?) from tx.context? -> would require a unique req.id
96
+ const is404 = e.code === 404 || e.status === 404 || e.statusCode === 404
97
+ if (is404 && _isUpsertAllowed(req.target)) {
98
+ // PUT/ PATCH with if-match header means "only if already exists", i.e., no insert if not
99
+ if (req.headers['if-match'])
100
+ throw Object.assign(new Error('412'), { statusCode: 412 })
101
+ // REVISIT: remove error (and child?) from tx.context? -> would require a unique req.id
98
102
  ;[result, req] = await _create(req, odataReq, odataRes, tx)
103
+ odataRes.setStatusCode(201, { overwrite: true })
99
104
  } else {
100
105
  throw e
101
106
  }
@@ -109,6 +114,10 @@ const _shouldReadPreviousResult = req =>
109
114
  !isReturnMinimal(req) &&
110
115
  (hasOmitValuesPreference(req.headers.prefer, 'defaults') || hasOmitValuesPreference(req.headers.prefer, 'nulls'))
111
116
 
117
+ const _hasEtag = target => {
118
+ return target._etag
119
+ }
120
+
112
121
  /**
113
122
  * The handler that will be registered with odata-v4.
114
123
  *
@@ -149,12 +158,13 @@ const update = service => {
149
158
  // try UPDATE and, on 404 error, try CREATE
150
159
  ;[result, req] = await _updateThenCreate(req, odataReq, odataRes, tx)
151
160
 
152
- if (!primitive && req._.readAfterWrite) {
161
+ if (!(primitive && !_hasEtag(req.target)) && req._.readAfterWrite) {
153
162
  // REVISIT:
154
163
  // Performance: For `isReturnMinimal` it's enough to just read the etag.
155
164
  // Note: Without read access, one cannot return the etag.
156
165
  result = await readAfterWrite(req, service, { operation: { result } })
157
166
  }
167
+
158
168
  if (!isReturnMinimal(req)) {
159
169
  postProcess(req, odataRes, service, result, previousResult)
160
170
  } else {
@@ -169,7 +179,7 @@ const update = service => {
169
179
  }
170
180
 
171
181
  if (result == null || isReturnMinimal(req)) {
172
- odataRes.setStatusCode(204)
182
+ odataRes.setStatusCode(204, { overwrite: true })
173
183
  }
174
184
  } catch (e) {
175
185
  err = e
@@ -7,6 +7,7 @@ const ResourceKind = odata.uri.UriResource.ResourceKind
7
7
  const EdmPrimitiveTypeKind = odata.edm.EdmPrimitiveTypeKind
8
8
  const { getFeatureNotSupportedError } = require('../../../util/errors')
9
9
  const { getSegmentKeyValue } = require('./utils')
10
+ const normalizeTimestamp = require('../../../../common/utils/normalizeTimestamp')
10
11
 
11
12
  const _binaryOperatorToCQN = new Map([
12
13
  [BinaryOperatorKind.EQ, '='],
@@ -46,10 +47,10 @@ class ExpressionToCQN {
46
47
  return { val: parseFloat(value) }
47
48
  case EdmPrimitiveTypeKind.DateTimeOffset: {
48
49
  try {
49
- let val = new Date(value).toISOString()
50
- // cut off ms if cds.DateTime
51
- if (expression._cdsType === 'cds.DateTime') val = val.replace(/\.\d\d\dZ$/, 'Z')
52
- return { val }
50
+ if (expression._cdsType === 'cds.DateTime')
51
+ // cut off ms if cds.DateTime
52
+ return { val: new Date(value).toISOString().replace(/\.\d\d\dZ$/, 'Z') }
53
+ return { val: normalizeTimestamp(value) }
53
54
  } catch (e) {
54
55
  throw Object.assign(new Error(`The type 'Edm.DateTimeOffset' is not compatible with '${value}'`), {
55
56
  status: 400