@sap/cds 6.5.0 → 6.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/CHANGELOG.md +53 -2
  2. package/README.md +5 -0
  3. package/apis/services.d.ts +5 -0
  4. package/bin/build/buildTaskEngine.js +0 -2
  5. package/bin/build/buildTaskFactory.js +1 -1
  6. package/bin/build/buildTaskHandler.js +1 -1
  7. package/bin/build/provider/buildTaskProviderInternal.js +10 -6
  8. package/bin/build/provider/fiori/index.js +5 -10
  9. package/bin/build/provider/hana/2migration.js +11 -2
  10. package/bin/build/provider/hana/index.js +17 -14
  11. package/bin/build/provider/hana/template/.hdiconfig-hanacloud +137 -0
  12. package/bin/build/provider/mtx-extension/index.js +18 -1
  13. package/bin/build/provider/mtx-sidecar/index.js +1 -1
  14. package/bin/build/util.js +1 -1
  15. package/bin/cds.js +1 -5
  16. package/bin/deploy/to-hana/hana.js +10 -3
  17. package/bin/deploy/to-hana/hdiDeployUtil.js +24 -12
  18. package/bin/serve.js +32 -20
  19. package/lib/auth/jwt-auth.js +4 -4
  20. package/lib/compile/for/lean_drafts.js +55 -6
  21. package/lib/dbs/cds-deploy.js +6 -8
  22. package/lib/env/schemas/cds-rc.json +4 -0
  23. package/lib/index.js +4 -2
  24. package/lib/req/cds-context.js +3 -3
  25. package/lib/srv/bindings.js +1 -2
  26. package/lib/srv/cds-serve.js +2 -1
  27. package/lib/srv/middlewares/trace.js +31 -15
  28. package/lib/srv/protocols/odata-v2-proxy.js +8 -8
  29. package/lib/srv/srv-handlers.js +26 -7
  30. package/lib/srv/srv-methods.js +2 -2
  31. package/lib/srv/srv-models.js +4 -3
  32. package/lib/utils/cds-test.js +7 -5
  33. package/libx/_runtime/auth/strategies/ias-auth.js +1 -1
  34. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +6 -2
  35. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +26 -1
  36. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -0
  37. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +1 -1
  38. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +11 -2
  39. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +8 -8
  40. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +1 -1
  41. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/validator/ValueValidator.js +14 -14
  42. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/DeserializerFactory.js +7 -8
  43. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ResourceJsonSerializer.js +3 -0
  44. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/UriHelper.js +2 -1
  45. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +3 -2
  46. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +7 -0
  47. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +0 -3
  48. package/libx/_runtime/cds-services/services/Service.js +8 -19
  49. package/libx/_runtime/cds-services/services/utils/columns.js +7 -4
  50. package/libx/_runtime/cds-services/util/assert.js +7 -1
  51. package/libx/_runtime/common/code-ext/WorkerReq.js +3 -1
  52. package/libx/_runtime/common/code-ext/execute.js +9 -2
  53. package/libx/_runtime/common/code-ext/handlers.js +2 -2
  54. package/libx/_runtime/common/code-ext/worker.js +9 -5
  55. package/libx/_runtime/common/code-ext/workerQueryExecutor.js +5 -2
  56. package/libx/_runtime/common/composition/data.js +5 -2
  57. package/libx/_runtime/common/composition/tree.js +2 -0
  58. package/libx/_runtime/common/generic/auth/restrict.js +1 -1
  59. package/libx/_runtime/common/generic/etag.js +22 -10
  60. package/libx/_runtime/common/generic/input.js +12 -14
  61. package/libx/_runtime/common/utils/cqn2cqn4sql.js +31 -11
  62. package/libx/_runtime/common/utils/path.js +0 -1
  63. package/libx/_runtime/common/utils/search2cqn4sql.js +4 -1
  64. package/libx/_runtime/common/utils/structured.js +1 -0
  65. package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +19 -13
  66. package/libx/_runtime/db/data-conversion/post-processing.js +1 -1
  67. package/libx/_runtime/db/expand/expandCQNToJoin.js +5 -3
  68. package/libx/_runtime/db/expand/rawToExpanded.js +3 -2
  69. package/libx/_runtime/db/generic/input.js +2 -2
  70. package/libx/_runtime/db/generic/integrity.js +1 -0
  71. package/libx/_runtime/db/generic/virtual.js +1 -0
  72. package/libx/_runtime/db/query/read.js +3 -2
  73. package/libx/_runtime/fiori/generic/activate.js +3 -1
  74. package/libx/_runtime/fiori/generic/before.js +1 -0
  75. package/libx/_runtime/fiori/generic/edit.js +6 -1
  76. package/libx/_runtime/fiori/generic/new.js +2 -0
  77. package/libx/_runtime/fiori/generic/patch.js +2 -0
  78. package/libx/_runtime/fiori/generic/prepare.js +2 -0
  79. package/libx/_runtime/fiori/generic/read.js +8 -2
  80. package/libx/_runtime/fiori/generic/readOverDraft.js +2 -0
  81. package/libx/_runtime/fiori/lean-draft.js +498 -245
  82. package/libx/_runtime/fiori/utils/delete.js +2 -0
  83. package/libx/_runtime/messaging/Outbox.js +1 -1
  84. package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +1 -0
  85. package/libx/_runtime/messaging/enterprise-messaging.js +2 -6
  86. package/libx/_runtime/messaging/file-based.js +1 -2
  87. package/libx/_runtime/messaging/outbox/OutboxRunner.js +1 -1
  88. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  89. package/libx/_runtime/messaging/service.js +0 -1
  90. package/libx/_runtime/remote/Service.js +1 -0
  91. package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +19 -3
  92. package/libx/_runtime/sqlite/customBuilder/CustomExpressionBuilder.js +0 -18
  93. package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +0 -18
  94. package/libx/_runtime/sqlite/customBuilder/CustomSelectBuilder.js +0 -24
  95. package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +2 -1
  96. package/libx/_runtime/sqlite/customBuilder/index.js +47 -32
  97. package/libx/odata/afterburner.js +17 -5
  98. package/libx/odata/grammar.pegjs +3 -4
  99. package/libx/odata/index.js +5 -1
  100. package/libx/odata/parseToCqn.js +3 -3
  101. package/libx/odata/parser.js +1 -1
  102. package/libx/odata/utils.js +58 -1
  103. package/package.json +1 -1
  104. package/server.js +1 -1
  105. package/libx/_runtime/sqlite/customBuilder/CustomDeleteBuilder.js +0 -17
  106. package/libx/_runtime/sqlite/customBuilder/CustomReferenceBuilder.js +0 -11
  107. package/libx/_runtime/sqlite/customBuilder/CustomUpdateBuilder.js +0 -17
  108. /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,48 +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
