@sap/cds 6.4.1 → 6.6.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 (139) hide show
  1. package/CHANGELOG.md +79 -6
  2. package/README.md +5 -0
  3. package/apis/cqn.d.ts +14 -3
  4. package/apis/ql.d.ts +8 -8
  5. package/apis/services.d.ts +37 -65
  6. package/apis/test.d.ts +7 -0
  7. package/bin/build/buildTaskEngine.js +9 -14
  8. package/bin/build/buildTaskFactory.js +1 -1
  9. package/bin/build/buildTaskHandler.js +3 -14
  10. package/bin/build/index.js +8 -2
  11. package/bin/build/provider/buildTaskProviderInternal.js +18 -13
  12. package/bin/build/provider/fiori/index.js +5 -10
  13. package/bin/build/provider/hana/2migration.js +11 -2
  14. package/bin/build/provider/hana/index.js +17 -14
  15. package/bin/build/provider/hana/template/.hdiconfig-hanacloud +137 -0
  16. package/bin/build/provider/hana/template/package.json +3 -0
  17. package/bin/build/provider/mtx/resourcesTarBuilder.js +12 -3
  18. package/bin/build/provider/mtx-extension/index.js +57 -37
  19. package/bin/build/provider/mtx-sidecar/index.js +1 -1
  20. package/bin/build/util.js +18 -1
  21. package/bin/cds.js +1 -5
  22. package/bin/deploy/to-hana/hana.js +10 -3
  23. package/bin/serve.js +36 -20
  24. package/common.cds +7 -0
  25. package/lib/auth/jwt-auth.js +8 -7
  26. package/lib/compile/for/lean_drafts.js +55 -6
  27. package/lib/compile/minify.js +3 -3
  28. package/lib/dbs/cds-deploy.js +18 -17
  29. package/lib/env/cds-requires.js +1 -1
  30. package/lib/env/defaults.js +5 -1
  31. package/lib/env/schemas/cds-rc.json +74 -3
  32. package/lib/index.js +4 -2
  33. package/lib/lazy.js +6 -8
  34. package/lib/log/cds-error.js +2 -2
  35. package/lib/ql/Whereable.js +22 -11
  36. package/lib/ql/cds-ql.js +1 -1
  37. package/lib/req/cds-context.js +3 -3
  38. package/lib/req/response.js +8 -3
  39. package/lib/req/user.js +12 -2
  40. package/lib/srv/bindings.js +1 -2
  41. package/lib/srv/cds-serve.js +2 -1
  42. package/lib/srv/middlewares/trace.js +31 -15
  43. package/lib/srv/protocols/odata-v2-proxy.js +8 -8
  44. package/lib/srv/srv-handlers.js +26 -7
  45. package/lib/srv/srv-methods.js +2 -2
  46. package/lib/srv/srv-models.js +8 -3
  47. package/lib/utils/cds-test.js +7 -5
  48. package/lib/utils/cds-utils.js +3 -1
  49. package/lib/utils/tar.js +6 -3
  50. package/libx/_runtime/auth/strategies/JWT.js +1 -0
  51. package/libx/_runtime/auth/strategies/ias-auth.js +3 -2
  52. package/libx/_runtime/auth/strategies/mock.js +12 -1
  53. package/libx/_runtime/auth/strategies/xssecUtils.js +7 -8
  54. package/libx/_runtime/auth/strategies/xsuaa.js +1 -0
  55. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +6 -2
  56. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -2
  57. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +26 -1
  58. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +8 -0
  59. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +1 -1
  60. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +11 -2
  61. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +8 -8
  62. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +1 -1
  63. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/validator/ValueValidator.js +14 -14
  64. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/DeserializerFactory.js +1 -0
  65. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ResourceJsonSerializer.js +3 -0
  66. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/UriHelper.js +2 -1
  67. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +3 -2
  68. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +7 -0
  69. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +0 -3
  70. package/libx/_runtime/cds-services/services/Service.js +11 -19
  71. package/libx/_runtime/cds-services/services/utils/columns.js +42 -40
  72. package/libx/_runtime/cds-services/util/assert.js +7 -1
  73. package/libx/_runtime/common/code-ext/WorkerReq.js +81 -0
  74. package/libx/_runtime/common/code-ext/config.js +13 -0
  75. package/libx/_runtime/common/code-ext/execute.js +113 -0
  76. package/libx/_runtime/common/code-ext/handlers.js +49 -0
  77. package/libx/_runtime/common/code-ext/worker.js +40 -0
  78. package/libx/_runtime/common/code-ext/workerQuery.js +45 -0
  79. package/libx/_runtime/common/code-ext/workerQueryExecutor.js +36 -0
  80. package/libx/_runtime/common/composition/data.js +5 -2
  81. package/libx/_runtime/common/composition/tree.js +2 -0
  82. package/libx/_runtime/common/generic/auth/restrict.js +1 -1
  83. package/libx/_runtime/common/generic/crud.js +4 -0
  84. package/libx/_runtime/common/generic/etag.js +3 -1
  85. package/libx/_runtime/common/generic/input.js +12 -14
  86. package/libx/_runtime/common/i18n/index.js +1 -1
  87. package/libx/_runtime/common/utils/cqn2cqn4sql.js +47 -22
  88. package/libx/_runtime/common/utils/path.js +5 -26
  89. package/libx/_runtime/common/utils/search2cqn4sql.js +16 -9
  90. package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +19 -13
  91. package/libx/_runtime/db/data-conversion/post-processing.js +1 -1
  92. package/libx/_runtime/db/expand/expandCQNToJoin.js +7 -4
  93. package/libx/_runtime/db/expand/rawToExpanded.js +3 -2
  94. package/libx/_runtime/db/generic/input.js +2 -2
  95. package/libx/_runtime/db/generic/integrity.js +1 -0
  96. package/libx/_runtime/db/generic/virtual.js +1 -0
  97. package/libx/_runtime/db/query/read.js +3 -2
  98. package/libx/_runtime/db/utils/localized.js +1 -1
  99. package/libx/_runtime/fiori/generic/activate.js +7 -1
  100. package/libx/_runtime/fiori/generic/before.js +9 -1
  101. package/libx/_runtime/fiori/generic/edit.js +8 -1
  102. package/libx/_runtime/fiori/generic/new.js +2 -0
  103. package/libx/_runtime/fiori/generic/patch.js +2 -0
  104. package/libx/_runtime/fiori/generic/prepare.js +2 -0
  105. package/libx/_runtime/fiori/generic/read.js +16 -5
  106. package/libx/_runtime/fiori/generic/readOverDraft.js +2 -0
  107. package/libx/_runtime/fiori/lean-draft.js +505 -241
  108. package/libx/_runtime/fiori/utils/delete.js +2 -0
  109. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -5
  110. package/libx/_runtime/hana/pool.js +1 -1
  111. package/libx/_runtime/hana/search2cqn4sql.js +51 -51
  112. package/libx/_runtime/messaging/Outbox.js +1 -1
  113. package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +1 -0
  114. package/libx/_runtime/messaging/enterprise-messaging.js +2 -6
  115. package/libx/_runtime/messaging/file-based.js +1 -2
  116. package/libx/_runtime/messaging/outbox/OutboxRunner.js +1 -1
  117. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  118. package/libx/_runtime/messaging/service.js +0 -1
  119. package/libx/_runtime/remote/Service.js +1 -0
  120. package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +19 -3
  121. package/libx/_runtime/sqlite/customBuilder/CustomExpressionBuilder.js +0 -18
  122. package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +0 -18
  123. package/libx/_runtime/sqlite/customBuilder/CustomSelectBuilder.js +0 -24
  124. package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +2 -1
  125. package/libx/_runtime/sqlite/customBuilder/index.js +47 -32
  126. package/libx/odata/afterburner.js +23 -8
  127. package/libx/odata/cqn2odata.js +1 -1
  128. package/libx/odata/grammar.pegjs +3 -4
  129. package/libx/odata/index.js +5 -1
  130. package/libx/odata/parseToCqn.js +3 -3
  131. package/libx/odata/parser.js +1 -1
  132. package/libx/odata/utils.js +58 -1
  133. package/libx/rest/middleware/parse.js +26 -4
  134. package/package.json +1 -1
  135. package/server.js +1 -1
  136. package/libx/_runtime/sqlite/customBuilder/CustomDeleteBuilder.js +0 -17
  137. package/libx/_runtime/sqlite/customBuilder/CustomReferenceBuilder.js +0 -11
  138. package/libx/_runtime/sqlite/customBuilder/CustomUpdateBuilder.js +0 -17
  139. /package/bin/build/provider/hana/template/{.hdiconfig → .hdiconfig-haas} +0 -0
