@sap/cds 9.0.4 → 9.2.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 (77) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/bin/deploy.js +29 -0
  3. package/bin/serve.js +1 -5
  4. package/lib/compile/etc/csv.js +11 -6
  5. package/lib/compile/for/lean_drafts.js +29 -7
  6. package/lib/compile/load.js +8 -5
  7. package/lib/compile/to/hdbtabledata.js +1 -1
  8. package/lib/dbs/cds-deploy.js +5 -34
  9. package/lib/env/cds-env.js +2 -1
  10. package/lib/env/cds-requires.js +4 -1
  11. package/lib/env/defaults.js +0 -11
  12. package/lib/env/schemas/cds-rc.js +218 -6
  13. package/lib/index.js +38 -38
  14. package/lib/log/cds-error.js +12 -11
  15. package/lib/log/format/json.js +1 -1
  16. package/lib/ql/SELECT.js +31 -0
  17. package/lib/ql/resolve.js +1 -1
  18. package/lib/req/context.js +1 -1
  19. package/lib/req/request.js +1 -1
  20. package/lib/req/validate.js +17 -19
  21. package/lib/srv/cds.Service.js +18 -28
  22. package/lib/srv/middlewares/auth/ias-auth.js +29 -2
  23. package/lib/srv/middlewares/auth/jwt-auth.js +11 -1
  24. package/lib/srv/middlewares/auth/xssec.js +1 -1
  25. package/lib/srv/srv-models.js +1 -1
  26. package/lib/srv/srv-tx.js +2 -2
  27. package/lib/utils/cds-utils.js +35 -2
  28. package/lib/utils/csv-reader.js +1 -1
  29. package/lib/utils/inflect.js +2 -2
  30. package/lib/utils/tar.js +60 -23
  31. package/lib/utils/version.js +18 -0
  32. package/libx/_runtime/cds.js +1 -1
  33. package/libx/_runtime/common/aspects/any.js +1 -23
  34. package/libx/_runtime/common/generic/crud.js +1 -3
  35. package/libx/_runtime/common/generic/input.js +113 -52
  36. package/libx/_runtime/common/generic/sorting.js +1 -1
  37. package/libx/_runtime/common/generic/temporal.js +0 -6
  38. package/libx/_runtime/common/utils/draft.js +1 -1
  39. package/libx/_runtime/common/utils/entityFromCqn.js +1 -1
  40. package/libx/_runtime/common/utils/propagateForeignKeys.js +1 -1
  41. package/libx/_runtime/common/utils/resolveView.js +2 -2
  42. package/libx/_runtime/common/utils/structured.js +2 -2
  43. package/libx/_runtime/common/utils/templateProcessor.js +0 -5
  44. package/libx/_runtime/common/utils/vcap.js +1 -1
  45. package/libx/_runtime/fiori/lean-draft.js +529 -143
  46. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +3 -2
  47. package/libx/_runtime/messaging/service.js +1 -1
  48. package/libx/_runtime/remote/utils/client.js +2 -1
  49. package/libx/common/assert/utils.js +2 -12
  50. package/libx/common/utils/streaming.js +4 -9
  51. package/libx/http/location.js +1 -0
  52. package/libx/odata/ODataAdapter.js +47 -43
  53. package/libx/odata/index.js +1 -1
  54. package/libx/odata/middleware/batch.js +6 -2
  55. package/libx/odata/middleware/create.js +1 -1
  56. package/libx/odata/middleware/error.js +27 -17
  57. package/libx/odata/middleware/operation.js +15 -21
  58. package/libx/odata/middleware/stream.js +1 -1
  59. package/libx/odata/parse/afterburner.js +22 -8
  60. package/libx/odata/parse/cqn2odata.js +16 -10
  61. package/libx/odata/parse/grammar.peggy +185 -134
  62. package/libx/odata/parse/parser.js +1 -1
  63. package/libx/odata/utils/index.js +1 -36
  64. package/libx/odata/utils/metadata.js +34 -1
  65. package/libx/odata/utils/odataBind.js +2 -1
  66. package/libx/odata/utils/result.js +22 -20
  67. package/libx/queue/index.js +7 -4
  68. package/libx/rest/RestAdapter.js +1 -2
  69. package/libx/rest/middleware/create.js +5 -2
  70. package/package.json +2 -2
  71. package/server.js +1 -1
  72. package/bin/deploy/to-hana.js +0 -1
  73. package/lib/utils/check-version.js +0 -9
  74. package/lib/utils/unit.js +0 -19
  75. package/libx/_runtime/cds-services/util/assert.js +0 -181
  76. package/libx/_runtime/types/api.js +0 -129
  77. package/libx/common/assert/validation.js +0 -109
@@ -10,20 +10,23 @@ const { handler: commonGenericSorting } = require('../common/generic/sorting')
10
10
  const { addEtagColumns } = require('../common/utils/etag')
11
11
  const { handleStreamProperties } = require('../common/utils/streamProp')
12
12
 
13
+ const { getLocalizedMessages } = require('../../odata/middleware/error')
14
+ const { Responses } = require('../../../lib/req/response')
13
15
  const location4 = require('../../http/location')
14
16
 
15
17
  const $original = Symbol('original')
16
18
  const $draftParams = Symbol('draftParams')
17
19
 
18
20
  const AGGREGATION_FUNCTIONS = ['sum', 'min', 'max', 'avg', 'average', 'count']
21
+ const MAX_RECURSION_DEPTH = cds.env.features.recursion_depth != null ? Number(cds.env.features.recursion_depth) : 4
19
22
 
