@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
@@ -2,6 +2,7 @@ const cds = require('../cds'),
2
2
  { Object_keys } = cds.utils
3
3
  const LOG = cds.log('fiori|drafts')
4
4
  const original = Symbol('original')
5
+ const DRAFT_PARAMS = Symbol('draftParams')
5
6
 
6
7
  const DRAFT_ELEMENTS = new Set([
7
8
  'IsActiveEntity',
@@ -32,23 +33,11 @@ const _fillIsActiveEntity = (row, IsActiveEntity, target) => {
32
33
  if (!prop) continue
33
34
  const el = target.elements[key]
34
35
  const childIsActiveEntity = el._target.isDraft ? IsActiveEntity : true
35
- if (Array.isArray(prop)) prop.map(r => _fillIsActiveEntity(r, childIsActiveEntity, el._target))
36
- else if (typeof prop === 'object') _fillIsActiveEntity(prop, childIsActiveEntity, el._target)
36
+ const propArray = Array.isArray(prop) ? prop : [prop]
37
+ propArray.forEach(r => _fillIsActiveEntity(r, childIsActiveEntity, el._target))
37
38
  }
38
39
  }
39
40
 
40
- // REVISIT: should not be necessary
41
- const _runWithContext = (srv, req, obj) => {
42
- const r = new cds.Request(obj)
43
- r.event // invoke getter
44
- r._ = Object.assign(r._, req._)
45
- if (req.getUriInfo) r.getUriInfo = () => req.getUriInfo()
46
- if (req.getUrlObject) r.getUrlObject = () => req.getUrlObject()
47
- r._.params = req.params
48
- r._.query = req.query
49
- return srv.dispatch(r)
50
- }
51
-
52
41
  /// It's important to wait for the completion of all promises, otherwise a rollback might happen too soon
53
42
  const _promiseAll = async array => {
54
43
  const results = await Promise.allSettled(array)
@@ -105,25 +94,40 @@ cds.ApplicationService.prototype.handle = async function (req) {
105
94
  if (
106
95
  !req.query ||
107
96
  req.query.UPSERT || // skip UPSERTs (might have an additional INSERT)
108
- (!req.query.SELECT && !req.query.INSERT && !req.query.UPDATE && !req.query.DELETE) ||
109
- req.query._draftParams
110
- )
97
+ (!req.query.SELECT && !req.query.INSERT && !req.query.UPDATE && !req.query.DELETE && !req.query.STREAM) ||
98
+ req.query[DRAFT_PARAMS]
99
+ ) {
111
100
  return handle(req)
101
+ }
102
+
103
+ const _etagValidationType = req.headers['if-match']
104
+ ? 'if-match'
105
+ : req.headers['if-none-match']
106
+ ? 'if-none-match'
107
+ : undefined
108
+
112
109
  const query = _cleansed(req.query, this.model)
113
110
  _cleanseParams(req.params, req.target)
114
111
  if (req.data) _cleanseParams(req.data, req.target)
115
- const draftParams = query._draftParams
112
+ const draftParams = query[DRAFT_PARAMS]
116
113
 
117
- const _newReq = (req, query, draftParams, event) => {
114
+ const _newReq = (req, query, draftParams, { event, headers }) => {
118
115
  // REVISIT: This is a bit hacky -> better way?
119
116
  query._target = undefined
120
- query._draftParams = draftParams
117
+ query[DRAFT_PARAMS] = draftParams
121
118
  cds.infer(query, this.model.definitions)
122
119
 
123
120
  // REVISIT: This is extremely bad. We should be able to just create a copy without such hacks.
124
121
  const _req = cds.Request.for(req._) // REVISIT: this causes req._.data of WRITE reqs copied to READ reqs
122
+
123
+ if (headers) {
124
+ _req.headers = Object.create(req.headers)
125
+ Object.assign(_req.headers, headers)
126
+ }
127
+
125
128
  // If we create a `READ` event based on a modifying request, we delete data
126
129
  if (event === 'READ' && req.event !== 'READ') delete _req.data // which we fix here -> but this is an ugly workaround
130
+
127
131
  _req.query = query
128
132
  _req.event =
129
133
  event ||
@@ -137,10 +141,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
137
141
  _req._ = Object.assign({}, req._ || {})
138
142
  _req._.params = req.params
139
143
  _req._.query = query
140
- const props = ['_isRest', '_isOData', 'isConcurrentResource', 'isConditional', 'validateEtag']
141
- props.forEach(p => {
142
- if (req[p]) _req.p = req[p]
143
- })
144
+ if (req.protocol) _req.protocol = req.protocol
144
145
  _req._ = req._
145
146
  const cqnData = _req.query.UPDATE?.data || _req.query.INSERT?.entries?.[0]
146
147
  if (cqnData) _req.data = cqnData // must point to the same object
@@ -150,14 +151,48 @@ cds.ApplicationService.prototype.handle = async function (req) {
150
151
  }
151
152
  })
152
153
  if (req.tx) _req.tx = req.tx
154
+
153
155
  return _req
154
156
  }
155
157
 
156
- const run = async query => {
157
- const _req = _newReq(req, query, draftParams)
158
+ const run = (query, options = {}) => {
159
+ const _req = _newReq(req, query, draftParams, options)
158
160
  return handle(_req)
159
161
  }
160
162
 
163
+ if (req.event === 'READ') {
164
+ if (
165
+ !Object.keys(draftParams).length &&
166
+ !req.query._target.name?.endsWith('DraftAdministrativeData') &&
167
+ !req.query._target.drafts
168
+ ) {
169
+ req.query = query
170
+ return handle(req)
171
+ }
172
+ const read = req.query._target.name.endsWith('.drafts')
173
+ ? Read.ownDrafts
174
+ : draftParams.IsActiveEntity === false && draftParams.SiblingEntity_IsActiveEntity === null
175
+ ? Read.all
176
+ : draftParams.IsActiveEntity === true &&
177
+ draftParams.SiblingEntity_IsActiveEntity === null &&
178
+ (draftParams.DraftAdministrativeData_InProcessByUser === 'not null' ||
179
+ draftParams.DraftAdministrativeData_InProcessByUser === 'not ')
180
+ ? Read.lockedByAnotherUser
181
+ : draftParams.IsActiveEntity === true &&
182
+ draftParams.SiblingEntity_IsActiveEntity === null &&
183
+ draftParams.DraftAdministrativeData_InProcessByUser === ''
184
+ ? Read.unsavedChangesByAnotherUser
185
+ : draftParams.IsActiveEntity === true && draftParams.HasDraftEntity === false
186
+ ? Read.unchanged
187
+ : draftParams.IsActiveEntity === true
188
+ ? Read.onlyActives
189
+ : draftParams.IsActiveEntity === false
190
+ ? Read.ownDrafts
191
+ : Read.onlyActives
192
+ const result = await read(run, query)
193
+ return result
194
+ }
195
+
161
196
  if (req.event === 'NEW' || req.event === 'CANCEL' || req.event === 'draftPrepare') {
162
197
  if (draftParams.IsActiveEntity) req.reject(501)
163
198
  req.target = req.target.drafts
@@ -167,34 +202,29 @@ cds.ApplicationService.prototype.handle = async function (req) {
167
202
  else if (query.INSERT.into.ref) query.INSERT.into.ref = _redirectRefToDrafts(query.INSERT.into.ref, this.model)
168
203
  } else if (query.DELETE?.from?.ref) query.DELETE.from.ref = _redirectRefToDrafts(query.DELETE.from.ref, this.model)
169
204
  else if (query.SELECT?.from?.ref) query.SELECT.from.ref = _redirectRefToDrafts(query.SELECT.from.ref, this.model)
170
- const _req = _newReq(req, query, draftParams, req.event)
205
+ const _req = _newReq(req, query, draftParams, { event: req.event })
171
206
  const result = await handle(_req)
172
207
  return result
173
208
  }
174
209
 
175
210
  if (req.event === 'DELETE' && draftParams.IsActiveEntity) {
176
211
  const draftsRef = _redirectRefToDrafts(query.DELETE.from.ref, this.model)
177
- // NOTE: Check if draft is locked!
178
- const draft = await run(
179
- SELECT.one.from({ ref: draftsRef }).columns([
180
- { ref: ['DraftAdministrativeData_DraftUUID'] },
181
- {
182
- ref: ['DraftAdministrativeData'],
183
- expand: [_inProcessByUserXpr(_lock.shiftedNow)]
184
- }
185
- ])
186
- )
212
+ const draft = await SELECT.one.from({ ref: draftsRef }).columns([
213
+ { ref: ['DraftAdministrativeData_DraftUUID'] },
214
+ {
215
+ ref: ['DraftAdministrativeData'],
216
+ expand: [_inProcessByUserXpr(_lock.shiftedNow)]
217
+ }
218
+ ])
187
219
  const inProcessByUser = draft?.DraftAdministrativeData?.InProcessByUser
188
220
  if (inProcessByUser && inProcessByUser !== cds.context.user.id)
189
221
  req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [inProcessByUser])
190
222
  const deletes = [run(DELETE.from({ ref: query.DELETE.from.ref }))]
191
223
  if (draft)
192
224
  deletes.push(
193
- run(
194
- DELETE.from(req.target.drafts).where({
195
- DraftAdministrativeData_DraftUUID: draft.DraftAdministrativeData_DraftUUID
196
- })
197
- )
225
+ DELETE.from(req.target.drafts).where({
226
+ DraftAdministrativeData_DraftUUID: draft.DraftAdministrativeData_DraftUUID
227
+ })
198
228
  )
199
229
  if (draft && req.target['@Common.DraftRoot.ActivationAction'])
200
230
  deletes.push(
@@ -214,18 +244,19 @@ cds.ApplicationService.prototype.handle = async function (req) {
214
244
  const targetDraft = req.target.drafts
215
245
  const targetWhere = query.SELECT.from.ref[0].where
216
246
  const cols = expandStarStar(targetDraft)
247
+ // Use `run` (since also etags might need to be checked)
248
+ // REVISIT: Find a better approach (`etag` as part of CQN?)
217
249
  const res = await run(
218
250
  SELECT.one
219
- .from(targetDraft)
251
+ .from({ ref: _redirectRefToDrafts(query.SELECT.from.ref, this.model) })
220
252
  .columns(cols)
221
253
  .columns([
222
254
  'HasActiveEntity',
223
255
  'DraftAdministrativeData_DraftUUID',
224
256
  { ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] }
225
257
  ])
226
- .where(targetWhere)
227
258
  )
228
- if (!res) req.reject(404)
259
+ if (!res) req.reject(_etagValidationType ? 412 : 404)
229
260
  if (res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
230
261
  req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [res.DraftAdministrativeData?.InProcessByUser])
231
262
  const DraftAdministrativeData_DraftUUID = res.DraftAdministrativeData_DraftUUID
@@ -236,14 +267,15 @@ cds.ApplicationService.prototype.handle = async function (req) {
236
267
  delete res.IsActiveEntity
237
268
  // First run the handlers as they might need access to DraftAdministrativeData or the draft entities
238
269
  const result = await run(
239
- HasActiveEntity ? UPDATE(req.target).data(res).where(targetWhere) : INSERT.into(req.target).entries(res)
270
+ HasActiveEntity ? UPDATE(req.target).data(res).where(targetWhere) : INSERT.into(req.target).entries(res),
271
+ { headers: { 'if-match': '*' } }
240
272
  )
241
273
  await _promiseAll([
242
- run(DELETE.from(targetDraft).where(targetWhere)),
274
+ DELETE.from(targetDraft).where(targetWhere),
243
275
  DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: DraftAdministrativeData_DraftUUID })
244
276
  ])
245
277
 
246
- req?._?.odataRes?.setStatusCode(201)
278
+ if (!HasActiveEntity) req?._?.odataRes?.setStatusCode(201, { overwrite: true })
247
279
 
248
280
  return Object.assign(result, { IsActiveEntity: true })
249
281
 
@@ -256,35 +288,24 @@ cds.ApplicationService.prototype.handle = async function (req) {
256
288
  const rootQuery = query.clone()
257
289
  rootQuery.SELECT.columns = [{ ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] }]
258
290
  rootQuery.SELECT.one = true
259
- const root = await run(rootQuery)
291
+ const root = await rootQuery
260
292
  if (!root) req.reject(404)
261
293
  if (root.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id) req.reject(403)
262
- const _req = _newReq(req, query, draftParams, req.event)
294
+ const _req = _newReq(req, query, draftParams, { event: req.event })
263
295
  const result = await handle(_req)
264
296
  return result
265
297
  }
266
298
 
267
299
  if (req.event === 'PATCH') {
268
- if (draftParams.IsActiveEntity) req.reject(501)
269
- if (!('IsActiveEntity' in draftParams)) {
270
- const res = await run(
271
- SELECT.one.from({ ref: req.UPDATE.entity.ref }).columns('DraftAdministrativeData_DraftUUID')
272
- )
273
- if (res) req.reject(403, 'DRAFT_ALREADY_EXISTS')
274
- const _req = _newReq(req, query, draftParams, 'UPDATE')
275
- const result = await handle(_req)
276
- return result
277
- }
300
+ if (draftParams.IsActiveEntity || !('IsActiveEntity' in draftParams)) req.reject(501)
278
301
  if (draftParams.IsActiveEntity === false) {
279
302
  LOG.debug('patch draft')
280
303
  if (req.target?.name.endsWith('DraftAdministrativeData')) req.reject(405)
281
304
  const draftsRef = _redirectRefToDrafts(query.UPDATE.entity.ref, this.model)
282
- const res = await run(
283
- SELECT.one.from({ ref: draftsRef }).columns('DraftAdministrativeData_DraftUUID', {
284
- ref: ['DraftAdministrativeData'],
285
- expand: [{ ref: ['InProcessByUser'] }]
286
- })
287
- )
305
+ const res = await SELECT.one.from({ ref: draftsRef }).columns('DraftAdministrativeData_DraftUUID', {
306
+ ref: ['DraftAdministrativeData'],
307
+ expand: [{ ref: ['InProcessByUser'] }]
308
+ })
288
309
  if (!res) req.reject(404)
289
310
  if (res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
290
311
  req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [res.DraftAdministrativeData?.InProcessByUser])
@@ -304,36 +325,11 @@ cds.ApplicationService.prototype.handle = async function (req) {
304
325
  }
305
326
  }
306
327
 
307
- if (req.event === 'READ') {
308
- if (
309
- !Object.keys(draftParams).length &&
310
- !req.query._target.name?.endsWith('DraftAdministrativeData') &&
311
- !req.query._target.drafts
312
- ) {
313
- req.query = query
314
- return handle(req)
315
- }
316
- const read = req.query._target.name.endsWith('.drafts')
317
- ? Read.ownDrafts
318
- : draftParams.IsActiveEntity === false && draftParams.SiblingEntity_IsActiveEntity === null
319
- ? Read.all
320
- : draftParams.IsActiveEntity === true &&
321
- draftParams.SiblingEntity_IsActiveEntity === null &&
322
- (draftParams.DraftAdministrativeData_InProcessByUser === 'not null' ||
323
- draftParams.DraftAdministrativeData_InProcessByUser === 'not ')
324
- ? Read.lockedByAnotherUser
325
- : draftParams.IsActiveEntity === true &&
326
- draftParams.SiblingEntity_IsActiveEntity === null &&
327
- draftParams.DraftAdministrativeData_InProcessByUser === ''
328
- ? Read.unsavedChangesByAnotherUser
329
- : draftParams.IsActiveEntity === true && draftParams.HasDraftEntity === false
330
- ? Read.unchanged
331
- : draftParams.IsActiveEntity === true
332
- ? Read.onlyActives
333
- : draftParams.IsActiveEntity === false
334
- ? Read.ownDrafts
335
- : Read.onlyActives
336
- const result = await read(run, query)
328
+ if (req.event === 'STREAM' && draftParams.IsActiveEntity === false) {
329
+ if (query.STREAM.into?.ref) query.STREAM.into.ref = _redirectRefToDrafts(query.STREAM.into.ref, this.model)
330
+ else if (query.STREAM.from?.ref) query.STREAM.from.ref = _redirectRefToDrafts(query.STREAM.from.ref, this.model)
331
+ const _req = _newReq(req, query, draftParams, { event: req.event })
332
+ const result = await handle(_req)
337
333
  return result
338
334
  }
339
335
 
@@ -387,7 +383,7 @@ const Read = {
387
383
  if (ignoreDrafts) drafts = []
388
384
  else {
389
385
  try {
390
- drafts = await Read.complementaryDrafts(run, query, actives)
386
+ drafts = await Read.complementaryDrafts(query, actives)
391
387
  } catch (e) {
392
388
  drafts = []
393
389
  }
@@ -414,7 +410,7 @@ const Read = {
414
410
  draftsQuery.SELECT.orderBy = undefined
415
411
  draftsQuery.SELECT.columns = keys.map(k => ({ ref: [k] }))
416
412
 
417
- const drafts = await run(draftsQuery)
413
+ const drafts = await draftsQuery
418
414
  const res = await Read.onlyActives(run, query.where(Read.whereNotIn(query._target, drafts)), {
419
415
  ignoreDrafts: true
420
416
  })
@@ -498,7 +494,7 @@ const Read = {
498
494
  _fillIsActiveEntity(row, false, query._drafts._target)
499
495
  })
500
496
  Read.delete(query._target, actives, ownEditDrafts)
501
- const otherEditDrafts = await Read.complementaryDrafts(run, query, actives)
497
+ const otherEditDrafts = await Read.complementaryDrafts(query, actives)
502
498
  Read.merge(query._target, actives, otherEditDrafts, (row, other) => {
503
499
  if (other) {
504
500
  Object.assign(row, {
@@ -537,7 +533,7 @@ const Read = {
537
533
  [isLocked ? '>' : '<']: _lock.shiftedNow
538
534
  }
539
535
  })
540
- const drafts = await run(draftsQuery)
536
+ const drafts = await draftsQuery
541
537
  const actives = drafts.length
542
538
  ? await run(query.where(Read.whereIn(query._target, drafts)))
543
539
  : Object.assign([], { $count: 0 })
@@ -561,13 +557,12 @@ const Read = {
561
557
  const keys = Object_keys(target.keys).filter(k => k !== 'IsActiveEntity')
562
558
  const dataArray = data ? (Array.isArray(data) ? data : [data]) : []
563
559
  if (not && !dataArray.length) return []
564
- return [
565
- { list: keys.map(k => ({ ref: [k] })) },
566
- not ? 'not in' : 'in',
567
- { list: dataArray.map(r => ({ list: keys.map(k => ({ val: r[k] })) })) }
568
- ]
560
+ const left = { list: keys.map(k => ({ ref: [k] })) }
561
+ const op = not ? ['not', 'in'] : ['in']
562
+ const right = { list: dataArray.map(r => ({ list: keys.map(k => ({ val: r[k] })) })) }
563
+ return [left, ...op, right]
569
564
  },
570
- complementaryDrafts: (run, query, _actives) => {
565
+ complementaryDrafts: (query, _actives) => {
571
566
  const actives = Array.isArray(_actives) ? _actives : [_actives]
572
567
  if (!actives.length) return []
573
568
  const drafts = cds.ql.clone(query._drafts)
@@ -590,7 +585,7 @@ const Read = {
590
585
  drafts.SELECT.count = undefined
591
586
  drafts.SELECT.search = undefined
592
587
  drafts.SELECT.one = undefined
593
- return run(drafts)
588
+ return drafts
594
589
  },
595
590
  _makeArray: data => (Array.isArray(data) ? data : data ? [data] : []),
596
591
  _index: (target, data) => {
@@ -656,7 +651,7 @@ function _cleanseCols(columns, elements, target) {
656
651
  }
657
652
 
658
653
  /**
659
- * Creates a clone of the query, cleanses and collects all draft parameters into ._draftParams.
654
+ * Creates a clone of the query, cleanses and collects all draft parameters into DRAFT_PARAMS.
660
655
  */
661
656
  function _cleansed(query, model) {
662
657
  const draftParams = {} //> used to collect draft filter criteria
@@ -680,7 +675,7 @@ function _cleansed(query, model) {
680
675
  } else if (draftsQuery._target?.name.endsWith('.drafts')) {
681
676
  draftsQuery.SELECT.columns = _tweakAdminExpand(draftsQuery.SELECT.columns)
682
677
  }
683
- Object.defineProperty(draftsQuery, '_draftParams', { value: draftParams, enumerable: false })
678
+ draftsQuery[DRAFT_PARAMS] = draftParams
684
679
  Object.defineProperty(q, '_drafts', { value: draftsQuery })
685
680
  return draftsQuery
686
681
  }
@@ -692,7 +687,7 @@ function _cleansed(query, model) {
692
687
  })
693
688
  }
694
689
 
695
- Object.defineProperty(q, '_draftParams', { value: draftParams, enumerable: false })
690
+ q[DRAFT_PARAMS] = draftParams
696
691
  q[original] = query
697
692
  return q
698
693
 
@@ -700,8 +695,14 @@ function _cleansed(query, model) {
700
695
  const target = query._target
701
696
  const q = cds.ql.clone(query)
702
697
 
703
- const ref = q.SELECT?.from.ref || q.UPDATE?.entity.ref || q.INSERT?.into.ref || q.DELETE?.from.ref
704
- const cqn = q.SELECT || q.UPDATE || q.INSERT || q.DELETE
698
+ const ref =
699
+ q.SELECT?.from.ref ||
700
+ q.UPDATE?.entity.ref ||
701
+ q.INSERT?.into.ref ||
702
+ q.DELETE?.from.ref ||
703
+ q.STREAM?.into?.ref ||
704
+ q.STREAM?.from?.ref
705
+ const cqn = q.SELECT || q.UPDATE || q.INSERT || q.DELETE || q.STREAM
705
706
 
706
707
  if (ref) {
707
708
  let entity
@@ -714,6 +715,8 @@ function _cleansed(query, model) {
714
715
  else if (q.DELETE) q.DELETE.from = { ...q.DELETE.from, ref: cleansedRef }
715
716
  else if (q.UPDATE) q.UPDATE.entity = { ...q.UPDATE.entity, ref: cleansedRef }
716
717
  else if (q.INSERT) q.INSERT.into = { ...q.INSERT.into, ref: cleansedRef }
718
+ else if (q.STREAM?.from) q.STREAM.from = { ...q.STREAM.from, ref: cleansedRef }
719
+ else if (q.STREAM?.into) q.STREAM.into = { ...q.STREAM.into, ref: cleansedRef }
717
720
 
718
721
  // This only works for simple cases of `SiblingEntity`, e.g. `root(ID=1,IsActiveEntity=false)/SiblingEntity`
719
722
  // , check if there are more complicated use cases
@@ -797,15 +800,17 @@ function _cleansed(query, model) {
797
800
  function _cleanseWhere(xpr, draftParams) {
798
801
  const cleansed = []
799
802
  for (let i = 0; i < xpr.length; ++i) {
800
- let x = xpr[i],
801
- e = x.ref?.[0]
803
+ let x = xpr[i]
804
+ const e = x.ref?.[0]
802
805
  if (DRAFT_ELEMENTS.has(e) && !xpr[i + 2]) {
803
806
  continue
804
807
  }
805
808
  if (DRAFT_ELEMENTS.has(e) && xpr[i + 2]) {
806
809
  let { val } = xpr[i + 2]
807
810
  draftParams[x.ref.join('_')] = xpr[i + 1] === '!=' ? (typeof val === 'boolean' ? !val : 'not ' + val) : val
808
- i += 3
811
+ i += 2
812
+ const last = cleansed[cleansed.length - 1]
813
+ if (last === 'and' || last === 'or') cleansed.pop()
809
814
  continue
810
815
  }
811
816
  if (x.xpr) {
@@ -817,6 +822,8 @@ function _cleansed(query, model) {
817
822
  }
818
823
  cleansed.push(x)
819
824
  }
825
+ const first = cleansed[0]
826
+ if (first === 'and' || first === 'or') cleansed.shift()
820
827
  const last = cleansed[cleansed.length - 1]
821
828
  if (last === 'and' || last === 'or') cleansed.pop()
822
829
  if (cleansed.length) return cleansed
@@ -849,28 +856,26 @@ function expandStarStar(target, recursion = new Map()) {
849
856
 
850
857
  async function onNew(req) {
851
858
  LOG.debug('new draft')
852
- const isRoot = typeof req.query.INSERT.into === 'string' || req.query.INSERT.into.ref?.length === 1
859
+ const isDirectAccess = typeof req.query.INSERT.into === 'string' || req.query.INSERT.into.ref?.length === 1
853
860
  // Only allowed for pseudo draft roots (entities with this action)
854
- if (isRoot && !req.target.actives['@Common.DraftRoot.ActivationAction'])
861
+ if (isDirectAccess && !req.target.actives['@Common.DraftRoot.ActivationAction'])
855
862
  req.reject(403, 'DRAFT_MODIFICATION_ONLY_VIA_ROOT')
856
863
  let DraftUUID
857
- if (isRoot) DraftUUID = cds.utils.uuid()
864
+ if (isDirectAccess) DraftUUID = cds.utils.uuid()
858
865
  else {
859
- const rootData = await _runWithContext(this, req, {
860
- query: SELECT.one(req.query.INSERT.into.ref[0].id)
861
- .columns([
862
- { ref: ['DraftAdministrativeData_DraftUUID'] },
863
- { ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] }
864
- ])
865
- .where(req.query.INSERT.into.ref[0].where)
866
- })
866
+ const rootData = await SELECT.one(req.query.INSERT.into.ref[0].id)
867
+ .columns([
868
+ { ref: ['DraftAdministrativeData_DraftUUID'] },
869
+ { ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] }
870
+ ])
871
+ .where(req.query.INSERT.into.ref[0].where)
867
872
  if (!rootData) req.reject(404)
868
873
  if (rootData.DraftAdministrativeData?.InProcessByUser !== req.user.id)
869
874
  req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [rootData.DraftAdministrativeData.InProcessByUser])
870
875
  DraftUUID = rootData.DraftAdministrativeData_DraftUUID
871
876
  }
872
877
  const timestamp = cds.context.timestamp.toISOString() // REVISIT: toISOString should be done on db layer
873
- const adminDataCQN = isRoot
878
+ const adminDataCQN = isDirectAccess
874
879
  ? INSERT.into('DRAFT.DraftAdministrativeData').entries({
875
880
  DraftUUID,
876
881
  CreationDateTime: timestamp,
@@ -889,36 +894,38 @@ async function onNew(req) {
889
894
  })
890
895
  .where({ DraftUUID })
891
896
 
892
- const _assignDraftData = (obj, target) => {
897
+ const _setDraftUUIDandHasActiveEntity = (obj, target) => {
893
898
  const newObj = Object.assign({}, obj, { DraftAdministrativeData_DraftUUID: DraftUUID, HasActiveEntity: false })
894
899
  if (!target) return newObj
895
900
 
896
901
  // Also support deep insertions
897
902
  for (const key in newObj) {
898
903
  if (!target.elements[key]?.isComposition) continue
904
+ // do array trick
899
905
  if (Array.isArray(newObj[key]))
900
- newObj[key] = newObj[key].map(v => _assignDraftData(v, target.elements[key]._target))
906
+ newObj[key] = newObj[key].map(v => _setDraftUUIDandHasActiveEntity(v, target.elements[key]._target))
901
907
  else if (typeof newObj[key] === 'object') {
902
- newObj[key] = _assignDraftData(newObj[key], target.elements[key]._target)
908
+ newObj[key] = _setDraftUUIDandHasActiveEntity(newObj[key], target.elements[key]._target)
903
909
  }
904
910
  }
905
911
 
906
912
  return newObj
907
913
  }
908
914
 
909
- const draftData = _assignDraftData(req.query.INSERT.entries[0], req.target)
915
+ const draftData = _setDraftUUIDandHasActiveEntity(req.query.INSERT.entries[0], req.target)
910
916
 
911
917
  delete draftData.IsActiveEntity
912
918
  const draftCQN = INSERT.into(req.target).entries(draftData)
913
919
 
914
- await _promiseAll([cds.run(adminDataCQN), _runWithContext(this, req, { query: draftCQN })])
920
+ await _promiseAll([adminDataCQN, this.run(draftCQN)])
915
921
  req._.readAfterWrite = true
916
922
  return { ...draftData, IsActiveEntity: false }
917
923
  }
918
924
 
919
925
  async function onEdit(req) {
920
926
  LOG.debug('edit active')
921
- const draftParams = req.query._draftParams
927
+ // use symbol for _draftParams
928
+ const draftParams = req.query[DRAFT_PARAMS]
922
929
  if (req.query.SELECT.from.ref.length > 1 || draftParams.IsActiveEntity !== true) {
923
930
  req.reject(400, 'Action "draftEdit" can only be called on the root active entity')
924
931
  }
@@ -928,6 +935,7 @@ async function onEdit(req) {
928
935
 
929
936
  const DraftUUID = cds.utils.uuid()
930
937
 
938
+ // REVISIT: Later optimization if datasource === db: INSERT FROM SELECT
931
939
  const cols = expandStarStar(req.target)
932
940
  const _addDraftColumns = (target, columns) => {
933
941
  if (target.drafts) {
@@ -947,22 +955,24 @@ async function onEdit(req) {
947
955
  .columns({ ref: ['DraftAdministrativeData'], expand: [_inProcessByUserXpr(_lock.shiftedNow)] })
948
956
  .where(targetWhere)
949
957
  // prevent service to check for own user
950
- Object.defineProperty(existingDraft, '_draftParams', { value: draftParams, enumerable: false })
958
+ existingDraft[DRAFT_PARAMS] = draftParams
951
959
 
952
960
  const activeCQN = SELECT.one.from(req.target).columns(cols).where(targetWhere)
953
961
  activeCQN._suppressLocalization = true // in the future we should be able to just set activeCQN.SELECT.localized = false
954
962
 
955
963
  const activeCheck = SELECT.one(req.target).columns([1]).where(targetWhere).forUpdate()
956
- Object.defineProperty(activeCheck, '_draftParams', { value: draftParams, enumerable: false })
964
+ activeCheck[DRAFT_PARAMS] = draftParams
957
965
  // It's not possible to use `FOR UPDATE` in HANA if the view contains joins/unions. Unfortunately, we can't resolve the table entity
958
966
  // because we must trigger the app-service request on the target entity (which could be delegated to a remote service).
959
967
  // The best we can do is to catch a potential error
960
- await _runWithContext(this, req, { query: activeCheck }).catch(_ => {})
968
+ try {
969
+ await activeCheck
970
+ } catch {} // eslint-disable-line no-empty
961
971
 
962
972
  const [res, draft] = await _promiseAll([
963
- _runWithContext(this, req, { query: activeCQN }),
973
+ this._datasource.run(activeCQN),
964
974
  // no user check must be done here...
965
- _runWithContext(this, req, { query: existingDraft })
975
+ existingDraft
966
976
  ])
967
977
 
968
978
  if (!res) req.reject(404)
@@ -974,7 +984,7 @@ async function onEdit(req) {
974
984
  for (const key in req.target.drafts.keys) keys[key] = res[key]
975
985
  await _promiseAll([
976
986
  DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID }),
977
- _runWithContext(this, req, { query: DELETE.from(req.target.drafts).where(keys) })
987
+ DELETE.from(req.target.drafts).where(keys)
978
988
  ])
979
989
  }
980
990
 
@@ -995,11 +1005,12 @@ async function onEdit(req) {
995
1005
  res.DraftAdministrativeData_DraftUUID = DraftUUID
996
1006
  res.HasActiveEntity = true
997
1007
  delete res.DraftAdministrativeData
998
- await _runWithContext(this, req, { query: INSERT.into(targetDraft).entries(res) })
1008
+ // change to db run
1009
+ await INSERT.into(targetDraft).entries(res)
999
1010
 
1000
1011
  // REVISIT: we need to use okra API here because it must be set in the batched request
1001
1012
  // status code must be set in handler to allow overriding for FE V2
1002
- req?._?.odataRes?.setStatusCode(201)
1013
+ req?._?.odataRes?.setStatusCode(201, { overwrite: true })
1003
1014
 
1004
1015
  return { ...res, IsActiveEntity: false } // REVISIT: Flatten?
1005
1016
  }
@@ -1007,21 +1018,21 @@ async function onEdit(req) {
1007
1018
  async function onCancel(req) {
1008
1019
  LOG.debug('delete draft')
1009
1020
  const activeRef = _redirectRefToActives(req.query.DELETE.from.ref, this.model)
1010
- const draftParams = req.query._draftParams
1021
+ const draftParams = req.query[DRAFT_PARAMS]
1011
1022
 
1012
- const draftDelete = SELECT.one
1023
+ const draftQuery = SELECT.one
1013
1024
  .from({ ref: req.query.DELETE.from.ref })
1014
1025
  .columns([
1015
1026
  'DraftAdministrativeData_DraftUUID',
1016
1027
  { ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] }
1017
1028
  ])
1029
+ if (req._etagValidationClause) draftQuery.where(req._etagValidationClause)
1018
1030
  // do not add InProcessByUser restriction
1019
- Object.defineProperty(draftDelete, '_draftParams', { value: draftParams, enumerable: false })
1020
- const draft = await _runWithContext(this, req, { query: draftDelete })
1021
- if (draftParams.IsActiveEntity === false && !draft) req.reject(404)
1031
+ const draft = await draftQuery
1032
+ if (draftParams.IsActiveEntity === false && !draft) req.reject(req._etagValidationType ? 412 : 404)
1022
1033
  if (draft && draft.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
1023
1034
  req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [draft.DraftAdministrativeData?.InProcessByUser])
1024
- const queries = !draft ? [] : [_runWithContext(this, req, { query: DELETE.from({ ref: req.query.DELETE.from.ref }) })]
1035
+ const queries = !draft ? [] : [this.run(DELETE.from({ ref: req.query.DELETE.from.ref }))]
1025
1036
  if (draft && req.target['@Common.DraftRoot.ActivationAction'])
1026
1037
  // only for draft root
1027
1038
  queries.push(
@@ -1037,14 +1048,14 @@ async function onCancel(req) {
1037
1048
  })
1038
1049
  .where({ DraftUUID: draft.DraftAdministrativeData_DraftUUID })
1039
1050
  )
1040
- if (draftParams.IsActiveEntity) queries.push(_runWithContext(this, req, { query: DELETE.from({ ref: activeRef }) }))
1051
+ if (draftParams.IsActiveEntity) queries.push(this.run(DELETE.from({ ref: activeRef })))
1041
1052
  await _promiseAll(queries)
1042
1053
  return req.data
1043
1054
  }
1044
1055
 
1045
1056
  async function onPrepare(req) {
1046
1057
  LOG.debug('prepare draft')
1047
- const draftParams = req.query._draftParams
1058
+ const draftParams = req.query[DRAFT_PARAMS]
1048
1059
  if (req.query.SELECT.from.ref.length > 1 || draftParams.IsActiveEntity !== false) {
1049
1060
  req.reject(400, 'Action "draftPrepare" can only be called on the root draft entity')
1050
1061
  }
@@ -1057,8 +1068,8 @@ async function onPrepare(req) {
1057
1068
  })
1058
1069
  .columns(keys)
1059
1070
  .where(where)
1060
- Object.defineProperty(draftQuery, '_draftParams', { value: draftParams, enumerable: false })
1061
- const data = await _runWithContext(this, req, { query: draftQuery })
1071
+ draftQuery[DRAFT_PARAMS] = draftParams
1072
+ const data = await draftQuery
1062
1073
  if (!data) req.reject(404)
1063
1074
  if (data.DraftAdministrativeData?.InProcessByUser !== req.user.id)
1064
1075
  req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [data.DraftAdministrativeData?.InProcessByUser])
@@ -1066,4 +1077,48 @@ async function onPrepare(req) {
1066
1077
  return { ...data, IsActiveEntity: false }
1067
1078
  }
1068
1079
 
1069
- module.exports = { onNew, onEdit, onCancel, onPrepare }
1080
+ module.exports = {
1081
+ impl() {
1082
+ if (!this._datasource) this._datasource = cds.db
1083
+ function _wrapped(handler, isActiveEntity) {
1084
+ const fn = function (req, next) {
1085
+ if (!req.target?.drafts || (isActiveEntity && req.target.isDraft) || (!isActiveEntity && !req.target.isDraft))
1086
+ return next.call(this)
1087
+ return handler.call(this, req, next)
1088
+ }
1089
+ return fn
1090
+ }
1091
+ // Also runs those handlers if they're annotated with @odata.draft.enabled through extensibility
1092
+ this.on('NEW', '*', _wrapped(onNew, false))
1093
+ this.on('EDIT', '*', _wrapped(onEdit, true))
1094
+ this.on('CANCEL', '*', _wrapped(onCancel, false))
1095
+ this.on('draftPrepare', '*', _wrapped(onPrepare, false))
1096
+
1097
+ async function fioriReadCompat(req, next) {
1098
+ if (!req.target) return next()
1099
+ const data = await next()
1100
+ if (!data) return data
1101
+ let _key
1102
+ for (const key in req.target.keys) {
1103
+ if (key === 'IsActiveEntity') continue
1104
+ _key = key
1105
+ break
1106
+ }
1107
+ const _addIsActiveEntity = (data, IsActiveEntity) => {
1108
+ if (Array.isArray(data)) return data.map(d => _addIsActiveEntity(d, IsActiveEntity))
1109
+ if (_key in data) data.IsActiveEntity = IsActiveEntity
1110
+ }
1111
+ _addIsActiveEntity(data, !req.target?.isDraft)
1112
+ return data
1113
+ }
1114
+
1115
+ if (cds.env.fiori.draft_compat) {
1116
+ // register after read handlers to add `IsActiveEntity`,
1117
+ // so stakeholders have access to it when calling next()
1118
+
1119
+ // to check if data contains a key value
1120
+ this.on('READ', '*', _wrapped(fioriReadCompat, true))
1121
+ this.on('READ', '*', _wrapped(fioriReadCompat, false))
1122
+ }
1123
+ }
1124
+ }