85
  const _req = cds.Request.for(req._) // REVISIT: this causes req._.data of WRITE reqs copied to READ reqs
37
86
  if (query.SELECT) delete _req.data // which we fix here -> but this is an ugly workaround
38
87
  _req.query = query
39
- _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
40
95
  _req.target = query._target
41
96
  _req._.params = req.params
97
+ _req.params = req.params
42
98
  _req._.query = query
43
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
+
44
109
  return _req
45
110
  }
46
111
 
47
- if (!req.query || req.query._draftParams) return handle(req)
48
- const query = _cleansed(req.query, this.model)
49
- const draftParams = query._draftParams
50
- if (req.event !== 'READ') {
51
- const _req = _newReq(req, query)
112
+ const run = async query => {
113
+ const _req = _newReq(req, query, draftParams)
52
114
  return handle(_req)
53
115
  }
54
116
 
55
- const read =
56
- 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
57
263
  ? Read.all
58
264
  : draftParams.IsActiveEntity === true &&
59
265
  draftParams.SiblingEntity_IsActiveEntity === null &&
60
- draftParams.DraftAdministrativeData_InProcessByUser === 'not null'
266
+ (draftParams.DraftAdministrativeData_InProcessByUser === 'not null' ||
267
+ draftParams.DraftAdministrativeData_InProcessByUser === 'not ')
61
268
  ? Read.lockedByAnotherUser
62
269
  : draftParams.IsActiveEntity === true &&
63
270
  draftParams.SiblingEntity_IsActiveEntity === null &&
@@ -70,15 +277,18 @@ cds.ApplicationService.prototype.handle = function (req) {
70
277
  : draftParams.IsActiveEntity === false
71
278
  ? Read.ownDrafts
72
279
  : Read.onlyActives
73
- const run = async query => {
74
- const _req = _newReq(req, query)
75
- return handle(_req)
280
+ const result = await read(run, query)
281
+ return result
76
282
  }
77
- return read(run, query)
283
+
284
+ const _req = _newReq(req, query, draftParams, req.event)
285
+ const result = await handle(_req)
286
+ return result
78
287
  }
79
288
 
80
289
  const Read = {
81
290
  onlyActives: async function (run, query, { ignoreDrafts } = {}) {
291
+ LOG.debug('List Editing Status: Only Active')
82
292
  // DraftAdministrativeData is only accessible via drafts
83
293
  if (query._target.name.endsWith('.DraftAdministrativeData')) return run(query._drafts)
84
294
  const actives = await run(query)
@@ -106,6 +316,7 @@ const Read = {
106
316
  return actives
107
317
  },
108
318
  unchanged: async function (run, query) {
319
+ LOG.debug('List Editing Status: Unchanged')
109
320
  const draftsQuery = query._drafts
110
321
  const keys = Object_keys(query._target.keys).filter(k => k !== 'IsActiveEntity')
111
322
  draftsQuery.SELECT.count = undefined
@@ -120,10 +331,29 @@ const Read = {
120
331
  return res
121
332
  },
122
333
  ownDrafts: async function (run, query) {
123
- if (!query._target.drafts) return run(query._drafts)
124
- const drafts = await run(
125
- 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
126
355
  )
356
+ const drafts = await run(draftsQuery)
127
357
  Read.merge(query._target, drafts, [], row =>
128
358
  Object.assign(row, {
129
359
  IsActiveEntity: false,
@@ -133,52 +363,67 @@ const Read = {
133
363
  return drafts
134
364
  },
135
365
  all: async function (run, query) {
136
- query._drafts.SELECT.count = true
137
- const drafts = await run(
138
- query._drafts.where({ ref: ['DraftAdministrativeData', 'InProcessByUser'] }, '=', cds.context.user.id)
139
- )
140
-
141
- const skip = query.SELECT.limit?.offset?.val
142
- 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
+ }
143
393
 
144
- 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 }
145
396
 
146
- const topActives = top && Math.max(0, top - drafts.length)
147
- let actives
148
- if (topActives === 0) {
149
- actives = []
150
- } else {
151
- const skipActives = skip && skip - appliedSkip
152
- query.SELECT.count = true
153
- actives = await run(query.limit(topActives, skipActives).where(Read.whereNotIn(query._target, drafts)))
154
- const draftsFromActives = await Read.complementaryDrafts(run, query, actives)
155
- Read.merge(query._target, actives, draftsFromActives, (row, other) => {
156
- if (other) {
157
- Object.assign(row, other, {
158
- IsActiveEntity: true,
159
- HasDraftEntity: true,
160
- HasActiveEntity: false
161
- })
162
- } else {
163
- Object.assign(row, {
164
- IsActiveEntity: true,
165
- HasDraftEntity: false,
166
- HasActiveEntity: false,
167
- DraftAdministrativeData_DraftUUID: null,
168
- DraftAdministrativeData: null
169
- })
170
- }
171
- })
172
- }
173
- Read.merge(query._target, drafts, [], row =>
397
+ Read.merge(query._target, ownDrafts, [], row =>
174
398
  Object.assign(row, {
175
399
  IsActiveEntity: false,
176
400
  HasDraftEntity: false
177
401
  })
178
402
  )
179
- const totalCount = actives.$count + drafts.$count
180
- const result = Object.assign([...drafts, ...actives], { $count: totalCount })
181
- 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
182
427
  },
183
428
  activesFromDrafts: async function (run, query, { isLocked = true }) {
184
429
  const draftsQuery = query._drafts
@@ -193,7 +438,7 @@ const Read = {
193
438
  HasActiveEntity: true,
194
439
  'DraftAdministrativeData.InProcessByUser': { '!=': cds.context.user.id },
195
440
  'DraftAdministrativeData.LastChangeDateTime': {
196
- [isLocked ? '>' : '<']: Read.lockshiftedNow
441
+ [isLocked ? '>' : '<']: _lock.shiftedNow
197
442
  }
198
443
  })
199
444
  const drafts = await run(draftsQuery)
@@ -208,14 +453,13 @@ const Read = {
208
453
  return actives
209
454
  },
210
455
  unsavedChangesByAnotherUser: async function (run, query) {
456
+ LOG.debug('List Editing Status: Unsaved Changes by Another User')
211
457
  return Read.activesFromDrafts(run, query, { isLocked: false })
212
458
  },
213
459
  lockedByAnotherUser: async function (run, query) {
460
+ LOG.debug('List Editing Status: Locked by Another User')
214
461
  return Read.activesFromDrafts(run, query, { isLocked: true })
215
462
  },
216
- get lockshiftedNow() {
217
- return new Date(Date.now() - DRAFT_CANCEL_TIMEOUT_IN_MIN() * 60 * 1000).toISOString()
218
- },
219
463
  whereNotIn: (target, data) => Read.whereIn(target, data, true),
220
464
  whereIn: (target, data, not = false) => {
221
465
  const keys = Object_keys(target.keys).filter(k => k !== 'IsActiveEntity')
@@ -231,6 +475,12 @@ const Read = {
231
475
  if (!actives.length) return []
232
476
  const drafts = cds.ql.clone(query._drafts)
233
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
+ }
234
484
  const relevantColumns = ['DraftAdministrativeData', 'DraftAdministrativeData_DraftUUID']
235
485
  drafts.SELECT.columns = (
236
486
  drafts.SELECT.columns?.filter(c => c.ref && relevantColumns.includes(c.ref[0])) ||
@@ -241,60 +491,100 @@ const Read = {
241
491
  .map(k => ({ ref: [k] }))
242
492
  )
243
493
  drafts.SELECT.count = undefined
494
+ drafts.SELECT.search = undefined
244
495
  drafts.SELECT.one = undefined
245
496
  return run(drafts)
246
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
247
510
  merge: (target, data, otherData, cb) => {
248
- if (!data) return
249
- const dataArray = Array.isArray(data) ? data : [data]
511
+ const dataArray = Read._makeArray(data)
250
512
  if (!dataArray.length) return
251
- const otherDataArray = Array.isArray(otherData) ? otherData : otherData ? [otherData] : []
252
- if (otherDataArray.length) {
253
- const _keys = Object_keys(target.keys).filter(k => k !== 'IsActiveEntity')
254
- const _hash = row => _keys.map(k => row[k]).reduce((res, curr) => res + '|$|' + curr, '')
255
- const d2hash = new Map()
256
- for (const row of otherDataArray) d2hash.set(_hash(row), row)
257
- for (const row of dataArray) {
258
- const other = d2hash.get(_hash(row))
259
- cb(row, other)
260
- }
261
- } else {
262
- for (const row of dataArray) {
263
- cb(row, undefined)
264
- }
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)
265
528
  }
266
529
  }
267
530
  }
268
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
+
269
557
  /**
270
558
  * Creates a clone of the query, cleanses and collects all draft parameters into ._draftParams.
271
559
  */
272
- function _cleansed(query, model, target) {
560
+ function _cleansed(query, model) {
273
561
  const draftParams = {} //> used to collect draft filter criteria
274
562
  const q = _cleanseQuery(query, draftParams)
275
563
  if (query.SELECT) {
276
- let cache
277
564
  const getDrafts = () => {
278
- if (cache) return cache
279
565
  const draftsQuery = _cleanseQuery(query, {}) // could just clone `q` but the latter is ruined by database layer
566
+ draftsQuery._target = undefined
280
567
  const [root, ...tail] = draftsQuery.SELECT.from.ref
281
568
  const draft = model.definitions[root.id || root].drafts
282
569
  draftsQuery.SELECT.from = {
283
570
  ref: [root.id ? { ...root, id: draft.name } : draft.name, ...tail]
284
571
  }
572
+ cds.infer(draftsQuery, model.definitions)
573
+ // draftsQuery._target = draftsQuery._target?.drafts || draftsQuery._target
285
574
  if (query.SELECT.columns && query._target.drafts)
286
- 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)
287
576
 
288
577
  if (draftsQuery._target.name.endsWith('.DraftAdministrativeData')) {
289
578
  draftsQuery.SELECT.columns = _tweakAdminCols(draftsQuery.SELECT.columns)
290
- } else if (q._target.drafts) {
579
+ } else if (draftsQuery._target?.name.endsWith('.drafts')) {
291
580
  draftsQuery.SELECT.columns = _tweakAdminExpand(draftsQuery.SELECT.columns)
292
581
  }
293
582
  Object.defineProperty(draftsQuery, '_draftParams', { value: draftParams, enumerable: false })
294
- cache = draftsQuery
583
+ Object.defineProperty(q, '_drafts', { value: draftsQuery })
295
584
  return draftsQuery
296
585
  }
297
586
  Object.defineProperty(q, '_drafts', {
587
+ configurable: true,
298
588
  get() {
299
589
  return getDrafts()
300
590
  }
@@ -325,7 +615,7 @@ function _cleansed(query, model, target) {
325
615
  }
326
616
 
327
617
  if (cqn.where) cqn.where = _cleanseWhere(cqn.where, draftParams)
328
- 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)
329
619
  if (cqn.orderBy) cqn.orderBy = _cleanseWhere(cqn.orderBy, {})
330
620
 
331
621
  return q
@@ -342,7 +632,7 @@ function _cleansed(query, model, target) {
342
632
  }
343
633
 
344
634
  function _tweakAdminCols(columns) {
345
- 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] }))
346
636
  return columns.map(col => {
347
637
  const name = col.ref?.[0]
348
638
  if (!name) return col
@@ -367,22 +657,7 @@ function _cleansed(query, model, target) {
367
657
  cast: { type: 'cds.Boolean' }
368
658
  }
369
659
  case 'InProcessByUser':
370
- return {
371
- xpr: [
372
- 'case',
373
- 'when',
374
- { ref: ['LastChangeDateTime'] },
375
- '<',
376
- { val: Read.lockshiftedNow },
377
- 'then',
378
- { val: '' },
379
- 'else',
380
- { ref: ['InProcessByUser'] },
381
- 'end'
382
- ],
383
- as: 'InProcessByUser',
384
- cast: { type: 'cds.String' }
385
- }
660
+ return _inProcessByUserXpr(_lock.shiftedNow)
386
661
  case 'DraftIsProcessedByMe':
387
662
  return {
388
663
  xpr: [
@@ -391,6 +666,10 @@ function _cleansed(query, model, target) {
391
666
  { ref: ['InProcessByUser'] },
392
667
  '=',
393
668
  { val: cds.context.user.id },
669
+ 'and',
670
+ { ref: ['LastChangeDateTime'] },
671
+ '>',
672
+ { val: _lock.shiftedNow },
394
673
  'then',
395
674
  { val: true },
396
675
  'else',
@@ -411,9 +690,12 @@ function _cleansed(query, model, target) {
411
690
  for (let i = 0; i < xpr.length; ++i) {
412
691
  let x = xpr[i],
413
692
  e = x.ref?.[0]
414
- 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]) {
415
697
  let { val } = xpr[i + 2]
416
- 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
417
699
  i += 3
418
700
  continue
419
701
  }
@@ -432,27 +714,46 @@ function _cleansed(query, model, target) {
432
714
  }
433
715
  }
434
716
 
435
- function _draftIsLocked(LastChangeDateTime) {
436
- 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
437
739
  }
438
740
 
439
- async function onNewDraft(req) {
741
+ async function onNew(req) {
440
742
  LOG.debug('new draft')
441
743
  const isRoot = typeof req.query.INSERT.into === 'string'
442
744
  let DraftUUID
443
745
  if (isRoot) DraftUUID = cds.utils.uuid()
444
746
  else {
445
- const rootData = await SELECT.one(req.query.INSERT.into.ref[0].id + '.drafts', d => {
446
- d.DraftAdministrativeData_DraftUUID,
447
- d.DraftAdministrativeData(a => {
448
- a.LastChangeDateTime, a.InProcessByUser
449
- })
450
- }).where(req.query.INSERT.into.ref[0].where)
451
- if (!rootData) req.reject(404)
452
- if (
453
- !rootData.DraftAdministrativeData.InProcessByUser === req.user.id &&
454
- _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)
455
754
  )
755
+ if (!rootData) req.reject(404)
756
+ if (rootData.DraftAdministrativeData?.InProcessByUser !== req.user.id)
456
757
  req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
457
758
  DraftUUID = rootData.DraftAdministrativeData_DraftUUID
458
759
  }
@@ -480,60 +781,21 @@ async function onNewDraft(req) {
480
781
  { DraftAdministrativeData_DraftUUID: DraftUUID, HasActiveEntity: false },
481
782
  req.query.INSERT.entries[0]
482
783
  )
784
+
483
785
  delete draftData.IsActiveEntity
484
- const draftCQN = INSERT.into(req.target.drafts).entries(draftData)
786
+ const draftCQN = INSERT.into(req.target).entries(draftData)
485
787
 
486
- // TODO: do this? req.query._draftParams.IsActiveEntity = false
487
- await Promise.all([adminDataCQN, draftCQN].map(cqn => cds.run(cqn)))
788
+ await Promise.all([cds.run(adminDataCQN), this.run(draftCQN)])
488
789
  req._.readAfterWrite = true
489
790
  return { ...draftData, IsActiveEntity: false }
490
791
  }
491
- exports.onNewDraft = onNewDraft
492
-
493
- async function onDraftPrepare(req) {
494
- LOG.debug('prepare draft')
495
-
496
- const draftParams = req.query._draftParams
497
- const where = req.query.SELECT.from.ref[0].where
498
- if (req.query.SELECT.from.ref.length > 1 || draftParams.IsActiveEntity !== false) {
499
- req.reject(400, 'Action "draftPrepare" can only be called on the root draft entity')
500
- }
501
-
502
- const keys = Object_keys(req.target.keys).filter(k => k !== 'IsActiveEntity')
503
- const data = await SELECT.one
504
- .from(req.target.drafts, d => {
505
- d.DraftAdministrativeData(a => a.InProcessByUser)
506
- })
507
- .columns(keys)
508
- .where(where)
509
- if (!data) req.reject(404)
510
- if (data.DraftAdministrativeData.InProcessByUser !== req.user.id) req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
511
- delete data.DraftAdministrativeData
512
- return { ...data, IsActiveEntity: false }
513
- }
514
- exports.onDraftPrepare = onDraftPrepare
515
-
516
- // This function is better defined on DB layer
517
- function expandStarStar(target, recursion = new Map()) {
518
- const MAX_RECURSION_DEPTH = (cds.env.features.recursion_depth && Number(cds.env.features.recursion_depth)) || 4
519
- // TODO: Remove draftcolumns from payload
520
- const columns = []
521
- for (const el in target.elements) {
522
- const element = target.elements[el]
523
- if (!element.isAssociation && !DRAFT_ELEMENTS.has(el)) columns.push({ ref: [el] })
524
- if (!element.isComposition) continue
525
- const _key = target.name + ':' + el
526
- let cache = recursion.get(_key)
527
- if (!cache) cache = 1 && recursion.set(_key, cache)
528
- if (cache >= MAX_RECURSION_DEPTH) return
529
- columns.push({ ref: [el], expand: expandStarStar(element._target, recursion) })
530
- }
531
- return columns
532
- }
533
792
 
534
- async function onDraftEdit(req) {
793
+ async function onEdit(req) {
535
794
  LOG.debug('edit active')
536
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
+ }
537
799
  const targetWhere = req.query.SELECT.from.ref[0].where
538
800
 
539
801
  if (draftParams.IsActiveEntity !== true) req.reject(400)
@@ -554,13 +816,30 @@ async function onDraftEdit(req) {
554
816
  }
555
817
  }
556
818
  _addDraftColumns(req.target, cols)
557
-
558
- const [res, draftExists] = await Promise.all([
559
- SELECT.one.from(req.target).columns(cols).where(targetWhere),
560
- 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)
561
830
  ])
562
831
  if (!res) req.reject(404)
563
- 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
+ }
564
843
 
565
844
  const timestamp = cds.context.timestamp.toISOString() // REVISIT: toISOString should be done on db layer
566
845
  await INSERT.into('DRAFT.DraftAdministrativeData').entries({
@@ -575,7 +854,11 @@ async function onDraftEdit(req) {
575
854
  })
576
855
 
577
856
  const targetDraft = req.target.drafts
578
- await INSERT.into(targetDraft).entries(res)
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))
579
862
 
580
863
  // REVISIT: we need to use okra API here because it must be set in the batched request
581
864
  // status code must be set in handler to allow overriding for FE V2
@@ -583,86 +866,56 @@ async function onDraftEdit(req) {
583
866
 
584
867
  return { ...res, IsActiveEntity: false } // REVISIT: Flatten?
585
868
  }
586
- exports.onDraftEdit = onDraftEdit
587
-
588
- async function onDraftActivate(req) {
589
- LOG.debug('activate draft')
590
- // TODO: Read all drafts, send deep update/insert (based on HasActiveEntity, which should be fille with new!)
591
- // It would be great if we'd have a SELECT ** to deeply expand the entity (along compositions), that should
592
- // be implemented in expand implementation.
593
- const targetDraft = req.target.drafts
594
- const targetWhere = req.query.SELECT.from.ref[0].where
595
- const res = await SELECT.one
596
- .from(targetDraft)
597
- .columns(expandStarStar(targetDraft))
598
- .columns(['HasActiveEntity', 'DraftAdministrativeData_DraftUUID'])
599
- .where(targetWhere)
600
- const DraftAdministrativeData_DraftUUID = res.DraftAdministrativeData_DraftUUID
601
- delete res.DraftAdministrativeData_DraftUUID
602
- const HasActiveEntity = res.HasActiveEntity
603
- delete res.HasActiveEntity
604
- // TODO: Deep delete!
605
- await Promise.all([
606
- DELETE.from(targetDraft).where(targetWhere),
607
- DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: DraftAdministrativeData_DraftUUID })
608
- ])
609
- // TODO: Check for InProcessByUser
610
-
611
- let event, query
612
- if (HasActiveEntity) {
613
- query = UPDATE(req.target).data(res).where(targetWhere)
614
- event = 'UPDATE'
615
- } else {
616
- query = INSERT.into(req.target).entries(res)
617
- event = 'CREATE'
618
- }
619
- const r = new cds.Request({ event, query, data: res })
620
- const result = await this.dispatch(r)
621
-
622
- // REVISIT: we need to use okra API here because it must be set in the batched request
623
- // status code must be set in handler to allow overriding for FE V2
624
- req?._?.odataRes.setStatusCode(201)
625
-
626
- return Object.assign(result, { IsActiveEntity: true })
627
- }
628
- exports.onDraftActivate = onDraftActivate
629
869
 
630
- async function onPatch(req) {
631
- LOG.debug('patch draft')
632
- const targetDraft = req.target.drafts
633
- const targetWhere = req.query.UPDATE.entity.ref[0].where
634
- const res = await SELECT.one.from(targetDraft).columns('DraftAdministrativeData_DraftUUID').where(targetWhere)
635
- if (!res) req.reject(404)
636
- await UPDATE('DRAFT.DraftAdministrativeData')
637
- .data({
638
- InProcessByUser: req.user.id,
639
- LastChangedByUser: req.user.id,
640
- LastChangeDateTime: new Date()
641
- })
642
- .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
643
874
 
644
- const updateData = { ...req.data }
645
- delete updateData.IsActiveEntity
646
- 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)
647
895
  return req.data
648
896
  }
649
- exports.onPatch = onPatch
650
897
 
651
- async function onDelete(req) {
652
- LOG.debug('delete')
653
- const targetDraft = req.target.drafts
898
+ async function onPrepare(req) {
899
+ LOG.debug('prepare draft')
654
900
  const draftParams = req.query._draftParams
655
- const targetWhere = req.query.DELETE.from.ref[0].where
656
- const res = await SELECT.one.from(targetDraft).columns('DraftAdministrativeData_DraftUUID').where(targetWhere)
657
- if (!res && draftParams.IsActiveEntity === false) req.reject(404)
658
- const deletes = !res
659
- ? []
660
- : [
661
- DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: res.DraftAdministrativeData_DraftUUID }),
662
- DELETE.from(targetDraft).where(targetWhere)
663
- ]
664
- if (draftParams.IsActiveEntity) deletes.push(DELETE.from(req.target).where(targetWhere))
665
- await Promise.all(deletes)
666
- 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 }
667
919
  }
668
- exports.onDelete = onDelete
920
+
921
+ module.exports = { onNew, onEdit, onCancel, onPrepare }