@@ -1,5 +1,5 @@
1
1
  const cds = require('../cds'),
2
- { Object_keys, results } = cds.utils
2
+ { Object_keys } = cds.utils
3
3
  const LOG = cds.log('fiori|drafts')
4
4
 
5
5
  const DRAFT_ELEMENTS = new Set([
@@ -16,47 +16,255 @@ const REDUCED_DRAFT_ELEMENTS = new Set(['IsActiveEntity', 'HasDraftEntity', 'Sib
16
16
  const DRAFT_ADMIN_ELEMENTS = [
17
17
  'DraftUUID',
18
18
  'LastChangedByUser',
19
+ 'LastChangeDateTime',
19
20
  'CreatedByUser',
21
+ 'CreationDateTime',
20
22
  'InProcessByUser',
21
23
  'DraftIsCreatedByMe',
22
24
  'DraftIsProcessedByMe'
23
25
  ]
24
26
 
27
+ const _inProcessByUserXpr = lockShiftedNow => ({
28
+ xpr: [
29
+ 'case',
30
+ 'when',
31
+ { ref: ['LastChangeDateTime'] },
32
+ '<',
33
+ { val: lockShiftedNow },
34
+ 'then',
35
+ { val: '' },
36
+ 'else',
37
+ { ref: ['InProcessByUser'] },
38
+ 'end'
39
+ ],
40
+ as: 'InProcessByUser',
41
+ cast: { type: 'cds.String' }
42
+ })
43
+
44
+ const _lock = {
45
+ get shiftedNow() {
46
+ return new Date(Math.max(0, Date.now() - DRAFT_CANCEL_TIMEOUT_IN_MIN() * 60 * 1000)).toISOString()
47
+ }
48
+ }
49
+
25
50
  const DRAFT_CANCEL_TIMEOUT_IN_MIN = () =>
26
51
  (cds.env.drafts?.cancellationTimeout && Number(cds.env.drafts?.cancellationTimeout)) || 15
27
52
 
53
+ const _redirectRefToDrafts = (ref, model) => {
54
+ const [root, ...tail] = ref
55
+ const draft = model.definitions[root.id || root].drafts
56
+ return [root.id ? { ...root, id: draft.name } : draft.name, ...tail]
57
+ }
58
+
59
+ const _redirectRefToActives = (ref, model) => {
60
+ const [root, ...tail] = ref
61
+ const active = model.definitions[root.id || root].actives
62
+ return [root.id ? { ...root, id: active.name } : active.name, ...tail]
63
+ }
64
+
28
65
  const h = cds.ApplicationService.prototype.handle
29
- cds.ApplicationService.prototype.handle = function (req) {
66
+ /* eslint-disable complexity */
67
+ cds.ApplicationService.prototype.handle = async function (req) {
30
68
  const handle = h.bind(this)
31
69
 
32
- const _newReq = (req, query) => {
70
+ if (
71
+ !req.query ||
72
+ (!req.query.SELECT && !req.query.INSERT && !req.query.UPDATE && !req.query.DELETE) ||
73
+ req.query._draftParams
74
+ )
75
+ return handle(req)
76
+ const query = _cleansed(req.query, this.model)
77
+ _cleanseParams(req.params)
78
+ const draftParams = query._draftParams
79
+
80
+ const _newReq = (req, query, draftParams, event) => {
33
81
  // REVISIT: This is a bit hacky -> better way?
34
82
  query._target = undefined
83
+ query._draftParams = draftParams
35
84
  cds.infer(query, this.model.definitions)
36
- const _req = cds.Request.for(req._)
85
+ 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
37
87
  _req.query = query
38
- _req.event = req.event
88
+ _req.event =
89
+ event ||
90
+ (query.SELECT && 'READ') ||
91
+ (query.INSERT && 'CREATE') ||
92
+ (query.UPDATE && 'UPDATE') ||
93
+ (query.DELETE && 'DELETE') ||
94
+ req.event
39
95
  _req.target = query._target
40
96
  _req._.params = req.params
97
+ _req.params = req.params
41
98
  _req._.query = query
42
99
  _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
+
43
109
  return _req
44
110
  }
45
111
 
46
- if (!req.query || req.query._draftParams) return handle(req)
47
- const query = _cleansed(req.query, this.model)
48
- const draftParams = query._draftParams
49
- if (req.event !== 'READ') {
50
- const _req = _newReq(req, query)
112
+ const run = async query => {
113
+ const _req = _newReq(req, query, draftParams)
51
114
  return handle(_req)
52
115
  }
53
116
 
54
- const read =
55
- draftParams.IsActiveEntity === false && draftParams.SiblingEntity_IsActiveEntity === null
117
+ if (req.event === 'NEW' || req.event === 'CANCEL' || req.event === 'draftPrepare') {
118
+ if (draftParams.IsActiveEntity) req.reject(501)
119
+ req.target = req.target.drafts
120
+
121
+ if (query.INSERT?.into) {
122
+ if (typeof query.INSERT.into === 'string') query.INSERT.into = req.target.name
123
+ else if (query.INSERT.into.ref) query.INSERT.into.ref = _redirectRefToDrafts(query.INSERT.into.ref, this.model)
124
+ } else if (query.DELETE?.from?.ref) query.DELETE.from.ref = _redirectRefToDrafts(query.DELETE.from.ref, this.model)
125
+ else if (query.SELECT?.from?.ref) query.SELECT.from.ref = _redirectRefToDrafts(query.SELECT.from.ref, this.model)
126
+ const _req = _newReq(req, query, draftParams, req.event)
127
+ const result = await handle(_req)
128
+ return result
129
+ }
130
+
131
+ if (req.event === 'DELETE' && draftParams.IsActiveEntity) {
132
+ const draftsRef = _redirectRefToDrafts(query.DELETE.from.ref, this.model)
133
+ // NOTE: Check if draft is locked!
134
+ const draft = await run(
135
+ SELECT.one.from({ ref: draftsRef }).columns([
136
+ { ref: ['DraftAdministrativeData_DraftUUID'] },
137
+ {
138
+ ref: ['DraftAdministrativeData'],
139
+ expand: [_inProcessByUserXpr(_lock.shiftedNow)]
140
+ }
141
+ ])
142
+ )
143
+ const inProcessByUser = draft?.DraftAdministrativeData?.InProcessByUser
144
+ if (inProcessByUser && inProcessByUser !== cds.context.user.id) req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
145
+ const deletes = [run(DELETE.from({ ref: query.DELETE.from.ref }))]
146
+ if (draft)
147
+ deletes.push(
148
+ run(
149
+ DELETE.from(req.target.drafts).where({
150
+ DraftAdministrativeData_DraftUUID: draft.DraftAdministrativeData_DraftUUID
151
+ })
152
+ )
153
+ )
154
+ if (draft && req.target['@Common.DraftRoot.ActivationAction'])
155
+ deletes.push(
156
+ DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: draft.DraftAdministrativeData_DraftUUID })
157
+ )
158
+ await Promise.all(deletes)
159
+ return req.data
160
+ }
161
+
162
+ if (req.event === 'draftActivate') {
163
+ LOG.debug('activate draft')
164
+ // It would be great if we'd have a SELECT ** to deeply expand the entity (along compositions), that should
165
+ // be implemented in expand implementation.
166
+ if (req.query.SELECT.from.ref.length > 1 || draftParams.IsActiveEntity !== false) {
167
+ req.reject(400, 'Action "draftActivate" can only be called on the root draft entity')
168
+ }
169
+ const targetDraft = req.target.drafts
170
+ const targetWhere = query.SELECT.from.ref[0].where
171
+ const cols = expandStarStar(targetDraft)
172
+ const res = await run(
173
+ SELECT.one
174
+ .from(targetDraft)
175
+ .columns(cols)
176
+ .columns([
177
+ 'HasActiveEntity',
178
+ 'DraftAdministrativeData_DraftUUID',
179
+ { ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] }
180
+ ])
181
+ .where(targetWhere)
182
+ )
183
+ if (!res) req.reject(404)
184
+ if (res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
185
+ req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
186
+ const DraftAdministrativeData_DraftUUID = res.DraftAdministrativeData_DraftUUID
187
+ delete res.DraftAdministrativeData_DraftUUID
188
+ delete res.DraftAdministrativeData
189
+ const HasActiveEntity = res.HasActiveEntity
190
+ delete res.HasActiveEntity
191
+ await Promise.all([
192
+ run(DELETE.from(targetDraft).where(targetWhere)),
193
+ DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: DraftAdministrativeData_DraftUUID })
194
+ ])
195
+
196
+ const result = await run(
197
+ HasActiveEntity ? UPDATE(req.target).data(res).where(targetWhere) : INSERT.into(req.target).entries(res)
198
+ )
199
+ req?._?.odataRes.setStatusCode(201)
200
+
201
+ return Object.assign(result, { IsActiveEntity: true })
202
+
203
+ // REVISIT: we need to use okra API here because it must be set in the batched request
204
+ // status code must be set in handler to allow overriding for FE V2
205
+ }
206
+
207
+ if (req.target.actions?.[req.event] && draftParams.IsActiveEntity === false) {
208
+ if (query.SELECT?.from?.ref) query.SELECT.from.ref = _redirectRefToDrafts(query.SELECT.from.ref, this.model)
209
+ const rootQuery = query.clone()
210
+ rootQuery.SELECT.columns = [{ ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] }]
211
+ rootQuery.SELECT.one = true
212
+ const root = await run(rootQuery)
213
+ if (!root) req.reject(404)
214
+ if (root.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id) req.reject(403)
215
+ const _req = _newReq(req, query, draftParams, req.event)
216
+ const result = await handle(_req)
217
+ return result
218
+ }
219
+
220
+ if (req.event === 'PATCH') {
221
+ if (draftParams.IsActiveEntity) req.reject(501)
222
+ if (req.event === 'PATCH' && !('IsActiveEntity' in draftParams)) {
223
+ const res = await run(
224
+ SELECT.one.from({ ref: req.UPDATE.entity.ref }).columns('DraftAdministrativeData_DraftUUID')
225
+ )
226
+ if (res) req.reject(403, 'DRAFT_ALREADY_EXISTS')
227
+ const _req = _newReq(req, query, draftParams, 'UPDATE')
228
+ const result = await handle(_req)
229
+ return result
230
+ }
231
+ if (req.event === 'PATCH' && draftParams.IsActiveEntity === false) {
232
+ LOG.debug('patch draft')
233
+ if (req.target?.name.endsWith('DraftAdministrativeData')) req.reject(405)
234
+ const draftsRef = _redirectRefToDrafts(query.UPDATE.entity.ref, this.model)
235
+ const res = await run(
236
+ SELECT.one.from({ ref: draftsRef }).columns('DraftAdministrativeData_DraftUUID', {
237
+ ref: ['DraftAdministrativeData'],
238
+ expand: [{ ref: ['InProcessByUser'] }]
239
+ })
240
+ )
241
+ if (!res) req.reject(404)
242
+ if (res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
243
+ req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
244
+ await UPDATE('DRAFT.DraftAdministrativeData')
245
+ .data({
246
+ InProcessByUser: req.user.id,
247
+ LastChangedByUser: req.user.id,
248
+ LastChangeDateTime: new Date()
249
+ })
250
+ .where({ DraftUUID: res.DraftAdministrativeData_DraftUUID })
251
+
252
+ const updateData = { ...req.data }
253
+ delete updateData.IsActiveEntity
254
+ await run(UPDATE({ ref: draftsRef }).data(updateData))
255
+ return Object.assign(req.data, { IsActiveEntity: false })
256
+ }
257
+ }
258
+
259
+ if (req.event === 'READ') {
260
+ const read = req.query._target.name.endsWith('.drafts')
261
+ ? Read.ownDrafts
262
+ : draftParams.IsActiveEntity === false && draftParams.SiblingEntity_IsActiveEntity === null
56
263
  ? Read.all
57
264
  : draftParams.IsActiveEntity === true &&
58
265
  draftParams.SiblingEntity_IsActiveEntity === null &&
59
- draftParams.DraftAdministrativeData_InProcessByUser === 'not null'
266
+ (draftParams.DraftAdministrativeData_InProcessByUser === 'not null' ||
267
+ draftParams.DraftAdministrativeData_InProcessByUser === 'not ')
60
268
  ? Read.lockedByAnotherUser
61
269
  : draftParams.IsActiveEntity === true &&
62
270
  draftParams.SiblingEntity_IsActiveEntity === null &&
@@ -69,15 +277,18 @@ cds.ApplicationService.prototype.handle = function (req) {
69
277
  : draftParams.IsActiveEntity === false
70
278
  ? Read.ownDrafts
71
279
  : Read.onlyActives
72
- const run = async query => {
73
- const _req = _newReq(req, query)
74
- return handle(_req)
280
+ const result = await read(run, query)
281
+ return result
75
282
  }
76
- return read(run, query)
283
+
284
+ const _req = _newReq(req, query, draftParams, req.event)
285
+ const result = await handle(_req)
286
+ return result
77
287
  }
78
288
 
79
289
  const Read = {
80
290
  onlyActives: async function (run, query, { ignoreDrafts } = {}) {
291
+ LOG.debug('List Editing Status: Only Active')
81
292
  // DraftAdministrativeData is only accessible via drafts
82
293
  if (query._target.name.endsWith('.DraftAdministrativeData')) return run(query._drafts)
83
294
  const actives = await run(query)
@@ -105,6 +316,7 @@ const Read = {
105
316
  return actives
106
317
  },
107
318
  unchanged: async function (run, query) {
319
+ LOG.debug('List Editing Status: Unchanged')
108
320
  const draftsQuery = query._drafts
109
321
  const keys = Object_keys(query._target.keys).filter(k => k !== 'IsActiveEntity')
110
322
  draftsQuery.SELECT.count = undefined
@@ -119,10 +331,29 @@ const Read = {
119
331
  return res
120
332
  },
121
333
  ownDrafts: async function (run, query) {
122
- if (!query._target.drafts) return run(query._drafts)
123
- const drafts = await run(
124
- query._drafts.where({ ref: ['DraftAdministrativeData', 'InProcessByUser'] }, '=', cds.context.user.id)
334
+ LOG.debug('List Editing Status: Own Draft')
335
+
336
+ // read active from draft
337
+ if (!query._drafts._target?.name.endsWith('.drafts')) {
338
+ const result = await run(query._drafts)
339
+
340
+ // active entity is draft enabled, draft columns have to be removed
341
+ if (query._drafts._target?.drafts) {
342
+ Read.merge(query._drafts._target, result, [], row => {
343
+ delete row.IsActiveEntity
344
+ delete row.HasDraftEntity
345
+ delete row.HasActiveEntity
346
+ delete row.DraftAdministrativeData_DraftUUID
347
+ })
348
+ }
349
+ return result
350
+ }
351
+ const draftsQuery = query._drafts.where(
352
+ { ref: ['DraftAdministrativeData', 'InProcessByUser'] },
353
+ '=',
354
+ cds.context.user.id
125
355
  )
356
+ const drafts = await run(draftsQuery)
126
357
  Read.merge(query._target, drafts, [], row =>
127
358
  Object.assign(row, {
128
359
  IsActiveEntity: false,
@@ -132,52 +363,67 @@ const Read = {
132
363
  return drafts
133
364
  },
134
365
  all: async function (run, query) {
135
- query._drafts.SELECT.count = true
136
- const drafts = await run(
137
- query._drafts.where({ ref: ['DraftAdministrativeData', 'InProcessByUser'] }, '=', cds.context.user.id)
138
- )
139
-
140
- const skip = query.SELECT.limit?.offset?.val
141
- const top = query.SELECT.limit?.rows?.val
366
+ LOG.debug('List Editing Status: All')
367
+ query._drafts.SELECT.count = false
368
+ query._drafts.SELECT.limit = undefined // We need all entries for the keys to properly select actives (count)
369
+ const isCount = query._drafts.SELECT.columns?.[0]?.func === 'count'
370
+ if (isCount) {
371
+ const keys = Object_keys(query._target.keys).filter(k => k !== 'IsActiveEntity')
372
+ query._drafts.SELECT.columns = keys.map(k => ({ ref: [k] }))
373
+ }
374
+ if (!query._drafts.SELECT.columns) query._drafts.SELECT.columns = ['*']
375
+ if (!query._drafts.SELECT.columns.some(c => c.ref?.[0] === 'HasActiveEntity'))
376
+ query._drafts.SELECT.columns.push({ ref: ['HasActiveEntity'] })
377
+
378
+ const isFirstPage = !query.SELECT.limit?.offset?.val
379
+
380
+ const [ownDrafts, actives] = await Promise.all([
381
+ isFirstPage
382
+ ? run(query._drafts.where({ ref: ['DraftAdministrativeData', 'InProcessByUser'] }, '=', cds.context.user.id))
383
+ : [], // we only show drafts on the first page
384
+ run(query)
385
+ ])
386
+
387
+ const ownNewDrafts = []
388
+ const ownEditDrafts = []
389
+ for (const draft of ownDrafts) {
390
+ if (draft.HasActiveEntity) ownEditDrafts.push(draft)
391
+ else ownNewDrafts.push(draft)
392
+ }
142
393
 
143
- const appliedSkip = drafts.length ? skip : drafts.$count
394
+ const count = isFirstPage ? ownNewDrafts.length + (isCount ? actives[0]?.$count : actives.$count) : actives.$count
395
+ if (isCount) return { $count: count }
144
396
 
145
- const topActives = top && Math.max(0, top - drafts.length)
146
- let actives
147
- if (topActives === 0) {
148
- actives = []
149
- } else {
150
- const skipActives = skip && skip - appliedSkip
151
- query.SELECT.count = true
152
- actives = await run(query.limit(topActives, skipActives).where(Read.whereNotIn(query._target, drafts)))
153
- const draftsFromActives = await Read.complementaryDrafts(run, query, actives)
154
- Read.merge(query._target, actives, draftsFromActives, (row, other) => {
155
- if (other) {
156
- Object.assign(row, other, {
157
- IsActiveEntity: true,
158
- HasDraftEntity: true,
159
- HasActiveEntity: false
160
- })
161
- } else {
162
- Object.assign(row, {
163
- IsActiveEntity: true,
164
- HasDraftEntity: false,
165
- HasActiveEntity: false,
166
- DraftAdministrativeData_DraftUUID: null,
167
- DraftAdministrativeData: null
168
- })
169
- }
170
- })
171
- }
172
- Read.merge(query._target, drafts, [], row =>
397
+ Read.merge(query._target, ownDrafts, [], row =>
173
398
  Object.assign(row, {
174
399
  IsActiveEntity: false,
175
400
  HasDraftEntity: false
176
401
  })
177
402
  )
178
- const totalCount = actives.$count + drafts.$count
179
- const result = Object.assign([...drafts, ...actives], { $count: totalCount })
180
- return result
403
+ Read.delete(query._target, actives, ownEditDrafts)
404
+ const otherEditDrafts = await Read.complementaryDrafts(run, query, actives)
405
+ Read.merge(query._target, actives, otherEditDrafts, (row, other) => {
406
+ if (other) {
407
+ Object.assign(row, {
408
+ IsActiveEntity: true,
409
+ HasDraftEntity: true,
410
+ HasActiveEntity: false,
411
+ DraftAdministrativeData_DraftUUID: other.DraftAdministrativeData_DraftUUID,
412
+ DraftAdministrativeData: other.DraftAdministrativeData
413
+ })
414
+ } else {
415
+ Object.assign(row, {
416
+ IsActiveEntity: true,
417
+ HasDraftEntity: false,
418
+ HasActiveEntity: false,
419
+ DraftAdministrativeData_DraftUUID: null,
420
+ DraftAdministrativeData: null
421
+ })
422
+ }
423
+ })
424
+ const res = isFirstPage ? [...ownNewDrafts, ...ownEditDrafts, ...actives] : actives
425
+ if (query.SELECT.count) res.$count = count
426
+ return res
181
427
  },
182
428
  activesFromDrafts: async function (run, query, { isLocked = true }) {
183
429
  const draftsQuery = query._drafts
@@ -192,7 +438,7 @@ const Read = {
192
438
  HasActiveEntity: true,
193
439
  'DraftAdministrativeData.InProcessByUser': { '!=': cds.context.user.id },
194
440
  'DraftAdministrativeData.LastChangeDateTime': {
195
- [isLocked ? '>' : '<']: Read.lockshiftedNow
441
+ [isLocked ? '>' : '<']: _lock.shiftedNow
196
442
  }
197
443
  })
198
444
  const drafts = await run(draftsQuery)
@@ -207,14 +453,13 @@ const Read = {
207
453
  return actives
208
454
  },
209
455
  unsavedChangesByAnotherUser: async function (run, query) {
456
+ LOG.debug('List Editing Status: Unsaved Changes by Another User')
210
457
  return Read.activesFromDrafts(run, query, { isLocked: false })
211
458
  },
212
459
  lockedByAnotherUser: async function (run, query) {
460
+ LOG.debug('List Editing Status: Locked by Another User')
213
461
  return Read.activesFromDrafts(run, query, { isLocked: true })
214
462
  },
215
- get lockshiftedNow() {
216
- return new Date(Date.now() - DRAFT_CANCEL_TIMEOUT_IN_MIN() * 60 * 1000).toISOString()
217
- },
218
463
  whereNotIn: (target, data) => Read.whereIn(target, data, true),
219
464
  whereIn: (target, data, not = false) => {
220
465
  const keys = Object_keys(target.keys).filter(k => k !== 'IsActiveEntity')
@@ -230,6 +475,12 @@ const Read = {
230
475
  if (!actives.length) return []
231
476
  const drafts = cds.ql.clone(query._drafts)
232
477
  drafts.SELECT.where = Read.whereIn(query._target, actives)
478
+ if (drafts.SELECT.columns?.some(c => c === '*')) {
479
+ drafts.SELECT.columns = drafts.SELECT.columns.filter(c => c !== '*')
480
+ if (!drafts.SELECT.columns.some(c => c.ref?.[0] === 'DraftAdministrativeData_DraftUUID')) {
481
+ drafts.SELECT.columns.push({ ref: ['DraftAdministrativeData_DraftUUID'] })
482
+ }
483
+ }
233
484
  const relevantColumns = ['DraftAdministrativeData', 'DraftAdministrativeData_DraftUUID']
234
485
  drafts.SELECT.columns = (
235
486
  drafts.SELECT.columns?.filter(c => c.ref && relevantColumns.includes(c.ref[0])) ||
@@ -240,60 +491,100 @@ const Read = {
240
491
  .map(k => ({ ref: [k] }))
241
492
  )
242
493
  drafts.SELECT.count = undefined
494
+ drafts.SELECT.search = undefined
243
495
  drafts.SELECT.one = undefined
244
496
  return run(drafts)
245
497
  },
498
+ _makeArray: data => (Array.isArray(data) ? data : data ? [data] : []),
499
+ _index: (target, data) => {
500
+ // Indexes the data for fast key access
501
+ const dataArray = Read._makeArray(data)
502
+ if (!dataArray.length) return
503
+ const _keys = Object_keys(target.keys).filter(k => k !== 'IsActiveEntity')
504
+ const hash = row => _keys.map(k => row[k]).reduce((res, curr) => res + '|$|' + curr, '')
505
+ const hashMap = new Map()
506
+ for (const row of dataArray) hashMap.set(hash(row), row)
507
+ return { hashMap, hash }
508
+ },
509
+ // Calls `cb` for each entry of data with a potential counterpart in otherData
246
510
  merge: (target, data, otherData, cb) => {
247
- if (!data) return
248
- const dataArray = Array.isArray(data) ? data : [data]
511
+ const dataArray = Read._makeArray(data)
249
512
  if (!dataArray.length) return
250
- const otherDataArray = Array.isArray(otherData) ? otherData : otherData ? [otherData] : []
251
- if (otherDataArray.length) {
252
- const _keys = Object_keys(target.keys).filter(k => k !== 'IsActiveEntity')
253
- const _hash = row => _keys.map(k => row[k]).reduce((res, curr) => res + '|$|' + curr, '')
254
- const d2hash = new Map()
255
- for (const row of otherDataArray) d2hash.set(_hash(row), row)
256
- for (const row of dataArray) {
257
- const other = d2hash.get(_hash(row))
258
- cb(row, other)
259
- }
260
- } else {
261
- for (const row of dataArray) {
262
- cb(row, undefined)
263
- }
513
+
514
+ const index = Read._index(target, otherData)
515
+ for (const row of dataArray) {
516
+ const other = index?.hashMap.get(index.hash(row))
517
+ cb(row, other)
518
+ }
519
+ },
520
+ // Deletes entries of data with a counterpart in otherData
521
+ delete: (target, data, otherData) => {
522
+ if (!Array.isArray(data) || !data.length) return
523
+
524
+ const index = Read._index(target, otherData)
525
+ let i = data.length
526
+ while (i--) {
527
+ if (index?.hashMap.get(index.hash(data[i]))) data.splice(i, 1)
264
528
  }
265
529
  }
266
530
  }
267
531
 
532
+ function _cleanseParams(params) {
533
+ if (Array.isArray(params)) {
534
+ for (const param of params) _cleanseParams(param)
535
+ return
536
+ }
537
+ if (typeof params === 'object') {
538
+ for (const key in params) {
539
+ if (key === 'IsActiveEntity') delete params[key]
540
+ }
541
+ }
542
+ }
543
+
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
+ )
555
+ }
556
+
268
557
  /**
269
558
  * Creates a clone of the query, cleanses and collects all draft parameters into ._draftParams.
270
559
  */
271
- function _cleansed(query, model, target) {
560
+ function _cleansed(query, model) {
272
561
  const draftParams = {} //> used to collect draft filter criteria
273
562
  const q = _cleanseQuery(query, draftParams)
274
563
  if (query.SELECT) {
275
- let cache
276
564
  const getDrafts = () => {
277
- if (cache) return cache
278
565
  const draftsQuery = _cleanseQuery(query, {}) // could just clone `q` but the latter is ruined by database layer
566
+ draftsQuery._target = undefined
279
567
  const [root, ...tail] = draftsQuery.SELECT.from.ref
280
568
  const draft = model.definitions[root.id || root].drafts
281
569
  draftsQuery.SELECT.from = {
282
570
  ref: [root.id ? { ...root, id: draft.name } : draft.name, ...tail]
283
571
  }
572
+ cds.infer(draftsQuery, model.definitions)
573
+ // draftsQuery._target = draftsQuery._target?.drafts || draftsQuery._target
284
574
  if (query.SELECT.columns && query._target.drafts)
285
- draftsQuery.SELECT.columns = query.SELECT.columns.filter(c => !REDUCED_DRAFT_ELEMENTS.has(c.ref?.[0]))
575
+ draftsQuery.SELECT.columns = _cleanseCols(query.SELECT.columns, REDUCED_DRAFT_ELEMENTS)
286
576
 
287
577
  if (draftsQuery._target.name.endsWith('.DraftAdministrativeData')) {
288
578
  draftsQuery.SELECT.columns = _tweakAdminCols(draftsQuery.SELECT.columns)
289
- } else if (q._target.drafts) {
579
+ } else if (draftsQuery._target?.name.endsWith('.drafts')) {
290
580
  draftsQuery.SELECT.columns = _tweakAdminExpand(draftsQuery.SELECT.columns)
291
581
  }
292
582
  Object.defineProperty(draftsQuery, '_draftParams', { value: draftParams, enumerable: false })
293
- cache = draftsQuery
583
+ Object.defineProperty(q, '_drafts', { value: draftsQuery })
294
584
  return draftsQuery
295
585
  }
296
586
  Object.defineProperty(q, '_drafts', {
587
+ configurable: true,
297
588
  get() {
298
589
  return getDrafts()
299
590
  }
@@ -324,7 +615,7 @@ function _cleansed(query, model, target) {
324
615
  }
325
616
 
326
617
  if (cqn.where) cqn.where = _cleanseWhere(cqn.where, draftParams)
327
- if (cqn.columns) cqn.columns = q.SELECT.columns.filter(c => !DRAFT_ELEMENTS.has(c.ref?.[0]))
618
+ if (cqn.columns) cqn.columns = _cleanseCols(q.SELECT.columns, DRAFT_ELEMENTS)
328
619
  if (cqn.orderBy) cqn.orderBy = _cleanseWhere(cqn.orderBy, {})
329
620
 
330
621
  return q
@@ -341,7 +632,7 @@ function _cleansed(query, model, target) {
341
632
  }
342
633
 
343
634
  function _tweakAdminCols(columns) {
344
- if (!columns) columns = DRAFT_ADMIN_ELEMENTS.map(k => ({ ref: [k] }))
635
+ if (!columns || columns.some(c => c === '*')) columns = DRAFT_ADMIN_ELEMENTS.map(k => ({ ref: [k] }))
345
636
  return columns.map(col => {
346
637
  const name = col.ref?.[0]
347
638
  if (!name) return col
@@ -366,22 +657,7 @@ function _cleansed(query, model, target) {
366
657
  cast: { type: 'cds.Boolean' }
367
658
  }
368
659
  case 'InProcessByUser':
369
- return {
370
- xpr: [
371
- 'case',
372
- 'when',
373
- { ref: ['LastChangeDateTime'] },
374
- '<',
375
- { val: Read.lockshiftedNow },
376
- 'then',
377
- { val: '' },
378
- 'else',
379
- { ref: ['InProcessByUser'] },
380
- 'end'
381
- ],
382
- as: 'InProcessByUser',
383
- cast: { type: 'cds.String' }
384
- }
660
+ return _inProcessByUserXpr(_lock.shiftedNow)
385
661
  case 'DraftIsProcessedByMe':
386
662
  return {
387
663
  xpr: [
@@ -390,6 +666,10 @@ function _cleansed(query, model, target) {
390
666
  { ref: ['InProcessByUser'] },
391
667
  '=',
392
668
  { val: cds.context.user.id },
669
+ 'and',
670
+ { ref: ['LastChangeDateTime'] },
671
+ '>',
672
+ { val: _lock.shiftedNow },
393
673
  'then',
394
674
  { val: true },
395
675
  'else',
@@ -410,9 +690,12 @@ function _cleansed(query, model, target) {
410
690
  for (let i = 0; i < xpr.length; ++i) {
411
691
  let x = xpr[i],
412
692
  e = x.ref?.[0]
413
- if (DRAFT_ELEMENTS.has(e)) {
693
+ if (DRAFT_ELEMENTS.has(e) && !xpr[i + 2]) {
694
+ continue
695
+ }
696
+ if (DRAFT_ELEMENTS.has(e) && xpr[i + 2]) {
414
697
  let { val } = xpr[i + 2]
415
- draftParams[x.ref.join('_')] = xpr[i + 1] === '!=' ? 'not ' + val : val
698
+ draftParams[x.ref.join('_')] = xpr[i + 1] === '!=' ? (typeof val === 'boolean' ? !val : 'not ' + val) : val
416
699
  i += 3
417
700
  continue
418
701
  }
@@ -431,27 +714,46 @@ function _cleansed(query, model, target) {
431
714
  }
432
715
  }
433
716
 
434
- function _draftIsLocked(LastChangeDateTime) {
435
- return DRAFT_CANCEL_TIMEOUT_IN_MIN() * 60 * 1000 > Date.now() - Date.parse(LastChangeDateTime)
717
+ // This function is better defined on DB layer
718
+ function expandStarStar(target, recursion = new Map()) {
719
+ const MAX_RECURSION_DEPTH = (cds.env.features.recursion_depth && Number(cds.env.features.recursion_depth)) || 4
720
+ const columns = []
721
+ for (const el in target.elements) {
722
+ const element = target.elements[el]
723
+ if (!element.isAssociation && !DRAFT_ELEMENTS.has(el)) columns.push({ ref: [el] })
724
+ if (!element.isComposition || element._target['@odata.draft.enabled'] === false) continue // happens for texts if not @fiori.draft.enabled
725
+ const _key = target.name + ':' + el
726
+ let cache = recursion.get(_key)
727
+ if (!cache) {
728
+ cache = 1
729
+ recursion.set(_key, cache)
730
+ } else {
731
+ cache++
732
+ recursion.set(_key, cache)
733
+ }
734
+ if (cache >= MAX_RECURSION_DEPTH) return
735
+ const expand = expandStarStar(element._target, recursion)
736
+ if (expand) columns.push({ ref: [el], expand })
737
+ }
738
+ return columns
436
739
  }
437
740
 
438
- async function onNewDraft(req) {
741
+ async function onNew(req) {
439
742
  LOG.debug('new draft')
440
743
  const isRoot = typeof req.query.INSERT.into === 'string'
441
744
  let DraftUUID
442
745
  if (isRoot) DraftUUID = cds.utils.uuid()
443
746
  else {
444
- const rootData = await SELECT.one(req.query.INSERT.into.ref[0].id + '.drafts', d => {
445
- d.DraftAdministrativeData_DraftUUID,
446
- d.DraftAdministrativeData(a => {
447
- a.LastChangeDateTime, a.InProcessByUser
448
- })
449
- }).where(req.query.INSERT.into.ref[0].where)
450
- if (!rootData) req.reject(404)
451
- if (
452
- !rootData.DraftAdministrativeData.InProcessByUser === req.user.id &&
453
- _draftIsLocked(rootData.DraftAdministrativeData.LastChangeDateTime)
747
+ const rootData = await this.run(
748
+ SELECT.one(req.query.INSERT.into.ref[0].id)
749
+ .columns([
750
+ { ref: ['DraftAdministrativeData_DraftUUID'] },
751
+ { ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] }
752
+ ])
753
+ .where(req.query.INSERT.into.ref[0].where)
454
754
  )
755
+ if (!rootData) req.reject(404)
756
+ if (rootData.DraftAdministrativeData?.InProcessByUser !== req.user.id)
455
757
  req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
456
758
  DraftUUID = rootData.DraftAdministrativeData_DraftUUID
457
759
  }
@@ -479,60 +781,21 @@ async function onNewDraft(req) {
479
781
  { DraftAdministrativeData_DraftUUID: DraftUUID, HasActiveEntity: false },
480
782
  req.query.INSERT.entries[0]
481
783
  )
784
+
482
785
  delete draftData.IsActiveEntity
483
- const draftCQN = INSERT.into(req.target.drafts).entries(draftData)
786
+ const draftCQN = INSERT.into(req.target).entries(draftData)
484
787
 
485
- // TODO: do this? req.query._draftParams.IsActiveEntity = false
486
- await Promise.all([adminDataCQN, draftCQN].map(cqn => cds.run(cqn)))
788
+ await Promise.all([cds.run(adminDataCQN), this.run(draftCQN)])
487
789
  req._.readAfterWrite = true
488
790
  return { ...draftData, IsActiveEntity: false }
489
791
  }
490
- exports.onNewDraft = onNewDraft
491
-
492
- async function onDraftPrepare(req) {
493
- LOG.debug('prepare draft')
494
-
495
- const draftParams = req.query._draftParams
496
- const where = req.query.SELECT.from.ref[0].where
497
- if (req.query.SELECT.from.ref.length > 1 || draftParams.IsActiveEntity !== false) {
498
- req.reject(400, 'Action "draftPrepare" can only be called on the root draft entity')
499
- }
500
-
501
- const keys = Object_keys(req.target.keys).filter(k => k !== 'IsActiveEntity')
502
- const data = await SELECT.one
503
- .from(req.target.drafts, d => {
504
- d.DraftAdministrativeData(a => a.InProcessByUser)
505
- })
506
- .columns(keys)
507
- .where(where)
508
- if (!data) req.reject(404)
509
- if (data.DraftAdministrativeData.InProcessByUser !== req.user.id) req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
510
- delete data.DraftAdministrativeData
511
- return { ...data, IsActiveEntity: false }
512
- }
513
- exports.onDraftPrepare = onDraftPrepare
514
-
515
- // This function is better defined on DB layer
516
- function expandStarStar(target, recursion = new Map()) {
517
- const MAX_RECURSION_DEPTH = (cds.env.features.recursion_depth && Number(cds.env.features.recursion_depth)) || 4
518
- // TODO: Remove draftcolumns from payload
519
- const columns = []
520
- for (const el in target.elements) {
521
- const element = target.elements[el]
522
- if (!element.isAssociation && !DRAFT_ELEMENTS.has(el)) columns.push({ ref: [el] })
523
- if (!element.isComposition) continue
524
- const _key = target.name + ':' + el
525
- let cache = recursion.get(_key)
526
- if (!cache) cache = 1 && recursion.set(_key, cache)
527
- if (cache >= MAX_RECURSION_DEPTH) return
528
- columns.push({ ref: [el], expand: expandStarStar(element._target, recursion) })
529
- }
530
- return columns
531
- }
532
792
 
533
- async function onDraftEdit(req) {
793
+ async function onEdit(req) {
534
794
  LOG.debug('edit active')
535
795
  const draftParams = req.query._draftParams
796
+ 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')
798
+ }
536
799
  const targetWhere = req.query.SELECT.from.ref[0].where
537
800
 
538
801
  if (draftParams.IsActiveEntity !== true) req.reject(400)
@@ -553,13 +816,30 @@ async function onDraftEdit(req) {
553
816
  }
554
817
  }
555
818
  _addDraftColumns(req.target, cols)
556
-
557
- const [res, draftExists] = await Promise.all([
558
- SELECT.one.from(req.target).columns(cols).where(targetWhere),
559
- SELECT.one(req.target.drafts).columns('1 as _exists').where(targetWhere)
819
+ const draftsCheck = SELECT.one(req.target.drafts)
820
+ .columns({ ref: ['DraftAdministrativeData'], expand: [_inProcessByUserXpr(_lock.shiftedNow)] })
821
+ .where(targetWhere)
822
+ .forUpdate({ wait: 0 })
823
+ // prevent service to check for own user
824
+ Object.defineProperty(draftsCheck, '_draftParams', { value: draftParams, enumerable: false })
825
+
826
+ const [res, draft] = await Promise.all([
827
+ this.run(SELECT.one.from(req.target).columns(cols).where(targetWhere).forUpdate({ wait: 0 })),
828
+ // no user check must be done here...
829
+ this.run(draftsCheck)
560
830
  ])
561
831
  if (!res) req.reject(404)
562
- if (draftExists) req.reject(409, 'DRAFT_ALREADY_EXISTS')
832
+ const preserveChanges = req.context?.data?.PreserveChanges
833
+ const inProcessByUser = draft?.DraftAdministrativeData?.InProcessByUser
834
+ if (draft) {
835
+ if (inProcessByUser || preserveChanges) req.reject(409, 'DRAFT_ALREADY_EXISTS')
836
+ const keys = {}
837
+ for (const key in req.target.drafts.keys) keys[key] = res[key]
838
+ await Promise.all([
839
+ DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID }),
840
+ this.run(DELETE.from(req.target.drafts).where(keys))
841
+ ])
842
+ }
563
843
 
564
844
  const timestamp = cds.context.timestamp.toISOString() // REVISIT: toISOString should be done on db layer
565
845
  await INSERT.into('DRAFT.DraftAdministrativeData').entries({
@@ -574,84 +854,68 @@ async function onDraftEdit(req) {
574
854
  })
575
855
 
576
856
  const targetDraft = req.target.drafts
577
- await INSERT.into(targetDraft).entries(res)
578
- return { ...res, IsActiveEntity: false } // REVISIT: Flatten?
579
- }
580
- exports.onDraftEdit = onDraftEdit
857
+ // is set to `null` on srv layer
858
+ res.DraftAdministrativeData_DraftUUID = DraftUUID
859
+ res.HasActiveEntity = true
860
+ delete res.DraftAdministrativeData
861
+ await this.run(INSERT.into(targetDraft).entries(res))
581
862
 
582
- async function onDraftActivate(req) {
583
- LOG.debug('activate draft')
584
- // TODO: Read all drafts, send deep update/insert (based on HasActiveEntity, which should be fille with new!)
585
- // It would be great if we'd have a SELECT ** to deeply expand the entity (along compositions), that should
586
- // be implemented in expand implementation.
587
- const targetDraft = req.target.drafts
588
- const targetWhere = req.query.SELECT.from.ref[0].where
589
- const res = await SELECT.one
590
- .from(targetDraft)
591
- .columns(expandStarStar(targetDraft))
592
- .columns(['HasActiveEntity', 'DraftAdministrativeData_DraftUUID'])
593
- .where(targetWhere)
594
- const DraftAdministrativeData_DraftUUID = res.DraftAdministrativeData_DraftUUID
595
- delete res.DraftAdministrativeData_DraftUUID
596
- const HasActiveEntity = res.HasActiveEntity
597
- delete res.HasActiveEntity
598
- // TODO: Deep delete!
599
- await Promise.all([
600
- DELETE.from(targetDraft).where(targetWhere),
601
- DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: DraftAdministrativeData_DraftUUID })
602
- ])
603
- // TODO: Check for InProcessByUser
604
-
605
- let event, query
606
- if (HasActiveEntity) {
607
- query = UPDATE(req.target).data(res).where(targetWhere)
608
- event = 'UPDATE'
609
- } else {
610
- query = INSERT.into(req.target).entries(res)
611
- event = 'CREATE'
612
- }
613
- const r = new cds.Request({ event, query, data: res })
614
- const result = await this.dispatch(r)
615
- return Object.assign(result, { IsActiveEntity: true })
863
+ // REVISIT: we need to use okra API here because it must be set in the batched request
864
+ // status code must be set in handler to allow overriding for FE V2
865
+ req?._?.odataRes.setStatusCode(201)
866
+
867
+ return { ...res, IsActiveEntity: false } // REVISIT: Flatten?
616
868
  }
617
- exports.onDraftActivate = onDraftActivate
618
869
 
619
- async function onPatch(req) {
620
- LOG.debug('patch draft')
621
- const targetDraft = req.target.drafts
622
- const targetWhere = req.query.UPDATE.entity.ref[0].where
623
- const res = await SELECT.one.from(targetDraft).columns('DraftAdministrativeData_DraftUUID').where(targetWhere)
624
- if (!res) req.reject(404)
625
- await UPDATE('DRAFT.DraftAdministrativeData')
626
- .data({
627
- InProcessByUser: req.user.id,
628
- LastChangedByUser: req.user.id,
629
- LastChangeDateTime: new Date()
630
- })
631
- .where({ DraftUUID: res.DraftAdministrativeData_DraftUUID })
870
+ async function onCancel(req) {
871
+ LOG.debug('delete draft')
872
+ const activeRef = _redirectRefToActives(req.query.DELETE.from.ref, this.model)
873
+ const draftParams = req.query._draftParams
632
874
 
633
- const updateData = { ...req.data }
634
- delete updateData.IsActiveEntity
635
- await UPDATE(targetDraft).data(updateData).where(targetWhere)
875
+ const draftDelete = SELECT.one
876
+ .from({ ref: req.query.DELETE.from.ref })
877
+ .columns([
878
+ 'DraftAdministrativeData_DraftUUID',
879
+ { ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] }
880
+ ])
881
+ // do not add InProcessByUser restriction
882
+ Object.defineProperty(draftDelete, '_draftParams', { value: draftParams, enumerable: false })
883
+ const draft = await this.run(draftDelete)
884
+ if (draftParams.IsActiveEntity === false && !draft) req.reject(404)
885
+ if (draft && draft.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
886
+ req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
887
+ const deletes = !draft ? [] : [this.run(DELETE.from({ ref: req.query.DELETE.from.ref }))]
888
+ if (draft && req.target['@Common.DraftRoot.ActivationAction'])
889
+ // only for draft root
890
+ deletes.push(
891
+ DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: draft.DraftAdministrativeData_DraftUUID })
892
+ )
893
+ if (draftParams.IsActiveEntity) deletes.push(this.run(DELETE.from({ ref: activeRef })))
894
+ await Promise.all(deletes)
636
895
  return req.data
637
896
  }
638
- exports.onPatch = onPatch
639
897
 
640
- async function onDelete(req) {
641
- LOG.debug('delete')
642
- const targetDraft = req.target.drafts
898
+ async function onPrepare(req) {
899
+ LOG.debug('prepare draft')
643
900
  const draftParams = req.query._draftParams
644
- const targetWhere = req.query.DELETE.from.ref[0].where
645
- const res = await SELECT.one.from(targetDraft).columns('DraftAdministrativeData_DraftUUID').where(targetWhere)
646
- if (!res && draftParams.IsActiveEntity === false) req.reject(404)
647
- const deletes = !res
648
- ? []
649
- : [
650
- DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: res.DraftAdministrativeData_DraftUUID }),
651
- DELETE.from(targetDraft).where(targetWhere)
652
- ]
653
- if (draftParams.IsActiveEntity) deletes.push(DELETE.from(req.target).where(targetWhere))
654
- await Promise.all(deletes)
655
- return req.data
901
+ if (req.query.SELECT.from.ref.length > 1 || draftParams.IsActiveEntity !== false) {
902
+ req.reject(400, 'Action "draftPrepare" can only be called on the root draft entity')
903
+ }
904
+ const where = req.query.SELECT.from.ref[0].where
905
+
906
+ const keys = Object_keys(req.target.keys).filter(k => k !== 'IsActiveEntity')
907
+ const draftQuery = SELECT.one
908
+ .from(req.target, d => {
909
+ d.DraftAdministrativeData(a => a.InProcessByUser)
910
+ })
911
+ .columns(keys)
912
+ .where(where)
913
+ Object.defineProperty(draftQuery, '_draftParams', { value: draftParams, enumerable: false })
914
+ const data = await this.run(draftQuery)
915
+ if (!data) req.reject(404)
916
+ if (data.DraftAdministrativeData?.InProcessByUser !== req.user.id) req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
917
+ delete data.DraftAdministrativeData
918
+ return { ...data, IsActiveEntity: false }
656
919
  }
657
- exports.onDelete = onDelete
920
+
921
+ module.exports = { onNew, onEdit, onCancel, onPrepare }