20
23
  const _config_to_ms = (config, _default) => {
21
24
  const timeout = cds.env.fiori?.[config]
22
25
  let timeout_ms
23
26
  if (timeout === true) {
24
- timeout_ms = cds.utils._unit.time2ms(_default)
27
+ timeout_ms = cds.utils.ms4(_default)
25
28
  } else if (typeof timeout === 'string') {
26
- timeout_ms = cds.utils._unit.time2ms(timeout)
29
+ timeout_ms = cds.utils.ms4(timeout)
27
30
  if (!timeout_ms)
28
31
  throw new Error(`
29
32
  ${timeout} is an invalid value for \`cds.fiori.${config}\`.
@@ -71,16 +74,6 @@ const DRAFT_ELEMENTS = new Set([
71
74
  const DRAFT_ELEMENTS_WITHOUT_HASACTIVE = new Set(DRAFT_ELEMENTS)
72
75
  DRAFT_ELEMENTS_WITHOUT_HASACTIVE.delete('HasActiveEntity')
73
76
  const REDUCED_DRAFT_ELEMENTS = new Set(['IsActiveEntity', 'HasDraftEntity', 'SiblingEntity'])
74
- const DRAFT_ADMIN_ELEMENTS = [
75
- 'DraftUUID',
76
- 'LastChangedByUser',
77
- 'LastChangeDateTime',
78
- 'CreatedByUser',
79
- 'CreationDateTime',
80
- 'InProcessByUser',
81
- 'DraftIsCreatedByMe',
82
- 'DraftIsProcessedByMe'
83
- ]
84
77
 
85
78
  const numericCollator = { numeric: true }
86
79
  const emptyObject = {}
@@ -279,6 +272,96 @@ const _removeEmptyStreams = async result => {
279
272
  }
280
273
  }
281
274
 
275
+ const getUpdateFromSelectQueries = (target, draftRef, activeRef, depth = 0) => {
276
+ const updateMediaDataQueries = []
277
+
278
+ // Collect relevant elements
279
+ const compositionElements = []
280
+ const mediaDataElements = []
281
+ const autogeneratedElements = []
282
+ const targetElements = Array.isArray(target.elements) ? target.elements : Object.values(target.elements)
283
+ for (const element of targetElements) {
284
+ if (element['@cds.on.update']) autogeneratedElements.push(element)
285
+ else if (element.isComposition) compositionElements.push(element)
286
+ else if (element._type === 'cds.LargeBinary') mediaDataElements.push(element)
287
+ }
288
+
289
+ // Recurse into compositions
290
+ const nextDepth = depth + 1
291
+ if (nextDepth <= MAX_RECURSION_DEPTH) {
292
+ for (const composition of compositionElements) {
293
+ const compositionTargetElement = cds.model.definitions[composition.target]
294
+
295
+ const nextDraftRef = [...draftRef, composition.name]
296
+ const nextActiveRef = [...activeRef, composition.name]
297
+
298
+ updateMediaDataQueries.push(
299
+ ...getUpdateFromSelectQueries(compositionTargetElement, nextDraftRef, nextActiveRef, nextDepth)
300
+ )
301
+ }
302
+ }
303
+
304
+ // Construct update queries for media data elements
305
+ if (mediaDataElements.length) {
306
+ const updateWith = {}
307
+ for (const element of autogeneratedElements) {
308
+ updateWith[element.name] = { ref: ['active', element.name] }
309
+ }
310
+
311
+ // Construct WHERE to match draft and active entities
312
+ const selectWhere = Object.values(target.keys).reduce((acc, key) => {
313
+ if (key.virtual || key.isAssociation) return acc
314
+ if (acc.length) acc.push('and')
315
+ acc.push({ ref: ['draft', key.name] })
316
+ acc.push('=')
317
+ acc.push({ ref: ['active', key.name] })
318
+ return acc
319
+ }, [])
320
+
321
+ for (const element of mediaDataElements) {
322
+ const activeElementRef = { ref: ['active', element.name] }
323
+ const draftElementRef = { ref: ['draft', element.name] }
324
+
325
+ // cds.ql.xpr`CASE WHEN (${draftElementRef} IS NULL OR length(${draftElementRef}) > 0) THEN ${draftElementRef} ELSE ${activeElementRef} END`
326
+ const columnExpression = {
327
+ xpr: [
328
+ 'case',
329
+ 'when',
330
+ {
331
+ xpr: [
332
+ draftElementRef,
333
+ '=',
334
+ { val: null },
335
+ 'or',
336
+ { func: 'length', args: [draftElementRef] },
337
+ '>',
338
+ { val: 0 }
339
+ ]
340
+ },
341
+ 'then',
342
+ draftElementRef,
343
+ 'else',
344
+ activeElementRef,
345
+ 'end'
346
+ ]
347
+ }
348
+ columnExpression.as = element.name
349
+
350
+ const qSelectNextMediaData = SELECT.columns([columnExpression])
351
+ .from({ ref: draftRef, as: 'draft' })
352
+ .where(selectWhere)
353
+
354
+ updateWith[element.name] = qSelectNextMediaData
355
+ }
356
+
357
+ // Construct & Collect the actual update query for this composition level
358
+ const updateMediaDataQuery = UPDATE.entity({ ref: activeRef, as: 'active' }).with(updateWith)
359
+ updateMediaDataQueries.push(updateMediaDataQuery)
360
+ }
361
+
362
+ return updateMediaDataQueries
363
+ }
364
+
282
365
  // REVISIT: Can be replaced with SQL WHEN statement (see commented code in expandStarStar) in the new HANA db layer - doesn't work with old db layer
283
366
  const _replaceStreams = result => {
284
367
  if (!result) return
@@ -299,6 +382,108 @@ const _replaceStreams = result => {
299
382
  }
300
383
  }
301
384
 
385
+ const _compileUpdatedDraftMessages = (newMessages, persistedMessages, requestData, draftsRef) => {
386
+ const prefixRef = []
387
+
388
+ // Determine the path prefix required for a fully qualified validation message 'target'
389
+ let targetEntity = cds.context.tx.model.definitions[draftsRef[0].id || draftsRef[0]]
390
+ for (let refIdx = 0; refIdx < draftsRef.length; refIdx++) {
391
+ const dRef = draftsRef[refIdx],
392
+ dRefId = dRef.id ?? dRef
393
+
394
+ // Determine entity, referenced by the processesd segment of 'draftsRef'
395
+ if (refIdx > 0) targetEntity = targetEntity.elements[dRefId]._target
396
+
397
+ // Construct 'prefixRef' segment
398
+ const pRef = { where: [] }
399
+
400
+ if (dRefId === targetEntity.name) {
401
+ if (!targetEntity.isDraft) pRef.id = targetEntity.name.replace(`${targetEntity._service.name}.`, '')
402
+ else pRef.id = targetEntity.actives.name.replace(`${targetEntity.actives._service.name}.`, '')
403
+ } else pRef.id = dRefId
404
+
405
+ if (targetEntity.isDraft) targetEntity = targetEntity.actives
406
+
407
+ if (typeof dRef === 'string' || !dRef.where?.length) {
408
+ // In case of CREATE: The WHERE for the created entity must be constructed for use in 'prefixRef'
409
+
410
+ for (const k in targetEntity.keys) {
411
+ const key = targetEntity.keys[k]
412
+ let v = requestData[key.name]
413
+ if (v === undefined)
414
+ if (key.name === 'IsActiveEntity') v = false
415
+ else return null
416
+ if (pRef.where.length > 0) pRef.where.push('and')
417
+ pRef.where.push(key.name, '=', v)
418
+ }
419
+ } else {
420
+ // 'dRef.where' regards the draft and will never contain 'IsActiveEntity=false'
421
+ pRef.where.push(...dRef.where, 'and', 'IsActiveEntity', '=', false)
422
+ }
423
+
424
+ prefixRef.push(pRef)
425
+ }
426
+
427
+ const nextMessages = []
428
+
429
+ // Collect messages that were created during the most recent validation run
430
+ const newMessagesByCodeAndTarget = newMessages.reduce((acc, message) => {
431
+ message.numericSeverity ??= 4
432
+
433
+ // Handle validation messages produced during draftActivate, that went through error normalization already
434
+ // > We must not store pre-localized data in DraftAdministrativeData.DraftMessages
435
+ const messageTarget = message.target.startsWith('in/') ? message.target.slice(3) : message.target
436
+ if (message.code) {
437
+ message.message = message.code
438
+ delete message.code
439
+ }
440
+
441
+ // Process the message target produced by validation
442
+ // > The message target contains the relative path of the erroneous entity
443
+ // > For a message specific 'prefixRef', this info must be added to the entity 'prefixRef'
444
+ const messagePrefixRef = [...prefixRef]
445
+ const messageTargetRef = cds.odata.parse(messageTarget).SELECT.from.ref
446
+ message.target = messageTargetRef.pop()
447
+
448
+ for (const tRef of messageTargetRef) {
449
+ messagePrefixRef.push(tRef)
450
+ if ((tRef.where ??= []).some(w => w === 'IsActiveEntity' || w.ref?.[0] === 'IsActiveEntity')) continue
451
+ if (tRef.where.length > 0) tRef.where.push('and', 'IsActiveEntity', '=', false)
452
+ }
453
+
454
+ message.prefix = cds.odata.urlify({ SELECT: { from: { ref: messagePrefixRef } } }).path
455
+
456
+ nextMessages.push(message)
457
+
458
+ return acc.set(`${message.message}:${message.prefix}:${message.target}`, message)
459
+ }, new Map())
460
+
461
+ const draftMessageTargetPrefix = cds.odata.urlify({ SELECT: { from: { ref: prefixRef } } }).path
462
+
463
+ // Merge new messages with persisted ones & eliminate outdated ones
464
+ const simplePathElements = prefixRef.map(pRef => pRef.id)
465
+ for (const message of persistedMessages) {
466
+ // Drop persisted draft messages that are replaced by new ones
467
+ if (newMessagesByCodeAndTarget.has(`${message.message}:${message.prefix}:${message.target}`)) continue
468
+
469
+ // Drop persisted draft messages where the value of the target field changed without a new error
470
+ if (message.prefix === draftMessageTargetPrefix && requestData[message.target] !== undefined) continue
471
+
472
+ // Drop persisted draft messages, whose target's use navigations, where the value of the target field changed without a new error
473
+ const messageSimplePathElements = message.prefix.replaceAll(/\([^(]*\)/g, '').split('/')
474
+ if (messageSimplePathElements.length > simplePathElements.length)
475
+ if (requestData[messageSimplePathElements[simplePathElements.length]] !== undefined) continue
476
+
477
+ nextMessages.push(message)
478
+ }
479
+
480
+ // REVISIT: Do this by default in validation?
481
+ for (const msg of nextMessages)
482
+ for (const i in msg.args) if (msg.args[i] instanceof RegExp) msg.args[i] = msg.args[i].toString()
483
+
484
+ return nextMessages
485
+ }
486
+
302
487
  // REVISIT: Can we do a regular handler function instead of monky patching?
303
488
  const h = cds.ApplicationService.prototype.handle
304
489
  const handle = async function (req) {
@@ -365,12 +550,23 @@ const handle = async function (req) {
365
550
  if (!_req._.event) _req._.event = req.event
366
551
  const cqnData = _req.query.UPDATE?.data || _req.query.INSERT?.entries?.[0]
367
552
  if (cqnData) _req.data = cqnData // must point to the same object
553
+ if (req.tx && !_req.tx) _req.tx = req.tx
554
+
555
+ // Ensure messages are added to the original request
368
556
  Object.defineProperty(_req, '_messages', {
369
557
  get: function () {
370
558
  return req._messages
371
559
  }
372
560
  })
373
- if (req.tx && !_req.tx) _req.tx = req.tx
561
+
562
+ if (cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages) {
563
+ if (_req.target.isDraft && (_req.event === 'UPDATE' || _req.event === 'NEW')) {
564
+ // Degrade all errors to messages & prevent !!req.errors into req.reject() in dispatch
565
+ _req.error = (...args) => {
566
+ for (const err of args) _req._messages.add(4, err)
567
+ }
568
+ }
569
+ }
374
570
 
375
571
  return _req
376
572
  }
@@ -447,6 +643,7 @@ const handle = async function (req) {
447
643
  typeof query.INSERT.into === 'string'
448
644
  ? [req.target.drafts.name]
449
645
  : _redirectRefToDrafts([query.INSERT.into.ref[0]], this.model)
646
+
450
647
  let rootHasDraft
451
648
 
452
649
  // children: check root entity has no draft
@@ -478,6 +675,33 @@ const handle = async function (req) {
478
675
  if (req.event === 'NEW' || req.event === 'CANCEL' || req.event === 'draftPrepare') {
479
676
  if (!req.target.isDraft) req.target = req.target.drafts // COMPAT: also support these events for actives
480
677
 
678
+ if (cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages && query.DELETE) {
679
+ const prefixRef = query.DELETE.from.ref.map(dRef => {
680
+ const pRef = { id: dRef.id, where: [...dRef.where] }
681
+ pRef.where.push('and', 'IsActiveEntity', '=', false)
682
+ return pRef
683
+ })
684
+ const messageTargetPrefix = cds.odata.urlify({ SELECT: { from: { ref: prefixRef } } }).path
685
+
686
+ const draftData = await SELECT.one
687
+ .from({ ref: _redirectRefToDrafts(query.DELETE.from.ref, this.model) }) // REVISIT: Avoid redundant redirect
688
+ .columns('DraftAdministrativeData_DraftUUID', {
689
+ ref: ['DraftAdministrativeData'],
690
+ expand: [{ ref: ['DraftMessages'] }]
691
+ })
692
+
693
+ const draftAdminDataUUID = draftData?.DraftAdministrativeData_DraftUUID
694
+ const persistedDraftMessages = draftData?.DraftAdministrativeData?.DraftMessages || []
695
+
696
+ if (draftAdminDataUUID && persistedDraftMessages?.length) {
697
+ const nextDraftMessages = persistedDraftMessages.filter(msg => !msg.prefix.startsWith(messageTargetPrefix))
698
+
699
+ await UPDATE('DRAFT.DraftAdministrativeData')
700
+ .set({ DraftMessages: nextDraftMessages })
701
+ .where({ DraftUUID: draftAdminDataUUID })
702
+ }
703
+ }
704
+
481
705
  if (query.INSERT) {
482
706
  if (typeof query.INSERT.into === 'string') query.INSERT.into = req.target.name
483
707
  else if (query.INSERT.into.ref) query.INSERT.into.ref = _redirectRefToDrafts(query.INSERT.into.ref, this.model)
@@ -490,15 +714,12 @@ const handle = async function (req) {
490
714
  const _req = _newReq(req, query, draftParams, { event: req.event })
491
715
 
492
716
  // Do not allow to create active instances via drafts
493
- if (
494
- (req.event === 'NEW' || req.event === 'CREATE') &&
495
- draftParams.IsActiveEntity === false &&
496
- !_req.target.isDraft
497
- ) {
717
+ if (req.event === 'NEW' && draftParams.IsActiveEntity === false && !_req.target.isDraft) {
498
718
  req.reject({ code: 403, statusCode: 403, message: 'ACTIVE_MODIFICATION_VIA_DRAFT' })
499
719
  }
500
720
 
501
721
  const result = await h.call(this, _req)
722
+
502
723
  req.data = result //> make keys available via req.data (as with normal crud)
503
724
  return result
504
725
  }
@@ -506,13 +727,13 @@ const handle = async function (req) {
506
727
  // Delete active instance of draft-enabled entity
507
728
  if (req.target.drafts && !req.target.isDraft && req.event === 'DELETE' && draftParams.IsActiveEntity !== false) {
508
729
  const draftsRef = _redirectRefToDrafts(query.DELETE.from.ref, this.model)
509
- const draftQuery = SELECT.one.from({ ref: draftsRef }).columns([
510
- { ref: ['DraftAdministrativeData_DraftUUID'] },
511
- {
512
- ref: ['DraftAdministrativeData'],
513
- expand: [_inProcessByUserXpr(_lock.shiftedNow)]
514
- }
515
- ])
730
+ const inProcessByUserCol = {
731
+ ref: ['DraftAdministrativeData'],
732
+ expand: [_inProcessByUserXpr(_lock.shiftedNow)]
733
+ }
734
+ const draftQuery = SELECT.one
735
+ .from({ ref: draftsRef })
736
+ .columns([{ ref: ['DraftAdministrativeData_DraftUUID'] }, inProcessByUserCol])
516
737
  if (query.DELETE.where) draftQuery.where(query.DELETE.where)
517
738
 
518
739
  // Deletion of active instance outside draft tree, no need to check for draft
@@ -523,11 +744,59 @@ const handle = async function (req) {
523
744
  }
524
745
 
525
746
  // Deletion of active instance inside draft tree, need to check that no draft exists
526
- const draft = await draftQuery
527
- const inProcessByUser = draft?.DraftAdministrativeData?.InProcessByUser
528
- if (!cds.context.user._is_privileged && inProcessByUser && inProcessByUser !== cds.context.user.id)
529
- req.reject({ code: 403, statusCode: 403, message: 'DRAFT_LOCKED_BY_ANOTHER_USER', args: [inProcessByUser] })
530
- if (draft) req.reject({ code: 403, statusCode: 403, message: 'DRAFT_ACTIVE_DELETE_FORBIDDEN_DRAFT_EXISTS' })
747
+ const drafts = []
748
+ const draftsRes = await draftQuery
749
+ if (draftsRes) drafts.push(draftsRes)
750
+
751
+ // For hierarchies, check that no sub node exists.
752
+ if (target?.elements?.LimitedDescendantCount) {
753
+ let key
754
+ for (const _key in req.target.keys) {
755
+ if (_key === 'IsActiveEntity') continue
756
+ key = _key // only single key supported
757
+ }
758
+ // We must only do this for recursive _composition_ children.
759
+ // For recursive _association_ children, app developers must deal with dangling pointers themselves.
760
+ const _recursiveComposition = target => {
761
+ for (const _comp in target.compositions) {
762
+ if (target.compositions[_comp]['@odata.draft.ignore']) {
763
+ return target.compositions[_comp]
764
+ }
765
+ }
766
+ }
767
+ const recursiveComposition = _recursiveComposition(req.target)
768
+ if (recursiveComposition) {
769
+ let uplinkName
770
+ for (const key in req.target) {
771
+ if (key.match(/@Aggregation\.RecursiveHierarchy\s*#.*\.ParentNavigationProperty/)) {
772
+ uplinkName = req.target[key]['=']
773
+ break
774
+ }
775
+ }
776
+ const keyVal = req.query.DELETE.from.ref[0].where?.[2]?.val
777
+ if (keyVal === undefined) req.reject(400, 'Deletion not supported')
778
+ // We must select actives and check for corresponding drafts (drafts themselve don't necessarily form a hierarchy)
779
+ const recursiveQ = SELECT.from(req.target).columns(key)
780
+ recursiveQ.SELECT.recurse = {
781
+ ref: [uplinkName],
782
+ where: [{ func: 'DistanceTo', args: [{ val: keyVal }, { val: null }] }]
783
+ }
784
+ const recursives = await recursiveQ
785
+ if (recursives.length) {
786
+ const recursiveDrafts = await SELECT.from(req.target.drafts)
787
+ .columns(inProcessByUserCol)
788
+ .where(Read.whereIn(req.target, recursives))
789
+ drafts.push(...recursiveDrafts)
790
+ }
791
+ }
792
+ }
793
+
794
+ for (const draft of drafts) {
795
+ const inProcessByUser = draft?.DraftAdministrativeData?.InProcessByUser
796
+ if (!cds.context.user._is_privileged && inProcessByUser && inProcessByUser !== cds.context.user.id)
797
+ req.reject({ code: 403, statusCode: 403, message: 'DRAFT_LOCKED_BY_ANOTHER_USER', args: [inProcessByUser] })
798
+ else req.reject({ code: 403, statusCode: 403, message: 'DRAFT_ACTIVE_DELETE_FORBIDDEN_DRAFT_EXISTS' })
799
+ }
531
800
  await run(query)
532
801
  return req.data
533
802
  }
@@ -535,8 +804,6 @@ const handle = async function (req) {
535
804
  if (req.event === 'draftActivate') {
536
805
  LOG.debug('activate draft')
537
806
 
538
- // It would be great if we'd have a SELECT ** to deeply expand the entity (along compositions), that should
539
- // be implemented in expand implementation.
540
807
  if (req.query.SELECT.from.ref.length > 1 || draftParams.IsActiveEntity === true) {
541
808
  req.reject({
542
809
  code: 400,
@@ -549,21 +816,29 @@ const handle = async function (req) {
549
816
  req.reject({ code: 428, statusCode: 428 })
550
817
  }
551
818
 
552
- const cols = expandStarStar(req.target.drafts, true)
819
+ const columns = expandStarStar(req.target.drafts, true)
553
820
 
554
- // Use `run` (since also etags might need to be checked)
555
- // REVISIT: Find a better approach (`etag` as part of CQN?)
556
821
  const draftRef = _redirectRefToDrafts(query.SELECT.from.ref, this.model)
557
822
  const draftQuery = SELECT.one
558
823
  .from({ ref: draftRef })
559
- .columns(cols)
824
+ .columns(columns)
560
825
  .columns([
561
- 'HasActiveEntity',
562
- 'DraftAdministrativeData_DraftUUID',
563
- { ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] }
826
+ // Will automatically select 'IsActiveEntity' as key column
827
+ { ref: ['HasActiveEntity'] },
828
+ { ref: ['DraftAdministrativeData_DraftUUID'] },
829
+ {
830
+ ref: ['DraftAdministrativeData'],
831
+ expand: [
832
+ { ref: ['InProcessByUser'] },
833
+ ...(cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages
834
+ ? [{ ref: ['DraftMessages'] }]
835
+ : [])
836
+ ]
837
+ }
564
838
  ])
565
839
  .where(query.SELECT.where)
566
840
  const res = await run(draftQuery)
841
+
567
842
  if (!res) {
568
843
  const _etagValidationType = req.headers['if-match']
569
844
  ? 'if-match'
@@ -581,23 +856,64 @@ const handle = async function (req) {
581
856
  })
582
857
  }
583
858
 
859
+ // Remove draft artefacts from persistedDraft entry
584
860
  const DraftAdministrativeData_DraftUUID = res.DraftAdministrativeData_DraftUUID
861
+ const persistedDraftMessages = res.DraftAdministrativeData?.DraftMessages || []
585
862
  delete res.DraftAdministrativeData_DraftUUID
586
863
  delete res.DraftAdministrativeData
587
864
  const HasActiveEntity = res.HasActiveEntity
588
865
  delete res.HasActiveEntity
589
866
 
590
- if (_hasStreaming(draftQuery.SELECT.columns, draftQuery._target, true) && !cds.env.features.binary_draft_compat)
867
+ if (
868
+ _hasStreaming(draftQuery.SELECT.columns, draftQuery._target, true) &&
869
+ !cds.env.features.binary_draft_compat &&
870
+ !cds.env.fiori.move_media_data_in_db
871
+ ) {
591
872
  await _removeEmptyStreams(res)
873
+ }
592
874
 
593
875
  // First run the handlers as they might need access to DraftAdministrativeData or the draft entities
594
876
  const activesRef = _redirectRefToActives(query.SELECT.from.ref, this.model)
595
- const result = await run(
596
- HasActiveEntity
597
- ? UPDATE({ ref: activesRef }).data(res).where(query.SELECT.where)
598
- : INSERT.into({ ref: activesRef }).entries(res),
599
- { headers: Object.assign({}, req.headers, { 'if-match': '*' }) }
600
- )
877
+
878
+ // Upsert draft into active, not considering media data columns
879
+ const upsertQuery = HasActiveEntity
880
+ ? UPDATE({ ref: activesRef }).data(res).where(query.SELECT.where)
881
+ : INSERT.into({ ref: activesRef }).entries(res)
882
+ const upsertOptions = { headers: Object.assign({}, req.headers, { 'if-match': '*' }) }
883
+
884
+ const _req = _newReq(req, upsertQuery, draftParams, upsertOptions)
885
+
886
+ let result
887
+ try {
888
+ result = await h.call(this, _req)
889
+ } catch (error) {
890
+ if (cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages && _req.errors)
891
+ _req.on('failed', async () => {
892
+ const nextDraftMessages = _compileUpdatedDraftMessages(
893
+ _req.errors.map(e => ({ ...e })),
894
+ persistedDraftMessages,
895
+ {},
896
+ draftRef
897
+ )
898
+
899
+ await cds.tx(async () => {
900
+ await UPDATE('DRAFT.DraftAdministrativeData')
901
+ .set({ DraftMessages: nextDraftMessages })
902
+ .where({ DraftUUID: DraftAdministrativeData_DraftUUID })
903
+ })
904
+ })
905
+
906
+ throw error
907
+ }
908
+
909
+ // REVISIT: Remove feature flag dependency
910
+ if (cds.env.fiori.move_media_data_in_db) {
911
+ // Move cds.LargeBinary data from draft to active
912
+ const updateMediaDataQueries = getUpdateFromSelectQueries(req.target, draftRef, activesRef)
913
+ await _promiseAll(updateMediaDataQueries)
914
+ }
915
+
916
+ // Delete draft artefacts
601
917
  await _promiseAll([
602
918
  DELETE.from({ ref: draftRef }).where(query.SELECT.where),
603
919
  DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: DraftAdministrativeData_DraftUUID })
@@ -656,15 +972,23 @@ const handle = async function (req) {
656
972
  }
657
973
  }
658
974
  _rmIsActiveEntity(req.data, req.target)
975
+
659
976
  if (draftParams.IsActiveEntity === false) {
660
977
  LOG.debug('patch draft')
661
978
 
662
979
  if (req.target?.name.endsWith('DraftAdministrativeData')) req.reject({ code: 405, statusCode: 405 })
980
+
663
981
  const draftsRef = _redirectRefToDrafts(query.UPDATE.entity.ref, this.model)
664
982
  const res = await SELECT.one.from({ ref: draftsRef }).columns('DraftAdministrativeData_DraftUUID', {
665
983
  ref: ['DraftAdministrativeData'],
666
- expand: [{ ref: ['InProcessByUser'] }]
984
+ expand: [
985
+ { ref: ['InProcessByUser'] },
986
+ ...(cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages
987
+ ? [{ ref: ['DraftMessages'] }]
988
+ : [])
989
+ ]
667
990
  })
991
+
668
992
  if (!res) req.reject({ code: 404, statusCode: 404 })
669
993
  if (!cds.context.user._is_privileged && res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id) {
670
994
  req.reject({
@@ -674,15 +998,31 @@ const handle = async function (req) {
674
998
  args: [res.DraftAdministrativeData?.InProcessByUser]
675
999
  })
676
1000
  }
1001
+
1002
+ await run(UPDATE({ ref: draftsRef }).data(req.data))
1003
+
1004
+ const nextDraftAdminData = {
1005
+ InProcessByUser: req.user.id,
1006
+ LastChangedByUser: req.user.id,
1007
+ LastChangeDateTime: new Date()
1008
+ }
1009
+
1010
+ if (cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages) {
1011
+ nextDraftAdminData.DraftMessages = _compileUpdatedDraftMessages(
1012
+ req.messages ?? [],
1013
+ res.DraftAdministrativeData?.DraftMessages ?? [],
1014
+ req.data ?? {},
1015
+ draftsRef
1016
+ )
1017
+
1018
+ // Prevent validation errors from being sent in 'sap-messages' header
1019
+ req.messages = req._set('_messages', new Responses())
1020
+ }
1021
+
677
1022
  await UPDATE('DRAFT.DraftAdministrativeData')
678
- .data({
679
- InProcessByUser: req.user.id,
680
- LastChangedByUser: req.user.id,
681
- LastChangeDateTime: new Date()
682
- })
1023
+ .data(nextDraftAdminData)
683
1024
  .where({ DraftUUID: res.DraftAdministrativeData_DraftUUID })
684
1025
 
685
- await run(UPDATE({ ref: draftsRef }).data(req.data))
686
1026
  req.data.IsActiveEntity = false
687
1027
  return req.data
688
1028
  }
@@ -877,6 +1217,16 @@ const Read = {
877
1217
  }
878
1218
  return result
879
1219
  }
1220
+
1221
+ if (cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages) {
1222
+ // Replace selection of 'DraftMessages' with the proper path expression
1223
+
1224
+ query._drafts.SELECT.columns = [
1225
+ ...(query._drafts.SELECT.columns ?? ['*']).filter(c => c.ref?.[0] !== 'DraftMessages'),
1226
+ { ref: ['DraftAdministrativeData', 'DraftMessages'], as: 'DraftMessages' }
1227
+ ]
1228
+ }
1229
+
880
1230
  const draftsQuery = query._drafts.where(
881
1231
  { ref: ['DraftAdministrativeData', 'InProcessByUser'] },
882
1232
  '=',
@@ -889,7 +1239,33 @@ const Read = {
889
1239
  HasDraftEntity: false
890
1240
  })
891
1241
  _fillIsActiveEntity(row, false, query._drafts._target)
1242
+
1243
+ if (cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages && row.DraftMessages?.length) {
1244
+ // Reduce persisted draft messages to the set of those that are part of the queried entities tree
1245
+
1246
+ const simplePath = query._drafts.SELECT.from.ref
1247
+ .map(refElement => {
1248
+ refElement = (refElement.id ?? refElement).split('.')
1249
+ if (refElement.length === 1) return refElement[0]
1250
+ if (refElement[refElement.length - 1] === 'drafts') return refElement[refElement.length - 2]
1251
+ return refElement[refElement.length - 1]
1252
+ })
1253
+ .join('/')
1254
+
1255
+ row.DraftMessages = row.DraftMessages.reduce((acc, msg) => {
1256
+ const messageSimplePath = msg.prefix.replaceAll(/\([^(]*\)/g, '')
1257
+ if (!messageSimplePath.startsWith(simplePath)) return acc
1258
+ msg.target = `/${msg.prefix}/${msg.target}`
1259
+ delete msg.prefix
1260
+ delete msg.simplePathElements
1261
+ acc.push(msg)
1262
+ return acc
1263
+ }, [])
1264
+
1265
+ row.DraftMessages = getLocalizedMessages(row.DraftMessages, cds.context.http.req)
1266
+ }
892
1267
  })
1268
+
893
1269
  return _requested(drafts, query)
894
1270
  },
895
1271
 
@@ -1229,6 +1605,7 @@ function _cleanseCols(columns, elements, target) {
1229
1605
  })
1230
1606
  }
1231
1607
 
1608
+ let DRAFT_ADMIN_ELEMENTS
1232
1609
  /**
1233
1610
  * Creates a clone of the query, cleanses and collects all draft parameters into DRAFT_PARAMS.
1234
1611
  */
@@ -1355,6 +1732,11 @@ function _cleansed(query, model) {
1355
1732
  }
1356
1733
 
1357
1734
  function _tweakAdminCols(columns) {
1735
+ if (!DRAFT_ADMIN_ELEMENTS) {
1736
+ DRAFT_ADMIN_ELEMENTS = []
1737
+ for (const key in cds.model.definitions['DRAFT.DraftAdministrativeData'].elements) DRAFT_ADMIN_ELEMENTS.push(key)
1738
+ }
1739
+
1358
1740
  if (!columns || columns.some(c => c === '*')) columns = DRAFT_ADMIN_ELEMENTS.map(k => ({ ref: [k] }))
1359
1741
  return columns.map(col => {
1360
1742
  const name = col.ref?.[0]
@@ -1444,59 +1826,40 @@ function _cleansed(query, model) {
1444
1826
  }
1445
1827
  }
1446
1828
 
1447
- // This function is better defined on DB layer
1829
+ // REVISIT: This function is better defined on DB layer
1448
1830
  function expandStarStar(target, draftActivate, recursion = new Map()) {
1449
- const MAX_RECURSION_DEPTH = (cds.env.features.recursion_depth && Number(cds.env.features.recursion_depth)) || 4
1450
1831
  const columns = []
1832
+
1451
1833
  for (const el in target.elements) {
1452
1834
  const element = target.elements[el]
1453
1835
 
1454
- // no need to read calculated elements
1836
+ // Skip calculated elements
1455
1837
  if (draftActivate && element.value) continue
1456
1838
 
1457
- // REVISIT: This does not work with old HANA db layer.
1458
- // Use it after removing old db layer.
1459
- /*
1460
- if (element._type === 'cds.LargeBinary') {
1461
- if (!draftActivate) {
1462
- columns.push({
1463
- xpr: [
1464
- 'case',
1465
- 'when',
1466
- { ref: [el] },
1467
- 'IS',
1468
- 'NULL',
1469
- 'then',
1470
- { val: null },
1471
- 'else',
1472
- { val: '' },
1473
- 'end'
1474
- ],
1475
- as: el,
1476
- // cast: 'cds.LargeBinary' <-- This should be fixed for new HANA db layer
1477
- })
1478
- continue
1479
- }
1480
- }*/
1481
-
1482
- const skip_managed = draftActivate && (element['@cds.on.insert'] || element['@cds.on.update'])
1483
- if (!skip_managed && !element.isAssociation && !DRAFT_ELEMENTS.has(el) && !element['@odata.draft.skip'])
1484
- columns.push({ ref: [el] })
1485
-
1486
- if (!element.isComposition || element._target['@odata.draft.enabled'] === false) continue // happens for texts if not @fiori.draft.enabled
1487
- const _key = target.name + ':' + el
1488
- let cache = recursion.get(_key)
1489
- if (!cache) {
1490
- cache = 1
1491
- recursion.set(_key, cache)
1492
- } else {
1493
- cache++
1494
- recursion.set(_key, cache)
1495
- }
1496
- if (cache >= MAX_RECURSION_DEPTH) return
1497
- const expand = expandStarStar(element._target, draftActivate, recursion)
1498
- if (expand) columns.push({ ref: [el], expand })
1839
+ // Make sure column is releveant & add it to output
1840
+ const isDraftInfo = DRAFT_ELEMENTS.has(el)
1841
+ const isGenerated = draftActivate && !!(element['@cds.on.insert'] || element['@cds.on.update'])
1842
+ const isLargeBinary = cds.env.fiori.move_media_data_in_db && draftActivate && element._type === 'cds.LargeBinary'
1843
+ const isSkippedType = element.isAssociation || isLargeBinary
1844
+ if (!isDraftInfo && !isGenerated && !isSkippedType && !element['@odata.draft.skip']) columns.push({ ref: [el] })
1845
+
1846
+ // Skip children where draft is explicitly disabled
1847
+ if (!element.isComposition || element._target['@odata.draft.enabled'] === false || element['@odata.draft.ignore'])
1848
+ continue // happens for texts if not @fiori.draft.enabled
1849
+
1850
+ // Make sure recursion depth is not exceeded
1851
+ const cacheKey = target.name + ':' + el
1852
+ let cache = recursion.get(cacheKey) ?? 0
1853
+ recursion.set(cacheKey, ++cache)
1854
+ if (cache >= MAX_RECURSION_DEPTH) continue
1855
+
1856
+ // Recurse
1857
+ const expandedColumns = expandStarStar(element._target, draftActivate, recursion)
1858
+
1859
+ // Unpack recursion result
1860
+ if (expandedColumns.length) columns.push({ ref: [el], expand: expandedColumns })
1499
1861
  }
1862
+
1500
1863
  return columns
1501
1864
  }
1502
1865
 
@@ -1521,7 +1884,6 @@ async function beforeNew(req) {
1521
1884
  }
1522
1885
  _cleanseData(req.data, req.target)
1523
1886
  }
1524
-
1525
1887
  beforeNew._initial = true
1526
1888
 
1527
1889
  async function onNew(req) {
@@ -1529,8 +1891,10 @@ async function onNew(req) {
1529
1891
 
1530
1892
  if (req.target.actives['@Capabilities.InsertRestrictions.Insertable'] === false || req.target.actives['@readonly'])
1531
1893
  req.reject({ code: 405, statusCode: 405 })
1894
+
1532
1895
  req.query ??= INSERT.into(req.subject).entries(req.data || {}) //> support simple srv.send('NEW',entity,...)
1533
1896
  const isDirectAccess = typeof req.query.INSERT.into === 'string' || req.query.INSERT.into.ref?.length === 1
1897
+
1534
1898
  // Only allowed for pseudo draft roots (entities with this action)
1535
1899
  if (isDirectAccess && !req.target.actives['@Common.DraftRoot.ActivationAction'])
1536
1900
  req.reject({ code: 403, statusCode: 403, message: 'DRAFT_MODIFICATION_ONLY_VIA_ROOT' })
@@ -1538,14 +1902,25 @@ async function onNew(req) {
1538
1902
  _cleanUpOldDrafts(this, req.tenant)
1539
1903
 
1540
1904
  let DraftUUID
1905
+ let DraftMessages = []
1906
+ let newDraftMessages = req.messages ?? req._messages ?? []
1541
1907
  if (isDirectAccess) DraftUUID = cds.utils.uuid()
1542
1908
  else {
1543
1909
  const rootData = await SELECT.one(req.query.INSERT.into.ref[0].id)
1544
1910
  .columns([
1545
1911
  { ref: ['DraftAdministrativeData_DraftUUID'] },
1546
- { ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] }
1912
+ {
1913
+ ref: ['DraftAdministrativeData'],
1914
+ expand: [
1915
+ { ref: ['InProcessByUser'] },
1916
+ ...(cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages
1917
+ ? [{ ref: ['DraftMessages'] }]
1918
+ : [])
1919
+ ]
1920
+ }
1547
1921
  ])
1548
1922
  .where(req.query.INSERT.into.ref[0].where)
1923
+
1549
1924
  if (!rootData) req.reject({ code: 404, statusCode: 404 })
1550
1925
  if (!cds.context.user._is_privileged && rootData.DraftAdministrativeData?.InProcessByUser !== req.user.id)
1551
1926
  req.reject({
@@ -1554,27 +1929,10 @@ async function onNew(req) {
1554
1929
  message: 'DRAFT_LOCKED_BY_ANOTHER_USER',
1555
1930
  args: [rootData.DraftAdministrativeData.InProcessByUser]
1556
1931
  })
1932
+
1557
1933
  DraftUUID = rootData.DraftAdministrativeData_DraftUUID
1934
+ DraftMessages = rootData.DraftAdministrativeData?.DraftMessages || []
1558
1935
  }
1559
- const timestamp = cds.context.timestamp.toISOString() // REVISIT: toISOString should be done on db layer
1560
- const adminDataCQN = isDirectAccess
1561
- ? INSERT.into('DRAFT.DraftAdministrativeData').entries({
1562
- DraftUUID,
1563
- CreationDateTime: timestamp,
1564
- CreatedByUser: req.user.id,
1565
- LastChangeDateTime: timestamp,
1566
- LastChangedByUser: req.user.id,
1567
- DraftIsCreatedByMe: true, // Dummy values
1568
- DraftIsProcessedByMe: true, // Dummy values
1569
- InProcessByUser: req.user.id
1570
- })
1571
- : UPDATE('DRAFT.DraftAdministrativeData')
1572
- .data({
1573
- InProcessByUser: req.user.id,
1574
- LastChangedByUser: req.user.id,
1575
- LastChangeDateTime: timestamp
1576
- })
1577
- .where({ DraftUUID })
1578
1936
 
1579
1937
  const _setDraftColumns = (obj, target) => {
1580
1938
  const newObj = Object.assign({}, obj, { DraftAdministrativeData_DraftUUID: DraftUUID, HasActiveEntity: false })
@@ -1594,15 +1952,49 @@ async function onNew(req) {
1594
1952
  return newObj
1595
1953
  }
1596
1954
 
1955
+ const timestamp = cds.context.timestamp.toISOString() // REVISIT: toISOString should be done on db layer
1597
1956
  const draftData = _setDraftColumns(req.query.INSERT.entries[0], req.target)
1598
-
1599
1957
  const draftCQN = INSERT.into(req.subject).entries(draftData)
1600
1958
 
1601
- await _promiseAll([adminDataCQN, this.run(draftCQN)])
1959
+ await this.run(draftCQN)
1602
1960
 
1603
- // flag to trigger read after write in legacy odata adapter
1604
- if (req.constructor.name in { ODataRequest: 1 }) req._.readAfterWrite = true
1605
- if (req.protocol?.match(/odata/)) req._.readAfterWrite = true //> REVISIT for noah
1961
+ if (cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages) {
1962
+ newDraftMessages = _compileUpdatedDraftMessages(
1963
+ newDraftMessages,
1964
+ DraftMessages,
1965
+ draftData,
1966
+ req.query.INSERT.into.ref
1967
+ )
1968
+ }
1969
+
1970
+ const adminDataCQN = isDirectAccess
1971
+ ? INSERT.into('DRAFT.DraftAdministrativeData').entries({
1972
+ DraftUUID,
1973
+ CreationDateTime: timestamp,
1974
+ CreatedByUser: req.user.id,
1975
+ LastChangeDateTime: timestamp,
1976
+ LastChangedByUser: req.user.id,
1977
+ DraftIsCreatedByMe: true, // Dummy values
1978
+ DraftIsProcessedByMe: true, // Dummy values
1979
+ InProcessByUser: req.user.id,
1980
+ ...(cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages
1981
+ ? { DraftMessages: newDraftMessages }
1982
+ : {})
1983
+ })
1984
+ : UPDATE('DRAFT.DraftAdministrativeData')
1985
+ .data({
1986
+ InProcessByUser: req.user.id,
1987
+ LastChangedByUser: req.user.id,
1988
+ LastChangeDateTime: timestamp,
1989
+ ...(cds.model.definitions['DRAFT.DraftAdministrativeData'].elements.DraftMessages
1990
+ ? { DraftMessages: newDraftMessages }
1991
+ : {})
1992
+ })
1993
+ .where({ DraftUUID })
1994
+
1995
+ await adminDataCQN
1996
+
1997
+ if (req.protocol?.match(/odata/)) req._.readAfterWrite = true
1606
1998
 
1607
1999
  return { ...draftData, IsActiveEntity: false }
1608
2000
  }
@@ -1612,8 +2004,8 @@ async function onEdit(req) {
1612
2004
 
1613
2005
  req.query ??= SELECT.from(req.target, req.data).where({ IsActiveEntity: true }) //> support simple srv.send('EDIT',entity,...)
1614
2006
 
1615
- // use symbol for _draftParams
1616
- const draftParams = req.query[$draftParams] || { IsActiveEntity: true } // REVISIT: can draftParams in the edit caser ever be undefined or other than IsActiveEntity=true ?
2007
+ // REVISIT: can draftParams in the edit case ever be undefined or other than IsActiveEntity=true ?
2008
+ const draftParams = req.query[$draftParams] || { IsActiveEntity: true }
1617
2009
  if (req.query.SELECT.from.ref.length > 1 || draftParams.IsActiveEntity !== true) {
1618
2010
  req.reject({
1619
2011
  code: 400,
@@ -1650,6 +2042,7 @@ async function onEdit(req) {
1650
2042
  }
1651
2043
  }
1652
2044
  }
2045
+
1653
2046
  _addDraftColumns(req.target, cols)
1654
2047
 
1655
2048
  const draftsRef = _redirectRefToDrafts(req.query.SELECT.from.ref, this.model)
@@ -1698,6 +2091,7 @@ async function onEdit(req) {
1698
2091
  })
1699
2092
  return w
1700
2093
  })()
2094
+
1701
2095
  const activeLockCQN = SELECT.from(lockTarget, [1]).where(lockWhere).forUpdate({ wait: 0 })
1702
2096
  activeLockCQN.SELECT.localized = false
1703
2097
  activeLockCQN[$draftParams] = draftParams
@@ -1775,16 +2169,11 @@ async function onEdit(req) {
1775
2169
  // change to db run
1776
2170
  await INSERT.into(req.target.drafts).entries(res)
1777
2171
 
1778
- // REVISIT: we need to use okra API here because it must be set in the batched request
1779
- // status code must be set in handler to allow overriding for FE V2
1780
- // REVISIT: needs reworking for new adapter, especially re $batch
1781
- if (req._?.odataRes) {
1782
- req._?.odataRes?.setStatusCode(201, { overwrite: true })
1783
- } else if (req.res) {
2172
+ if (req.res) {
2173
+ // status code must be set in handler to allow overriding for FE V2
2174
+ // REVISIT: needs reworking for new adapter, especially re $batch
1784
2175
  req.res.status(201)
1785
- }
1786
2176
 
1787
- if (req.res) {
1788
2177
  const read_result = await _readAfterDraftAction.bind(this)({
1789
2178
  req,
1790
2179
  payload: res,
@@ -1804,7 +2193,6 @@ async function onCancel(req) {
1804
2193
  LOG.debug('delete draft')
1805
2194
 
1806
2195
  req.query ??= DELETE(req.target, req.data) //> support simple srv.send('CANCEL',entity,...)
1807
- const activeRef = _redirectRefToActives(req.query.DELETE.from.ref, this.model)
1808
2196
  const draftParams = req.query[$draftParams] || { IsActiveEntity: false } // REVISIT: can draftParams in the cancel case ever be undefined or other than IsActiveEntity=false ?
1809
2197
 
1810
2198
  const draftQuery = SELECT.one
@@ -1816,16 +2204,16 @@ async function onCancel(req) {
1816
2204
  if (req._etagValidationClause && draftParams.IsActiveEntity === false) draftQuery.where(req._etagValidationClause)
1817
2205
  // do not add InProcessByUser restriction
1818
2206
  const draft = await draftQuery
1819
- if (draftParams.IsActiveEntity === false && !draft) req.reject(req._etagValidationType ? 412 : 404)
2207
+ if (!draft) req.reject(req._etagValidationType ? 412 : 404)
1820
2208
  if (draft) {
1821
2209
  const processByUser = draft.DraftAdministrativeData?.InProcessByUser
1822
2210
  if (!cds.context.user._is_privileged && processByUser && processByUser !== cds.context.user.id)
1823
2211
  req.reject({ code: 403, statusCode: 403, message: 'DRAFT_LOCKED_BY_ANOTHER_USER', args: [processByUser] })
1824
2212
  }
2213
+
1825
2214
  const draftDeleteQuery = DELETE.from({ ref: req.query.DELETE.from.ref }) // REVISIT: Isn't that == req.query ?
1826
- const queries = !draft
1827
- ? []
1828
- : [draftParams.IsActiveEntity === false ? this.run(draftDeleteQuery, req.data) : draftDeleteQuery]
2215
+
2216
+ const queries = !draft ? [] : [this.run(draftDeleteQuery, req.data)]
1829
2217
  if (draft && req.target['@Common.DraftRoot.ActivationAction'])
1830
2218
  // only for draft root
1831
2219
  queries.push(
@@ -1841,8 +2229,6 @@ async function onCancel(req) {
1841
2229
  })
1842
2230
  .where({ DraftUUID: draft.DraftAdministrativeData_DraftUUID })
1843
2231
  )
1844
- if (draftParams.IsActiveEntity !== false && !req.target.isDraft)
1845
- queries.push(this.run(DELETE.from({ ref: activeRef })))
1846
2232
  await _promiseAll(queries)
1847
2233
  return req.data
1848
2234
  }
@@ -1862,7 +2248,7 @@ async function onPrepare(req) {
1862
2248
 
1863
2249
  const draftQuery = SELECT.one
1864
2250
  .from(req.target, d => {
1865
- d`.*`, d.DraftAdministrativeData(a => a.InProcessByUser)
2251
+ ;(d`.*`, d.DraftAdministrativeData(a => a.InProcessByUser))
1866
2252
  })
1867
2253
  .where(where)
1868
2254
  draftQuery[$draftParams] = draftParams
@@ -1959,8 +2345,8 @@ module.exports = cds.service.impl(function () {
1959
2345
  }
1960
2346
 
1961
2347
  // Also runs those handlers if they're annotated with @odata.draft.enabled through extensibility
1962
- this.on('NEW', '*', _wrapped(onNew, false))
1963
2348
  this.prepend(s => s.before('NEW', '*', _wrapped(beforeNew, false)))
2349
+ this.on('NEW', '*', _wrapped(onNew, false))
1964
2350
  this.on('EDIT', '*', _wrapped(onEdit, true))
1965
2351
  this.on('CANCEL', '*', _wrapped(onCancel, false))
1966
2352
  this.on('draftPrepare', '*', _wrapped(onPrepare, false))