@sap/cds 6.6.2 → 6.7.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 (136) hide show
  1. package/CHANGELOG.md +72 -2
  2. package/README.md +1 -1
  3. package/apis/connect.d.ts +11 -4
  4. package/apis/core.d.ts +1 -1
  5. package/apis/csn.d.ts +1 -0
  6. package/apis/internal/inference.d.ts +15 -2
  7. package/apis/log.d.ts +10 -0
  8. package/apis/serve.d.ts +4 -9
  9. package/apis/services.d.ts +86 -19
  10. package/bin/build/buildTaskEngine.js +16 -42
  11. package/bin/build/constants.js +4 -2
  12. package/bin/build/provider/buildTaskProviderInternal.js +117 -85
  13. package/bin/build/provider/hana/index.js +6 -1
  14. package/bin/build/provider/mtx-extension/index.js +74 -34
  15. package/bin/build/provider/mtx-sidecar/index.js +3 -3
  16. package/bin/build/provider/nodejs/index.js +2 -2
  17. package/bin/build/util.js +63 -14
  18. package/bin/cds-serve.js +6 -0
  19. package/bin/cds.js +22 -4
  20. package/bin/deploy/to-hana/cfUtil.js +15 -1
  21. package/bin/mtx/in-cds.js +2 -9
  22. package/bin/plugins.js +31 -0
  23. package/bin/serve.js +12 -12
  24. package/lib/compile/etc/_localized.js +1 -1
  25. package/lib/compile/for/lean_drafts.js +23 -6
  26. package/lib/compile/for/nodejs.js +4 -1
  27. package/lib/compile/load.js +4 -2
  28. package/lib/core/index.js +35 -15
  29. package/lib/dbs/cds-deploy.js +129 -133
  30. package/lib/env/cds-env.js +25 -17
  31. package/lib/env/cds-requires.js +10 -40
  32. package/lib/env/compat.js +12 -0
  33. package/lib/env/defaults.js +17 -9
  34. package/lib/env/plugins.js +29 -0
  35. package/lib/env/schemas/cds-rc.json +14 -0
  36. package/lib/index.js +3 -0
  37. package/lib/log/cds-log.js +7 -4
  38. package/lib/ql/CREATE.js +1 -1
  39. package/lib/ql/DELETE.js +1 -1
  40. package/lib/ql/DROP.js +3 -3
  41. package/lib/ql/INSERT.js +1 -1
  42. package/lib/ql/Query.js +14 -6
  43. package/lib/ql/SELECT.js +8 -2
  44. package/lib/ql/UPDATE.js +1 -1
  45. package/lib/ql/Whereable.js +1 -1
  46. package/lib/ql/cds-ql.js +1 -9
  47. package/lib/req/cds-context.js +1 -4
  48. package/lib/req/request.js +63 -2
  49. package/lib/req/response.js +3 -2
  50. package/lib/srv/bindings.js +69 -71
  51. package/lib/srv/cds-connect.js +4 -1
  52. package/lib/srv/cds-serve.js +4 -0
  53. package/lib/srv/middlewares/index.js +37 -6
  54. package/lib/srv/protocols/_legacy.js +1 -1
  55. package/lib/srv/protocols/index.js +1 -1
  56. package/lib/srv/srv-api.js +4 -6
  57. package/lib/srv/srv-dispatch.js +4 -3
  58. package/lib/srv/srv-handlers.js +1 -1
  59. package/lib/srv/srv-methods.js +8 -2
  60. package/lib/utils/cds-test.js +4 -1
  61. package/libx/_runtime/audit/Service.js +8 -9
  62. package/libx/_runtime/audit/generic/personal/index.js +1 -1
  63. package/libx/_runtime/audit/generic/personal/utils.js +1 -1
  64. package/libx/_runtime/audit/utils/v2.js +17 -20
  65. package/libx/_runtime/auth/strategies/mock.js +1 -1
  66. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +2 -0
  67. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
  68. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
  69. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -1
  70. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +12 -5
  71. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -2
  72. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +5 -5
  73. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -1
  74. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +4 -4
  75. package/libx/_runtime/cds-services/adapter/odata-v4/utils/oDataConfiguration.js +2 -2
  76. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +1 -1
  77. package/libx/_runtime/cds-services/services/Service.js +28 -1
  78. package/libx/_runtime/cds-services/util/assert.js +41 -65
  79. package/libx/_runtime/common/code-ext/WorkerPool.js +90 -0
  80. package/libx/_runtime/common/code-ext/WorkerReq.js +0 -4
  81. package/libx/_runtime/common/code-ext/execute.js +28 -18
  82. package/libx/_runtime/common/code-ext/handlers.js +5 -4
  83. package/libx/_runtime/common/code-ext/worker.js +45 -3
  84. package/libx/_runtime/common/code-ext/workerQueryExecutor.js +8 -7
  85. package/libx/_runtime/common/composition/delete.js +1 -1
  86. package/libx/_runtime/common/composition/update.js +3 -5
  87. package/libx/_runtime/common/generic/auth/expand.js +1 -1
  88. package/libx/_runtime/common/generic/auth/readOnly.js +5 -4
  89. package/libx/_runtime/common/generic/auth/restrict.js +7 -2
  90. package/libx/_runtime/common/generic/auth/utils.js +5 -2
  91. package/libx/_runtime/common/generic/crud.js +12 -1
  92. package/libx/_runtime/common/generic/etag.js +11 -3
  93. package/libx/_runtime/common/generic/input.js +8 -6
  94. package/libx/_runtime/common/generic/paging.js +25 -8
  95. package/libx/_runtime/common/generic/put.js +1 -1
  96. package/libx/_runtime/common/generic/sorting.js +0 -1
  97. package/libx/_runtime/common/i18n/messages.properties +1 -0
  98. package/libx/_runtime/common/utils/cqn.js +5 -1
  99. package/libx/_runtime/common/utils/cqn2cqn4sql.js +3 -3
  100. package/libx/_runtime/common/utils/resolveView.js +14 -10
  101. package/libx/_runtime/common/utils/rewriteAsterisks.js +2 -3
  102. package/libx/_runtime/common/utils/templateProcessor.js +15 -17
  103. package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +18 -6
  104. package/libx/_runtime/db/Service.js +1 -0
  105. package/libx/_runtime/db/data-conversion/post-processing.js +0 -18
  106. package/libx/_runtime/db/expand/expand-v2.js +2 -2
  107. package/libx/_runtime/db/expand/rawToExpanded.js +6 -6
  108. package/libx/_runtime/db/generic/integrity.js +1 -1
  109. package/libx/_runtime/db/utils/columns.js +5 -5
  110. package/libx/_runtime/fiori/generic/activate.js +3 -3
  111. package/libx/_runtime/fiori/generic/edit.js +1 -1
  112. package/libx/_runtime/fiori/generic/new.js +4 -0
  113. package/libx/_runtime/fiori/lean-draft.js +178 -68
  114. package/libx/_runtime/hana/execute.js +3 -1
  115. package/libx/_runtime/hana/pool.js +10 -2
  116. package/libx/_runtime/hana/search2cqn4sql.js +1 -1
  117. package/libx/_runtime/messaging/common-utils/AMQPClient.js +6 -1
  118. package/libx/_runtime/messaging/enterprise-messaging.js +1 -0
  119. package/libx/_runtime/remote/Service.js +16 -13
  120. package/libx/_runtime/remote/utils/client.js +6 -1
  121. package/libx/_runtime/sqlite/Service.js +5 -59
  122. package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +1 -0
  123. package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +2 -2
  124. package/libx/_runtime/sqlite/execute.js +3 -1
  125. package/libx/_runtime/types/api.js +12 -3
  126. package/libx/odata/afterburner.js +38 -2
  127. package/libx/odata/cqn2odata.js +3 -2
  128. package/libx/odata/grammar.pegjs +5 -3
  129. package/libx/odata/parser.js +1 -1
  130. package/libx/odata/utils.js +1 -1
  131. package/libx/rest/RestAdapter.js +1 -1
  132. package/libx/rest/RestRequest.js +1 -0
  133. package/package.json +5 -2
  134. package/libx/_runtime/common/code-ext/workerQuery.js +0 -45
  135. package/libx/_runtime/common/constants/limit.js +0 -12
  136. package/libx/_runtime/common/utils/page.js +0 -39
@@ -111,7 +111,7 @@ class RawToExpanded {
111
111
  let expandedItems = this._getResultCache(toManyTree.concat(key))[mapping[GET_KEY_VALUE](false, entry)] || []
112
112
 
113
113
  // the expanded items may include the actives of the deleted drafts -> filter out
114
- if (!cds.env.features.lean_draft && rootIsActiveEntity !== null) {
114
+ if (!cds.env.fiori.lean_draft && rootIsActiveEntity !== null) {
115
115
  if (mapping[TO_ACTIVE]) expandedItems = expandedItems.filter(ele => ele.IsActiveEntity !== false)
116
116
  else expandedItems = expandedItems.filter(ele => !!ele.IsActiveEntity === rootIsActiveEntity)
117
117
  }
@@ -144,7 +144,7 @@ class RawToExpanded {
144
144
  else if (rootIsActiveEntity) row[key] = parsed && parsed.IsActiveEntity !== false ? parsed : null
145
145
  else row[key] = parsed && parsed.IsActiveEntity === rootIsActiveEntity ? parsed : null
146
146
  }
147
- if (mapping[CLEANUP_KEYS]) {
147
+ if (parsed && mapping[CLEANUP_KEYS]) {
148
148
  for (const key in mapping[CLEANUP_KEYS]) delete parsed[key]
149
149
  }
150
150
  } else {
@@ -153,7 +153,7 @@ class RawToExpanded {
153
153
  // Assume a DB will not return undefined, but always null
154
154
  this._convertValue(rawValue, conversionMapper.get(mapping), mapping, row, key)
155
155
 
156
- isEntityNull = this._isNull(isEntityNull, rawValue)
156
+ isEntityNull = this._isNull(isEntityNull, rawValue, key)
157
157
  }
158
158
  }
159
159
 
@@ -173,12 +173,12 @@ class RawToExpanded {
173
173
  * @returns {boolean}
174
174
  * @private
175
175
  */
176
- _isNull(isEntityNull, value) {
176
+ _isNull(isEntityNull, value, key) {
177
177
  if (isEntityNull === undefined) {
178
- return value === null || value === undefined
178
+ return value === null || value === undefined || key === 'IsActiveEntity'
179
179
  }
180
180
 
181
- return isEntityNull === true && (value === null || value === undefined)
181
+ return isEntityNull === true && (value === null || value === undefined || key === 'IsActiveEntity')
182
182
  }
183
183
 
184
184
  /**
@@ -333,7 +333,7 @@ const _checkReferenceIntegrity = (entity, data, req, csn, run) => {
333
333
  }
334
334
 
335
335
  const _checkIntegrityWrapper = (req, csn, run) => async (data, entity) => {
336
- if (cds.env.features.lean_draft && entity.name?.endsWith('.drafts')) return
336
+ if (cds.env.fiori.lean_draft && entity.name?.endsWith('.drafts')) return
337
337
  const errors = await _checkReferenceIntegrity(entity, data, req, csn, run)
338
338
  if (errors && errors.length !== 0) for (const err of errors) req.error(err)
339
339
  }
@@ -16,15 +16,15 @@ const getColumns = (entity, { _4db, onlyKeys } = { _4db: true, onlyKeys: false }
16
16
  if (!(entity && entity.elements)) return []
17
17
  const columnNames = []
18
18
  // REVISIT!!!
19
- const elements = cds.env.features.lean_draft
20
- ? entity.elements
21
- : Object.getPrototypeOf(entity.elements) || entity.elements
19
+ const { structs = cds.env.features.ucsn_struct_conversion } = cds.env.effective.odata
20
+ const { lean_draft } = cds.env.fiori
21
+ const elements = lean_draft ? entity.elements : Object.getPrototypeOf(entity.elements) || entity.elements
22
22
  for (const elementName in elements) {
23
23
  const element = elements[elementName]
24
24
  if (onlyKeys && !element.key) continue
25
25
  if (element.isAssociation) continue
26
- if (!cds.env.features.lean_draft && _4db && entity._isDraftEnabled && elementName in DRAFT_COLUMNS_MAP) continue
27
- if ((cds.env.effective.odata.structs || cds.env.features.ucsn_struct_conversion) && element.elements) {
26
+ if (!lean_draft && _4db && entity._isDraftEnabled && elementName in DRAFT_COLUMNS_MAP) continue
27
+ if (structs && element.elements) {
28
28
  columnNames.push(...resolveStructured({ element, structProperties: [] }, false))
29
29
  continue
30
30
  }
@@ -146,8 +146,8 @@ const fioriGenericActivate = async function (req) {
146
146
 
147
147
  // REVISIT: should not be necessary
148
148
  r._ = Object.assign(r._, req._)
149
- r.getUriInfo = () => req.getUriInfo()
150
- r.getUrlObject = () => req.getUrlObject()
149
+ if (req.getUriInfo) r.getUriInfo = () => req.getUriInfo()
150
+ if (req.getUrlObject) r.getUrlObject = () => req.getUrlObject()
151
151
  r._.params = req.params
152
152
  r._.query = req.query
153
153
 
@@ -178,7 +178,7 @@ const fioriGenericActivate = async function (req) {
178
178
 
179
179
  // REVISIT: we need to use okra API here because it must be set in the batched request
180
180
  // status code must be set in handler to allow overriding for FE V2
181
- req?._?.odataRes.setStatusCode(201)
181
+ req?._?.odataRes?.setStatusCode(201)
182
182
 
183
183
  return result
184
184
  }
@@ -164,7 +164,7 @@ const fioriGenericEdit = async function (req) {
164
164
 
165
165
  // REVISIT: we need to use okra API here because it must be set in the batched request
166
166
  // status code must be set in handler to allow overriding for FE V2
167
- req?._?.odataRes.setStatusCode(201)
167
+ req?._?.odataRes?.setStatusCode(201)
168
168
 
169
169
  return results[0][0]
170
170
  }
@@ -56,6 +56,10 @@ const fioriGenericNew = async function (req, next) {
56
56
 
57
57
  if (!cds.db) req.reject('NO_DATABASE_CONNECTION')
58
58
 
59
+ const isRoot = typeof req.query.INSERT.into === 'string' || req.query.INSERT.into.ref?.length === 1
60
+ // Only allowed for pseudo draft roots (entities with this action)
61
+ if (isRoot && !req.target['@Common.DraftRoot.ActivationAction']) req.reject(403, 'DRAFT_MODIFICATION_ONLY_VIA_ROOT')
62
+
59
63
  const navigationToMany = isNavigationToMany(req)
60
64
 
61
65
  const adminDataCQN = navigationToMany
@@ -1,6 +1,7 @@
1
1
  const cds = require('../cds'),
2
2
  { Object_keys } = cds.utils
3
3
  const LOG = cds.log('fiori|drafts')
4
+ const original = Symbol('original')
4
5
 
5
6
  const DRAFT_ELEMENTS = new Set([
6
7
  'IsActiveEntity',
@@ -41,6 +42,14 @@ const _inProcessByUserXpr = lockShiftedNow => ({
41
42
  cast: { type: 'cds.String' }
42
43
  })
43
44
 
45
+ /// It's important to wait for the completion of all promises, otherwise a rollback might happen too soon
46
+ const _promiseAll = async array => {
47
+ const results = await Promise.allSettled(array)
48
+ const e = results.find(r => r.status === 'rejected')
49
+ if (e) throw e.reason
50
+ return results.map(r => r.value)
51
+ }
52
+
44
53
  const _lock = {
45
54
  get shiftedNow() {
46
55
  return new Date(Math.max(0, Date.now() - DRAFT_CANCEL_TIMEOUT_IN_MIN() * 60 * 1000)).toISOString()
@@ -69,12 +78,14 @@ cds.ApplicationService.prototype.handle = async function (req) {
69
78
 
70
79
  if (
71
80
  !req.query ||
81
+ req.query.UPSERT || // skip UPSERTs (might have an additional INSERT)
72
82
  (!req.query.SELECT && !req.query.INSERT && !req.query.UPDATE && !req.query.DELETE) ||
73
83
  req.query._draftParams
74
84
  )
75
85
  return handle(req)
76
86
  const query = _cleansed(req.query, this.model)
77
- _cleanseParams(req.params)
87
+ _cleanseParams(req.params, req.target)
88
+ if (req.data) _cleanseParams(req.data, req.target)
78
89
  const draftParams = query._draftParams
79
90
 
80
91
  const _newReq = (req, query, draftParams, event) => {
@@ -82,8 +93,11 @@ cds.ApplicationService.prototype.handle = async function (req) {
82
93
  query._target = undefined
83
94
  query._draftParams = draftParams
84
95
  cds.infer(query, this.model.definitions)
96
+
97
+ // REVISIT: This is extremely bad. We should be able to just create a copy without such hacks.
85
98
  const _req = cds.Request.for(req._) // REVISIT: this causes req._.data of WRITE reqs copied to READ reqs
86
- if (query.SELECT) delete _req.data // which we fix here -> but this is an ugly workaround
99
+ // If we create a `READ` event based on a modifying request, we delete data
100
+ if (event === 'READ' && req.event !== 'READ') delete _req.data // which we fix here -> but this is an ugly workaround
87
101
  _req.query = query
88
102
  _req.event =
89
103
  event ||
@@ -93,19 +107,24 @@ cds.ApplicationService.prototype.handle = async function (req) {
93
107
  (query.DELETE && 'DELETE') ||
94
108
  req.event
95
109
  _req.target = query._target
110
+ _req._ = Object.assign({}, req._ || {}) // don't share the same `_` object
96
111
  _req._.params = req.params
97
112
  _req.params = req.params
98
113
  _req._.query = query
99
114
  _req._ = req._
100
- _req.data = _req.query.UPDATE?.data || _req.query.INSERT?.entries?.[0]
101
-
102
- // Dirty hack: delegate messages to original request by binding the getter _messages to req
103
- let proto = req
104
- let _messagesDescr
105
- while (proto && !(_messagesDescr = Object.getOwnPropertyDescriptor(proto, '_messages')))
106
- proto = Object.getPrototypeOf(proto)
107
- Object.defineProperty(_req, '_messages', { ..._messagesDescr, get: _messagesDescr.get.bind(req) })
108
-
115
+ _req._isRest = req._isRest
116
+ _req._isOData = req._isOData
117
+ _req.isConcurrentResource = req.isConcurrentResource
118
+ _req.isConditional = req.isConditional
119
+ _req.validateEtag = req.validateEtag
120
+ const cqnData = _req.query.UPDATE?.data || _req.query.INSERT?.entries?.[0]
121
+ if (cqnData) _req.data = cqnData // must point to the same object
122
+ Object.defineProperty(_req, '_messages', {
123
+ get: function () {
124
+ return req._messages
125
+ }
126
+ })
127
+ if (req.tx) _req.tx = req.tx
109
128
  return _req
110
129
  }
111
130
 
@@ -155,7 +174,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
155
174
  deletes.push(
156
175
  DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: draft.DraftAdministrativeData_DraftUUID })
157
176
  )
158
- await Promise.all(deletes)
177
+ await _promiseAll(deletes)
159
178
  return req.data
160
179
  }
161
180
 
@@ -188,7 +207,8 @@ cds.ApplicationService.prototype.handle = async function (req) {
188
207
  delete res.DraftAdministrativeData
189
208
  const HasActiveEntity = res.HasActiveEntity
190
209
  delete res.HasActiveEntity
191
- await Promise.all([
210
+ delete res.IsActiveEntity
211
+ await _promiseAll([
192
212
  run(DELETE.from(targetDraft).where(targetWhere)),
193
213
  DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: DraftAdministrativeData_DraftUUID })
194
214
  ])
@@ -196,7 +216,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
196
216
  const result = await run(
197
217
  HasActiveEntity ? UPDATE(req.target).data(res).where(targetWhere) : INSERT.into(req.target).entries(res)
198
218
  )
199
- req?._?.odataRes.setStatusCode(201)
219
+ req?._?.odataRes?.setStatusCode(201)
200
220
 
201
221
  return Object.assign(result, { IsActiveEntity: true })
202
222
 
@@ -252,11 +272,20 @@ cds.ApplicationService.prototype.handle = async function (req) {
252
272
  const updateData = { ...req.data }
253
273
  delete updateData.IsActiveEntity
254
274
  await run(UPDATE({ ref: draftsRef }).data(updateData))
255
- return Object.assign(req.data, { IsActiveEntity: false })
275
+ req.data.IsActiveEntity = false
276
+ return req.data
256
277
  }
257
278
  }
258
279
 
259
280
  if (req.event === 'READ') {
281
+ if (
282
+ !Object.keys(draftParams).length &&
283
+ !req.query._target.name?.endsWith('DraftAdministrativeData') &&
284
+ !req.query._target.drafts
285
+ ) {
286
+ req.query = query
287
+ return handle(req)
288
+ }
260
289
  const read = req.query._target.name.endsWith('.drafts')
261
290
  ? Read.ownDrafts
262
291
  : draftParams.IsActiveEntity === false && draftParams.SiblingEntity_IsActiveEntity === null
@@ -281,8 +310,34 @@ cds.ApplicationService.prototype.handle = async function (req) {
281
310
  return result
282
311
  }
283
312
 
284
- const _req = _newReq(req, query, draftParams, req.event)
285
- const result = await handle(_req)
313
+ req.query = query
314
+ const result = await handle(req)
315
+ return result
316
+ }
317
+
318
+ // REVISIT: It's not optimal to first calculate the whole result array and only later
319
+ // delete unrequested properties. However, as a first step, we do it that way,
320
+ // especially since the current db driver always adds those fields.
321
+ // Once we switch to the new driver, we'll adapt it.
322
+ const _requested = (result, query) => {
323
+ const originalQuery = query[original]
324
+ if (!result || !originalQuery) return result
325
+ const all = ['HasActiveEntity', 'HasDraftEntity']
326
+
327
+ const ignoredCols = new Set(all.concat('DraftAdministrativeData'))
328
+ const _isODataV2 = cds.context?.http?.req?.headers?.['x-cds-odata-version'] === 'v2'
329
+ if (!_isODataV2) ignoredCols.add('DraftAdministrativeData_DraftUUID')
330
+ for (const col of originalQuery.SELECT.columns || ['*']) {
331
+ const name = col.as || col.ref?.[0] || col
332
+ if (all.includes(name) || name === 'DraftAdministrativeData' || name === 'DraftAdministrativeData_DraftUUID')
333
+ ignoredCols.delete(name)
334
+ if (name === '*') all.forEach(c => ignoredCols.delete(c))
335
+ }
336
+ if (!ignoredCols.size) return result
337
+ const resArray = Array.isArray(result) ? result : [result]
338
+ for (const row of resArray) {
339
+ for (const ignoredCol of ignoredCols) delete row[ignoredCol]
340
+ }
286
341
  return result
287
342
  }
288
343
 
@@ -291,6 +346,13 @@ const Read = {
291
346
  LOG.debug('List Editing Status: Only Active')
292
347
  // DraftAdministrativeData is only accessible via drafts
293
348
  if (query._target.name.endsWith('.DraftAdministrativeData')) return run(query._drafts)
349
+ if (!query._target._isDraftEnabled) return run(query)
350
+ if (!query.SELECT.groupBy && query.SELECT.columns && !query.SELECT.columns.some(c => c === '*')) {
351
+ const keys = Object_keys(query._target.keys).filter(k => k !== 'IsActiveEntity')
352
+ for (const key of keys) {
353
+ if (!query.SELECT.columns.some(c => c.ref?.[0] === key)) query.SELECT.columns.push({ ref: [key] })
354
+ }
355
+ }
294
356
  const actives = await run(query)
295
357
  if (!actives || (Array.isArray(actives) && !actives.length) || !query._target.drafts) return actives
296
358
  let drafts
@@ -313,7 +375,7 @@ const Read = {
313
375
  DraftAdministrativeData_DraftUUID: null
314
376
  })
315
377
  )
316
- return actives
378
+ return _requested(actives, query)
317
379
  },
318
380
  unchanged: async function (run, query) {
319
381
  LOG.debug('List Editing Status: Unchanged')
@@ -325,10 +387,10 @@ const Read = {
325
387
  draftsQuery.SELECT.columns = keys.map(k => ({ ref: [k] }))
326
388
 
327
389
  const drafts = await run(draftsQuery)
328
- const res = Read.onlyActives(run, query.where(Read.whereNotIn(query._target, drafts)), {
390
+ const res = await Read.onlyActives(run, query.where(Read.whereNotIn(query._target, drafts)), {
329
391
  ignoreDrafts: true
330
392
  })
331
- return res
393
+ return _requested(res, query)
332
394
  },
333
395
  ownDrafts: async function (run, query) {
334
396
  LOG.debug('List Editing Status: Own Draft')
@@ -356,11 +418,11 @@ const Read = {
356
418
  const drafts = await run(draftsQuery)
357
419
  Read.merge(query._target, drafts, [], row =>
358
420
  Object.assign(row, {
359
- IsActiveEntity: false,
360
- HasDraftEntity: false
421
+ HasDraftEntity: false,
422
+ IsActiveEntity: false
361
423
  })
362
424
  )
363
- return drafts
425
+ return _requested(drafts, query)
364
426
  },
365
427
  all: async function (run, query) {
366
428
  LOG.debug('List Editing Status: All')
@@ -391,6 +453,13 @@ const Read = {
391
453
  else ownNewDrafts.push(draft)
392
454
  }
393
455
 
456
+ // We can't properly calculate `count`:
457
+ // - Not all actives are retrieved (e.g. top = 0), hence there could be more deletes if more actives are requested,
458
+ // hence we cannot count deletions based on data.
459
+ // - We can't rely on the fact that `HasActiveEntity` always has an active counterpart because the filter
460
+ // is applied on draft and active data respectively (you could fetch a draft but not an active instance).
461
+ // However, there's not much we can do, so we use use this as a best guess.
462
+
394
463
  const count = isFirstPage ? ownNewDrafts.length + (isCount ? actives[0]?.$count : actives.$count) : actives.$count
395
464
  if (isCount) return { $count: count }
396
465
 
@@ -423,7 +492,7 @@ const Read = {
423
492
  })
424
493
  const res = isFirstPage ? [...ownNewDrafts, ...ownEditDrafts, ...actives] : actives
425
494
  if (query.SELECT.count) res.$count = count
426
- return res
495
+ return _requested(res, query)
427
496
  },
428
497
  activesFromDrafts: async function (run, query, { isLocked = true }) {
429
498
  const draftsQuery = query._drafts
@@ -450,7 +519,7 @@ const Read = {
450
519
  ? Object.assign(row, other, { IsActiveEntity: true, HasDraftEntity: true, HasActiveEntity: false })
451
520
  : Object.assign({ IsActiveEntity: true, HasDraftEntity: false, HasActiveEntity: false })
452
521
  )
453
- return actives
522
+ return _requested(actives, query)
454
523
  },
455
524
  unsavedChangesByAnotherUser: async function (run, query) {
456
525
  LOG.debug('List Editing Status: Unsaved Changes by Another User')
@@ -464,6 +533,7 @@ const Read = {
464
533
  whereIn: (target, data, not = false) => {
465
534
  const keys = Object_keys(target.keys).filter(k => k !== 'IsActiveEntity')
466
535
  const dataArray = data ? (Array.isArray(data) ? data : [data]) : []
536
+ if (not && !dataArray.length) return []
467
537
  return [
468
538
  { list: keys.map(k => ({ ref: [k] })) },
469
539
  not ? 'not in' : 'in',
@@ -529,29 +599,33 @@ const Read = {
529
599
  }
530
600
  }
531
601
 
532
- function _cleanseParams(params) {
602
+ function _cleanseParams(params, target) {
603
+ if (!target?.drafts) return
533
604
  if (Array.isArray(params)) {
534
- for (const param of params) _cleanseParams(param)
605
+ for (const param of params) _cleanseParams(param, target)
535
606
  return
536
607
  }
537
608
  if (typeof params === 'object') {
538
609
  for (const key in params) {
539
- if (key === 'IsActiveEntity') delete params[key]
610
+ if (key === 'IsActiveEntity') {
611
+ const value = params[key]
612
+ delete params[key]
613
+ if (cds.env.fiori?.draft_compat) Object.defineProperty(params, key, { value, enumerable: false })
614
+ }
540
615
  }
541
616
  }
542
617
  }
543
618
 
544
- function _cleanseCols(columns, elements) {
545
- if (typeof columns?.filter !== 'function') return columns
546
- return (
547
- columns &&
548
- columns
549
- .filter(c => !elements.has(c.ref?.[0]))
550
- .map(c => {
551
- if (c.expand) return { ...c, expand: _cleanseCols(c.expand, elements) }
552
- return c
553
- })
554
- )
619
+ function _cleanseCols(columns, elements, target) {
620
+ // TODO: sometimes target is undefined
621
+ if (!target || typeof columns?.filter !== 'function') return columns
622
+ const filtered = target?.drafts ? columns.filter(c => !elements.has(c.ref?.[0])) : columns
623
+ return filtered.map(c => {
624
+ if (c.expand && c.ref) {
625
+ return { ...c, expand: _cleanseCols(c.expand, elements, target.elements[c.ref[0]]?._target) }
626
+ }
627
+ return c
628
+ })
555
629
  }
556
630
 
557
631
  /**
@@ -559,10 +633,10 @@ function _cleanseCols(columns, elements) {
559
633
  */
560
634
  function _cleansed(query, model) {
561
635
  const draftParams = {} //> used to collect draft filter criteria
562
- const q = _cleanseQuery(query, draftParams)
636
+ const q = _cleanseQuery(query, draftParams, model)
563
637
  if (query.SELECT) {
564
638
  const getDrafts = () => {
565
- const draftsQuery = _cleanseQuery(query, {}) // could just clone `q` but the latter is ruined by database layer
639
+ const draftsQuery = _cleanseQuery(query, {}, model) // could just clone `q` but the latter is ruined by database layer
566
640
  draftsQuery._target = undefined
567
641
  const [root, ...tail] = draftsQuery.SELECT.from.ref
568
642
  const draft = model.definitions[root.id || root].drafts
@@ -572,7 +646,7 @@ function _cleansed(query, model) {
572
646
  cds.infer(draftsQuery, model.definitions)
573
647
  // draftsQuery._target = draftsQuery._target?.drafts || draftsQuery._target
574
648
  if (query.SELECT.columns && query._target.drafts)
575
- draftsQuery.SELECT.columns = _cleanseCols(query.SELECT.columns, REDUCED_DRAFT_ELEMENTS)
649
+ draftsQuery.SELECT.columns = _cleanseCols(query.SELECT.columns, REDUCED_DRAFT_ELEMENTS, draft)
576
650
 
577
651
  if (draftsQuery._target.name.endsWith('.DraftAdministrativeData')) {
578
652
  draftsQuery.SELECT.columns = _tweakAdminCols(draftsQuery.SELECT.columns)
@@ -592,21 +666,30 @@ function _cleansed(query, model) {
592
666
  }
593
667
 
594
668
  Object.defineProperty(q, '_draftParams', { value: draftParams, enumerable: false })
669
+ q[original] = query
595
670
  return q
596
671
 
597
- function _cleanseQuery(query, draftParams) {
672
+ function _cleanseQuery(query, draftParams, model) {
673
+ const target = query._target
598
674
  const q = cds.ql.clone(query)
599
675
 
600
676
  const ref = q.SELECT?.from.ref || q.UPDATE?.entity.ref || q.INSERT?.into.ref || q.DELETE?.from.ref
601
677
  const cqn = q.SELECT || q.UPDATE || q.INSERT || q.DELETE
602
678
 
603
679
  if (ref) {
604
- const cleansedRef = ref.map(r => (r.where ? { ...r, where: _cleanseWhere(r.where, draftParams) } : r))
680
+ let entity
681
+ const cleansedRef = ref.map(r => {
682
+ entity = (entity && entity.elements[r.id || r]._target) || model.definitions[r.id || r]
683
+ if (!entity?.drafts) return r
684
+ return r.where ? { ...r, where: _cleanseWhere(r.where, draftParams) } : r
685
+ })
605
686
  if (q.SELECT) q.SELECT.from = { ...q.SELECT.from, ref: cleansedRef }
606
687
  else if (q.DELETE) q.DELETE.from = { ...q.DELETE.from, ref: cleansedRef }
607
688
  else if (q.UPDATE) q.UPDATE.entity = { ...q.UPDATE.entity, ref: cleansedRef }
608
689
  else if (q.INSERT) q.INSERT.into = { ...q.INSERT.into, ref: cleansedRef }
609
690
 
691
+ // This only works for simple cases of `SiblingEntity`, e.g. `root(ID=1,IsActiveEntity=false)/SiblingEntity`
692
+ // , check if there are more complicated use cases
610
693
  const siblingIdx = cleansedRef.findIndex(r => r === 'SiblingEntity')
611
694
  if (siblingIdx !== -1) {
612
695
  cleansedRef.splice(siblingIdx, 1)
@@ -614,10 +697,9 @@ function _cleansed(query, model) {
614
697
  }
615
698
  }
616
699
 
617
- if (cqn.where) cqn.where = _cleanseWhere(cqn.where, draftParams)
618
- if (cqn.columns) cqn.columns = _cleanseCols(q.SELECT.columns, DRAFT_ELEMENTS)
619
- if (cqn.orderBy) cqn.orderBy = _cleanseWhere(cqn.orderBy, {})
620
-
700
+ if (target.drafts && cqn.where) cqn.where = _cleanseWhere(cqn.where, draftParams)
701
+ if (target.drafts && cqn.orderBy) cqn.orderBy = _cleanseWhere(cqn.orderBy, {})
702
+ if (cqn.columns) cqn.columns = _cleanseCols(cqn.columns, DRAFT_ELEMENTS, target)
621
703
  return q
622
704
  }
623
705
 
@@ -648,9 +730,9 @@ function _cleansed(query, model) {
648
730
  '=',
649
731
  { val: cds.context.user.id },
650
732
  'then',
651
- { val: true },
733
+ 'true',
652
734
  'else',
653
- { val: false },
735
+ 'false',
654
736
  'end'
655
737
  ],
656
738
  as: 'DraftIsCreatedByMe',
@@ -671,9 +753,9 @@ function _cleansed(query, model) {
671
753
  '>',
672
754
  { val: _lock.shiftedNow },
673
755
  'then',
674
- { val: true },
756
+ 'true',
675
757
  'else',
676
- { val: false },
758
+ 'false',
677
759
  'end'
678
760
  ],
679
761
  as: 'DraftIsProcessedByMe',
@@ -740,7 +822,10 @@ function expandStarStar(target, recursion = new Map()) {
740
822
 
741
823
  async function onNew(req) {
742
824
  LOG.debug('new draft')
743
- const isRoot = typeof req.query.INSERT.into === 'string'
825
+ const isRoot = typeof req.query.INSERT.into === 'string' || req.query.INSERT.into.ref?.length === 1
826
+ // Only allowed for pseudo draft roots (entities with this action)
827
+ if (isRoot && !req.target.actives['@Common.DraftRoot.ActivationAction'])
828
+ req.reject(403, 'DRAFT_MODIFICATION_ONLY_VIA_ROOT')
744
829
  let DraftUUID
745
830
  if (isRoot) DraftUUID = cds.utils.uuid()
746
831
  else {
@@ -777,15 +862,29 @@ async function onNew(req) {
777
862
  })
778
863
  .where({ DraftUUID })
779
864
 
780
- const draftData = Object.assign(
781
- { DraftAdministrativeData_DraftUUID: DraftUUID, HasActiveEntity: false },
782
- req.query.INSERT.entries[0]
783
- )
865
+ const _assignDraftData = (obj, target) => {
866
+ const newObj = Object.assign({ DraftAdministrativeData_DraftUUID: DraftUUID, HasActiveEntity: false }, obj)
867
+ if (!target) return newObj
868
+
869
+ // Also support deep insertions
870
+ for (const key in newObj) {
871
+ if (!target.elements[key]?.isComposition) continue
872
+ if (Array.isArray(newObj[key]))
873
+ newObj[key] = newObj[key].map(v => _assignDraftData(v, target.elements[key]._target))
874
+ else if (typeof newObj[key] === 'object') {
875
+ newObj[key] = _assignDraftData(newObj[key], target.elements[key]._target)
876
+ }
877
+ }
878
+
879
+ return newObj
880
+ }
881
+
882
+ const draftData = _assignDraftData(req.query.INSERT.entries[0], req.target)
784
883
 
785
884
  delete draftData.IsActiveEntity
786
885
  const draftCQN = INSERT.into(req.target).entries(draftData)
787
886
 
788
- await Promise.all([cds.run(adminDataCQN), this.run(draftCQN)])
887
+ await _promiseAll([cds.run(adminDataCQN), this.run(draftCQN)])
789
888
  req._.readAfterWrite = true
790
889
  return { ...draftData, IsActiveEntity: false }
791
890
  }
@@ -794,7 +893,7 @@ async function onEdit(req) {
794
893
  LOG.debug('edit active')
795
894
  const draftParams = req.query._draftParams
796
895
  if (req.query.SELECT.from.ref.length > 1 || draftParams.IsActiveEntity !== true) {
797
- req.reject(400, 'Action "draftEdit" can only be called on the root entity')
896
+ req.reject(400, 'Action "draftEdit" can only be called on the root active entity')
798
897
  }
799
898
  const targetWhere = req.query.SELECT.from.ref[0].where
800
899
 
@@ -816,18 +915,29 @@ async function onEdit(req) {
816
915
  }
817
916
  }
818
917
  _addDraftColumns(req.target, cols)
819
- const draftsCheck = SELECT.one(req.target.drafts)
918
+
919
+ const existingDraft = SELECT.one(req.target.drafts)
820
920
  .columns({ ref: ['DraftAdministrativeData'], expand: [_inProcessByUserXpr(_lock.shiftedNow)] })
821
921
  .where(targetWhere)
822
- .forUpdate({ wait: 0 })
823
922
  // prevent service to check for own user
824
- Object.defineProperty(draftsCheck, '_draftParams', { value: draftParams, enumerable: false })
923
+ Object.defineProperty(existingDraft, '_draftParams', { value: draftParams, enumerable: false })
924
+
925
+ const activeCQN = SELECT.one.from(req.target).columns(cols).where(targetWhere)
926
+ activeCQN._suppressLocalization = true // in the future we should be able to just set activeCQN.SELECT.localized = false
825
927
 
826
- const [res, draft] = await Promise.all([
827
- this.run(SELECT.one.from(req.target).columns(cols).where(targetWhere).forUpdate({ wait: 0 })),
928
+ const activeCheck = SELECT.one(req.target).columns([1]).where(targetWhere).forUpdate()
929
+ Object.defineProperty(activeCheck, '_draftParams', { value: draftParams, enumerable: false })
930
+ // It's not possible to use `FOR UPDATE` in HANA if the view contains joins/unions. Unfortunately, we can't resolve the table entity
931
+ // because we must trigger the app-service request on the target entity (which could be delegated to a remote service).
932
+ // The best we can do is to catch a potential error
933
+ await this.run(activeCheck).catch(_ => {})
934
+
935
+ const [res, draft] = await _promiseAll([
936
+ this.run(activeCQN),
828
937
  // no user check must be done here...
829
- this.run(draftsCheck)
938
+ this.run(existingDraft)
830
939
  ])
940
+
831
941
  if (!res) req.reject(404)
832
942
  const preserveChanges = req.context?.data?.PreserveChanges
833
943
  const inProcessByUser = draft?.DraftAdministrativeData?.InProcessByUser
@@ -835,7 +945,7 @@ async function onEdit(req) {
835
945
  if (inProcessByUser || preserveChanges) req.reject(409, 'DRAFT_ALREADY_EXISTS')
836
946
  const keys = {}
837
947
  for (const key in req.target.drafts.keys) keys[key] = res[key]
838
- await Promise.all([
948
+ await _promiseAll([
839
949
  DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID }),
840
950
  this.run(DELETE.from(req.target.drafts).where(keys))
841
951
  ])
@@ -862,7 +972,7 @@ async function onEdit(req) {
862
972
 
863
973
  // REVISIT: we need to use okra API here because it must be set in the batched request
864
974
  // status code must be set in handler to allow overriding for FE V2
865
- req?._?.odataRes.setStatusCode(201)
975
+ req?._?.odataRes?.setStatusCode(201)
866
976
 
867
977
  return { ...res, IsActiveEntity: false } // REVISIT: Flatten?
868
978
  }
@@ -891,7 +1001,7 @@ async function onCancel(req) {
891
1001
  DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: draft.DraftAdministrativeData_DraftUUID })
892
1002
  )
893
1003
  if (draftParams.IsActiveEntity) deletes.push(this.run(DELETE.from({ ref: activeRef })))
894
- await Promise.all(deletes)
1004
+ await _promiseAll(deletes)
895
1005
  return req.data
896
1006
  }
897
1007
 
@@ -225,7 +225,9 @@ function _processExpand(model, dbc, cqn, user, locale, txTimestamp) {
225
225
  function executeSelectCQN(model, dbc, query, user, locale, txTimestamp) {
226
226
  if (hasExpand(query)) {
227
227
  // expand: '**' or '*3' is handled by new impl
228
- if (query.SELECT.columns.some(c => c.expand && typeof c.expand === 'string' && /^\*{1}[\d|*]+/.test(c.expand))) {
228
+ if (
229
+ query.SELECT.columns.some(c => c.expand && typeof c.expand[0] === 'string' && /^\*{1}[\d|*]+/.test(c.expand[0]))
230
+ ) {
229
231
  return expandV2(model, dbc, query, user, locale, txTimestamp, executeSelectCQN)
230
232
  }
231
233
  return _processExpand(model, dbc, query, user, locale, txTimestamp)